渠道与白标
同一套代码可以交付成多个渠道(channel)。渠道决定这一份交付物”叫什么、长什么样、 从哪里升级”,且渠道之间互不升级 —— 这是正确性要求,不是配置项。
这一页解决什么问题
Section titled “这一页解决什么问题”判断”我这一栈属于哪个渠道、升级时会不会被换成别的渠道”,以及渠道相关的失败长什么样。 本页覆盖:三类渠道 → 渠道内容的落点与字段 → 发布矩阵(哪些 tag 产出哪些渠道)→ 部署侧必须自洽的值 → 判据 → 边界与失败行为(含”升级后品牌没了”的定位)。
前提:渠道内容来自私有渠道仓(构建期拉取)并注入镜像,本文档一律用占位符
(<渠道 id>、<品牌名>、harness.example.com)——真实渠道身份不进公开仓库,也不进本文档。
| 渠道 | 用途 | 分发面 |
|---|---|---|
official | 正式发布 | 更新服务器 release.picoaide.com/official/ + GitHub Release(完整历史归档) |
beta | 预发布(版本 tag 含 -beta / -rc / -alpha) | 更新服务器 release.picoaide.com/beta/ + GitHub Pre-release |
<渠道 id> | 企业定制 / 白标交付 | 只经更新服务器 release.picoaide.com/<渠道 id>/(不进公开 Release) |
每个渠道在更新服务器上有独立目录,结构与官方完全同构:
<渠道>/latest.json<渠道>/releases/<版本>/picoaide-server-<版本>-amd64.zip<渠道>/releases/<版本>/SHA256SUMS为什么渠道内容随镜像构建,而不是运行时上传。 品牌、文案、标识、数据根与深链 scheme
都在构建期注入镜像(docker build --build-arg CHANNEL=<渠道 id>),管理后台不提供编辑入口。
两个原因:一是可审计 —— “这台机器上跑的是什么品牌”可以从镜像里读出来并逐字节复核,
而不是一条可以被改掉的数据库行;二是不可被误改 —— 渠道字段一旦能被运行时改写,
“把定制部署洗成官方部署”就只需要一次误操作。
为什么渠道之间互不升级(启动期拒绝,而不是回落)。 服务端启动时校验”镜像内渠道 ==
进程渠道”,检查更新时校验”清单 channel_id == 本部署渠道”。任一处不一致都拒绝:
PICOAI_CHANNEL 非法 → 拒绝启动;镜像内容与进程渠道冲突 → 拒绝启动;清单渠道不匹配 →
判为”检查更新不可用”(而不是”没有新版本”)。
为什么不回落成 official:回落会让定制部署接受官方清单,升级后品牌与渠道配置一起丢失,
且现场没有任何报错。
为什么同一个镜像归档在所有渠道里 tag 相同(而内容不同)。 渠道差异在镜像内容里
(/opt/picoaide/channel/ 与烘焙的渠道标记),不在 tag 里。这带来一个必须记住的操作约束:
同一台宿主机上第二个渠道栈 docker load 会覆盖同名 tag,因此每个归档额外带一个渠道专属 tag,
多栈宿主机的 .env 必须指向它(见升级、备份与回滚)。
渠道内容的落点与字段
Section titled “渠道内容的落点与字段”渠道不是运行时上传的配置,而是构建期注入并固化在镜像里:
镜像构建 --build-arg CHANNEL=<渠道 id> ├─ ENV PICOAI_CHANNEL=<渠道 id> ← 运行期渠道身份 ├─ /opt/picoaide/CHANNEL ← 权威声明文件(部署侧不配也生效) └─ /opt/picoaide/channel/ ← 渠道内容(名称 / 文案 / 标识 / 主题色)渠道内容包含:
| 类别 | 内容 |
|---|---|
| 标识 | 显示名、短名(侧边栏)、窗口标题、标语 |
| 文案 | 登录页与客户端欢迎语、门户欢迎语 |
| 素材 | 亮 / 暗两套 logo、favicon、主题色 |
| 客户端 | 数据根目录、内置服务端地址、安装包名(slug)、应用 ID、深链 scheme、应用源 scheme |
| 功能模块 | 技能 / WASM 应用 / 连接器各自的开关(features,缺省全开;见下) |
| 可选 | 语音模型是否随包分发(desktop.speech_bundle_model,缺省开启;开启时安装包增大约 +140~250 MiB)、是否允许使用系统代理(desktop.allow_system_proxy) |
这些内容同时作用于客户端登录页、客户端界面、管理后台侧栏与门户页 —— 一处配置,全产品一致。 服务端不可达或服务端版本较旧时,客户端使用随包副本展示品牌,不会回落到厂商标识。
desktop.app_origin_scheme(WASM 应用的 origin scheme,<scheme>://<app_id>)是全部渠道必填:
缺字段 / 非法 / 与深链 scheme 同值 / 跨渠道重复 ⇒ 构建期中止,该渠道本次没有产物(不静默回落)。
品牌渠道的 logo 与 app icon 同样是必需素材:缺了会被打包期检查拦下,而不是产出”带厂商图标”的包。
渠道交付哪些平台(desktop.platforms)
Section titled “渠道交付哪些平台(desktop.platforms)”渠道包默认交付三端(Windows / macOS / Linux)。如果这个渠道的员工只用其中一部分, 在渠道配置里显式声明交付面即可:
"desktop": { "platforms": ["win", "mac"] }- 取值只有三个:
win/mac/linux;不写这个键 = 三端全交付(既有渠道行为不变); - 声明之后四处一起收窄:平台 job 的打包跳过未声明平台 → R2 私密中转只搬声明的平台 →
镜像装配只装声明的平台(声明了却没有产物 ⇒ 构建期红,不静默少一个入口)→
门户下载区与
GET /api/client/v2/updates/manifest都不再出现未声明的平台; - 未声明的平台在门户上是整块不出现(不是”该平台暂无可用安装包”的灰卡片 —— 那会让人 以为是我们漏打包);
- 少带一个平台直接省下一份安装包(Linux 约 350 MiB/版本,macOS 约 380 MiB,Windows 约 430 MiB), 而每个渠道在更新服务器上要留”最新 + 上一个”两版,所以省的是两倍。
官方渠道是新增渠道的模板:CI 取渠道包时会校验官方渠道的
channel.json逐键显式 (键表见packages/host/desktop/scripts/channel-constants.ts的CHANNEL_TEMPLATE_KEYS), 缺键即失败并点名键名。所以”新增一个渠道”= 拷贝channels/official/→ 改身份字段 → 裁剪desktop.platforms与features,不需要翻源码才知道有哪些键。三个desktop.speech_*键是覆盖型(不写 = 用随包模型),刻意不进模板 —— 写进模板等于把路径钉死。
渠道交付哪些功能模块(features)
Section titled “渠道交付哪些功能模块(features)”渠道包默认交付全部功能模块(技能 / WASM 应用 / 连接器)。如果这个渠道不需要其中某个模块, 在渠道配置里显式关掉即可:
"features": { "skills": true, "apps": true, "connectors": false }- 键只有
skills/apps/connectors,取值必须是布尔;不写这个键 = 全部开启 (基础功能恒开,既有渠道行为不变); - 缺省是逐键的:只写
{"connectors": false}时其余模块保持开启,不需要把开关抄一遍; - 写错(未知模块名、取值不是布尔 —— 字符串
"false"在客户端读起来是真值、features不是对象)⇒ 构建期中止,该渠道本次没有产物:报错点名字段与合法模块名, 但不回显取值(渠道包里的任意字符串会进公开日志); - 官方渠道的模板里逐模块列出这三个键,所以”新增一个渠道”时一眼就能看到有哪几个模块可关。
当前这条链路的边界:品牌渠道的声明会随包写进渠道配置的
features(规范化后的开关, 三个键齐全)与features_variant(稳定变体标识,如skills,apps、connectors、none); 官方渠道没有这份随包配置(它用的是内置身份),服务端镜像里的渠道内容也仍是渠道仓原文。 变体标识就是后续交给服务端镜像的编译标签(Go-tags)、客户端组装与 管理后台构建参数(--build-arg FEATURES=<变体标识>)的同一个串 —— 三处必须同源 (镜像侧的两处由构建期从同一个真源解析,不假设产物里已经有现成值), 否则会出现”镜像里有、客户端里没有”这种没有任何红灯的半通状态。 在消费端接线之前,改features不会改变产物内容。完整契约(含”怎么证明某个模块 真的没被打进产物”)见仓库内docs/decisions/2026-10-13-channel-feature-modules.md。
语音识别模型怎么进到客户网(desktop.speech_*)
Section titled “语音识别模型怎么进到客户网(desktop.speech_*)”语音输入在本机识别,默认模型权重随客户端分发(desktop.speech_bundle_model 缺省 true):
构建时把权重打进产物,装配期指向上游官方的配置项 —— 零网络、零下载。
只有渠道显式关掉随包(写 false)才回到”首次使用下载”,而那条下载走直连
(客户端默认禁止使用任何代理),所以”只有认证代理能出公网”的网络里,关掉随包的渠道会一直停在准备失败。
| 字段 | 作用 | 取值 |
|---|---|---|
desktop.speech_bundle_model | 把权重随客户端分发(缺省开启:装上就能用、零网络、零下载) | 布尔;缺省 true。写 false 关闭。开启时安装包增大约 +140~250 MiB(权重约 230 MiB,增幅视平台打包格式的压缩率而定) |
desktop.speech_model_dir | 预置的模型目录(零下载) | 绝对路径,或 {default, darwin, linux, win32} 平台映射 |
desktop.speech_vad_path | 预置的 Silero VAD 文件(只预置模型目录时仍会下载它) | 同上 |
desktop.speech_model_origin | 内网镜像源(HuggingFace 兼容,模型路径与文件名不变) | 只写 http(s)://host[:port] 形状(明文 http:// 也接受,与上游配置 schema 逐字一致),不能带路径 |
几条必须知道的语义:
- 随包载荷优先于本机预置/镜像配置:两者都配时以随包载荷为准(本机装的东西比网络配置确定);
- 预置路径不做哈希校验(上游只查文件是否存在),版本匹配由部署方负责;配错的表现是准备阶段
明确报
Speech model verification failed,不会静默降级成”看起来正常但识别不出”; - 配了
modelOrigin就不再回落公网源:镜像不可用 = 下载失败(显式源 = 显式意图); - 形状不符(相对路径、非法平台键、带路径的镜像源、字符串
"true")时客户端回落下载, 但构建期 CI 会中止该渠道 —— 别把配错发到客户机器上。
发布矩阵(哪些 tag 产出哪些渠道)
Section titled “发布矩阵(哪些 tag 产出哪些渠道)”| 触发 | 构建的渠道 | 说明 |
|---|---|---|
预发 tag(含连字符,如 vX.Y.Z-beta.N) | 只构建 beta | 品牌渠道的客户端不会在预发 tag 产出 —— 要出品牌渠道包只能等正式 tag(工作流只有 pull_request 与 push 两个触发,没有 workflow_dispatch 手工入口) |
正式 tag(纯 vX.Y.Z) | 全部渠道(official + beta + 各品牌渠道) | 品牌渠道的镜像与安装包只出现在该渠道自己的更新目录 |
| 非 tag(PR / 分支推送) | 只构建 official | 用于门禁与冒烟,不作为交付物 |
⇒ 给品牌渠道加/改字段时:必须在渠道仓改好并 push,然后等正式 tag;否则客户拿到的
仍是旧行为的包。判据:更新服务器上该渠道 latest.json 的 server.version 是否已经指向新版本。
部署侧必须自洽的值
Section titled “部署侧必须自洽的值”| 值 | 由谁决定 | 说明 |
|---|---|---|
| 本部署的渠道 | 镜像自带(/opt/picoaide/CHANNEL) | .env 的 PICOAI_CHANNEL 留空即可;填了必须与镜像一致 |
| 更新清单地址 | PICOAI_UPDATE_ENDPOINT,留空 = 本渠道默认目录 | 留空不等于关闭;关闭请显式写 off |
清单里的 channel_id | 更新服务器上对应目录的 latest.json | 必须与本部署渠道相同 |
| 镜像 tag(多栈宿主机) | 每个归档里的 <渠道 id>-<版本> | 让每栈的 SERVER_IMAGE 指向自己的渠道 tag |
服务端在启动与检查更新时强制校验:
PICOAI_CHANNEL非法 → 拒绝启动(不会回落到official);- 镜像内渠道与进程渠道不一致(典型成因:
.env/ compose 覆盖了镜像声明)→ 拒绝启动; - 清单
channel_id与本部署不一致 → 判为”检查更新不可用”,而不是”没有新版本”, 绝不静默跨渠道升级。
从旧版本升级过来的部署注意:compose 的默认值不再写死渠道。如果
.env里手工留着PICOAI_CHANNEL=official,定制渠道部署会被判为不一致 —— 删掉这一行即可。
cd /opt/picoaideDOMAIN=$(grep '^DOMAIN=' .env | cut -d= -f2-)
# 1) 镜像里烘焙的渠道(权威声明)docker exec picoaide-server cat /opt/picoaide/CHANNEL
# 2) 服务端对外自报的渠道curl -sk --resolve "$DOMAIN:443:127.0.0.1" "https://$DOMAIN/api/client/v2/channel"
# 3) 启动日志里解析出的更新端点docker compose logs --tail=200 server | grep -i 'channel resolved'
# 4) 清单里的渠道(也是客户端会校验的那一个)curl -sk --resolve "$DOMAIN:443:127.0.0.1" "https://$DOMAIN/api/client/v2/updates/manifest"| # | 判据 | 通过的样子 | 失败的样子 |
|---|---|---|---|
| 1 | 镜像渠道可读 | cat /opt/picoaide/CHANNEL 输出本栈渠道 id | 文件缺失 ⇒ 镜像不是正式发布包 |
| 2 | 对外渠道一致 | /api/client/v2/channel 的 channel_id == 判据 1 | 两者不同 ⇒ 渠道被覆盖,容器可能已经拒绝启动 |
| 3 | 更新端点正确 | 日志里 channel resolved: … (update endpoint …) 指向本渠道目录 | 端点为空 ⇒ 更新检查被显式设成 off;指向别渠道 ⇒ 升级会被拒(不是静默跨渠道) |
| 4 | 清单渠道一致 | channel_id == 判据 1 | 不一致 ⇒ 服务端把检查更新判为不可用 |
边界与失败行为
Section titled “边界与失败行为”| 现象 | 判据(怎么确认) | 恢复动作 |
|---|---|---|
| 一直不显示”发现新版本” | docker compose logs server 打印 manifest channel "…" != this server's channel "…" | 渠道三值不自洽:最常见是改了 PICOAI_CHANNEL 却没改端点(或反之);也可能是端点被设成 off。服务端有 6 小时缓存,刚发版时等待属正常延迟 |
| 容器启动即退出,打印”渠道配置非法” | 日志点名 PICOAI_CHANNEL | 该值(或镜像内渠道标记)不是合法渠道 id:修拼写或删掉覆盖 —— 不要指望回落 |
| 容器启动即退出,打印”渠道不一致” | 日志点名镜像内渠道与进程渠道 | .env / compose 覆盖的渠道与镜像内容不同:删掉覆盖或改成与镜像一致 |
| 升级后品牌变了 / 门户显示厂商品牌 | cat /opt/picoaide/CHANNEL ≠ /api/client/v2/channel 的 channel_id | 用错渠道镜像:改 .env 指回本渠道的渠道 tag 后重建;并按升级、备份与回滚核对 image id,不要继续用它升级 |
| 客户拿到的是旧行为的包 | 该渠道 latest.json 的 server.version 未变 | 品牌渠道只在正式 tag 产出:等正式 tag,或确认渠道仓的改动已经 push(CI 从渠道仓拉取) |
| 客户端看到的品牌与服务端不同 | 客户端登录页/侧边栏的品牌 | 客户端品牌来自随包副本(服务端不可达或较旧时用它):重装随服务端分发的客户端即可(见客户端分发与升级) |
| 同机两个渠道栈互相顶掉品牌 | 一栈的容器 image id 指向别渠道镜像 | 渠道 tag 覆盖问题:按升级、备份与回滚的多栈章节重做(.env 指向 <渠道 id>-<版本>) |
客户端数据隔离(渠道可共存)
Section titled “客户端数据隔离(渠道可共存)”渠道化客户端的配置、会话、连接器凭据与浏览器数据存放在各自的数据目录中, 不同渠道的客户端可以装在同一台机器上而互不影响:
- 官方渠道:
~/.picoaide-harness; - 预发布渠道(
beta):与官方正式版共用同一个数据目录 —— 预发版是正式版的前置验证, 登录态、设置与会话必须延续(这是有意的:独立目录会让预发版用户升级后看不到既有会话); - 品牌渠道:按渠道派生的独立目录(不得与官方目录共用:两代客户端的会话格式世代可能不同, 共用会静默分叉数据);
- 应用的用户数据目录与 Windows 应用标识同样按渠道区分,避免两个客户端互相顶掉单实例锁。
浏览器 SSO 回调使用渠道自己的深链 scheme,登录回调不会串到另一个渠道的客户端。
客户端升级与渠道的关系
Section titled “客户端升级与渠道的关系”员工客户端不需要知道自己属于哪个渠道:它只问”我登录的那台服务端”,而服务端的渠道由它自己的 镜像在结构上决定。因此不存在”客户端渠道与服务端渠道不一致”的状态,也不存在跨渠道被升级洗牌的可能。
企业定制的安装包由服务端门户直接下发(见客户端分发与升级), 员工机器全程不需要外网。