官网插件工坊(Plugin Hub)开发者与运维手册
面向两拨人:写插件的人(包要长什么样、怎么发)与运维的人(怎么上架、怎么下架、密钥在哪)。 客户端行为与 API 契约以
design/plugin-marketplace.md为准,本文是给作者看的落地版。 Steam 版仍走 Steam 创意工坊(见 创意工坊与自发行发布指南),两条路互不影响。
1. 两条分发路径
| Steam 创意工坊 | 官网插件工坊(Plugin Hub) | |
|---|---|---|
| 谁在用 | Steam 版客户端 | 自发行版(TRANSBOX_DISTRIBUTION=selfpub) |
| 分发 | Valve CDN | 本站 /api/workshop/* + R2 私有桶 |
| 登录 | Steam 账号 | 官网账号(客户端「个人中心」设备码登录) |
| 包格式 | .zip(根目录 mod.json) | .tbmod(同一个 zip,多一道签名) |
| 信任 | Steam 平台审核 | Ed25519 签名 + 客户端内置公钥验签 |
同一份插件可以两边都发;客户端装载优先级固定为 local > steam > web(同 id 只装一份)。
2. 包格式:.tbmod
就是一个 zip,根目录直接放 mod.json(不允许外包一层文件夹,也不认 manifest.json)。
my_mod.tbmod
├── mod.json 必填:id / name / version / type / **target_app_version**;可选:min_app_version …
├── terms.json 词库(type=glossary)
├── provider.py 引擎实现(type=mt/asr/tts…)
└── locales/zh-CN.json 插件自带词条(推荐)必须遵守(违反会被自动拒绝,客户端也会拒绝安装):
id至少两段,字符限[A-Za-z0-9_-](官方保留web.author.name之外的机构段,如transbox、steam);version严格 semver(1.2.3);新版本必须大于已发布的最高版本。软件内「插件工坊 → 发布」与官网「作者中心」(/workshop-author)都有必填的「版本号」字段(默认取 mod.json):直接改成更高的版本号就能提交,打包时用这个值写入包内 mod.json(只改version一个键);软件内发布成功后还会把值同步回本地mod.json文件(保留作者格式);target_app_version必填(写"0.2"):客户端靠它区分「适配 0.2 的新插件」与「0.1 老插件」。 不写的包安装时直接被拒(InstallError),本地放进extensions/也不会被识别—— 目的是防止旧版插件被自动装进新版客户端后原地炸掉。详见 兼容与版本门禁;min_app_version可选,缺省(或不写)=1.0.0= 「不限制」(与客户端、CLI 打包脚本同口径);一旦显式声明就必须是x.y.z,否则发布被拒;客户端「插件工坊 → 发布」上传时会自动补上这个缺省值,作者不写也能发;- 路径只能是 POSIX 相对路径:禁止
..、绝对路径、符号链接、重复路径; - 禁止出现
.workshop/(客户端保留放收据)、__pycache__/、*.pyc; - 不允许嵌套压缩包(
.zip/.7z/.rar/.tbmod); entrypoint/icon/preview_image指向的文件必须真的在包里。
体积与数量上限(服务端在上传时校验,并把结果写进签名清单):
| 项 | data 级(纯数据) | script 级(含代码) |
|---|---|---|
| 包大小 | ≤ 5 MB | ≤ 50 MB(格式上限) |
| 解压后 | ≤ 50 MB(压缩比 ≤ 20) | ≤ 200 MB |
| 文件数 | ≤ 500 | ≤ 2000 |
| 单文件 | ≤ 5 MB | ≤ 50 MB |
⚠️ 通过软件内或网页上传的包,硬上限是 20 MB(比上面的格式上限更严):工坊跑在 Cloudflare Pages Function 上,45MB 起的包会在上传中途被平台掐断,客户端只会看到写失败、看不到任何有用提示。 需要更大的包请联系我们走单独分发(或把大文件改成
model_artifacts)。
大模型/大体积运行库不要打进包:用 mod.json 的 model_artifacts 声明(HuggingFace / ModelScope,宿主会自己下载到数据目录)——DLL / .exe 这类原生运行库同样适用。
需要别的插件先装好:用 mod.json 的 requires_plugins 声明前置插件(对齐 Steam 工坊 Required Items),最多 8 条:
"requires_plugins": [
{ "id": "workshop.amd_llama", "min_version": "1.0.0", "reason": "提供 llama.cpp 运行时" }
]客户端行为:前置未安装 / 未启用 / 版本过低 → 禁止启用该插件并在卡片上提示「需要前置插件:…」+ 一键安装(含代码的插件不会静默安装,要用户点一下)。 取前置插件的文件请用 host.plugin_dir(id)(不要硬编码目录名——工坊安装时目录名 = 插件 id)。详见客户端 docs/WORKSHOP_GUIDE.md §3.11。
3. 分级:data 与 script
服务端按包内容推导,规则从严格:
- 含
.py/.qml/.js/.dll/.exe/...、声明了permissions、声明了ui_extensions、或type属于引擎类 →script; - 其余(纯
terms.json词库、语言包、无 QML 的主题)→data。
客户端的处理:
| 安装后 | 更新 | 新设备同步 | |
|---|---|---|---|
data | 按用户习惯启用 | 重启后生效 | 正常安装 |
script | 默认不启用,首次启用前弹窗确认(展示作者/权限/来源) | 重启后生效(权限增加要重新确认) | 只安装不启用 |
代码类插件与主程序同权限运行、没有沙箱。请在
mod.json的permissions里如实声明用到的能力(net/clipboard/hotkey/window/audio/fs_read/fs_write/process),审核与用户提示都看它。
4. 签名:workshop.json 与信封
审核通过后由服务端生成并签名(作者不需要做任何事):
// workshop.json(payload 原文,客户端直接对这段字节验签,不能重新序列化)
{
"schema": 1,
"id": "web.alice.my_glossary",
"version": "1.2.0",
"sha256": "…", // 整个 .tbmod 的 sha256
"size": 12345,
"unpacked_size": 40960,
"max_file_size": 20480,
"min_app_version": "1.0.0",
"permissions": ["net"],
"category_tier": "data",
"files": [{ "path": "mod.json", "sha256": "…" }], // 逐文件哈希,按 path 排序
"signed_by": "wk-2026-1", // 必须等于信封 key_id
"created_at": 1790999253
}签名信封(所有签名响应共用):
{ "payload": "<上面的原始文本>", "signature": "<Ed25519 标准 base64>", "key_id": "wk-2026-1" }客户端验签规则(缺一不可):key_id 必须在编译进客户端的白名单里 → 验 signature(对 payload 原始 UTF-8 字节) → signed_by == key_id → 字节数与 sha256 一致 → 解包后逐文件核对 + 清单外多一个文件即作废。
密钥管理:
| 密钥 | 在哪 | 用途 |
|---|---|---|
WORKSHOP_PKG_SIGNING_KEY(私钥) | Cloudflare Pages Secret | 签插件包 |
WORKSHOP_PKG_SIGNING_KEY_ID(如 wk-2026-1) | Pages 变量 + 客户端 | 标识用哪把公钥 |
| 同名公钥 | 客户端 plugins/workshop/signing.py 内置 | 验签(客户端不信任网络下发的公钥) |
换密钥:新客户端先内置新公钥(旧公钥保留)→ 官网切 key_id → 旧密钥保留一段时间后标 retired。
5. 客户端怎么装
- 安装目录:
<安装目录>/data/workshop/<id>/(与随应用发布的extensions/分开,跨版本保留); - 每个插件目录下有一份客户端自己的收据:
.workshop/receipt.json+ 签名的workshop.json/.workshop/workshop.json.sig(离线复验用); - 安装是原子的(先解包到临时目录、校验通过再整体换目录),旧版本留在
.previous/; - 更新重启后生效(只在磁盘上换,不在内存里热替换已加载的旧版本);
- 自动同步(官网渠道):启动后 10 秒 + 每 24 小时一轮——拉吊销列表(ETag + 序号防回滚)、同步订阅(增量游标 + 墓碑)、检查更新。
6. 发布流程
6.1 官方插件(现在就能用)
# 在 trans-box-website 仓库里跑;私钥文件 = Cloudflare Secret 里那一把(PKCS#8 Base64)
node scripts/workshop-publish.mjs \
--dir ../trans-box/extensions/community_theme_glassmorphism \
--official --changelog "首次上架" \
--private-key-file <你的私钥备份路径>
# 批量:--all-official 扫描 ../trans-box/extensions 下所有非 example_* 的插件
# 只验证不上传:--dry脚本做的事:打 .tbmod(排除 __pycache__/.pyc/.workshop)→ 本地先用服务端同一份代码校验 (server/workshop-zip.js)→ 走 wrangler 直连线上 R2 + D1 上架。代理变量在子进程里被显式清掉(桶与库都在 APAC)。
6.2 社区作者上传(已可用)
浏览器打开 /workshop-author(插件工坊 · 作者中心),登录官网账号后:
- 选插件包:直接选
.tbmod/.zip,或者选整个插件文件夹(根目录要有mod.json,服务端会帮你打包); 上传时服务端就会重算 sha256/文件清单并做全部校验(不安全/不合法当场退回)。 - 填展示信息:标题、简介、详细描述、英文槽、更新说明、可见性(public / unlisted / private)、许可证。
- 提交审核:
- 纯数据类(词库/语言包/无 QML 的主题)默认可自动上架;
- 含代码的一律进待审核队列,管理员在
/admin/workshop里批准(签名上架)或拒绝(原因会显示在作者中心)。
- 管理已有插件:同一物品上传新版本(版本号必须递增)、改标题/描述/可见性、下架(软删,已装用户不受影响)。
配额:同时待审 ≤ 5 个版本,每人每天 ≤ 10 个版本(防刷)。
客户端里也能传:官网分发版「扩展工坊 → 插件工坊 → 发布」选插件文件夹即可(走工坊令牌的 workshop:publish scope,服务端校验与网页完全一致);管理已有插件(改信息 / 提新版本 / 下架)在网页作者中心。 用 API(而不是网页)上传时,需要带 workshop:publish scope 的工坊令牌;官网页面走站点会话 Cookie,不需要该 scope。
自测提示:服务端变量
WORKSHOP_REVIEW_MODE=auto可让所有上传直接签名上架(仅用于自测,别在正式运营时打开)。
7. API 速查
前缀 /api/workshop;时间一律 Unix 秒;页码从 1 开始;size 为字节。
| 接口 | 鉴权 | 说明 |
|---|---|---|
GET /items?page&page_size&q&tag&type&sort&lang&min_app&official | 匿名 | 目录(sort: updated/recent/popular/top) |
GET /items/{id} | 匿名 | 详情(含 versions、permissions、多语言槽) |
GET /items/{id}/download?app_version= | 匿名 | 返回 {url, expires_at, size, sha256, signed};409 incompatible / 410 revoked |
GET /files/{token} | 令牌 | 流式下发 .tbmod,支持 Range(10 分钟有效) |
POST /updates/check | 匿名可调 | 批量检查更新/吊销,必带 app_version |
GET /revocations | 匿名 | 签名吊销列表,支持 ETag(304) |
GET /keys | 匿名 | 公钥信息(客户端不据此新增信任) |
GET /me/subscriptions?since= | 工坊令牌 / 站点 Cookie | 我的订阅(增量 + 墓碑 + 游标) |
PUT / DELETE /me/subscriptions/{id} | 同上(workshop:subscribe) | 订阅 / 退订(幂等) |
GET /me/published | 同上 | 我发布的物品(含每个版本的审核状态与拒绝原因) |
POST /uploads | 作者(Cookie 或 workshop:publish) | 上传包(archive 单包,或 files[]+paths[] 整目录),返回 upload_id 与校验摘要 |
POST /items / POST /items/{id}/versions | 同上 | 用 upload_id 建物品 / 提交新版本 |
PATCH / DELETE /items/{id} | 同上 | 改元数据/可见性、下架 |
POST /items/{id}/report | 登录用户 | 举报(malware/copyright/illegal/spam/other) |
POST /admin/publish 等 | Cloudflare Access | 管理端:上架、改可见性、队列、批准/拒绝、吊销 |
客户端请求头:Authorization: Bearer <TBW1 工坊令牌>、User-Agent: TransBox/<版本> (+站点)、 X-TransBox-App-Version、X-TransBox-Distribution。必须带规范 UA——Cloudflare 对默认的 Python-urllib 直接 403(error 1010)。
8. 常见拒绝原因
| 现象 | 原因 |
|---|---|
bad_package: mod.json must be at package root | 包了一层文件夹,或用了 manifest.json |
bad_package: forbidden path | 打进了 .workshop/、__pycache__/、.pyc |
reserved_id | id 的机构段用了保留名(transbox/steam/community…) |
409 conflict: version must be greater | 版本号没递增 |
413 too_large | 超过分级体积上限(见 §2),大模型请用 model_artifacts |
| 客户端拒绝安装 | key_id 不在内置白名单 / 签名不符 / 字节数或 sha256 不一致 / 清单外文件 |
