移动端远程配置接入
本文面向前端项目,说明 App 如何读取 /public/client-config,以及如何使用端点、模型、转写模式和功能开关。后端管理入口是 /admin/app-settings,但客户端只读取公开接口,不直接访问后台接口。
拉取配置
推荐 App 启动后尽早拉取需要的公开分组:
GET /public/client-config?groups=endpoints,models,transcribe,flags,thresholds&platform=ios&appVersion=1.0.0请求规则:
| 参数 | 用途 |
|---|---|
groups | 逗号分隔的分组列表。移动端通常读取 endpoints,models,transcribe,flags,thresholds |
platform | 客户端平台,例如 ios、android;当前服务端仅记录语义,不按平台分流 |
appVersion | App 版本;当前服务端仅记录语义,不按版本分流 |
响应示例:
{
"version": 1782480000000,
"updatedAt": "2026-06-26T10:00:00.000Z",
"groups": {
"endpoints": {
"apiBaseUrl": "https://block2-api.wainao.chat",
"oauthBaseUrl": "https://block2-api.wainao.chat",
"modelGatewayBaseUrl": "/api/model-gateway/v1",
"modelManifestUrl": "https://example.com/mobile/models/manifest.json",
"modelBaseUrl": "https://example.com/mobile/models"
},
"models": {
"asr": {
"lite": { "id": "sense-voice-small-int8", "enabled": true },
"pro": { "id": "sense-voice-small-fp16", "enabled": true }
},
"refine": {
"lite": { "id": "qwen3-0.6b-int4", "enabled": true },
"pro": { "id": "qwen3-1.7b-int4", "enabled": true }
},
"cloud": {
"asrModel": "sense-voice-small",
"refineModel": "qwen-flash"
}
},
"transcribe": {
"defaultMode": "local_first",
"allowedModes": ["local_first", "local_only"],
"defaultRefineMode": "cloud_first",
"allowLocalDownload": true
},
"flags": {
"enableLocalAsr": true,
"enableLocalRefine": true,
"enableModelDownload": true,
"enableTabAiFriends": true,
"enableTabAiCircle": true,
"enableTabMine": true,
"enableTabSearch": true
}
}
}客户端必须把 groups 当作部分配置处理:缺少某个分组或字段时,使用 App 内置默认值继续启动,不应因为远程配置为空阻塞首屏。
flags 可能携带历史安全字段。前端只应对本文列出的 boolean 功能开关做业务判断,未知字段保留但不要参与 Tab 或模型能力判断。
缓存策略
接口返回 ETag、version 和 updatedAt:
- 首次启动使用内置默认值,并异步拉取远程配置。
- 拉取成功后持久化完整响应和
ETag。 - 下次请求携带
If-None-Match。 - 收到
304 Not Modified时继续使用本地缓存。 - 收到新配置时以远程配置覆盖对应字段,未知字段保留但不参与业务判断。
远程配置失败时,前端应使用最近一次成功缓存;没有缓存时使用内置默认值。
端点使用
endpoints 只描述公开可用地址,不携带任何密钥。
| 字段 | 前端使用方式 |
|---|---|
apiBaseUrl | 普通后端 API base URL;origin 由后端按请求域名动态推导,见下节 |
oauthBaseUrl | OAuth 登录和 token 交换 base URL;为空时可回退到 apiBaseUrl;origin 同样按请求域名动态推导 |
modelGatewayBaseUrl | OpenAI-compatible 模型网关路径,默认 /api/model-gateway/v1;若配成绝对 URL,origin 同样按请求域名动态推导 |
modelManifestUrl | 模型清单稳定 URL,优先使用;强制 https://,不受请求域名影响 |
modelBaseUrl | 模型下载 base URL,用于 manifest 内相对路径补全或无 manifest 时兜底;强制 https://,不受请求域名影响 |
modelDownloadBaseUrl | 旧字段兼容别名,新客户端优先读取 modelBaseUrl;强制 https://,不受请求域名影响 |
modelManifestUrl 应是稳定 URL。不要把带短期签名 token 的 OSS URL 填到这里,否则客户端缓存和后端刷新都不可控。私有模型文件需要签名时,推荐 manifest 本身指向后端稳定接口,由后端在 manifest 或下载接口里生成短期下载 URL。
modelBaseUrl 可以为空。为空表示客户端不能靠 base URL 拼出模型文件地址,只能使用 modelManifestUrl 或 manifest 中的完整 url 字段。
apiBaseUrl / oauthBaseUrl / modelGatewayBaseUrl 按请求域名动态推导(issue #861 Phase 2b)
ReAI(block2-api.wainao.chat)和大陆 CloudOS(block-api.cloudos.com)两个节点共用同一个 selfhosted 数据库。为了避免两个域名的客户端都拿到同一个硬编码域名,GET /public/client-config 会按发起请求的 Host / X-Forwarded-Host / X-Forwarded-Proto 推导出请求方自己的 origin,只替换这三个字段的 origin 部分,路径后缀原样保留(例如后台配的是 .../api/model-gateway/v1,改写后还是 .../api/model-gateway/v1,只是域名换成了发请求那台节点自己的域名)。
前端不需要为这一行为做任何适配——响应里拿到的就是当前所连节点自己的真实域名。响应头会带 Vary: Host, X-Forwarded-Host, X-Forwarded-Proto。
modelManifestUrl / modelBaseUrl / modelDownloadBaseUrl 强制 HTTPS
这三个字段必须是 https://(或空/站内相对路径),不允许明文 http:// 或内嵌用户名密码——iOS App Transport Security(ATS)默认会拒绝明文 HTTP 请求。管理后台写入非法值会被拒绝;公开接口读取到历史脏数据时会跳过整个 endpoints 分组(不是只跳过单个字段),前端应按「远程配置可能整体缺失」的既有容错逻辑处理,不需要额外适配。
已知限制:
modelManifestUrl指向的 manifest JSON 文件本身由 ASR 本地模型上架流程在仓库之外单独生成上传,不受这里描述的校验覆盖。截至 2026-07-01,manifest 内部各模型文件的url仍可能是http://,App 实际下载模型文件时仍可能被 ATS 拦截,需要另外专项修正。
模型清单
客户端优先从 modelManifestUrl 获取模型文件清单。建议 manifest 至少包含模型 ID、版本、文件 URL、大小和校验和:
{
"version": "2026-06-27",
"models": [
{
"id": "sense-voice-small-int8",
"type": "asr",
"tier": "lite",
"version": "1.0.0",
"url": "sense-voice-small-int8/model.bin",
"sizeBytes": 123456789,
"sha256": "..."
}
]
}下载规则:
- 先检查
flags.enableModelDownload和transcribe.allowLocalDownload。 - 再检查对应模型档位的
enabled是否为true。 - 优先使用 manifest 中的完整
url。 - 如果
url是相对路径,用modelBaseUrl补全。 - 下载后必须校验
sizeBytes和sha256,校验失败不得启用模型。
客户端不能从远程配置中读取 OSS AccessKey、Secret、STS token、Bearer token 或任何私有凭据。
模型与转写模式
models.asr 和 models.refine 描述本地模型档位:
| 字段 | 说明 |
|---|---|
asr.lite.id | 本地轻量 ASR 模型 ID |
asr.pro.id | 本地高质量 ASR 模型 ID |
refine.lite.id | 本地轻量润色模型 ID |
refine.pro.id | 本地高质量润色模型 ID |
*.enabled | 是否允许客户端展示并使用该档位 |
models.cloud 描述云端模型:
| 字段 | 说明 |
|---|---|
cloud.asrModel | 云端 ASR 默认模型 ID,用于 /services/passthrough/asr/transcriptions 未显式传 model 时;2026-07-03 起生产库该字段已指向真实上架模型 cloudos-sensevoice-small-asr(CloudOS Audio API,接入记录见仓库根 docs/features/ai-agent/providers/cloudos_audio_asr.md,未纳入 docs-site) |
cloud.refineModel | 云端润色默认模型 ID,用于 /services/passthrough/text-refine 未显式传 model 时 |
转写模式由服务端策略和客户端运行时能力共同决定:
| 服务端字段 | 含义 |
|---|---|
transcribe.allowedModes | 服务端允许展示的模式集合 |
transcribe.defaultMode | 服务端建议默认模式 |
transcribe.defaultRefineMode | 服务端建议默认润色模式 |
transcribe.allowLocalDownload | 是否允许端上下载本地模型 |
客户端最终可用模式应取交集:服务端允许、功能开关打开、模型 ID 已配置、模型文件可用或可下载、当前设备满足运行条件。服务端勾选“允许模式”只是产品策略,不代表端上一定可运行。
参考逻辑:
const flags = config.groups.flags ?? {}
const transcribe = config.groups.transcribe ?? {}
const models = config.groups.models ?? {}
const allowedModes = new Set(transcribe.allowedModes ?? ['cloud_only'])
const canUseCloudAsr = Boolean(models.cloud?.asrModel) && oauthTokenReady
const canUseLocalAsr =
flags.enableLocalAsr === true &&
hasEnabledLocalAsrModel(models) &&
(localAsrReady || (flags.enableModelDownload === true && transcribe.allowLocalDownload === true))
const usableModes = [
allowedModes.has('cloud_only') && canUseCloudAsr ? 'cloud_only' : null,
allowedModes.has('cloud_first') && canUseCloudAsr ? 'cloud_first' : null,
allowedModes.has('local_first') && canUseLocalAsr ? 'local_first' : null,
allowedModes.has('local_only') && canUseLocalAsr ? 'local_only' : null,
].filter(Boolean)Tab 功能开关
后台 App 设置可以控制移动端 Tab 是否展示或启用:
| 字段 | 对应功能 |
|---|---|
enableTabAiFriends | AI 好友 |
enableTabAiCircle | AI 圈 |
enableTabMine | 我的 |
enableTabSearch | 搜索 |
推荐前端行为:
- 配置为
true时展示并允许进入。 - 配置为
false时隐藏或置灰,具体交互由产品决定。 - 如果当前选中的 Tab 被关闭,切到第一个仍可用的 Tab。
- 如果所有业务 Tab 都关闭,保留一个安全兜底页,避免底部导航为空。
与模型网关的关系
官方移动端不内置模型网关 API Key。正确链路是:
- App 通过 OAuth PKCE 获取
wno-access token。 - App 使用
Authorization: Bearer wno-...调用/services/passthrough/*业务包装接口。 - 后端校验
mobile:fullscope,并在服务端映射隐藏的系统 API Key。
云端 ASR 使用 /services/passthrough/asr/transcriptions,云端润色使用 /services/passthrough/text-refine。具体接口见 服务目录查询 API。
modelGatewayBaseUrl 仍保留给 OpenAI-compatible 兼容链路;移动端业务流程优先使用包装接口,以获得稳定的业务响应结构、默认模型选择和服务端限额。
兼容字段
后台会继续写入部分旧字段,方便旧客户端过渡:
| 旧字段 | 新字段 |
|---|---|
endpoints.modelDownloadBaseUrl | endpoints.modelBaseUrl |
models.local.asrLite.modelId | models.asr.lite.id |
models.local.asrPro.modelId | models.asr.pro.id |
models.local.refineLite.modelId | models.refine.lite.id |
models.local.refinePro.modelId | models.refine.pro.id |
新客户端应优先读取新字段;如果新字段为空,再读取旧字段作为兼容兜底。