dsh-plugin-zbk 0.0.0-stage → 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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 智宝项目组
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/PLUGIN.md ADDED
@@ -0,0 +1,5 @@
1
+ # dsh-plugin-zbk(DSH 插件)
2
+
3
+ > 本文件只是指针:**安装 / 使用 / 安全 / 自检 / 实施记录见 [README.md](README.md)**。
4
+
5
+ 正文只维护 README.md 一份(npm 包内同名文件),此处不重复,避免双写。
package/README.md CHANGED
@@ -1,3 +1,342 @@
1
- # Temporary Holding Version
1
+ # dsh-plugin-zbk —— zbk 可移植看板工具包的 DSH 插件
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ > **状态(2026-11-11)**:**脚手架完成,未安装、未发布 npm**(按卡:安装由总控/项目方决定)。
4
+ > 本包把仓库内 `tools/zbk/` 工具包(第 1 步成果,commit `13383d28`,selftest 16/16)
5
+ > 包成 DSH 插件:让任意项目在 DSH 会话里**一句话拉起看板**(init → serve → check → open)。
6
+
7
+ ---
8
+
9
+ ## 1. 这是什么
10
+
11
+ | 工具 | 等价 CLI | 什么时候用 | 返回 |
12
+ |---|---|---|---|
13
+ | `zbk_init` | `python tools/zbk/zbk.py init --dir <项目>/.zbk` | 项目还没有 `.zbk` 数据目录,要开看板/反馈栈/流程图 | 数据目录、端口、**一次性令牌**、PASS/FAIL |
14
+ | `zbk_serve` | `python tools/zbk/zbk.py serve --dir … --port …` | 要在本机浏览器打开看板前 | 访问 URL、pid、是否新起/已在跑 |
15
+ | `zbk_check` | `python tools/zbk/zbk.py check --dir … --json` | 改过数据后确认没写坏 | **PASS/FAIL** + 错误列表 + 计数 |
16
+ | `zbk_open` | (无直接 CLI,等价于打开 `http://127.0.0.1:<port>/`) | 用户要看板地址 | URL + 服务是否健康 |
17
+
18
+ **插件只是入口,CLI 仍是事实源**:
19
+
20
+ - 真正干活的是 `tools/zbk/zbk.py`(init/serve/token/check),插件只负责在会话里**按顺序调用它**;
21
+ - 数据归项目自己的 `.zbk/` 目录(`config.json / data.json / feedback.json / flows.json / uploads/`),
22
+ 插件**不接管数据所有权、不复制数据、不改数据格式**;
23
+ - 四页静态资源(脑图/反馈栈/流程图/需求引导)仍由 `zbk.py serve` 自己提供,插件**不打包、不代理**页面;
24
+ - 不装本插件时,`tools/zbk/` 仍可独立用(与第 1 步的"可移植目录"定位一致)。
25
+
26
+ ---
27
+
28
+ ## 2. 目录结构
29
+
30
+ ```text
31
+ tools/zbk/dsh-plugin/
32
+ ├── package.json # name=dsh-plugin-zbk;type=module;main/exports;peerDependencies;dsh.bundle.patch
33
+ ├── cordis.patch.yml # 把插件行 insert 进 loader 树(id/name = dsh-plugin-zbk)
34
+ ├── lib/
35
+ │ └── index.js # host 入口(ESM):导出 name / inject / apply,注册 4 个工具
36
+ ├── README.md # 本文件(含安装/安全/自检/实施记录)
37
+ └── PLUGIN.md # 指针:正文只维护 README.md(照抄本机插件示例惯例)
38
+ ```
39
+
40
+ 三处一致性(loader 身份):`package.json.name` = `cordis.patch.yml` 的 `id`/`name` = `lib/index.js` 导出的 `name`
41
+ = **`dsh-plugin-zbk`**。
42
+
43
+ ---
44
+
45
+ ## 3. 运行时要求
46
+
47
+ | 依赖 | 要求 | 说明 |
48
+ |---|---|---|
49
+ | DSH | `>= 0.2.0-0`(见 peerDependencies) | 用 `@deepseek-ai/dsh-tools` 的 `defineTool` + `ctx.tools.register` |
50
+ | Cordis | `^4.0.5`(见 peerDependencies) | 插件框架 |
51
+ | Node.js | 18+(DSH host 自带) | 只用 `node:` 内置模块 + 全局 `fetch`/`AbortSignal` |
52
+ | Python | 3.8+(只用标准库) | 调用 `zbk.py`;默认 Windows=`python`、其它=`python3`(可用 `ZBK_PYTHON` 覆盖) |
53
+ | 项目文件 | 项目内有 `tools/zbk/zbk.py` | 找不到时插件报可读错误并提示 `project_dir` / `ZBK_ZBK_PY` |
54
+
55
+ **无第三方运行时依赖**:`dependencies: {}`,不声明 `Config`(不引 schemastery/zod),不引 React/client 产物。
56
+
57
+ ---
58
+
59
+ ## 4. 安装步骤(截图级;**本卡未执行安装**)
60
+
61
+ > 安装动作由总控/项目方做;本卡只交付脚手架 + 说明。安装会改 `~/.dsh/profiles/<profile>/`,
62
+ > 属"动用户环境",本卡红线不碰。
63
+
64
+ ### 4.1 推荐:Web 插件页 / `plugin_manager` 工具(本地绝对路径)
65
+
66
+ 1. 确认本包目录的**绝对路径**,例如:
67
+ `D:/deepseek harness/zhibao/tools/zbk/dsh-plugin`
68
+ (plugin manager 的本地路径 spec **必须绝对**,`file:`/`link:` 前缀可带可不带)。
69
+ 2. 在 DSH 的 **Plugins** 页选择安装并粘贴上述绝对路径;或在会话里调用 `plugin_manager` 工具:
70
+ ```json
71
+ {"action": "install_bundle", "target": "D:/deepseek harness/zhibao/tools/zbk/dsh-plugin"}
72
+ ```
73
+ 3. 安装器会(按 `@deepseek-ai/dsh-plugin-manager` 源码行为):
74
+ - 读本包 `package.json`(`dsh.bundle.patch` 必须存在,否则拒绝安装);
75
+ - 用 profile 的 pnpm 以本地路径依赖安装本包(`pnpm add <路径>`,save 本地路径);
76
+ - 校验 `engines.dsh` / `@deepseek-ai/dsh*` peer 与运行版本(本包 peer 用下界 `>=0.2.0-0`,见第 9 节);
77
+ - 把 `dsh-plugin-zbk` 加入 profile `package.json` 的 `dsh.profile.bundles` 并**默认启用**;
78
+ - profile 的 `cordis.patch.yml` **不需要手工编辑**:bundle 的补丁层会自动插入插件行。
79
+ 4. 无 HMR 的运行实例会提示 **restart-required** → 重启 DSH 后生效(Web 源/开发桌面支持热更新时可即时生效)。
80
+
81
+ ### 4.2 CLI(存在,但本卡**未实测语法**)
82
+
83
+ `dsh plugin` 支持安装本地路径(源码中 install 失败提示即 `run 'dsh plugin install'`);预期形如
84
+ `dsh plugin --profile desktop install <绝对路径>`。**本机 PATH 无 `dsh` 可执行文件,未实测**,以 §4.1 为准。
85
+
86
+ ### 4.3 本地开发(link:)
87
+
88
+ DSH Source Web / development Desktop 提供 `link:<绝对 workspace 包目录>` 形式;本包是纯 JS(无 build),
89
+ link 后重启 DSH 即可。修改本包后需重启/热更新,无编译步骤。
90
+
91
+ ### 4.4 安装后自检
92
+
93
+ - 会话里问"用 zbk 工具初始化看板"→ 模型应能调 `zbk_init`;
94
+ - 或在 Web Plugins 页看到 **dsh-plugin-zbk** 已启用、无 error;
95
+ - 若插件加载失败:Plugins 页会显示 error(bundle 列 `error` 字段),先看 DSH 日志里
96
+ `zbk: host plugin loaded` 是否出现。
97
+
98
+ ---
99
+
100
+ ## 5. 启用 / 禁用 / 卸载
101
+
102
+ | 操作 | 做法 | 影响 |
103
+ |---|---|---|
104
+ | 禁用/启用 | Plugins 页 toggle,或 `plugin_manager` `set_plugin`(`enabled:false/true`) | 只改 profile `cordis.patch.yml` 中该行的 `disabled`,不删包 |
105
+ | 卸载 | `plugin_manager` `remove_bundle`,target=`dsh-plugin-zbk` | 从 profile `package.json` 与 `dsh.profile.bundles` 移除;**项目 `.zbk/` 数据不受影响** |
106
+ | 手工兜底 | 从 profile `package.json` 依赖与 `dsh.profile.bundles` 删 `dsh-plugin-zbk`;必要时在用户 patch 层加 `- id: dsh-plugin-zbk\n disabled: true` | 与 CLI/管理器等价,不推荐手改 |
107
+
108
+ > 卸载只影响"能不能在会话里调 4 个工具";`tools/zbk/` 与项目 `.zbk/` 数据完全独立。
109
+
110
+ ---
111
+
112
+ ## 6. 配置(无 Config 块;全部走工具参数 / 环境变量)
113
+
114
+ 工具参数(每次调用可选):
115
+
116
+ | 参数 | 用于 | 说明 |
117
+ |---|---|---|
118
+ | `project_dir` | 全部 | 项目根目录(其下应有 `tools/zbk/zbk.py`);省略时按 环境变量 → 会话工作目录 → 进程 cwd |
119
+ | `data_dir` | 全部 | `.zbk` 数据目录,默认 `<project_dir>/.zbk`,也接受绝对路径 |
120
+ | `port` | init/serve/open | 端口;serve/open 省略时读 `config.json`,再缺省 8810 |
121
+ | `host` | serve | 绑定地址;默认读 `config.json`(通常 127.0.0.1);**0.0.0.0 需自备反代与鉴权** |
122
+ | `force` | init | 数据目录非空时强制重置(会丢弃现有数据并生成新令牌) |
123
+
124
+ 环境变量(DSH 进程级;适用"不想每次传参"的场景):
125
+
126
+ | 变量 | 作用 |
127
+ |---|---|
128
+ | `ZBK_PROJECT_DIR` | 默认项目根目录 |
129
+ | `ZBK_ZBK_PY` | `zbk.py` 绝对路径(找不到 `tools/zbk/zbk.py` 时用) |
130
+ | `ZBK_ROOT` | 工具包根目录(其下或 `tools/zbk/` 下有 `zbk.py`) |
131
+ | `ZBK_PYTHON` | Python 可执行文件(默认 Windows=`python`、其它=`python3`) |
132
+
133
+ `zbk.py` 定位顺序:`ZBK_ZBK_PY` → `ZBK_ROOT` 下两种布局 → 从 `project_dir` 向上最多 6 层找
134
+ `tools/zbk/zbk.py`、`zbk/zbk.py`。全找不到时报错里会列出**所有已查找路径**。
135
+
136
+ 子进程固定注入 `PYTHONIOENCODING=utf-8`、`PYTHONUTF8=1`、`PYTHONUNBUFFERED=1`
137
+ (Windows 下 Python 管道输出默认走 locale 代码页,不强制 UTF-8 会把"一次性令牌/端口"解析坏——本卡实测踩过)。
138
+
139
+ ---
140
+
141
+ ## 7. 安全
142
+
143
+ - **只绑本机**:`zbk.py serve` 默认 `127.0.0.1`;插件只在用户显式传 `host` 时才改绑定,
144
+ 且 README/工具描述都提示公网需自备反代 + 访问控制。
145
+ - **令牌**:`init` 生成随机编辑令牌,CLI 只打印一次;插件把令牌作为工具返回值交给会话
146
+ (等同用户在本机终端看到一次),**插件自身不写任何日志/文件**;令牌仍只存
147
+ `<数据目录>/config.json`。请把 `.zbk/` 加入项目 `.gitignore`(含令牌与业务数据)。
148
+ - **写权限模型不变**:页面/API 的鉴权仍是 `X-ZB-Map-Token`,插件不绕过、不改鉴权;
149
+ 未配置令牌时写操作 fail-closed(503/401 由 zbk.py 决定)。
150
+ - **无 shell 拼接**:调用 Python 用 `spawn(cmd, [args…])` 数组参数(无 shell),路径来自参数/环境变量。
151
+ - **上传文件不解析不执行**:由 zbk.py 既有实现负责(按扩展名落盘 `<数据目录>/uploads/`)。
152
+ - **进程不托管**:`zbk_serve` 以 detached 方式起本地服务,DSH 退出后**可能仍在运行**;
153
+ 停/重启由用户负责(插件不持有 pidfile、不自动清理)。这是有意的取舍,见第 9 节风险。
154
+ - **不做公网暴露**:本插件只面对本机会话;公网发布看板是用户侧反代/HTTPS 的事,不在本包范围。
155
+
156
+ ---
157
+
158
+ ## 8. 自检(本卡真跑证据)
159
+
160
+ 环境:Windows 10.0.26200 · Node **v24.19.0**(系统)/ v24.21.0(DSH 内置)· Python 3.12.10 · npm 11.x。
161
+
162
+ ### 8.1 语法:`node --check`
163
+
164
+ ```text
165
+ $ cd "D:\deepseek harness\zhibao\tools\zbk\dsh-plugin"
166
+ $ node --check lib\index.js ; echo $LASTEXITCODE
167
+ EXIT_CHECK=0
168
+ $ & "C:\Users\hrj\.dsh\dsh-runtimes\dsh-primary-runtime\dependencies\node\bin\node.exe" --check lib\index.js
169
+ EXIT_CHECK_BUNDLED=0
170
+ ```
171
+
172
+ ### 8.2 打包:`npm pack --dry-run`
173
+
174
+ 见 §8.6(只含预期 5 个文件:package.json / cordis.patch.yml / lib/index.js / README.md / PLUGIN.md;
175
+ 不产生实际 tgz)。
176
+
177
+ ### 8.3 脱离 DSH 运行时 import:给出明确报错(符合预期)
178
+
179
+ ```text
180
+ $ node -e "import(require('node:url').pathToFileURL('.../lib/index.js').href).catch(e=>{console.log(e.code);console.log(e.message.split('\n')[0])})"
181
+ ERR_MODULE_NOT_FOUND
182
+ Cannot find package '@deepseek-ai/dsh-tools' imported from D:\deepseek harness\zhibao\tools\zbk\dsh-plugin\lib\index.js
183
+ ```
184
+
185
+ ⇒ 插件依赖 DSH 运行时提供的 `@deepseek-ai/dsh-tools`(peer,由安装器提供、按 app-boot 的
186
+ "两锚点解析"从 DSH 安装目录解析);**不是可独立 `node` 运行的库**。
187
+
188
+ ### 8.4 stub 运行时冒烟(真 python + 真 zbk.py,跑完删除,不入库)
189
+
190
+ 用临时目录伪造 `node_modules/@deepseek-ai/dsh-tools`(`defineTool(d)=>d`)+ 假 `ctx`,
191
+ 导入本插件并**逐个执行 4 个工具的 `execute`**:
192
+ init(新目录)→ check(PASS)→ 写坏 data.json → check(FAIL)→ init --force → open(未起服务)→
193
+ serve(真起服务 + 健康检查)→ open(健康)→ 传一个没有 `tools/zbk` 的目录验证报错可读。
194
+
195
+ ```text
196
+ $ node run.mjs
197
+ ✓ 导出 name 与包名一致
198
+ ✓ inject 含 tools/workingDirectory
199
+ ✓ 注册 4 个工具
200
+ ✓ zbk_init 有 description/parameters/output/execute
201
+ ✓ zbk_serve 有 description/parameters/output/execute
202
+ ✓ zbk_check 有 description/parameters/output/execute
203
+ ✓ zbk_open 有 description/parameters/output/execute
204
+ ✓ zbk_init ok
205
+ ✓ zbk_init 返回一次性令牌
206
+ ✓ zbk_init 返回端口
207
+ ✓ init 令牌与 config.json 一致
208
+ ✓ zbk_check PASS
209
+ ✓ zbk_check 坏数据 FAIL
210
+ ✓ zbk_init force 重置
211
+ ✓ zbk_open 返回 URL(未起服务)
212
+ ✓ zbk_serve 启动或已运行
213
+ ✓ zbk_serve 返回 URL
214
+ ✓ zbk_open 服务健康
215
+ ✓ 找不到 zbk.py 时报错可读
216
+ (已结束临时 serve pid=25280)
217
+ SMOKE PASS
218
+ EXIT=0
219
+ ```
220
+
221
+ ### 8.5 结构断言(静态)
222
+
223
+ - `package.json`: `name=dsh-plugin-zbk`、`type=module`、`main=lib/index.js`、`dsh.bundle.patch=./cordis.patch.yml`、`dependencies={}`;
224
+ - `cordis.patch.yml`: 顶层 `insert` 一项,`id=name=dsh-plugin-zbk`;用 PyYAML 解析实测:
225
+ `[{'insert': [{'id': 'dsh-plugin-zbk', 'name': 'dsh-plugin-zbk'}]}]`;
226
+ - `lib/index.js`: `export { apply, inject, name }`,`name="dsh-plugin-zbk"`,`inject=["tools","workingDirectory"]`;
227
+ - 三处一致(脚本断言 + 冒烟输出第一行)。
228
+
229
+ ### 8.6 `npm pack --dry-run` 输出
230
+
231
+ ```text
232
+ $ cd "D:\deepseek harness\zhibao\tools\zbk\dsh-plugin"
233
+ $ npm pack --dry-run
234
+ npm notice package: dsh-plugin-zbk@0.1.0
235
+ npm notice Tarball Contents
236
+ npm notice 240B PLUGIN.md
237
+ npm notice 18.8kB README.md
238
+ npm notice 778B cordis.patch.yml
239
+ npm notice 23.2kB lib/index.js
240
+ npm notice 1.9kB package.json
241
+ npm notice Tarball Details
242
+ npm notice name: dsh-plugin-zbk
243
+ npm notice version: 0.1.0
244
+ npm notice filename: dsh-plugin-zbk-0.1.0.tgz
245
+ npm notice package size: 16.2 kB
246
+ npm notice unpacked size: 44.9 kB
247
+ npm notice shasum: e1d17c9e4bb9b4194d403249f3844b2185f5c484
248
+ npm notice integrity: sha512-pKnEIoxnRSo+m[...]WOOiYjKyVjv7Q==
249
+ npm notice total files: 5
250
+ ```
251
+
252
+ `--dry-run` 不落 tgz(目录内确认无 `dsh-plugin-zbk-0.1.0.tgz`)。
253
+
254
+ ---
255
+
256
+ ## 9. 未验证项 / 已知风险(本卡明确声明,**未装 profile**)
257
+
258
+ 1. **未安装进任何 DSH profile**(红线):未验证 loader 实际加载本包、peer 解析、4 个工具出现在模型工具集。
259
+ 上述结构/工具实现是按本机现成插件(`dsh-plugin-control-your-development`)与 DSH 内置
260
+ `@deepseek-ai/dsh-tool-todo` / `dsh-tool-present` 源码/README 的 API 写的,不是猜 API。
261
+ 2. **未发布 npm**:`plugin_manager` 从 registry 安装的路径未验证;本地绝对路径 spec 已按
262
+ `@deepseek-ai/dsh-plugin-manager` 的 `parseInstallSpec`(本地路径必须绝对)设计。
263
+ 3. **`dsh plugin install` CLI 语法未实测**(本机 PATH 无 `dsh`)。
264
+ 4. **未让真模型调用这 4 个工具**:工具 schema/description 只做静态校验 + stub execute 真跑。
265
+ 5. **`workingDirectory` 服务是 inject 必需项**:极简 profile 若没有该服务,插件不会加载
266
+ (桌面 profile 已有;见 `@deepseek-ai/dsh-working-directory`)。
267
+ 6. **zbk_serve 不托管进程**:detached 服务在 DSH 退出后可能残留;端口占用/多实例未做防护
268
+ (`zbk.py` 自身单进程文件锁只保护数据写;端口冲突会由 health 探测发现并在提示里给出换端口建议)。
269
+ 7. **未在 Linux/macOS 实测**:Python 候选(`python3`)、路径分隔符已按 `node:path` 处理,但只有 Windows 真跑。
270
+ 8. **`json` 类型**:`zbk_check` 的 `stats` 用 DSH schema DSL 的 author-only `json` 类型(按 dsh-tools README
271
+ 的 schema 支持列表),未在真 registry 里校验输出 schema。
272
+ 9. **工具名冲突**:若用户已装同名工具(zbk_init 等)会被 later registration 覆盖/报错,属 DSH 工具注册语义,
273
+ 本包不做改名开关。
274
+
275
+ ---
276
+
277
+ ## 10. 实施记录(六段式,2026-11-11)
278
+
279
+ > 任务来源:DSH 共享任务板 `task-13`《工具包第2步 dsh-plugin-zbk 插件脚手架》。
280
+ > 上游:`tools/zbk/` 第 1 步(commit `13383d28`,`selftest.py` 16/16,全量目录说明见 `tools/zbk/README.md`)。
281
+
282
+ ### ① 背景与范围
283
+
284
+ - **背景**:`tools/zbk` 已能独立用(CLI + 四页),但每次要在项目里手动跑 `init/serve`;
285
+ 目标是包成 DSH 插件,让任意 DSH 会话一句话拉起看板。
286
+ - **范围**:只新建 `tools/zbk/dsh-plugin/**`(package.json / cordis.patch.yml / lib/index.js / README.md / PLUGIN.md);
287
+ 不改 `tools/zbk` 现有文件、不改 `~/.dsh` profile、**不安装插件、不发布 npm**。
288
+ - **事实源**:本机现成插件 `C:\Users\hrj\.dsh\profiles\desktop\node_modules\dsh-plugin-control-your-development\`
289
+ (package.json / cordis.patch.yml / PLUGIN.md 结构)、DSH 内置 `@deepseek-ai/dsh-tool-todo` /
290
+ `dsh-tool-present` 的 `defineTool` 用法、`@deepseek-ai/dsh-app-boot` 的 profile/bundle 文档注释。
291
+
292
+ ### ② 做了什么(做了什么 + 能力地图)
293
+
294
+ | 文件 | 内容 |
295
+ |---|---|
296
+ | `package.json` | `name=dsh-plugin-zbk`、`type=module`、`main/exports`、`files`、`peerDependencies`(@deepseek-ai/cordis ^4.0.5;dsh-tools/dsh-working-directory `>=0.2.0-0`)、`dependencies:{}`、`dsh.bundle.patch` |
297
+ | `cordis.patch.yml` | `insert` 一行:`id=name=dsh-plugin-zbk`;无 config 块(本插件不声明 Config) |
298
+ | `lib/index.js` | host ESM:`name` / `inject=["tools","workingDirectory"]` / `apply`;注册 4 个 `defineTool` |
299
+ | `README.md` / `PLUGIN.md` | 安装/启用/卸载/安全/配置/自检/未验证项 + 本六段记录;PLUGIN.md 为指针(遵循示例惯例,正文只维护 README 一份) |
300
+
301
+ **4 个工具的实现要点**:
302
+
303
+ - `zbk_init`:`zbk.py init --dir <data> [--port] [--force]` → 解析「一次性令牌/端口」→ 返回并提示用户立即保存;
304
+ - `zbk_serve`:先 `/health` 探测;未运行则 **detached** 起 `serve` 并轮询 `/health`(≤3s),返回 URL/pid;
305
+ - `zbk_check`:`check --json` → 解析 `{ok,errors,stats}` → PASS/FAIL 摘要 + 错误列表;
306
+ - `zbk_open`:读 `config.json` 端口 → 探 `/health` → 返回 `http://127.0.0.1:<port>/`(服务未起也返回 URL + 提示)。
307
+
308
+ **能力地图更新(AGENTS §10.1b)**:本卡只新增 DSH 插件入口,**未改任何既有生产能力/端点/接线**;
309
+ `docs/代码地图-*.md` 无需更新(`tools/zbk` 属工具包,不在地图覆盖范围)。若总控希望收录,
310
+ 建议在工具类地图加一条:「zbk 看板工具包 + DSH 插件 dsh-plugin-zbk(`tools/zbk/dsh-plugin`)——宿主入口 4 工具;
311
+ 接线状态:脚手架已交付、未安装」。
312
+
313
+ ### ③ 部署台账
314
+
315
+ **不适用**——未部署、未安装插件、未改 `~/.dsh` profile(红线);无设备/二进制变更,父仓 `docs/部署台账.md` 无新增行。
316
+
317
+ ### ④ 验收证据
318
+
319
+ - 语法/打包/import 失败/stub 冒烟/结构断言:见 §8(`node --check` exit 0、`npm pack --dry-run` 只含预期文件、
320
+ 冒烟 19/19 PASS);
321
+ - 真跑覆盖:`zbk_init`(新目录/force)、`zbk_check`(PASS/FAIL 负控)、`zbk_open`(未起/已起)、
322
+ `zbk_serve`(真起服务 + health)、缺 `zbk.py` 的可读报错;
323
+ - 红线自查:本卡只新增 `tools/zbk/dsh-plugin/**` 五个文件;未安装插件、未改 profile、未发布 npm;
324
+ 提交逐路径 `git add --`。
325
+
326
+ ### ⑤ 红项 / 遗留
327
+
328
+ - 无红项(脚手架层面)。未验证项 9 条见 §9,其中最需要总控在安装时确认的是:
329
+ 1) 本地路径安装的 pnpm 行为与 peer 检查;2) 重启后 4 个工具是否出现在模型工具集;3) `workingDirectory` 注入可用性。
330
+
331
+ ### ⑥ 下一步
332
+
333
+ 1. **总控/项目方决定是否安装**(本卡红线:安装归你们):按 §4.1 传绝对路径安装到目标 profile,
334
+ 重启后按 §4.4 自检,把"已安装/已验证"写回本 README 的状态行。
335
+ 2. 安装后**真模型调用验收**:让模型执行 `zbk_init`(临时项目)→ `zbk_serve` → `zbk_open` → 浏览器打开四页 →
336
+ `zbk_check`;把真实输出补进 §8.6/§10④。
337
+ 3. 若确认可用,再考虑:发布 npm(`publishConfig`/仓库地址待定)、加 `zbk_stop` 工具(需先解决进程托管语义)、
338
+ 或把四页也纳入插件静态资源(当前有意不做,保持 CLI 为事实源)。
339
+
340
+ ---
341
+
342
+ *创建:2026-11-11 · 执行:队友 `executor-b`(DSH Agent Team)· 卡:共享任务板 `task-13` · 未安装/未发布*
@@ -0,0 +1,11 @@
1
+ # 只插入本插件自己的一行;不替换任何官方组件。
2
+ # 行 id 与 name 必须等于 package.json 的 name,也必须等于 lib/index.js 导出的 name(三处一致)。
3
+ # 安装(plugin_manager install_bundle / `dsh plugin install`)会把本包加入 profile 的
4
+ # dsh.profile.bundles 并默认启用;本文件只负责把插件挂到 loader 树上。
5
+ - insert:
6
+ - id: dsh-plugin-zbk
7
+ name: dsh-plugin-zbk
8
+ # 用户在自己的 profile patch 层覆盖本行时,config 是「整块替换」而不是深合并。
9
+ # 本插件不声明 Config:配置一律走工具参数(project_dir / data_dir / port)或环境变量
10
+ # (ZBK_PROJECT_DIR / ZBK_ZBK_PY / ZBK_PYTHON),因此这里不需要 config 块。
11
+ # config: {}
package/lib/index.js ADDED
@@ -0,0 +1,592 @@
1
+ /**
2
+ * dsh-plugin-zbk —— 把 tools/zbk 可移植看板工具包暴露成 4 个 DSH 工具:
3
+ * zbk_init / zbk_serve / zbk_check / zbk_open
4
+ *
5
+ * 设计边界(与任务卡一致):
6
+ * - 插件只是「入口」:真正干活的仍是 zbk.py(init/serve/token/check),数据归项目自己的 .zbk/ 目录;
7
+ * - 只依赖 node: 内置模块 + 宿主提供的 @deepseek-ai/dsh-tools(defineTool);
8
+ * - 不声明 Config(避免给用户 profile 增装 schemastery);配置走工具参数 + 环境变量;
9
+ * - 不托管服务生命周期:zbk_serve 只负责后台拉起 + 健康探测,停服务由用户/系统负责;
10
+ * - 不复制/代理四页静态资源:URL 指向 zbk.py serve 自己起的本地服务。
11
+ *
12
+ * 环境变量(均可选):
13
+ * ZBK_PROJECT_DIR 默认项目目录(工具参数 project_dir 优先)
14
+ * ZBK_ZBK_PY zbk.py 的绝对路径(找不到 tools/zbk/zbk.py 时使用)
15
+ * ZBK_PYTHON 指定 Python 可执行文件(默认 Windows=python、其它=python3)
16
+ */
17
+
18
+ import { spawn } from "node:child_process";
19
+ import { existsSync, readFileSync, statSync } from "node:fs";
20
+ import { dirname, isAbsolute, join, resolve } from "node:path";
21
+
22
+ import { defineTool } from "@deepseek-ai/dsh-tools";
23
+
24
+ /** Stable Loader identity:须与 package.json name、cordis.patch.yml 的 id/name 三处一致。 */
25
+ const name = "dsh-plugin-zbk";
26
+
27
+ /** 本插件只注册工具;另用 workingDirectory 把会话工作目录作为默认项目目录。 */
28
+ const inject = ["tools", "workingDirectory"];
29
+
30
+ const DEFAULT_PORT = 8810;
31
+ const DEFAULT_DATA_DIR = ".zbk";
32
+ const RUN_TIMEOUT_MS = 60000;
33
+ const REQUEST_TIMEOUT_MS = 8000;
34
+
35
+ // ── 小工具 ────────────────────────────────────────────────────────────────
36
+
37
+ /** @param {number} ms */
38
+ function sleep(ms) {
39
+ return new Promise((done) => setTimeout(done, ms));
40
+ }
41
+
42
+ /**
43
+ * 解析项目目录:工具参数 > ZBK_PROJECT_DIR > 会话工作目录 > 进程 cwd。
44
+ * @param {{project_dir?: string}} args
45
+ * @param {{agent?: unknown, signal?: AbortSignal}} exec
46
+ * @param {any} ctx
47
+ */
48
+ async function resolveProjectDir(args, exec, ctx) {
49
+ const explicit = typeof args.project_dir === "string" ? args.project_dir.trim() : "";
50
+ if (explicit) return resolve(explicit);
51
+ const fromEnv = (process.env.ZBK_PROJECT_DIR || "").trim();
52
+ if (fromEnv) return resolve(fromEnv);
53
+ try {
54
+ if (exec && exec.agent !== undefined && ctx.workingDirectory !== undefined) {
55
+ const cwd = await ctx.workingDirectory.ensure(exec.agent, exec.signal);
56
+ if (typeof cwd === "string" && cwd.trim() !== "") return resolve(cwd);
57
+ }
58
+ } catch {
59
+ /* 工作目录服务不可用(或 programmatic 调用无 agent)→ 落进程 cwd */
60
+ }
61
+ return resolve(process.cwd());
62
+ }
63
+
64
+ /** @param {string} projectDir @param {string | undefined} dataDir */
65
+ function resolveDataDir(projectDir, dataDir) {
66
+ const raw = typeof dataDir === "string" ? dataDir.trim() : "";
67
+ return isAbsolute(raw) ? raw : resolve(projectDir, raw || DEFAULT_DATA_DIR);
68
+ }
69
+
70
+ /**
71
+ * 找 tools/zbk/zbk.py:显式 env → 环境根 → 从项目目录向上最多 6 层。
72
+ * @param {string} projectDir
73
+ * @returns {string}
74
+ */
75
+ function resolveZbkPy(projectDir) {
76
+ const candidates = [];
77
+ const explicit = (process.env.ZBK_ZBK_PY || "").trim();
78
+ if (explicit) candidates.push(resolve(explicit));
79
+ const envRoot = (process.env.ZBK_ROOT || "").trim();
80
+ if (envRoot) {
81
+ const root = resolve(envRoot);
82
+ candidates.push(join(root, "zbk.py"), join(root, "tools", "zbk", "zbk.py"));
83
+ }
84
+ let dir = resolve(projectDir);
85
+ for (let i = 0; i < 6; i += 1) {
86
+ candidates.push(join(dir, "tools", "zbk", "zbk.py"), join(dir, "zbk", "zbk.py"));
87
+ const up = dirname(dir);
88
+ if (up === dir) break;
89
+ dir = up;
90
+ }
91
+ for (const file of candidates) {
92
+ try {
93
+ if (existsSync(file) && statSync(file).isFile()) return file;
94
+ } catch {
95
+ /* 忽略不可读候选,继续找 */
96
+ }
97
+ }
98
+ throw new Error(
99
+ "找不到 zbk.py(zbk 工具包)。已查找:" +
100
+ candidates.join(" ; ") +
101
+ "。请在调用时传 project_dir(含 tools/zbk/zbk.py 的项目根目录)," +
102
+ "或设置环境变量 ZBK_ZBK_PY(zbk.py 绝对路径)/ ZBK_PROJECT_DIR。",
103
+ );
104
+ }
105
+
106
+ /** Python 候选(环境变量优先;其次平台默认,找不到再退另一个)。 */
107
+ function pythonCandidates() {
108
+ const explicit = (process.env.ZBK_PYTHON || "").trim();
109
+ if (explicit) return [explicit];
110
+ return process.platform === "win32" ? ["python", "py"] : ["python3", "python"];
111
+ }
112
+
113
+ /**
114
+ * 子进程环境:强制 UTF-8 输出。
115
+ * Windows 下 python 的 stdout 默认走 locale 代码页(cp936 等),Node 按 UTF-8 解码会乱码,
116
+ * 导致「一次性令牌 / 端口」解析失败;PYTHONIOENCODING + PYTHONUTF8 双保险。
117
+ */
118
+ function childEnv() {
119
+ return { ...process.env, PYTHONIOENCODING: "utf-8", PYTHONUTF8: "1", PYTHONUNBUFFERED: "1" };
120
+ }
121
+
122
+ /**
123
+ * 跑一次性 zbk.py 命令,捕获 stdout/stderr,60s 超时,支持协作取消。
124
+ * @param {string} python
125
+ * @param {string} zbkPy
126
+ * @param {string[]} argv
127
+ * @param {{cwd?: string, signal?: AbortSignal, timeoutMs?: number}} options
128
+ * @returns {Promise<{code: number, stdout: string, stderr: string}>}
129
+ */
130
+ function runZbk(python, zbkPy, argv, options = {}) {
131
+ const timeoutMs = options.timeoutMs ?? RUN_TIMEOUT_MS;
132
+ return new Promise((settle) => {
133
+ let child;
134
+ try {
135
+ child = spawn(python, [zbkPy, ...argv], {
136
+ cwd: options.cwd,
137
+ windowsHide: true,
138
+ env: childEnv(),
139
+ stdio: ["ignore", "pipe", "pipe"],
140
+ });
141
+ } catch (error) {
142
+ settle({ code: -1, stdout: "", stderr: String(error && error.message ? error.message : error) });
143
+ return;
144
+ }
145
+ let stdout = "";
146
+ let stderr = "";
147
+ let finished = false;
148
+ const finish = (code) => {
149
+ if (finished) return;
150
+ finished = true;
151
+ clearTimeout(timer);
152
+ settle({ code, stdout, stderr });
153
+ };
154
+ child.stdout.on("data", (chunk) => {
155
+ stdout += String(chunk);
156
+ });
157
+ child.stderr.on("data", (chunk) => {
158
+ stderr += String(chunk);
159
+ });
160
+ child.on("error", (error) => {
161
+ stderr += String(error && error.message ? error.message : error);
162
+ finish(-1);
163
+ });
164
+ child.on("close", (code) => finish(typeof code === "number" ? code : -1));
165
+ const timer = setTimeout(() => {
166
+ stderr += `\n[zbk-plugin] 命令超时(${timeoutMs}ms),已终止`;
167
+ try {
168
+ child.kill();
169
+ } catch {
170
+ /* 已退出 */
171
+ }
172
+ finish(-2);
173
+ }, timeoutMs);
174
+ if (options.signal) {
175
+ const abort = () => {
176
+ try {
177
+ child.kill();
178
+ } catch {
179
+ /* 已退出 */
180
+ }
181
+ };
182
+ if (options.signal.aborted) abort();
183
+ else options.signal.addEventListener("abort", abort, { once: true });
184
+ }
185
+ });
186
+ }
187
+
188
+ /**
189
+ * 带 Python 候选回退地跑 zbk.py(仅在 spawn ENOENT 时换下一个候选)。
190
+ * @param {string} zbkPy
191
+ * @param {string[]} argv
192
+ * @param {{cwd?: string, signal?: AbortSignal}} options
193
+ */
194
+ async function runZbkWithFallback(zbkPy, argv, options) {
195
+ let last = null;
196
+ for (const python of pythonCandidates()) {
197
+ // 先用 `zbk.py --help` 探测该解释器是否可执行,避免把 ENOENT 混进业务错误。
198
+ const probe = await runZbk(python, zbkPy, ["--help"], { ...options, timeoutMs: 15000 });
199
+ if (probe.code !== -1 || !/ENOENT|not found|cannot find/i.test(probe.stderr)) {
200
+ if (argv.length === 0) return { result: probe, python };
201
+ const result = await runZbk(python, zbkPy, argv, options);
202
+ return { result, python };
203
+ }
204
+ last = probe;
205
+ }
206
+ throw new Error(
207
+ "找不到可用的 Python 解释器(已尝试:" +
208
+ pythonCandidates().join(", ") +
209
+ ")。请设置环境变量 ZBK_PYTHON 指向 python 可执行文件。" +
210
+ (last ? " 最后一次错误:" + last.stderr.trim() : ""),
211
+ );
212
+ }
213
+
214
+ /** @param {string} projectDir @param {string | undefined} dataDir */
215
+ function readConfig(dataDir) {
216
+ try {
217
+ const raw = readFileSync(join(dataDir, "config.json"), "utf8");
218
+ const parsed = JSON.parse(raw);
219
+ return parsed && typeof parsed === "object" ? parsed : null;
220
+ } catch {
221
+ return null;
222
+ }
223
+ }
224
+
225
+ /** 取 init 输出里的「只显示一次」令牌。 */
226
+ function parseToken(stdout) {
227
+ const match = /保存)[::]\s*(\S+)/.exec(stdout || "");
228
+ return match ? match[1] : "";
229
+ }
230
+
231
+ /** 取 init 输出里的端口。 */
232
+ function parseInitPort(stdout) {
233
+ const match = /端口[::]\s*(\d+)/.exec(stdout || "");
234
+ return match ? Number(match[1]) : undefined;
235
+ }
236
+
237
+ /** 从可能夹带日志的 stdout 里抠出第一个完整 JSON 对象(zbk.py check --json)。 */
238
+ function extractJsonObject(text) {
239
+ const source = String(text || "");
240
+ const start = source.indexOf("{");
241
+ const end = source.lastIndexOf("}");
242
+ if (start === -1 || end === -1 || end <= start) return null;
243
+ try {
244
+ return JSON.parse(source.slice(start, end + 1));
245
+ } catch {
246
+ return null;
247
+ }
248
+ }
249
+
250
+ /** 探测本地 zbk 服务 /health;返回健康体或 null。 */
251
+ async function probeHealth(port, timeoutMs) {
252
+ const deadline = Date.now() + (timeoutMs || 1500);
253
+ while (Date.now() < deadline) {
254
+ try {
255
+ const response = await fetch(`http://127.0.0.1:${port}/health`, {
256
+ signal: AbortSignal.timeout(600),
257
+ });
258
+ if (response.ok) return await response.json().catch(() => ({}));
259
+ } catch {
260
+ /* 还没起来 / 端口未监听,继续等 */
261
+ }
262
+ await sleep(150);
263
+ }
264
+ return null;
265
+ }
266
+
267
+ /**
268
+ * detached 起 zbk.py serve(不接管进程生命周期;host 退出后服务仍在)。
269
+ * @returns {number} pid(拿不到时 0)
270
+ */
271
+ function startDetachedServe(python, zbkPy, argv, cwd) {
272
+ const child = spawn(python, [zbkPy, ...argv], {
273
+ cwd,
274
+ detached: true,
275
+ windowsHide: true,
276
+ env: childEnv(),
277
+ stdio: "ignore",
278
+ });
279
+ child.unref();
280
+ return typeof child.pid === "number" ? child.pid : 0;
281
+ }
282
+
283
+ // ── 工具定义 ──────────────────────────────────────────────────────────────
284
+
285
+ /** 共用参数:project_dir / data_dir(写进每个工具的 parameters)。 */
286
+ const COMMON_PARAMETERS = {
287
+ project_dir: {
288
+ type: "string",
289
+ description:
290
+ "项目根目录(其下应有 tools/zbk/zbk.py)。省略时依次用 ZBK_PROJECT_DIR、会话工作目录、进程 cwd。",
291
+ },
292
+ data_dir: {
293
+ type: "string",
294
+ description: "zbk 数据目录,默认 <project_dir>/.zbk;也接受绝对路径。",
295
+ },
296
+ };
297
+
298
+ /** @param {any} ctx */
299
+ function registerTools(ctx) {
300
+ ctx.tools.register(
301
+ defineTool({
302
+ name: "zbk_init",
303
+ description:
304
+ "初始化 zbk 可移植看板的数据目录(等价于 `python tools/zbk/zbk.py init --dir <项目>/.zbk`):生成空模板 data/feedback/flows + uploads/ + config.json(随机编辑令牌,只打印一次)。什么时候用:用户要给某个项目开 zbk 看板/反馈栈/流程图,且该项目还没有 .zbk 数据目录时。返回数据目录、端口和一次性令牌——务必提醒用户立刻保存令牌。",
305
+ parameters: {
306
+ project_dir: COMMON_PARAMETERS.project_dir,
307
+ data_dir: COMMON_PARAMETERS.data_dir,
308
+ port: { type: "integer", description: "默认端口(写进 config.json),默认 8810。" },
309
+ force: { type: "boolean", description: "数据目录非空时强制重置为空模板并生成新令牌(会丢弃现有数据)。" },
310
+ },
311
+ output: {
312
+ schema: {
313
+ type: "object",
314
+ additionalProperties: false,
315
+ properties: {
316
+ ok: { type: "boolean", required: true },
317
+ exit_code: { type: "integer", required: true },
318
+ data_dir: { type: "string", required: true },
319
+ port: { type: "integer" },
320
+ token: { type: "string" },
321
+ summary: { type: "string", required: true },
322
+ hint: { type: "string" },
323
+ stdout: { type: "string", required: true },
324
+ stderr: { type: "string", required: true },
325
+ },
326
+ },
327
+ render: (_args, value) => [
328
+ {
329
+ type: "text",
330
+ text:
331
+ value.summary +
332
+ (value.token ? `\n令牌(只显示一次,请让用户立即保存):${value.token}` : "") +
333
+ (value.hint ? `\n${value.hint}` : ""),
334
+ },
335
+ ],
336
+ },
337
+ async execute(args, exec) {
338
+ const projectDir = await resolveProjectDir(args, exec, ctx);
339
+ const zbkPy = resolveZbkPy(projectDir);
340
+ const dataDir = resolveDataDir(projectDir, args.data_dir);
341
+ const argv = ["init", "--dir", dataDir];
342
+ if (Number.isInteger(args.port) && args.port > 0) argv.push("--port", String(args.port));
343
+ if (args.force === true) argv.push("--force");
344
+ const { result } = await runZbkWithFallback(zbkPy, argv, {
345
+ cwd: projectDir,
346
+ signal: exec.signal,
347
+ });
348
+ const ok = result.code === 0;
349
+ const token = ok ? parseToken(result.stdout) : "";
350
+ return {
351
+ ok,
352
+ exit_code: result.code,
353
+ data_dir: dataDir,
354
+ port: parseInitPort(result.stdout) ?? (Number.isInteger(args.port) ? args.port : DEFAULT_PORT),
355
+ token,
356
+ summary: ok
357
+ ? `zbk 初始化成功:${dataDir}(端口 ${parseInitPort(result.stdout) ?? DEFAULT_PORT})`
358
+ : `zbk 初始化失败(退出码 ${result.code})`,
359
+ hint: ok
360
+ ? "令牌只显示这一次:请让用户立即保存;接着调 zbk_serve 起服务、zbk_open 取地址。"
361
+ : result.code === 2
362
+ ? "数据目录已存在且非空;确认要重置时再传 force=true(会生成新令牌并清空空模板数据)。"
363
+ : "检查 python 与 zbk.py 路径:可传 project_dir,或设置 ZBK_ZBK_PY / ZBK_PYTHON。",
364
+ stdout: result.stdout,
365
+ stderr: result.stderr,
366
+ };
367
+ },
368
+ }),
369
+ );
370
+
371
+ ctx.tools.register(
372
+ defineTool({
373
+ name: "zbk_serve",
374
+ description:
375
+ "在后台启动 zbk 看板服务(等价于 `python tools/zbk/zbk.py serve --dir <项目>/.zbk --port <端口>`,只绑 127.0.0.1):先探测 /health,未运行则 detached 拉起并等待健康响应,返回访问 URL。什么时候用:用户要在本机打开看板/反馈栈/流程图页面前。插件不托管进程生命周期(停服务/重启由用户负责)。",
376
+ parameters: {
377
+ project_dir: COMMON_PARAMETERS.project_dir,
378
+ data_dir: COMMON_PARAMETERS.data_dir,
379
+ port: { type: "integer", description: "服务端口;省略时读 .zbk/config.json,再缺省 8810。" },
380
+ host: {
381
+ type: "string",
382
+ description: "绑定地址(默认读 config.json,通常 127.0.0.1);填 0.0.0.0 需用户自备反代与鉴权。",
383
+ },
384
+ },
385
+ output: {
386
+ schema: {
387
+ type: "object",
388
+ additionalProperties: false,
389
+ properties: {
390
+ ok: { type: "boolean", required: true },
391
+ started: { type: "boolean", required: true },
392
+ already_running: { type: "boolean", required: true },
393
+ url: { type: "string", required: true },
394
+ port: { type: "integer", required: true },
395
+ pid: { type: "integer" },
396
+ summary: { type: "string", required: true },
397
+ hint: { type: "string" },
398
+ },
399
+ },
400
+ render: (_args, value) => [
401
+ {
402
+ type: "text",
403
+ text: value.summary + (value.hint ? `\n${value.hint}` : ""),
404
+ },
405
+ ],
406
+ },
407
+ async execute(args, exec) {
408
+ const projectDir = await resolveProjectDir(args, exec, ctx);
409
+ const zbkPy = resolveZbkPy(projectDir);
410
+ const dataDir = resolveDataDir(projectDir, args.data_dir);
411
+ const config = readConfig(dataDir);
412
+ const configured = config && Number.isInteger(config.port) ? config.port : DEFAULT_PORT;
413
+ const port = Number.isInteger(args.port) && args.port > 0 ? args.port : configured;
414
+
415
+ const existing = await probeHealth(port, 700);
416
+ if (existing) {
417
+ return {
418
+ ok: true,
419
+ started: false,
420
+ already_running: true,
421
+ url: `http://127.0.0.1:${port}/`,
422
+ port,
423
+ pid: 0,
424
+ summary: `zbk 服务已在运行:http://127.0.0.1:${port}/`,
425
+ hint: "用 zbk_open 取地址;需要重启时先停掉现有进程再调本工具。",
426
+ };
427
+ }
428
+
429
+ const argv = ["serve", "--dir", dataDir, "--port", String(port)];
430
+ if (typeof args.host === "string" && args.host.trim() !== "") {
431
+ argv.push("--host", args.host.trim());
432
+ }
433
+ let pid = 0;
434
+ let errorText = "";
435
+ for (const python of pythonCandidates()) {
436
+ try {
437
+ pid = startDetachedServe(python, zbkPy, argv, projectDir);
438
+ break;
439
+ } catch (error) {
440
+ errorText = String(error && error.message ? error.message : error);
441
+ }
442
+ }
443
+ const health = pid > 0 || errorText === "" ? await probeHealth(port, 3000) : null;
444
+ const ok = Boolean(health);
445
+ return {
446
+ ok,
447
+ started: ok,
448
+ already_running: false,
449
+ url: `http://127.0.0.1:${port}/`,
450
+ port,
451
+ pid,
452
+ summary: ok
453
+ ? `zbk 服务已启动:http://127.0.0.1:${port}/`
454
+ : `zbk 服务启动失败(3s 内 /health 未响应)`,
455
+ hint: ok
456
+ ? "用 zbk_open 取地址后让用户用浏览器打开;停/重启由用户手动管理(插件不托管进程)。"
457
+ : `可手动执行:python "${zbkPy}" serve --dir "${dataDir}" --port ${port}` +
458
+ (errorText ? `(spawn 错误:${errorText})` : ""),
459
+ };
460
+ },
461
+ }),
462
+ );
463
+
464
+ ctx.tools.register(
465
+ defineTool({
466
+ name: "zbk_check",
467
+ description:
468
+ "校验 zbk 数据(等价于 `python tools/zbk/zbk.py check --dir <项目>/.zbk --json`):data.json 结构合法性 + 反馈/流程计数;坏数据 FAIL 且退出码非 0。什么时候用:用户改过看板/反馈数据后,或保存后要确认数据没坏时。返回 PASS/FAIL 摘要与错误列表。",
469
+ parameters: {
470
+ project_dir: COMMON_PARAMETERS.project_dir,
471
+ data_dir: COMMON_PARAMETERS.data_dir,
472
+ },
473
+ output: {
474
+ schema: {
475
+ type: "object",
476
+ additionalProperties: false,
477
+ properties: {
478
+ ok: { type: "boolean", required: true },
479
+ pass: { type: "boolean", required: true },
480
+ summary: { type: "string", required: true },
481
+ errors: {
482
+ type: "array",
483
+ required: true,
484
+ items: { type: "string" },
485
+ },
486
+ stats: { type: "json" },
487
+ hint: { type: "string" },
488
+ },
489
+ },
490
+ render: (_args, value) => [
491
+ {
492
+ type: "text",
493
+ text:
494
+ value.summary +
495
+ (value.errors.length ? "\n错误:\n- " + value.errors.join("\n- ") : "") +
496
+ (value.hint ? `\n${value.hint}` : ""),
497
+ },
498
+ ],
499
+ },
500
+ async execute(args, exec) {
501
+ const projectDir = await resolveProjectDir(args, exec, ctx);
502
+ const zbkPy = resolveZbkPy(projectDir);
503
+ const dataDir = resolveDataDir(projectDir, args.data_dir);
504
+ const { result } = await runZbkWithFallback(zbkPy, ["check", "--dir", dataDir, "--json"], {
505
+ cwd: projectDir,
506
+ signal: exec.signal,
507
+ });
508
+ const parsed = extractJsonObject(result.stdout);
509
+ const pass = result.code === 0 && parsed !== null && parsed.ok === true;
510
+ const errors = Array.isArray(parsed && parsed.errors)
511
+ ? parsed.errors.map((item) => String(item))
512
+ : pass
513
+ ? []
514
+ : [result.stderr.trim() || `zbk.py check 退出码 ${result.code}`];
515
+ return {
516
+ ok: pass,
517
+ pass,
518
+ summary: pass ? "PASS:zbk 数据校验通过" : "FAIL:zbk 数据校验未通过",
519
+ errors,
520
+ stats: (parsed && parsed.stats) || {},
521
+ hint: pass
522
+ ? undefined
523
+ : "按错误项修 .zbk/data.json(或重新 init 空模板);结构与校验规则见 tools/zbk/README.md。",
524
+ };
525
+ },
526
+ }),
527
+ );
528
+
529
+ ctx.tools.register(
530
+ defineTool({
531
+ name: "zbk_open",
532
+ description:
533
+ "返回 zbk 看板的本机访问 URL(http://127.0.0.1:<port>/)并顺带探测服务是否在跑。什么时候用:用户要看板地址、准备在浏览器打开/贴给别人时。服务未启动时仍返回 URL,并提示先调 zbk_serve。",
534
+ parameters: {
535
+ project_dir: COMMON_PARAMETERS.project_dir,
536
+ data_dir: COMMON_PARAMETERS.data_dir,
537
+ port: { type: "integer", description: "覆盖端口;省略时读 .zbk/config.json,再缺省 8810。" },
538
+ },
539
+ output: {
540
+ schema: {
541
+ type: "object",
542
+ additionalProperties: false,
543
+ properties: {
544
+ ok: { type: "boolean", required: true },
545
+ url: { type: "string", required: true },
546
+ port: { type: "integer", required: true },
547
+ healthy: { type: "boolean", required: true },
548
+ summary: { type: "string", required: true },
549
+ hint: { type: "string" },
550
+ },
551
+ },
552
+ render: (_args, value) => [
553
+ {
554
+ type: "text",
555
+ text: value.summary + (value.hint ? `\n${value.hint}` : ""),
556
+ },
557
+ ],
558
+ },
559
+ async execute(args, exec) {
560
+ const projectDir = await resolveProjectDir(args, exec, ctx);
561
+ const dataDir = resolveDataDir(projectDir, args.data_dir);
562
+ const config = readConfig(dataDir);
563
+ const configured = config && Number.isInteger(config.port) ? config.port : DEFAULT_PORT;
564
+ const port = Number.isInteger(args.port) && args.port > 0 ? args.port : configured;
565
+ const health = await probeHealth(port, 1200);
566
+ return {
567
+ ok: true,
568
+ url: `http://127.0.0.1:${port}/`,
569
+ port,
570
+ healthy: Boolean(health),
571
+ summary: health
572
+ ? `zbk 看板地址:http://127.0.0.1:${port}/(服务健康)`
573
+ : `zbk 看板地址:http://127.0.0.1:${port}/(服务当前未响应)`,
574
+ hint: health
575
+ ? "用浏览器打开该地址;四页(脑图/反馈栈/流程图/需求引导)互相有导航。"
576
+ : "先调 zbk_serve,或手动执行 `python tools/zbk/zbk.py serve`。",
577
+ };
578
+ },
579
+ }),
580
+ );
581
+ }
582
+
583
+ /**
584
+ * Cordis 插件入口。
585
+ * @param {any} ctx
586
+ */
587
+ function apply(ctx) {
588
+ ctx.logger?.info?.("zbk: host plugin loaded (tools: zbk_init / zbk_serve / zbk_check / zbk_open)");
589
+ registerTools(ctx);
590
+ }
591
+
592
+ export { apply, inject, name };
package/package.json CHANGED
@@ -1,6 +1,56 @@
1
- {
2
- "name": "dsh-plugin-zbk",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
1
+ {
2
+ "name": "dsh-plugin-zbk",
3
+ "version": "0.1.0",
4
+ "description": "zbk 可移植看板工具包的 DSH 插件:会话内一句话 init / serve / check / open(插件只是入口,CLI 仍是事实源)",
5
+ "type": "module",
6
+ "main": "lib/index.js",
7
+ "exports": {
8
+ ".": "./lib/index.js",
9
+ "./cordis.patch.yml": "./cordis.patch.yml",
10
+ "./package.json": "./package.json"
11
+ },
12
+ "files": [
13
+ "lib",
14
+ "cordis.patch.yml",
15
+ "README.md",
16
+ "PLUGIN.md",
17
+ "LICENSE"
18
+ ],
19
+ "license": "MIT",
20
+ "peerDependencies": {
21
+ "@deepseek-ai/cordis": "^4.0.5",
22
+ "@deepseek-ai/dsh-tools": ">=0.2.0-0",
23
+ "@deepseek-ai/dsh-working-directory": ">=0.2.0-0"
24
+ },
25
+ "dependencies": {},
26
+ "//": [
27
+ "JSON 没有注释语法,说明按惯例放在 \"//\" 键里(npm/pnpm 忽略,不影响 npm pack / npm pkg get)。",
28
+ "peerDependencies 按 DSH 插件工厂惯例:只写 @deepseek-ai/* 前缀 + 下界;@deepseek-ai/dsh-* 由 DSH 运行时装(含 app.asar 内置副本)提供,profile 的 pnpm-workspace 设 autoInstallPeers:false,因此写 peer 不会真的装进用户 profile。",
29
+ "dependencies 必须为空:本插件只用 node: 内置模块 + 宿主提供的 @deepseek-ai/dsh-tools;不引 schemastery/zod(不声明 Config),避免给用户 profile 增装第三方运行时依赖。",
30
+ "本包无 client 产物:只有 host 工具(lib/index.js),因此 exports 不含 \"./client\";四页静态资源仍由 zbk.py 自带的 web/ 提供,插件不复制、不代理页面。",
31
+ "dsh.bundle.patch 必须存在:plugin_manager 的 installBundle 只接受带 bundle patch 的包;本文件把插件行插进 loader 树。"
32
+ ],
33
+ "dsh": {
34
+ "bundle": {
35
+ "patch": "./cordis.patch.yml"
36
+ }
37
+ },
38
+ "keywords": [
39
+ "dsh",
40
+ "dsh-plugin",
41
+ "deepseek-harness",
42
+ "zbk",
43
+ "看板",
44
+ "脑图",
45
+ "feedback"
46
+ ],
47
+ "engines": {
48
+ "dsh": ">=0.2.0-0"
49
+ },
50
+ "repository": {
51
+ "type": "git",
52
+ "url": "https://gitee.com/new-service/zb-kvm.git",
53
+ "directory": "tools/zbk/dsh-plugin"
54
+ },
55
+ "homepage": "https://zb.aiopc.space/map/"
56
+ }