dbx-plugin-skill 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/README.md +238 -0
- package/bin/dbx-plugin-skill.mjs +330 -0
- package/lib/installer.mjs +256 -0
- package/package.json +49 -0
- package/skill/SKILL.md +268 -0
- package/skill/references/cli.md +225 -0
- package/skill/references/contributions.md +229 -0
- package/skill/references/debugging.md +210 -0
- package/skill/references/host-api.md +195 -0
- package/skill/references/manifest.md +229 -0
- package/skill/references/packaging.md +183 -0
- package/skill/references/publishing.md +374 -0
- package/skill/references/sidecar-protocol.md +242 -0
- package/skill/references/troubleshooting.md +171 -0
- package/skill/scripts/check-project.mjs +915 -0
- package/skill/scripts/dev-logs.mjs +191 -0
- package/skill/scripts/inspect-dbxp.mjs +400 -0
- package/skill/scripts/make-candidate.mjs +413 -0
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
# 发布与上架(dbx-store)参考
|
|
2
|
+
|
|
3
|
+
> 本文件的结论来自 `t8y2/dbx-store` 当前源码(`scripts/validate.mjs`、`scripts/sync-release-candidate.mjs`、`scripts/discover-plugin-releases.mjs`、`scripts/finalize-candidates.mjs`、`.github/workflows/*`)与 `t8y2/dbx/plugins/RELEASING.md`。**当文档与脚本冲突时,以脚本为准** —— 文末列出已确认的冲突点。
|
|
4
|
+
>
|
|
5
|
+
> 上游 `plugins/RELEASING.md` 仍描述「先开 Issue、再开 catalog PR」的两段式流程;**当前实际流程是「一个 PR」**,以 `dbx-store/CONTRIBUTING.md` 为准。
|
|
6
|
+
|
|
7
|
+
## 1. 四类产物,四个归属
|
|
8
|
+
|
|
9
|
+
| 产物 | 存放在哪 | 谁维护 |
|
|
10
|
+
| --- | --- | --- |
|
|
11
|
+
| 插件源码 | 你自己的仓库 | 你 |
|
|
12
|
+
| 未签名候选 `.dbxp` + `.artifact.json` + `release-candidates.json` | 你自己仓库的 GitHub Release / CDN / 对象存储 | 你的 CI(`dbx-plugin package`) |
|
|
13
|
+
| 最终签名 `.dbxp` + `.artifact.json` + `.signing-receipt.json` | DBX Store 控制的 R2(`https://dl.dbxio.com/plugins/...`) | DBX Store 受保护 Workflow |
|
|
14
|
+
| 目录元数据 `plugins/<id>.json`、`catalog/index.json` | `t8y2/dbx-store` Git 仓库 | 签名 Workflow 生成,维护者合并 |
|
|
15
|
+
|
|
16
|
+
**开发者永远不接触官方私钥。**
|
|
17
|
+
|
|
18
|
+
## 2. 提交位置速查
|
|
19
|
+
|
|
20
|
+
| 你要改的 | 提到哪 |
|
|
21
|
+
| --- | --- |
|
|
22
|
+
| 插件功能代码 | 你自己的仓库 |
|
|
23
|
+
| 上架 / 版本更新 / 商店文案 | `t8y2/dbx-store` → `main` |
|
|
24
|
+
| Host / SDK / CLI / Schema / 官方示例 / 插件文档 | `t8y2/dbx` → `main` |
|
|
25
|
+
| 第三方插件自身的 Bug | 那个插件的仓库 |
|
|
26
|
+
|
|
27
|
+
## 3. 插件仓库里的 `.dbx-store.json`(可选)
|
|
28
|
+
|
|
29
|
+
放在**插件源码仓库根目录**(路径由 `dbx-store/automation/plugin-sources.json` 的 `metadataPath` 配置,默认 `.dbx-store.json`)。它在**每个 Release tag** 上被读取,用于商店展示字段。
|
|
30
|
+
|
|
31
|
+
```json
|
|
32
|
+
{
|
|
33
|
+
"name": "Example Files",
|
|
34
|
+
"description": "Browse files from an example service.",
|
|
35
|
+
"icon": "assets/plugin.svg",
|
|
36
|
+
"tags": ["files", "storage"],
|
|
37
|
+
"permissions": ["host.workbench"],
|
|
38
|
+
"source": "https://github.com/example/dbx-plugin-files",
|
|
39
|
+
"homepage": "https://github.com/example/dbx-plugin-files",
|
|
40
|
+
"license": "Apache-2.0",
|
|
41
|
+
"releaseNotes": "Initial release.",
|
|
42
|
+
"localizations": {
|
|
43
|
+
"zh-CN": { "name": "示例文件", "description": "浏览示例服务的文件。" }
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
- **允许字段恰好是这 10 个**:`name`、`description`、`icon`、`tags`、`permissions`、`source`、`homepage`、`license`、`releaseNotes`、`localizations`。多任何键 → `Unsupported store metadata field '<k>'`。
|
|
49
|
+
- **首次上架必需**:`name` 和 `license`(否则 `New plugins require store metadata 'name'` / `'license'`)。
|
|
50
|
+
- `icon` 写相对路径时会被自动转成该 tag 下的 `raw.githubusercontent.com` HTTPS 地址;写绝对 URL 则原样使用。**必须是 `.svg` 或 `.png`**。
|
|
51
|
+
- `releaseNotes` 是**唯一**进入目录 `versions[].releaseNotes` 的来源 —— GitHub Release 的正文不会被读取。
|
|
52
|
+
- **身份与 target 不能从这里设置**:`id`、`publisher`、`version`、`targets`、`schemaVersion`、`verified`、`latestVersion`、`releasedAt` 及所有 artifact 字段都由别处决定。
|
|
53
|
+
- 版本更新时**不需要**改它;只在首次登记或确实要改展示信息时更新。但它每次同步都会被重新读取,所以改了就会带进下一次候选。
|
|
54
|
+
|
|
55
|
+
## 4. `release-candidates.json`(Release 资产)
|
|
56
|
+
|
|
57
|
+
由你的 Release 工作流产出,作为 **Release 资产**上传,文件名必须**恰好**是 `release-candidates.json`。
|
|
58
|
+
|
|
59
|
+
```json
|
|
60
|
+
{
|
|
61
|
+
"plugin": {
|
|
62
|
+
"id": "com.example.plugin",
|
|
63
|
+
"publisher": "example",
|
|
64
|
+
"version": "1.0.0",
|
|
65
|
+
"name": "Example Plugin",
|
|
66
|
+
"description": "Browse files from an example service."
|
|
67
|
+
},
|
|
68
|
+
"artifacts": [
|
|
69
|
+
{
|
|
70
|
+
"target": "darwin-arm64",
|
|
71
|
+
"url": "com.example.plugin-1.0.0-darwin-arm64.dbxp",
|
|
72
|
+
"sha256": "0f0e...(64 位十六进制,未签名候选包)",
|
|
73
|
+
"size": 12345678
|
|
74
|
+
}
|
|
75
|
+
]
|
|
76
|
+
}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
**最关键的一条:`artifacts[].url` 必须是纯文件名,不是 URL。**
|
|
80
|
+
|
|
81
|
+
同步脚本用 `path.posix.basename` + 正则 `^[A-Za-z0-9._-]+\.dbxp$` 校验,然后**自己拼出**
|
|
82
|
+
`https://github.com/<你的仓库>/releases/download/<tag>/<文件名>`。
|
|
83
|
+
|
|
84
|
+
| `url` 写法 | 结果 |
|
|
85
|
+
| --- | --- |
|
|
86
|
+
| `a.dbxp` | ✅ |
|
|
87
|
+
| `a.dbxp?x=1` | ✅(query 被忽略) |
|
|
88
|
+
| `sub/a.dbxp` | ❌ `Invalid candidate artifact` |
|
|
89
|
+
| `https://github.com/.../a.dbxp` | ❌ 完整 URL 被拒 |
|
|
90
|
+
| `a.zip` | ❌ 必须以 `.dbxp` 结尾 |
|
|
91
|
+
| `a-1.2.3+build.dbxp` | ❌ `+` 不在允许字符集内 |
|
|
92
|
+
|
|
93
|
+
其他硬约束:
|
|
94
|
+
|
|
95
|
+
- `plugin.id` / `plugin.publisher` / `plugin.version` 必需;`artifacts` 非空。
|
|
96
|
+
- `target` 必须匹配 `^[a-z0-9-]{1,64}$` 且唯一。
|
|
97
|
+
- `sha256` 64 位十六进制;`size` 1 … 512 MiB。
|
|
98
|
+
- **`+build` 版本号会破坏自动化链路**(拼出的文件名含 `+`)—— 上架版本不要用 build metadata。
|
|
99
|
+
|
|
100
|
+
用随附脚本从 `dist/*.artifact.json` 生成,避免手写出错:
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
node <skill-root>/scripts/make-candidate.mjs . --release-notes "Initial release."
|
|
104
|
+
# 产出 dist/candidates/<id>.json 与 dist/release-candidates.json,并复核每个包的 sha256/size
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## 5. 候选文件 `candidates/<plugin-id>.json`
|
|
108
|
+
|
|
109
|
+
这是提到 `dbx-store` 的 PR 内容。**允许的顶层键恰好 15 个**:
|
|
110
|
+
|
|
111
|
+
`schemaVersion`、`id`、`publisher`、`version`、`releaseNotes`、`name`、`description`、`icon`、`tags`、`permissions`、`source`、`homepage`、`license`、`localizations`、`targets`。
|
|
112
|
+
|
|
113
|
+
```json
|
|
114
|
+
{
|
|
115
|
+
"schemaVersion": 1,
|
|
116
|
+
"id": "com.example.plugin",
|
|
117
|
+
"publisher": "example",
|
|
118
|
+
"version": "1.0.0",
|
|
119
|
+
"name": "Example Plugin",
|
|
120
|
+
"description": "One-line description shown in the marketplace.",
|
|
121
|
+
"icon": "https://example.com/icon.svg",
|
|
122
|
+
"tags": ["files"],
|
|
123
|
+
"permissions": ["host.events"],
|
|
124
|
+
"source": "https://github.com/example/dbx-plugin/tree/v1.0.0",
|
|
125
|
+
"homepage": "https://github.com/example/dbx-plugin",
|
|
126
|
+
"license": "Apache-2.0",
|
|
127
|
+
"releaseNotes": "Initial release.",
|
|
128
|
+
"targets": [
|
|
129
|
+
{
|
|
130
|
+
"target": "darwin-arm64",
|
|
131
|
+
"url": "https://github.com/example/dbx-plugin/releases/download/v1.0.0/com.example.plugin-1.0.0-darwin-arm64.dbxp",
|
|
132
|
+
"sha256": "0f0e...64 位十六进制...",
|
|
133
|
+
"size": 12345678
|
|
134
|
+
}
|
|
135
|
+
]
|
|
136
|
+
}
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
| 字段 | 必需 | 约束 |
|
|
140
|
+
| --- | --- | --- |
|
|
141
|
+
| `schemaVersion` | 是 | 必须 `1` |
|
|
142
|
+
| `id` | 是 | `^[a-z0-9][a-z0-9._-]{0,127}$`;**文件名必须是 `<id>.json`** |
|
|
143
|
+
| `publisher` | 是 | 同 id 规则,且**必须在 `publishers/` 已登记**并拥有该插件 |
|
|
144
|
+
| `version` | 是 | SemVer;不能已列出,不能已撤销 |
|
|
145
|
+
| `name` | **新插件必需** | 非空 |
|
|
146
|
+
| `source` | **新插件必需** | 必须 `https://` |
|
|
147
|
+
| `license` | **新插件必需** | 非空 |
|
|
148
|
+
| `description` / `icon` / `homepage` / `tags` / `permissions` / `releaseNotes` / `localizations` | 否 | 出现时必须**非空**;`icon`/`homepage` 允许 http(s);`tags`/`permissions` 为唯一非空字符串数组;`localizations` 每项只允许 `name`、`description` |
|
|
149
|
+
| `targets` | 是 | 非空数组 |
|
|
150
|
+
|
|
151
|
+
`targets[]` 每项**恰好 4 个键**:`target`(`^[a-z0-9-]{1,64}$`,唯一)、`url`(**HTTPS**)、`sha256`(64 位十六进制)、`size`(1 … 512 MiB)。
|
|
152
|
+
|
|
153
|
+
### 绝对不要放进候选
|
|
154
|
+
|
|
155
|
+
- `signingKeyId` —— 属于商店 artifact metadata,候选里出现会报 `contains unknown field(s): signingKeyId`。
|
|
156
|
+
- `verified` —— 候选里出现同样报未知字段。`verified` 由维护者决定,签名时硬编码为 `false`。
|
|
157
|
+
- **指向 `t8y2/dbx-store` Release 的 URL** → `candidate URLs must not reference DBX Store releases; submit the unsigned candidate artifact`。
|
|
158
|
+
- 任何非 HTTPS URL。
|
|
159
|
+
|
|
160
|
+
## 6. 发布者记录 `publishers/<publisher-id>.json`
|
|
161
|
+
|
|
162
|
+
**只在首次上架时需要**,且只允许 3 个字段:
|
|
163
|
+
|
|
164
|
+
```json
|
|
165
|
+
{ "id": "example", "name": "Example", "status": "unverified" }
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
- `id` 匹配 `^[a-z0-9][a-z0-9._-]{0,127}$`,文件名必须是 `<id>.json`,不可重复。
|
|
169
|
+
- `name` 非空;`status` 非空(实践中只用 `verified` / `unverified`)。
|
|
170
|
+
- **不含联系人/邮箱/URL 要求**,也不含任何密钥。
|
|
171
|
+
- `publisher` 字段引用的是记录的 **`id`**,不是 `name`。
|
|
172
|
+
|
|
173
|
+
> ⚠️ **实测坑(文档未提)**:签名 Workflow 会先把 base 分支的 `publishers/` 覆盖回 PR 分支,**PR 里新增的 `publishers/<新id>.json` 会被删除**,随后校验失败 `publisher '<id>' is not registered`。
|
|
174
|
+
> **实践做法**:新发布者记录必须先落到 `main`(单独提一个只加 publisher 的 PR 并合并,或请维护者登记),**再**在候选 PR 上运行 `/sign`。
|
|
175
|
+
|
|
176
|
+
## 7. 首次上架流程
|
|
177
|
+
|
|
178
|
+
1. 插件源码放在**公开可审阅**的仓库。
|
|
179
|
+
2. 每个支持的 target 构建未签名 `.dbxp`,发布到不可变 HTTPS 地址(Release / 对象存储 / CDN)。
|
|
180
|
+
3. Fork `t8y2/dbx-store`,向 `main` 提**一个** PR:
|
|
181
|
+
- `publishers/<publisher-id>.json`(仅首次,**但要先合并到 main**,见 §6 的坑);
|
|
182
|
+
- `candidates/<plugin-id>.json`。
|
|
183
|
+
4. 按 PR 模板填写:插件 ID/版本/发布者、源码仓库 + 精确 tag、capabilities、**每一项 Manifest 权限**以及数据/网络访问、原生 Sidecar 行为(纯前端写 None)、license、主页/支持地址。
|
|
184
|
+
5. CI 会**故意保持红色**:`open candidate(s) awaiting DBX Store signing` —— 这是为了阻止未签名内容被合并。
|
|
185
|
+
6. 维护者审核后运行受保护签名 Workflow(PR 下评论 `/sign`,或手动 `workflow_dispatch` 指定 PR 号)。
|
|
186
|
+
7. Workflow 回写 `plugins/<id>.json`、重建的 `catalog/index.json` 并删除 `candidates/<id>.json`,同时把签名包发到 R2。
|
|
187
|
+
8. CI 转绿后由维护者合并。
|
|
188
|
+
|
|
189
|
+
## 8. 版本更新流程
|
|
190
|
+
|
|
191
|
+
1. 改代码 → **递增 `manifest.json` 的 `version`**(不要复用旧版本号)。
|
|
192
|
+
2. 打新源码 Tag、发新 Release;**旧 Release 资产不可覆盖**。
|
|
193
|
+
3. 若仓库已登记 `autoUpdate: true`,Store 的**每小时**同步 Workflow 会在最新 Release 中找到 `release-candidates.json`,自动创建/更新候选 PR:
|
|
194
|
+
- 分支 `automation/plugin-release/<plugin-id>/<version>`
|
|
195
|
+
- PR 标题 `feat(store): submit <plugin-id>@<version>`
|
|
196
|
+
- 候选内容未变化时**不提交不推送**(幂等)
|
|
197
|
+
4. 未登记则手工提 `candidates/<plugin-id>.json`,**只写新版本**。
|
|
198
|
+
5. 审核 → `/sign` → 合并。
|
|
199
|
+
|
|
200
|
+
**省略的展示字段保持当前值,写了的字段会替换。** 新版本不需要动 `.dbx-store.json`。
|
|
201
|
+
|
|
202
|
+
## 9. 只改商店展示信息
|
|
203
|
+
|
|
204
|
+
更新 `.dbx-store.json` 并在**新 Release tag** 上发布 → 同步器读取该 tag 下的文件 → 带进下一次候选。
|
|
205
|
+
|
|
206
|
+
注意:同步是**由 Release 驱动**的,而且候选只有在签名后才最终生效。所以「只改文案」也要走一次 Release + 审核 + 签名。
|
|
207
|
+
|
|
208
|
+
## 10. 自动同步的登记与触发
|
|
209
|
+
|
|
210
|
+
`dbx-store/automation/plugin-sources.json`:
|
|
211
|
+
|
|
212
|
+
```json
|
|
213
|
+
{
|
|
214
|
+
"version": 1,
|
|
215
|
+
"plugins": [
|
|
216
|
+
{ "repository": "owner/repo", "metadataPath": ".dbx-store.json", "autoUpdate": true }
|
|
217
|
+
]
|
|
218
|
+
}
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
- 只有 `autoUpdate: true` 的条目会被处理(缺省即跳过)。
|
|
222
|
+
- `repository` 必须匹配 `^[^/]+/[A-Za-z0-9._-]+$`;`metadataPath` 不能含 `..`。
|
|
223
|
+
- 同步只在**最新**的(非 draft、非 prerelease)Release 中查找名为 `release-candidates.json` 的资产;找不到就静默跳过(`No published candidate release found for <repo>`)。只拉取最近 30 个 Release。
|
|
224
|
+
- 想让你的仓库加入自动同步,向 `dbx-store` 提一个登记 PR,或请维护者登记。**插件仓库不需要配置任何自动化 Secret。**
|
|
225
|
+
|
|
226
|
+
## 11. 审核与签名
|
|
227
|
+
|
|
228
|
+
### `/sign` 触发
|
|
229
|
+
|
|
230
|
+
- 在候选 PR 下评论 `/sign`(目前只有维护者账号 `t8y2` 可以触发;命令以 `issue_comment` 事件运行,用的是默认分支的 Workflow 定义,PR 无法篡改这道闸门)。
|
|
231
|
+
|
|
232
|
+
### 手动签名 Workflow 输入
|
|
233
|
+
|
|
234
|
+
`candidate-url`、`candidate-sha256`、`candidate-size`、`output-name`、`plugin-id`、`publisher`、`version`、`target`、`sdk-ref`。
|
|
235
|
+
|
|
236
|
+
- `output-name` 必须**恰好等于** `<plugin-id>-<version>-<target>.dbxp`。
|
|
237
|
+
- `sdk-ref` 决定用哪个 DBX 版本里的 packager 契约。
|
|
238
|
+
|
|
239
|
+
### 签名前会验证什么
|
|
240
|
+
|
|
241
|
+
1. 输入格式(HTTPS、64 位十六进制 sha256、size 范围、id/publisher/version/target/output-name 规则)。
|
|
242
|
+
2. 仓库密钥已登记、`status: "active"`、未撤销。
|
|
243
|
+
3. 下载候选包,重算 SHA-256 与 size,必须与审核值一致。
|
|
244
|
+
4. 包内**没有 `signature.json`**(`Official signing accepts only unsigned candidates`)。
|
|
245
|
+
5. 包内 `manifest.json` 的 `id`/`publisher`/`version` 与候选完全一致。
|
|
246
|
+
|
|
247
|
+
### 签名后产出
|
|
248
|
+
|
|
249
|
+
- `<id>-<version>-<target>.dbxp`(含 Ed25519 签名)
|
|
250
|
+
- `<id>-<version>-<target>.artifact.json`(含 `signingKeyId`)
|
|
251
|
+
- `<id>-<version>-<target>.signing-receipt.json`
|
|
252
|
+
|
|
253
|
+
发布路径(R2,全部**不可覆盖**):
|
|
254
|
+
|
|
255
|
+
```
|
|
256
|
+
plugins/<plugin-id>/<version>/<id>-<version>-<target>.dbxp
|
|
257
|
+
plugins/<plugin-id>/<version>/<id>-<version>-<target>.artifact.json
|
|
258
|
+
plugins/<plugin-id>/<version>/<id>-<version>-<target>.signing-receipt.json
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
已存在同名对象 → `R2 object already exists and cannot be overwritten`。**改字节就必须升版本。**
|
|
262
|
+
|
|
263
|
+
## 12. 目录与密钥
|
|
264
|
+
|
|
265
|
+
生成后的 `plugins/<id>.json`(不要手工编辑)形如:
|
|
266
|
+
|
|
267
|
+
```json
|
|
268
|
+
{
|
|
269
|
+
"id": "com.example.plugin",
|
|
270
|
+
"name": "Example Plugin",
|
|
271
|
+
"description": "...",
|
|
272
|
+
"publisher": "example",
|
|
273
|
+
"verified": false,
|
|
274
|
+
"icon": "https://dl.dbxio.com/plugins/com.example.plugin/1.0.0/icon.svg",
|
|
275
|
+
"tags": ["files"],
|
|
276
|
+
"permissions": ["host.events"],
|
|
277
|
+
"source": "https://github.com/example/dbx-plugin/tree/<commit>",
|
|
278
|
+
"homepage": "https://github.com/example/dbx-plugin",
|
|
279
|
+
"license": "Apache-2.0",
|
|
280
|
+
"latestVersion": "1.0.0",
|
|
281
|
+
"versions": [
|
|
282
|
+
{
|
|
283
|
+
"version": "1.0.0",
|
|
284
|
+
"releasedAt": "2026-09-16T05:28:49.440Z",
|
|
285
|
+
"releaseNotes": "Initial release.",
|
|
286
|
+
"artifacts": [
|
|
287
|
+
{
|
|
288
|
+
"target": "darwin-arm64",
|
|
289
|
+
"url": "https://dl.dbxio.com/plugins/com.example.plugin/1.0.0/com.example.plugin-1.0.0-darwin-arm64.dbxp",
|
|
290
|
+
"sha256": "5e5c...",
|
|
291
|
+
"signingKeyId": "dbx-store-release-2026",
|
|
292
|
+
"size": 30810286
|
|
293
|
+
}
|
|
294
|
+
]
|
|
295
|
+
}
|
|
296
|
+
],
|
|
297
|
+
"localizations": { "zh-CN": { "name": "示例插件", "description": "..." } }
|
|
298
|
+
}
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
- **没有 `$schema` 字段**,也没有 `releases[]` —— 结构是 `versions[]`(每版一项)→ `artifacts[]`(每 target 一项,恰好 5 个键)。
|
|
302
|
+
- `releasedAt` 由签名者设置;`latestVersion` 由签名流程按 semver 重算。
|
|
303
|
+
- **图标在目录里被硬编码重写为 `https://dl.dbxio.com/plugins/<id>/<latestVersion>/icon.<svg|png>`**,扩展名取自原图标,只接受 SVG/PNG。
|
|
304
|
+
- 目录消费地址:
|
|
305
|
+
|
|
306
|
+
```text
|
|
307
|
+
https://raw.githubusercontent.com/t8y2/dbx-store/main/catalog/index.json
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
### 签名密钥与撤销
|
|
311
|
+
|
|
312
|
+
- `signing-keys.json` 的 key 状态:`preview`(**目录里禁止使用**)、`active`、`retired`。`algorithm` 必须是 `ed25519`,`purpose` 必须是 `repository-package-signing`,`publicKey` 必须是 base64 的 32 字节。
|
|
313
|
+
- 自定义/私有仓库才用 `dbx-plugin keygen`;官方商店作者不需要。
|
|
314
|
+
- 轮换:**换新 Key ID**,先让支持的 DBX 版本信任新公钥,再停用旧 key。泄露的 key 记入 `revoked.json`,旧 ID 永不复用。
|
|
315
|
+
- `revoked.json` 可撤销插件版本(`{ "pluginId": "...", "version": "..." }`)或签名 key ID。
|
|
316
|
+
|
|
317
|
+
## 13. 提交 PR 中不能包含
|
|
318
|
+
|
|
319
|
+
- 插件源码目录、完整源码副本
|
|
320
|
+
- `.dbxp` 二进制(仓库校验器会拒绝**任何**位置的 `.dbxp`)
|
|
321
|
+
- Ed25519 私钥、Token、下载凭据
|
|
322
|
+
- `plugins/*.json` 或 `catalog/index.json` 的手工改动
|
|
323
|
+
- 把未签名候选 URL 当作最终下载地址
|
|
324
|
+
- 自行伪造 `verified: true`
|
|
325
|
+
- 工作区中任何 **> 1 MiB** 的文件(`validate.mjs` 会扫描整个工作树,包括未跟踪/被 gitignore 的文件)
|
|
326
|
+
|
|
327
|
+
## 14. 已确认的「文档 vs 脚本」冲突
|
|
328
|
+
|
|
329
|
+
| # | 文档说 | 脚本实际 |
|
|
330
|
+
| --- | --- | --- |
|
|
331
|
+
| 1 | `CONTRIBUTING.md` 说首次 PR 携带 `publishers/<id>.json` 即可 | 签名 overlay 会删除 PR 新增的 publisher 记录 → **必须先在 main 上登记** |
|
|
332
|
+
| 2 | `CONTRIBUTING.md` 说候选要「把 `verified` 保持 `false`」 | 候选**根本不能含** `verified` 字段 |
|
|
333
|
+
| 3 | 上游 `RELEASING.md` 描述「先开 Issue,再开 catalog PR」 | 当前流程是**一个 PR**,无 Issue |
|
|
334
|
+
| 4 | `README.md` 列的手动签名输入 | 实际还必需 `output-name`(且必须等于 `<id>-<version>-<target>.dbxp`)与 `sdk-ref` |
|
|
335
|
+
| 5 | 校验器描述 | 实际还检查 1 MiB 文件上限、preview key 禁用、publisher 归属、禁止 dbx-store Release URL、图标必须 SVG/PNG |
|
|
336
|
+
| 6 | 目录读取地址只写了 raw.githubusercontent | R2 上也发布了同一份 catalog,仓库未说明客户端实际读哪个 |
|
|
337
|
+
|
|
338
|
+
## 15. 校验错误对照
|
|
339
|
+
|
|
340
|
+
| 报错 | 原因 |
|
|
341
|
+
| --- | --- |
|
|
342
|
+
| `contains unknown field(s): X` | 多写了字段(候选/目标/插件/版本/artifact/本地化/publisher/密钥/撤销记录全部是严格键集) |
|
|
343
|
+
| `filename must match plugin id '<id>.json'` | 文件名与 `id` 不一致 |
|
|
344
|
+
| `publisher '<p>' is not registered` | publisher 记录不在 base 分支 |
|
|
345
|
+
| `publisher '<p>' does not own plugin '<id>'` | 该 publisher 不是现有插件的所有者 |
|
|
346
|
+
| `version '<v>' is already listed for plugin '<id>'` | 复用了已上架版本 |
|
|
347
|
+
| `plugin version '<id>@<v>' is revoked` | 该版本已被撤销 |
|
|
348
|
+
| `candidate URLs must use HTTPS` | 用了 http 或相对地址 |
|
|
349
|
+
| `candidate URLs must not reference DBX Store releases; submit the unsigned candidate artifact` | 指向了已签名的官方产物 |
|
|
350
|
+
| `name is required for a new plugin listing` / `license is required ...` | 新插件缺 `name`/`license` |
|
|
351
|
+
| `'<f>' must be a non-empty string when provided` | 字段存在但为空字符串 |
|
|
352
|
+
| `invalid target '<t>'` | target 含大写/下划线等非法字符 |
|
|
353
|
+
| `duplicate target '<t>'` | 同一版本重复 target |
|
|
354
|
+
| `invalid SHA-256 for target '<t>'` | 不是 64 位十六进制 |
|
|
355
|
+
| `invalid size for target '<t>'` | 不是 1 … 512 MiB 的安全整数 |
|
|
356
|
+
| `Candidate SHA-256 mismatch: expected <e>, got <a>` | 写了哈希之后重新打包了 |
|
|
357
|
+
| `Candidate size mismatch: expected <e>, got <a>` | 同上 |
|
|
358
|
+
| `Official signing accepts only unsigned candidates` | 提交了已签名的包 |
|
|
359
|
+
| `Expected plugin <id>, got <x>` / publisher / version | 候选与包内 manifest 身份不一致 |
|
|
360
|
+
| `preview signing key '<k>' cannot publish catalog artifacts` | 目录引用了 preview key |
|
|
361
|
+
| `Binary plugin package must not be committed: <path>` | 仓库里有 `.dbxp` |
|
|
362
|
+
| `Repository file exceeds 1 MiB: <path>` | 工作树里有大文件(含未跟踪文件) |
|
|
363
|
+
| `Invalid candidate artifact '<v>'` | `release-candidates.json` 里的 `url` 不是纯 `.dbxp` 文件名 |
|
|
364
|
+
| `No published candidate release found for <repo>` | 最新 Release 里没有名为 `release-candidates.json` 的资产,或 Release 是 draft/prerelease |
|
|
365
|
+
| `Unsupported store metadata field '<k>'` | `.dbx-store.json` 写了不在白名单的键 |
|
|
366
|
+
|
|
367
|
+
## 16. 自检脚本
|
|
368
|
+
|
|
369
|
+
```bash
|
|
370
|
+
node <skill-root>/scripts/make-candidate.mjs . --release-notes "..."
|
|
371
|
+
node <skill-root>/scripts/inspect-dbxp.mjs dist/*.dbxp
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
`make-candidate.mjs` 会检测:`id`/`publisher`/`version` 一致性、target 合法性、`sha256`/`size` 与实际字节一致、文件名是否符合 `<id>-<version>-<target>.dbxp`、是否误含 `signingKeyId`,并生成候选与聚合 JSON。
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
# Sidecar Protocol v1 参考
|
|
2
|
+
|
|
3
|
+
## 1. 什么时候需要原生后端
|
|
4
|
+
|
|
5
|
+
只有**浏览器沙箱无法完成**的能力才需要 Sidecar:
|
|
6
|
+
|
|
7
|
+
- 原生协议客户端(S3、SSH、SFTP、数据库 wire protocol)
|
|
8
|
+
- 系统凭据 / 系统 API / 本地文件系统读写
|
|
9
|
+
- 长连接、PTY、流式传输
|
|
10
|
+
- 高性能计算或大文件处理
|
|
11
|
+
|
|
12
|
+
> Sidecar **不是 OS 沙箱**,以当前用户权限运行。签名只证明「哪个仓库批准并发布了这个包」,**不保证代码无害**。能纯前端实现就不要加后端。
|
|
13
|
+
|
|
14
|
+
## 2. 进程模型
|
|
15
|
+
|
|
16
|
+
- Sidecar 是**持久子进程**,按插件共享(不是每个 Tab 一个)。插件自己维护内部会话注册表。
|
|
17
|
+
- **stdout 只用于协议消息**;所有日志/诊断写 **stderr**。
|
|
18
|
+
- 宿主支持并发在途请求、按请求超时、严格 JSON-RPC 校验、崩溃传播、状态上报、有界事件缓冲;握手/协议/输出失败会自动终止子进程。
|
|
19
|
+
- 安装/回滚/替换/卸载前,DBX 会先拆除插件拥有的连接池;**仍有已保存连接引用该插件时不允许卸载**。
|
|
20
|
+
|
|
21
|
+
## 3. 初始化握手
|
|
22
|
+
|
|
23
|
+
DBX 启动每个 Sidecar 后首先发送 `plugin/initialize`:
|
|
24
|
+
|
|
25
|
+
```json
|
|
26
|
+
{
|
|
27
|
+
"jsonrpc": "2.0",
|
|
28
|
+
"id": 1,
|
|
29
|
+
"method": "plugin/initialize",
|
|
30
|
+
"params": {
|
|
31
|
+
"host": { "dbxVersion": "0.5.68", "hostApiVersion": "1.0.0", "protocolVersions": [1] },
|
|
32
|
+
"plugin": { "id": "vendor.example", "version": "1.0.0" },
|
|
33
|
+
"permissions": ["host.events"]
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
插件必须回应(**使用相同的 `id`**):
|
|
39
|
+
|
|
40
|
+
```json
|
|
41
|
+
{
|
|
42
|
+
"jsonrpc": "2.0",
|
|
43
|
+
"id": 1,
|
|
44
|
+
"result": {
|
|
45
|
+
"protocolVersion": 1,
|
|
46
|
+
"capabilities": ["connections", "events"],
|
|
47
|
+
"plugin": { "id": "vendor.example", "version": "1.0.0" }
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
- 响应至少包含 `protocolVersion`、`capabilities`、`plugin: { id, version }`。
|
|
53
|
+
- `plugin.id` / `plugin.version` **必须与 `manifest.json` 完全一致**,否则报
|
|
54
|
+
`Sidecar identity or protocol does not match manifest`。
|
|
55
|
+
- 协议版本必须在双方支持范围内,否则返回 `-32001`。
|
|
56
|
+
- **初始化 `capabilities` 描述后端能力,不等于 Manifest 的 Host 权限列表** —— 两者是不同概念。
|
|
57
|
+
|
|
58
|
+
## 4. JSON-RPC 消息
|
|
59
|
+
|
|
60
|
+
请求/响应遵循 JSON-RPC 2.0;插件的**事件是 JSON-RPC 通知**(无 `id`)。
|
|
61
|
+
|
|
62
|
+
- 请求必须声明 `jsonrpc: "2.0"`;缺失会被拒绝。
|
|
63
|
+
- `id` 必须支持**并发关联** —— 响应可以乱序,但必须回填相同 `id`。
|
|
64
|
+
- **方法名规则**:非空、长度 ≤256、**不含任何空白字符**。违反返回 `-32600`。
|
|
65
|
+
- 通知(无 `id`)**不发送响应**。
|
|
66
|
+
|
|
67
|
+
### 错误码
|
|
68
|
+
|
|
69
|
+
| 码 | 含义 |
|
|
70
|
+
| --- | --- |
|
|
71
|
+
| `-32600` | 非法请求(缺 `jsonrpc`、方法名非法、消息过大) |
|
|
72
|
+
| `-32601` | 方法不存在(SDK 的 `method_not_found` / `MethodNotFound`) |
|
|
73
|
+
| `-32602` | 参数非法 |
|
|
74
|
+
| `-32603` | 内部错误 |
|
|
75
|
+
| `-32000` | 通用失败(IO 错误、锁中毒、二进制传输未启用) |
|
|
76
|
+
| `-32001` | DBX 与插件没有共同协议版本 |
|
|
77
|
+
|
|
78
|
+
## 5. 传输
|
|
79
|
+
|
|
80
|
+
### `stdio-jsonl`(默认)
|
|
81
|
+
|
|
82
|
+
一行一个 JSON 值,以 `\n` 分隔。**单条 JSON 消息 ≤ 8 MiB。**
|
|
83
|
+
|
|
84
|
+
### `stdio-framed`
|
|
85
|
+
|
|
86
|
+
需要 PTY / SFTP / 文件传输等二进制流时使用,**并同时声明 `host.binary`**。
|
|
87
|
+
|
|
88
|
+
```
|
|
89
|
+
kind: u8 | payload_length: u32 big-endian | payload
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
- `kind = 0`:UTF-8 JSON payload。
|
|
93
|
+
- `kind = 1`:`channel_length: u16 big-endian | channel UTF-8 | binary bytes`。
|
|
94
|
+
- 二进制单帧 **≤ 64 MiB**;channel 名同样受方法名规则约束。
|
|
95
|
+
|
|
96
|
+
### 尺寸限制总表
|
|
97
|
+
|
|
98
|
+
| 位置 | 上限 |
|
|
99
|
+
| --- | --- |
|
|
100
|
+
| Sidecar JSON 单条消息 | 8 MiB |
|
|
101
|
+
| Sidecar 二进制单帧 | 64 MiB |
|
|
102
|
+
| UI ↔ 宿主 JSON 桥参数 | 2 MiB |
|
|
103
|
+
| UI ↔ 宿主 二进制消息 | 8 MiB |
|
|
104
|
+
| Workbench context 快照 | 2 MiB |
|
|
105
|
+
| 显式桥接超时 | 1–120000 ms |
|
|
106
|
+
|
|
107
|
+
**大文件传输必须自己做分块、偏移、确认、取消与进度事件**,不要塞进一个巨大的 JSON/base64 值。
|
|
108
|
+
|
|
109
|
+
## 6. 连接生命周期方法
|
|
110
|
+
|
|
111
|
+
固定方法名(详见 `contributions.md` §2):
|
|
112
|
+
|
|
113
|
+
| 方法 | 说明 |
|
|
114
|
+
| --- | --- |
|
|
115
|
+
| `connection/test` | 校验连接,通常返回 `{ "success": true, "message": "..." }` |
|
|
116
|
+
| `connection/connect` | 建立会话,用 `connection.id` 作为 key;**幂等** |
|
|
117
|
+
| `connection/disconnect` | 拆除会话;**幂等**(重复断开不应报错) |
|
|
118
|
+
| `connection/action` | 自定义表单动作,参数额外含 `action: { id }` |
|
|
119
|
+
| `contextMenu/<contribution-id>` | 连接右键菜单,返回 `{ "message": "..." }` 弹 toast |
|
|
120
|
+
| `filesystem/*` | 声明 filesystem-provider 时实现 |
|
|
121
|
+
|
|
122
|
+
`connection` 只在**后端生命周期请求**中携带补齐的 Secret;`runtime.host` / `runtime.port` 是经过 DBX 隧道/代理后的**最终端点**,直接连它。
|
|
123
|
+
|
|
124
|
+
## 7. 事件
|
|
125
|
+
|
|
126
|
+
插件通过 emitter 主动推送事件(JSON-RPC 通知):
|
|
127
|
+
|
|
128
|
+
```
|
|
129
|
+
{"jsonrpc":"2.0","method":"<plugin>/progress","params":{...}}
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
前端用 `window.dbxPlugin.onEvent(fn)` 接收,**需要 `host.events` 权限**。转发后端事件必须有该权限。
|
|
133
|
+
|
|
134
|
+
**绝对不要在事件、context 或错误消息中泄露 Secret。**
|
|
135
|
+
|
|
136
|
+
## 8. Rust SDK
|
|
137
|
+
|
|
138
|
+
`dbx-plugin-sdk`(协议 v1,支持 JSONL 与 framed)。
|
|
139
|
+
|
|
140
|
+
```rust
|
|
141
|
+
use dbx_plugin_sdk::{PluginEmitter, PluginError, PluginHandler, PluginMetadata, PluginServer, RequestContext};
|
|
142
|
+
use serde_json::{json, Value};
|
|
143
|
+
|
|
144
|
+
struct Plugin;
|
|
145
|
+
|
|
146
|
+
impl PluginHandler for Plugin {
|
|
147
|
+
fn handle(&self, _context: RequestContext, method: &str, params: Value, emitter: &PluginEmitter)
|
|
148
|
+
-> Result<Value, PluginError> {
|
|
149
|
+
match method {
|
|
150
|
+
"example/echo" => {
|
|
151
|
+
emitter.event("example/progress", json!({ "done": true }))?;
|
|
152
|
+
Ok(params)
|
|
153
|
+
}
|
|
154
|
+
_ => Err(PluginError::method_not_found(method)),
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
fn main() -> std::io::Result<()> {
|
|
160
|
+
let metadata = PluginMetadata::new("com.example.files", env!("CARGO_PKG_VERSION"))
|
|
161
|
+
.with_capability("connections");
|
|
162
|
+
PluginServer::new(metadata, Plugin).serve()
|
|
163
|
+
}
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
API 速览:
|
|
167
|
+
|
|
168
|
+
| 项 | 说明 |
|
|
169
|
+
| --- | --- |
|
|
170
|
+
| `PluginMetadata::new(id, version)` | `.with_capability(name)` 追加能力 |
|
|
171
|
+
| `PluginServer::new(metadata, handler)` | `.transport(PluginTransport::Framed)`、`.worker_threads(n)`、`.work_queue_capacity(n)`、`.serve()` |
|
|
172
|
+
| `PluginHandler::handle(ctx, method, params, emitter)` | 主分发;`ctx` 含 `request_id`、`driver` |
|
|
173
|
+
| `PluginHandler::handle_binary(channel, data, emitter)` | framed 下的宿主→插件二进制;默认返回「不支持」 |
|
|
174
|
+
| `PluginError::new(code, msg)` / `PluginError::method_not_found(m)` | 错误构造 |
|
|
175
|
+
| `PluginEmitter::event(method, params)` / `PluginEmitter::binary(channel, data)` | 事件与二进制输出 |
|
|
176
|
+
|
|
177
|
+
- 默认 **2–16 个 worker 线程**(按可用并行度)与 **256 任务队列**;`worker_threads(0)` / `work_queue_capacity(0)` 会被 clamp 到 1。
|
|
178
|
+
- `PluginTransport::Framed` 需要在 manifest 声明 `"transport": "stdio-framed"`。
|
|
179
|
+
|
|
180
|
+
## 9. Go SDK
|
|
181
|
+
|
|
182
|
+
```go
|
|
183
|
+
metadata := dbxpluginsdk.Metadata{
|
|
184
|
+
ID: "vendor.example",
|
|
185
|
+
Version: "1.0.0",
|
|
186
|
+
Capabilities: []string{"connections"},
|
|
187
|
+
}
|
|
188
|
+
server := dbxpluginsdk.NewServer(metadata, handler)
|
|
189
|
+
if err := server.Serve(); err != nil {
|
|
190
|
+
log.Fatal(err) // 日志走 stderr
|
|
191
|
+
}
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
API 速览:
|
|
195
|
+
|
|
196
|
+
| 项 | 说明 |
|
|
197
|
+
| --- | --- |
|
|
198
|
+
| `Metadata{ID, Version, Capabilities}` | 身份与能力 |
|
|
199
|
+
| `NewServer(metadata, handler)` | `.WithTransport(TransportFramed)`、`.WithIO(in, out, errOut)`、`.Serve()` |
|
|
200
|
+
| `Handler` / `HandlerFunc` | `Handle(ctx, method, params json.RawMessage, emitter) (any, *PluginError)` |
|
|
201
|
+
| `BinaryHandler` | `HandleBinary(channel string, data []byte, emitter *Emitter) *PluginError` |
|
|
202
|
+
| `Emitter.Event(method, params)` / `Emitter.Binary(channel, data)` | 事件与二进制输出 |
|
|
203
|
+
| `NewError(code, msg)` / `MethodNotFound(method)` | 错误构造 |
|
|
204
|
+
|
|
205
|
+
- Go SDK 当前支持 JSONL;**framed 也支持**(`TransportFramed`)。
|
|
206
|
+
- 不要只改 Manifest 的 `transport` 就认为 Go 已具备二进制帧处理能力 —— 必须真正用 framed 并实现 `HandleBinary`。
|
|
207
|
+
|
|
208
|
+
## 10. 构建集成
|
|
209
|
+
|
|
210
|
+
模板生成的项目已经接好 SDK,正常路径是直接用 CLI:
|
|
211
|
+
|
|
212
|
+
```bash
|
|
213
|
+
dbx-plugin package . # Rust: cargo build --release;Go: go build -trimpath
|
|
214
|
+
dbx-plugin dev # Rust 用 debug target;Go 用缓存输出目录
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
- 生成项目的 `Cargo.toml` 依赖 `dbx-plugin-sdk`(版本号由 CLI 注入),`go.mod` require `github.com/t8y2/dbx/plugins/sdk/go/dbx-plugin-sdk`。
|
|
218
|
+
- CLI 会通过 `DBX_PLUGIN_SDK_ROOT` 把依赖 **patch/replace 到自带或指定的 SDK 源码**(npm 包内置 `sdk-root`,所以默认即生效)。
|
|
219
|
+
- **不要绕过 CLI 直接 `cargo build` / `go build`**,除非你自己设置了 `DBX_PLUGIN_SDK_ROOT`,或把依赖改成显式 path/git。
|
|
220
|
+
- 开发未发布的 SDK:`dbx-plugin create ... --sdk-root /path/to/dbx`,或打包前 `export DBX_PLUGIN_SDK_ROOT=/path/to/dbx`。
|
|
221
|
+
|
|
222
|
+
## 11. 工程约束清单
|
|
223
|
+
|
|
224
|
+
- [ ] stdout 只输出协议消息,日志一律 stderr。
|
|
225
|
+
- [ ] `plugin/initialize` 里的 id/version 与 manifest 一致。
|
|
226
|
+
- [ ] 请求 ID 支持并发关联(响应可乱序)。
|
|
227
|
+
- [ ] `connect` / `disconnect` 幂等。
|
|
228
|
+
- [ ] 长任务有超时、取消、分块确认。
|
|
229
|
+
- [ ] 不在事件/context/错误消息里泄露 Secret。
|
|
230
|
+
- [ ] 用 framed 时 manifest 声明 `stdio-framed` + `host.binary`。
|
|
231
|
+
- [ ] release 构建开启 strip/lto 控制体积(模板已配置)。
|
|
232
|
+
|
|
233
|
+
## 12. 常见错误
|
|
234
|
+
|
|
235
|
+
| 报错 | 原因 |
|
|
236
|
+
| --- | --- |
|
|
237
|
+
| `Sidecar identity or protocol does not match manifest` | 初始化响应的 `plugin.id`/`version` 与 manifest 不一致,或协议版本不兼容 |
|
|
238
|
+
| 后端启动后立即退出 | 用了 `println!` 污染 stdout;或 `jsonrpc` 字段缺失 |
|
|
239
|
+
| 请求一直不返回 | 没有用请求的 `id` 回填响应;或方法名含空格被拒 |
|
|
240
|
+
| `Binary messages require framed transport` | 没切换到 framed,或 manifest 未声明 |
|
|
241
|
+
| `plugin JSON line is too large` | 单条消息超过 8 MiB,需要分块 |
|
|
242
|
+
| 手动 `cargo build` 找不到 `dbx-plugin-sdk` | 没设置 `DBX_PLUGIN_SDK_ROOT`,请改用 `dbx-plugin package` |
|