跳转到内容

渠道与白标

同一套代码可以交付成多个渠道(channel)。渠道决定这一份交付物”叫什么、长什么样、 从哪里升级”,且渠道之间互不升级 —— 这是正确性要求,不是配置项。

判断”我这一栈属于哪个渠道、升级时会不会被换成别的渠道”,以及渠道相关的失败长什么样。 本页覆盖:三类渠道 → 渠道内容的落点与字段 → 发布矩阵(哪些 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 必须指向它(见升级、备份与回滚)。

渠道不是运行时上传的配置,而是构建期注入并固化在镜像里:

镜像构建 --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 是否已经指向新版本。

值由谁决定说明
本部署的渠道镜像自带(/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,定制渠道部署会被判为不一致 —— 删掉这一行即可。

Terminal window
cd /opt/picoaide
DOMAIN=$(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不一致 ⇒ 服务端把检查更新判为不可用
现象判据(怎么确认)恢复动作
一直不显示”发现新版本”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,登录回调不会串到另一个渠道的客户端。

员工客户端不需要知道自己属于哪个渠道:它只问”我登录的那台服务端”,而服务端的渠道由它自己的 镜像在结构上决定。因此不存在”客户端渠道与服务端渠道不一致”的状态,也不存在跨渠道被升级洗牌的可能。

企业定制的安装包由服务端门户直接下发(见客户端分发与升级), 员工机器全程不需要外网。