Skip to content

官网插件工坊(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 条:

json
"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 与信封 ​

审核通过后由服务端生成并签名(作者不需要做任何事):

jsonc
// 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
}

签名信封(所有签名响应共用):

jsonc
{ "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 官方插件(现在就能用) ​

bash
# 在 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(插件工坊 · 作者中心),登录官网账号后:

  1. 选插件包:直接选 .tbmod/.zip,或者选整个插件文件夹(根目录要有 mod.json,服务端会帮你打包); 上传时服务端就会重算 sha256/文件清单并做全部校验(不安全/不合法当场退回)。
  2. 填展示信息:标题、简介、详细描述、英文槽、更新说明、可见性(public / unlisted / private)、许可证。
  3. 提交审核:
    • 纯数据类(词库/语言包/无 QML 的主题)默认可自动上架;
    • 含代码的一律进待审核队列,管理员在 /admin/workshop 里批准(签名上架)或拒绝(原因会显示在作者中心)。
  4. 管理已有插件:同一物品上传新版本(版本号必须递增)、改标题/描述/可见性、下架(软删,已装用户不受影响)。

配额:同时待审 ≤ 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_idid 的机构段用了保留名(transbox/steam/community…)
409 conflict: version must be greater版本号没递增
413 too_large超过分级体积上限(见 §2),大模型请用 model_artifacts
客户端拒绝安装key_id 不在内置白名单 / 签名不符 / 字节数或 sha256 不一致 / 清单外文件

TransBox - 赋能全人类无界限跨语言交流