# 打包、签名与发布 这篇讲怎么把一个 `.js` 插件变成用户能装的东西:清单怎么写、签名怎么算、怎么提到官方源、怎么自建源、怎么发新版本。 先读 [写一个云取插件](PLUGIN-DEV.md) 把插件本身跑通;这篇只关心"发布"这一半。 --- ## 一个插件目录长什么样 ``` plugins/<插件 id>/ ├─ <插件 id>.js 插件本体(签名的就是它的原始字节) ├─ turbodl-plugin.json 清单(字段与 TurboDL 的 schema 一致) ├─ market.json 只放清单里没有的市场字段(简介、标签、最低宿主版本) └─ README.md 可选:写给人看的说明 ``` **目录名必须等于清单里的 `id`**。工具和 App 都靠它定位,不一致时 `index` 会直接报错。 参考实现:[`plugins/parser.example-cloud/`](https://github.com/jiayuxuan123/YunGet-Plugins/tree/main/plugins/parser.example-cloud)。 --- ## `turbodl-plugin.json` 字段定义来自 TurboDL 的 [`turbodl-plugin.schema.json`](https://github.com/jiayuxuan123/TurboDL/blob/main/docs/plugins/turbodl-plugin.schema.json)。这个 schema 是 `additionalProperties: false` —— **多写一个字段就会被拒**,别自己造字段。 ```json { "manifestVersion": "1.0", "id": "parser.mycloud", "name": "MyCloud Parser", "description": "把 mycloud:// 分享链接解析成可下载的 https 请求。", "version": "1.0.0", "author": "你的名字", "homepage": "https://github.com/you/mycloud-plugin", "license": "MIT", "category": "turbodl-parser", "capabilities": ["dev.turbodl.cap.parser.mycloud"], "protocols": ["mycloud"], "turbodl": { "apiMajor": 1, "requiredApiVersion": "1.0.0" }, "entry": { "language": "js", "pluginClass": "parser.mycloud.js" }, "extensionPoints": ["turbo.linkParser"], "services": [] } ``` ### 必填与可选 | 字段 | 必填 | 说明 | |---|---|---| | `manifestVersion` | ✅ | 只能是 `"1.0"` | | `id` | ✅ | `^[a-z0-9]+(\.[a-z0-9][a-z0-9-]*)+$`,全小写,长度 **3..64** | | `name` | ✅ | 非空字符串 | | `version` | ✅ | semver,`MAJOR.MINOR.PATCH[-prerelease][+build]` | | `category` | ✅ | 见下面的取值表 | | `turbodl` | ✅ | 对象,含 `apiMajor`(≥ 1 的整数)与 `requiredApiVersion`(`X.Y.Z`) | | `entry` | ✅ | 对象,含 `language`(`kotlin` / `js`)与 `pluginClass` | | `description` / `author` / `homepage` / `license` | | 字符串 | | `capabilities` | | 命名空间能力 id 数组,**不能重复** | | `protocols` | | 协议数组,**不能重复** | | `artifact` | | 对象:`type` 是 `maven`(需 `coordinates`)或 `jar`(需 `url`) | | `extensionPoints` / `services` | | 字符串数组,不能重复 | ### `category` 的六个取值 | 值 | 什么时候用 | |---|---| | `turbodl-protocol` | 插件声明它处理哪些协议/方案(`protocols` 字段),**也可以**实现一个下载后端 | | `turbodl-backend` | 插件的身份就是实现一个下载后端 | | `turbodl-parser` | 链接解析器 | | `turbodl-hook` | 任务钩子 | | `turbodl-adapter` | 适配器 | | `turbodl-loader` | 加载别种语言写的插件(`turbo-plugin-js` 自己就是这个) | `turbodl-protocol` 是给"我要声明我认哪些协议"的插件准备的落点 —— 单纯写解析器的插件用 `turbodl-parser` 就行。 ### `capabilities` 必须是命名空间形式 ``` dev.turbodl.cap.<名字> ✅ dev.turbodl.cap.parser.mycloud ✅ turbodl-parser ❌ 裸串会被拒 hls ❌ ``` 正则:`^[a-z0-9]+(\.[a-z0-9-]+)+\.cap\.[a-z0-9][a-z0-9.-]*$`。命名空间的作用是防止两个厂商同名能力撞车。项目自己的命名空间是 `dev.turbodl.cap.*`(比如 `dev.turbodl.cap.hls`)。 **能力是一个市场/诊断标签:它不授予任何权限、不改变任何行为。** 想让插件能上网,要写的是脚本里的 `plugin.requires({permissions: ['http']})`,跟 `capabilities` 是两回事。别把这两个概念搞混 —— 也别指望在 `capabilities` 里写 `http` 就能拿到 `http` 权限。 ### `protocols`:一个插件可以声明多个 ```json "protocols": ["magnet", "bt", "ed2k"] ``` - 格式:`^[a-z][a-z0-9+.-]{0,31}$`(小写、像 URL scheme 的 token)。 - **一个插件可以声明多个协议** —— 生态模型是「一个插件覆盖多个协议」,不是「一个协议一个插件」。宿主按这些声明建立「scheme → 插件」的索引,用于发现、市场筛选、诊断和冲突报告。 - 声明**不授予权限,也不自己路由下载** —— 真正决定谁处理一个请求的仍然是后端/解析器。 - 注意内容类型不是 URL scheme:一个 `.m3u8` 的 URL,scheme 是 `http(s)`。所以这里写的是"市场应该用它来认识这个插件"的名字。 ### `entry` 对 JS 插件怎么写 ```json "entry": { "language": "js", "pluginClass": "parser.mycloud.js" } ``` `language` 是 `js` 时,`pluginClass` 就是**插件目录内的脚本文件名**(工具允许写成 `main.js` 或 `./main.js`,但不接受绝对路径或含 `..` 的路径,且必须以 `.js` 结尾)。 --- ## `market.json` 清单里没有、但市场需要的字段。**只放这些键**,多一个都会被拒: | 键 | 必填 | 说明 | |---|---|---| | `minHostVersion` | ✅ | 最低宿主版本(semver)。App 低于它就不给装 —— `index` 缺这个字段会报错 | | `summary` | | 一句话简介(列表页显示) | | `description` | | 较长描述(详情页显示,可含 Markdown) | | `tags` | | 字符串数组 | | `changelog` | | 这次发布的说明 | | `releasedAt` | | UTC ISO-8601,形如 `2026-10-09T00:00:00Z`。**固定它可以让索引可复现** | | `permissions` | | 冗余字段:与脚本 `plugin.requires` 不一致时工具会警告,并以**脚本**为准 | `permissions` 之所以在两边都有:脚本是权威(它受签名保护),索引里的这份是给"不想下载脚本就能看到权限清单"的场景用的。 ### 条目级的 `supportedMimeTypes` 与 `dependencies` 这两个字段属于设计文档 §12 要求 Manifest 包含的项,索引里现在也认(P17)。它们写在**插件条目**上(与 `capabilities` 同级),不是每版本: | 键 | 类型 | 说明 | |---|---|---| | `supportedMimeTypes` | 字符串数组 | 这个插件能处理的 MIME 类型(如 `video/mp4`)。空表示未声明 —— 不限制,也不承诺 | | `dependencies` | 字符串数组 | 依赖的其他插件(`id` 或 `id@版本`) | > **`dependencies` 目前只声明、不解析。** 应用会把它展示出来(插件页会标注"不会自动安装依赖"),但**不会**替你装依赖、也不做版本冲突检查。真做解析需要版本约束求解、循环检测、缺失时的降级路径 —— 在还没有真实依赖关系之前实现它,只会做出一套没人验证过的规则。首次出现真实依赖需求时再实现,届时这里会同步更新。 > 所以:**现在写依赖是给自己和读者看的信息**,别指望它自动生效。 ### 条目级的 `level` `plugins.json` 里**每个插件条目**还有一个字段: | 键 | 取值 | 说明 | |---|---|---| | `level` | `"js"` / `"native"` | 能力级别:L1(JS 沙箱,可热加载)/ L2(宿主进程内的原生代码,需重启才生效)。详见 [插件安全模型](PLUGIN-SECURITY.md#能力级别l1--l2) | **不用手写**:`index` 子命令会从 `turbodl-plugin.json` 的 `entry.language` 推导(`js` → `"js"`,`kotlin` → `"native"`)。之所以不给作者再加一个必填字段,是因为那个字段只会与 `entry` 打架,而两处说法不一致时没人知道该信哪个。 缺这个字段的存量条目一律按 L1 读(向后兼容);写了当前应用不认识的值的条目不会被安装,并会在市场页显示"未显示"的原因。 --- ## 签名 工具在官方源仓库里:[`YunGet-Plugins/tools/sign_plugin.py`](https://github.com/jiayuxuan123/YunGet-Plugins/blob/main/tools/sign_plugin.py)。Ed25519 **不在 Python 标准库里**,所以它优先用 `cryptography`(装了就用),没装就回退到内置的纯标准库 RFC 8032 实现 —— 两条路产出的密钥与签名逐字节一致,`selftest` 会交叉验证这一点。所以签名/验签**零依赖可用**。 ```bash python tools/sign_plugin.py selftest # 先自检:RFC 8032 官方向量、PEM 往返、与 cryptography 交叉验证、semver ``` ### 签名载荷:脚本的原始字节 > **签名对象 = 脚本文件的原始字节(raw bytes)。一个字节都不多、一个字节都不少。** ``` signature = Ed25519_sign(privateKey, bytes_of(