dbx-plugin-skill 0.1.0 → 0.1.2

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/README.md CHANGED
@@ -130,6 +130,8 @@ npm test # 端到端自检(CI 在 Node 18/20/22 上跑)
130
130
  npm run verify:package # 校验 npm 打包内容与元数据
131
131
  npm run upstream:check # 对比上游 DBX 契约是否变化
132
132
  npm run upstream:update # 更新后重新基线化
133
+ npm run upstream:sync # 只同步清单中的上游文件/目录到 tmp/upstream
134
+ npm run upstream:report # 查看最近一次稀疏快照的提交与文件哈希
133
135
  ```
134
136
 
135
137
  ---
@@ -214,6 +216,24 @@ skill 大量记录 DBX 的**契约细节**(字段枚举、权限语法、协
214
216
  - 维护者按 issue 里的步骤核对并更新 `skill/references/*.md`,然后 `npm run upstream:update` 重新基线化并提交。
215
217
  - 只有 sha256 变化、事实无差异时,通常只是上游排版变动,确认后直接重新基线化即可。
216
218
 
219
+ ### 按需引用上游源码
220
+
221
+ 需要逐项核对实现、文档或 Workflow 时运行 `npm run upstream:sync`。它对 `t8y2/dbx` 使用
222
+ Git partial clone + sparse checkout,只下载 `tools/upstream-sources.json` 中列出的 schema、
223
+ 插件开发文档和 SDK 子目录;`t8y2/dbx-store` 则使用 depth=1 全量浅克隆。内容保存到本地
224
+ `tmp/upstream/`,不会进入提交或 npm 包。重复执行只 fetch 两个仓库的最新 `main`,并生成带
225
+ commit 与 SHA-256 的快照报告。
226
+
227
+ ```bash
228
+ npm run upstream:sync
229
+ npm run upstream:report
230
+ # 在 tmp/upstream/dbx 与 tmp/upstream/dbx-store 中核对后更新 skill/references/*.md
231
+ npm test
232
+ npm run upstream:update
233
+ ```
234
+
235
+ 需要扩大或缩小同步范围时,只修改 `tools/upstream-sources.json`,无需复制上游源码。
236
+
217
237
  ---
218
238
 
219
239
  ## 更新已安装的 skill
@@ -236,3 +256,10 @@ npm install --global dbx-plugin-skill@latest && dbx-plugin-skill install
236
256
  - 官方商店仓库 `t8y2/dbx-store`:`CONTRIBUTING.md`、`schemas/plugin-candidate.schema.json`、`scripts/validate.mjs`、同步与签名 Workflow
237
257
 
238
258
  > 插件上架 PR 提到 **`t8y2/dbx-store`**,不是 `t8y2/dbx`。普通插件源码留在你自己的仓库。
259
+
260
+ ## 其他
261
+
262
+ - [dbx](https://github.com/t8y2/dbx):官方仓库
263
+ - [dbx-store](https://github.com/t8y2/dbx-store):应用商店仓库
264
+ - [awesome-dbx-plugins](https://github.com/eryajf/awesome-dbx-plugins):🦄 汇集优秀的 DBX 开源插件
265
+ - [dbx-plugin-skill](https://github.com/eryajf/dbx-plugin-skill):一个用于 DBX 插件开发 的 agent skill(技能包),覆盖从创建到上架官方商店的全链路
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dbx-plugin-skill",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "DBX 插件开发 skill:覆盖插件创建、开发、调试、打包、发布到 dbx-store 上架的全流程,安装到 DSH / Claude Code / agents 技能目录",
5
5
  "type": "module",
6
6
  "bin": {
@@ -21,6 +21,8 @@
21
21
  "verify:package": "node tools/verify-package.mjs",
22
22
  "upstream:check": "node tools/upstream-drift.mjs",
23
23
  "upstream:update": "node tools/upstream-drift.mjs --update",
24
+ "upstream:sync": "node tools/upstream-sync.mjs",
25
+ "upstream:report": "node tools/upstream-sync.mjs --report-only",
24
26
  "release:patch": "npm version patch",
25
27
  "release:minor": "npm version minor",
26
28
  "prepublishOnly": "npm test && node tools/verify-package.mjs"
package/skill/SKILL.md CHANGED
@@ -16,7 +16,7 @@ description: DBX 插件开发全流程。Use when 创建、开发、调试、打
16
16
  | --- | --- | --- |
17
17
  | Manifest v1 | `manifest.json` | 插件身份、权限、入口、贡献点、国际化。运行时契约,**拒绝未声明字段** |
18
18
  | 构建配置 | `dbx-plugin.toml` | 打包包含哪些目录、是否有原生后端、dev 构建命令。**不进入插件包** |
19
- | Host API 1.x | `window.dbxPlugin` | 沙箱 UI 与 DBX 宿主通信 |
19
+ | Host API 1.x | `window.dbxPlugin` / Sidecar callback | 沙箱 UI 与 DBX 宿主通信;Host API 1.1 支持 `host/requestUserInput` |
20
20
  | Sidecar Protocol v1 | stdin/stdout JSON-RPC | 可选原生后端与 DBX 通信 |
21
21
  | 包格式 | `.dbxp`(ZIP 容器) | DBX 实际安装的东西 |
22
22
 
@@ -103,6 +103,7 @@ dbx-plugin create my-plugin \
103
103
  - 前端入口由 `entrypoints.ui.entry` 指定,必须落在 `ui.root` 内(root 为 `ui` 时写 `ui/index.html`)。
104
104
  - 所有 UI 必须构建为包内静态资源;**不要把 Vite dev server 或 CDN 地址写进发行包**。
105
105
  - 需要原生能力(S3/SSH/系统凭据/长连接/高性能)才写 Sidecar。Sidecar 不是 OS 沙箱,以当前用户权限运行。
106
+ - 连接表单可用 `visible_when` / `required_when` 的 `all_of`、`any_of`、`not` 组合条件,以及 `picker` 提供本地文件选择;依赖这些能力时设置足够新的 `engines.dbx`。
106
107
 
107
108
  ### 步骤 3:本地调试
108
109
 
@@ -55,7 +55,8 @@
55
55
  - `select` / `radio` **必须**提供非空 `options`;每项 `{ "label": 非空, "value": 字符串 }`。其它类型**不允许**出现 `options`。
56
56
  - 可选:`description`、`placeholder`、`required`(bool)、`default`(string|number|boolean|null,必须符合字段类型)、`binding`。
57
57
  - 条件字段:
58
- - `visible_when` / `required_when`:`{ "field": "<ident>", "one_of": ["v1", "v2"] }`(`one_of` 至少 1 项)。
58
+ - `picker`:仅用于 `text` / `password` / `textarea`,声明本地文件或目录选择;结构见下方。
59
+ - `visible_when` / `required_when`:叶子条件 `{ "field": "<ident>", "one_of": ["v1", "v2"] }`,也可用 `all_of`、`any_of`、`not` 递归组合(最多 8 层、64 个节点)。值按规范化字符串比较,`false` 与 `"false"` 等价。
59
60
  - `binding: "port"` **必须** `type: "number"`。
60
61
  - `binding` 为 `secret` / `name` / `host` / `username` / `password` / `database` 时,`type` 只能是 `text`、`password`、`select`、`radio`、`textarea`(不能是 `number`/`boolean`)。
61
62
 
@@ -71,6 +72,19 @@
71
72
 
72
73
  > **绝对不要**把密码、Token、私钥放到 `config`、Workbench context、事件或日志里。非敏感的补充配置才用 `config`。
73
74
 
75
+ ### `picker`(本地文件动作)
76
+
77
+ `text`、`password`、`textarea` 字段可以声明:
78
+
79
+ ```json
80
+ { "key": "private_key_path", "label": "Private key", "type": "text", "binding": "config", "picker": { "kind": "file", "accept": [".pem", ".key"], "content_field": "private_key" } }
81
+ ```
82
+
83
+ - `kind` 必填:`file` 或 `directory`;目录选择仅桌面端可用。
84
+ - 桌面端把绝对路径写入当前字段;浏览器端把文件内容写入 `content_field` 兄弟字段并清空路径字段。两种来源互斥。
85
+ - `accept` 最多 16 个唯一扩展名或 MIME 类型;浏览器上传上限 1 MiB。没有 `content_field` 的 picker 在浏览器端不可用。
86
+ - 依赖 `picker` 时应把 `engines.dbx` 设为包含该能力的版本。
87
+
74
88
  插件连接的 `external_config` 会在新建、编辑、保存和重新连接流程中保留;升级插件时要显式迁移自己拥有的 `external_config`。
75
89
 
76
90
  ### `capabilities`
@@ -28,7 +28,7 @@ DBX 启动每个 Sidecar 后首先发送 `plugin/initialize`:
28
28
  "id": 1,
29
29
  "method": "plugin/initialize",
30
30
  "params": {
31
- "host": { "dbxVersion": "0.5.68", "hostApiVersion": "1.0.0", "protocolVersions": [1] },
31
+ "host": { "dbxVersion": "0.5.68", "hostApiVersion": "1.1.0", "features": ["host.requestUserInput"], "protocolVersions": [1] },
32
32
  "plugin": { "id": "vendor.example", "version": "1.0.0" },
33
33
  "permissions": ["host.events"]
34
34
  }
@@ -133,7 +133,13 @@ kind: u8 | payload_length: u32 big-endian | payload
133
133
 
134
134
  **绝对不要在事件、context 或错误消息中泄露 Secret。**
135
135
 
136
- ## 8. Rust SDK
136
+ ## 8. Host API 1.1:请求用户输入
137
+
138
+ 连接期间需要 MFA、主机密钥确认或账号选择时,Sidecar 可以在宿主已通告 `hostApiVersion >= 1.1.0` 或 `host.features` 含 `host.requestUserInput` 后发起 `host/requestUserInput`。插件发起的请求使用字符串 id;响应为 `{ "action": "submit", "value": "123456" }`、`{ "action": "cancel" }` 或 `{ "action": "timeout" }`。只有 `submit` 带值,其余必须失败关闭。`prompt` 最多 2000 字符,`timeoutSecs` 为 5–600(默认 300),同时最多 4 个未关闭提示。错误 `-32001` 表示无 UI,`-32601` 表示宿主不支持,`-32602` 表示参数非法;均应优雅降级。宿主在提示期间会暂停等待该提示的连接请求超时计时。
139
+
140
+ Rust SDK 提供 `dbx_plugin_sdk::host_client()`、`HostClient::supports("host/requestUserInput")` 与 `HostClient::request_user_input(...)`。
141
+
142
+ ## 9. Rust SDK
137
143
 
138
144
  `dbx-plugin-sdk`(协议 v1,支持 JSONL 与 framed)。
139
145
 
@@ -519,6 +519,7 @@ function checkConnectionProvider(contribution, at, declared, usage) {
519
519
  "default",
520
520
  "options",
521
521
  "binding",
522
+ "picker",
522
523
  "visible_when",
523
524
  "required_when",
524
525
  ];
@@ -557,14 +558,53 @@ function checkConnectionProvider(contribution, at, declared, usage) {
557
558
  }
558
559
  }
559
560
  }
561
+ if (field.picker !== undefined) {
562
+ const picker = field.picker;
563
+ if (!isPlainObject(picker)) {
564
+ error(`${fAt}.picker 必须是对象`);
565
+ } else {
566
+ const pickerAllowed = ["kind", "accept", "content_field"];
567
+ const extra = Object.keys(picker).filter((k) => !pickerAllowed.includes(k));
568
+ if (extra.length) error(`${fAt}.picker 含未知字段: ${extra.join(", ")}`);
569
+ if (!["text", "password", "textarea"].includes(field.type)) {
570
+ error(`${fAt}.picker 只能用于 text/password/textarea 字段`);
571
+ }
572
+ if (!["file", "directory"].includes(picker.kind)) {
573
+ error(`${fAt}.picker.kind 必须是 file 或 directory`);
574
+ }
575
+ if (picker.accept !== undefined) {
576
+ if (!Array.isArray(picker.accept) || picker.accept.length > 16 || new Set(picker.accept).size !== picker.accept.length || picker.accept.some((v) => typeof v !== "string" || !/^(\.[A-Za-z0-9]{1,16}|[A-Za-z0-9+*._-]{1,64}\/[A-Za-z0-9+*._-]{1,64})$/.test(v))) {
577
+ error(`${fAt}.picker.accept 必须是最多 16 个唯一的扩展名或 MIME 类型`);
578
+ }
579
+ }
580
+ if (picker.content_field !== undefined && (typeof picker.content_field !== "string" || !IDENTIFIER.test(picker.content_field))) {
581
+ error(`${fAt}.picker.content_field 必须是字段标识符`);
582
+ }
583
+ }
584
+ }
560
585
  for (const conditionKey of ["visible_when", "required_when"]) {
561
586
  const condition = field[conditionKey];
562
587
  if (condition === undefined) continue;
563
- if (!isPlainObject(condition) || typeof condition.field !== "string" || !Array.isArray(condition.one_of) || condition.one_of.length === 0) {
564
- error(`${fAt}.${conditionKey} 必须是 { field: string, one_of: [至少 1 项] }`);
565
- } else if (Object.keys(condition).some((k) => !["field", "one_of"].includes(k))) {
566
- error(`${fAt}.${conditionKey} 含未知字段`);
567
- }
588
+ let conditionNodes = 0;
589
+ const validateCondition = (value, path, depth = 0) => {
590
+ conditionNodes += 1;
591
+ if (depth > 8 || conditionNodes > 64 || !isPlainObject(value)) { error(`${path} 条件嵌套无效、超过 8 层或超过 64 个节点`); return; }
592
+ const keys = Object.keys(value);
593
+ if (keys.length !== 1 && !(keys.length === 2 && keys.includes("field") && keys.includes("one_of"))) { error(`${path} 条件结构无效`); return; }
594
+ if ("field" in value || "one_of" in value) {
595
+ if (typeof value.field !== "string" || !IDENTIFIER.test(value.field) || !Array.isArray(value.one_of) || value.one_of.length === 0 || value.one_of.some((v) => !["string", "number", "boolean"].includes(typeof v))) error(`${path} 必须是 { field: 标识符, one_of: [字符串/数字/布尔值] }`);
596
+ return;
597
+ }
598
+ const operator = keys[0];
599
+ if (operator === "not") { validateCondition(value.not, `${path}.not`, depth + 1); return; }
600
+ if (operator === "all_of" || operator === "any_of") {
601
+ if (!Array.isArray(value[operator]) || value[operator].length === 0) { error(`${path}.${operator} 必须是非空数组`); return; }
602
+ value[operator].forEach((child, i) => validateCondition(child, `${path}.${operator}[${i}]`, depth + 1));
603
+ return;
604
+ }
605
+ error(`${path} 只支持 field/one_of、all_of、any_of、not`);
606
+ };
607
+ validateCondition(condition, `${fAt}.${conditionKey}`);
568
608
  }
569
609
  });
570
610
  }