dsh-armory 2.1.0 → 2.2.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.
@@ -1,184 +0,0 @@
1
- # 上游提案:stdio MCP transport 的 teardown 上界
2
-
3
- 这份文档回答一个问题:**Windows 上「MCP 子进程回收卡住」这件事,根因在哪一层的哪一行,该由谁修、修成什么样。**
4
-
5
- 它是给 `@modelcontextprotocol/client`(下文简称上游)维护者的技术提案,不是本插件的使用说明。日常使用请看 [README](../README.md) 与 [项目 MCP 生命周期](project-mcp-lifecycle.md)。
6
-
7
- - [问题陈述](#问题陈述)
8
- - [上游现状(贴证据)](#上游现状贴证据)
9
- - [可复现条件](#可复现条件)
10
- - [建议方案](#建议方案按优先级)
11
- - [对本项目的影响](#对本项目的影响)
12
- - [临时缓解(本侧已做)](#临时缓解本侧已做)
13
-
14
- ## 问题陈述
15
-
16
- Windows 上,stdio 传输的 MCP server 常常**自己再拉起子进程**:`npx` / `uvx` 这类包装器本身就是「一个 JS 进程去起真正的 server」,很多 server 内部还会再 spawn 语言运行时、helper、语言服务器。这些**孙进程默认继承父进程的标准句柄**,包括上游建的那两根管道(`stdin` / `stdout`)。
17
-
18
- 于是出现下面这条因果链:
19
-
20
- 1. 宿主决定回收某条连接,调 `transport.close()`。
21
- 2. `close()` 结束子进程的 `stdin` 并等 `'close'` 事件。
22
- 3. `'close'` 的语义不是「进程结束了」,而是「**进程结束了、且它的全部 stdio 流都关闭了**」。
23
- 4. 孙进程仍持有那两根管道的写端/读端,流就关不掉,`'close'` 永远不来。
24
- 5. 回收方一直在等。宿主侧看到的是「这条连接在释放中」,但没有上界、没有进度、没有错误——只是不动了。
25
-
26
- 后果不是「慢一点」:插件卸载、会话销毁、配置热更新全部挂在这一步后面。用户能观察到的现象是**面板停止响应、改配置不生效、退出宿主时进程残留**,而日志里一条错误都没有——因为根本没有错误,只有一个永远 pending 的 promise。
27
-
28
- ## 上游现状(贴证据)
29
-
30
- 证据版本:`@modelcontextprotocol/client@2.0.0`,`dist/stdio.mjs`。
31
-
32
- ### 1. `spawn` 没有建进程组
33
-
34
- `dist/stdio.mjs:68-81`:
35
-
36
- ```js
37
- this._process = spawn(this._serverParams.command, this._serverParams.args ?? [], {
38
- env: { ...getDefaultEnvironment(), ...this._serverParams.env },
39
- stdio: ["pipe", "pipe", this._serverParams.stderr ?? "inherit"],
40
- shell: false,
41
- windowsHide: process.platform === "win32",
42
- cwd: this._serverParams.cwd
43
- });
44
- ```
45
-
46
- `detached` 未设置(即 `false`)。在 POSIX 上这意味着子进程与宿主同进程组,`process.kill(-pid, sig)` 会连同宿主一起打中,所以**想按进程组回收也没有条件**;在 Windows 上则意味着没有任何可用于 `taskkill /T` 的归属标记被记录下来。
47
-
48
- ### 2. `close()` 等的是 `'close'`,且三段式只管直接子进程
49
-
50
- `dist/stdio.mjs:175-199`:
51
-
52
- ```js
53
- async close() {
54
- if (this._process) {
55
- const processToClose = this._process;
56
- this._process = void 0;
57
- const closePromise = new Promise((resolve) => {
58
- processToClose.once("close", () => { resolve(); });
59
- });
60
- try { processToClose.stdin?.end(); } catch {}
61
- await Promise.race([closePromise, new Promise((resolve) => setTimeout(resolve, 2e3).unref())]);
62
- if (processToClose.exitCode === null) {
63
- try { processToClose.kill("SIGTERM"); } catch {}
64
- await Promise.race([closePromise, new Promise((resolve) => setTimeout(resolve, 2e3).unref())]);
65
- }
66
- if (processToClose.exitCode === null) try {
67
- processToClose.kill("SIGKILL");
68
- } catch {}
69
- }
70
- this._readBuffer.clear();
71
- }
72
- ```
73
-
74
- 两个具体问题:
75
-
76
- - **等待条件选错了。** 三段都在等 `closePromise`,而它绑的是 `'close'`。孙进程持管道时,即使直接子进程已被 SIGKILL,`'close'` 依然可能不来。第三段之后**没有 await**,函数直接 `return` ——看起来是「兜底返回」,实际效果是把一个仍在 pending 的 `close` 事件留在身后,而 `_process` 已经被置为 `undefined`,后续没有任何路径再回收它。
77
- - **SIGKILL 只覆盖一个 pid。** `proc.kill("SIGKILL")` 打的是直接子进程;孙进程不在信号可达范围内(POSIX 需负 pid 打进程组,Windows 需 `taskkill /T`)。最需要被杀掉的那批进程恰好一个都没被打中。
78
-
79
- ### 3. 上游已经写出了正确解法,却只给内部探针用
80
-
81
- `dist/stdio.mjs:140-174`,原文注释:
82
-
83
- ```js
84
- /**
85
- * Reap a disposable probe sibling (see the version-negotiation sibling
86
- * flow): signal-first teardown awaiting process `exit` — never the `close`
87
- * event, so a helper process holding the child's stdio pipes can never
88
- * block disposal. Not part of the public transport lifecycle.
89
- *
90
- * @internal
91
- */
92
- async _dispose() {
93
- ...
94
- const exited = new Promise((resolve) => proc.once("exit", () => resolve()));
95
- ...
96
- await Promise.race([exited, new Promise((resolve) => setTimeout(resolve, 1e3).unref())]);
97
- if (proc.exitCode === null && proc.signalCode === null) try {
98
- proc.kill("SIGKILL");
99
- } catch {}
100
- await exited;
101
- ...
102
- }
103
- ```
104
-
105
- 注释原文那句是关键:
106
-
107
- > **"awaiting process `exit` — never the `close` event, so a helper process holding the child's stdio pipes can never block disposal"**
108
-
109
- 也就是说,**上游作者完全知道**「孙进程持有管道会让 `close` 永远不触发」,并且针对探针兄弟进程已经改用 `'exit'`、还做了「不设上界的 `await exited` 收尾」。但常规生命周期路径 `close()` 走的仍是 `'close'`。这不是设计取舍,是两条路径没对齐——`_dispose()` 的注释里 "Not part of the public transport lifecycle" 也侧面承认了这一点。
110
-
111
- ## 可复现条件
112
-
113
- 同时满足以下条件即会触发(可据此判断某个用户的现场是否属于此类):
114
-
115
- 1. **`command` 是包装器**:`npx`、`uvx`、`bunx`、`pnpm dlx`、`docker exec`、`cmd /c`、`.bat`/`.cmd` shim(Windows 上 npm 全局安装的 CLI 全是这一种)。
116
- 2. **server 进程内部再 spawn 至少一个子进程**,且该子进程**继承 stdout/stderr**(Node 的 `spawn` 默认 `stdio: "inherit"`/`"pipe"` 时子侧持有句柄;Python 的 `subprocess.Popen` 默认同样继承)。
117
- 3. **该孙进程生命周期长于父进程**:语言服务器、watcher、索引器、常驻 helper,或干脆是忘记 `unref`/忘记退出的失控进程。
118
- 4. 父进程被回收(`close()` / `SIGTERM` / 崩溃)——此时孙进程成为孤儿但仍持有管道句柄。
119
-
120
- 反过来,以下情况**不会**触发,不必怀疑本例:server 是单进程(如纯 Node 的 `node dist/index.js< /dev/null`、无内部 spawn 的 Python 脚本)、或孙进程启动时显式 `stdio: "ignore"` / `detached: true` 并自己管理生命周期。
121
-
122
- 判定手法:回收卡住时对目标 pid 跑一次 `wmic process where "ParentProcessId=<pid>" get ProcessId,CommandLine`(或 PowerShell `Get-CimInstance Win32_Process -Filter "ParentProcessId=<pid>"`)。有存活子进程且它们的父已经不在 → 就是本例。POSIX 上用 `ps -o pid,ppid,pgid,stat -g <pgid>` 看进程组里的残留。
123
-
124
- ## 建议方案(按优先级)
125
-
126
- ### 方案 A(推荐,最小改动):`close()` 与 `_dispose()` 对齐
127
-
128
- 三处改动,全部在上游内部,不改变公开 API 形状:
129
-
130
- 1. **把等待条件从 `'close'` 换成 `'exit'`**(与 `_dispose()` 完全一致)。`'exit'` 的语义是「本进程已终止」,不依赖任何流被关闭,因此孙进程持管道不再是阻塞条件。
131
- 2. **把 SIGKILL 阶段扩展为「按进程组」。** 这需要 spawn 时加 `detached: true`:POSIX 下子进程自成一组,用 `process.kill(-child.pid, signal)` 打整组;Windows 下 `detached: true` 让子进程成为新进程组的根,配合 `taskkill /T /F /PID <pid>` 才能连带孙进程。
132
- 3. **给最后的 `await exited` 加上上界**(`_dispose()` 现在这行是无上界的)。一组进程全杀光也可能因权限/句柄原因一直不 exit,收尾必须能失败返回,并把结果(`timedOut: true`)交给调用方。
133
-
134
- 代价:`detached: true` 是**行为变更**——POSIX 下子进程默认不再与宿主共享进程组,宿主的 Ctrl-C / 终端信号不再自动传给 MCP server。对 MCP 这类由宿主全权管理的子进程,这通常是想要的(宿主本来就有 `close()`),但对「依赖宿主信号传播来退出」的现有集成是破坏性的。**因此建议至少把 (2) 放进一个 opt-in 选项**(见方案 B),而 (1)(3) 可以直接改:它们只把「无限等待」变成「有界等待」,不改变任何成功路径的行为。
135
-
136
- ### 方案 B(推荐与 A 并行):`detached` 做成 transport 选项
137
-
138
- `StdioServerParameters` 加一个字段(名字供参考):
139
-
140
- ```ts
141
- /** 让 MCP 子进程自成进程组,使 close() 能按组回收孙进程。默认 false(保持现状)。 */
142
- detached?: boolean;
143
- ```
144
-
145
- 默认关闭即**零破坏性**:现有集成的行为一字不变,需要的宿主显式开启。开启后上游在 `close()` 中按平台选择 `process.kill(-pid, ...)` / `taskkill /T /F /PID`。
146
-
147
- 代价:多一个配置项要写文档、要写跨平台分支;Windows 的 `taskkill` 是外部进程,需处理「命令不存在」「拒绝访问」「进程已退出(退出码 128)」等噪音。相对收益(真正能杀掉进程树)这个代价是值得的。
148
-
149
- ### 方案 C(保底,成本最低):至少把 `child.pid` 暴露出来
150
-
151
- 即使前两条都不做,也建议把直接子进程的 pid 通过公开 API 给出,例如:
152
-
153
- ```ts
154
- get pid(): number | undefined;
155
- ```
156
-
157
- 理由:判定「是连接卡住还是宿主卡住」需要 pid(见上一节判定手法);有了 pid,宿主或插件可以自行做进程树回收、写诊断、在面板上显示「这条连接对应 pid 1234」。上游什么都不用改变,只是**把已有的内部事实变成可观察事实**。代价几乎为零(一个 getter + 生命周期上的 `undefined` 时机说明)。
158
-
159
- > 三条方案的取舍一句话:**A 修正确性,B 保兼容,C 保可观测。** A 里 (1)(3) 无破坏性、可直接进;(2) 有破坏性、建议走 B 的开关。
160
-
161
- ## 对本项目的影响
162
-
163
- `dsh-armory`(本插件)在 MCP 子进程这件事上是**纯观察者**:
164
-
165
- - 它**从不 spawn 任何 MCP 子进程**。对 `lib/` 全目录 grep `SIGTERM | SIGKILL | taskkill | kill(` —— **零命中**。
166
- - 子进程由宿主的 `@deepseek-ai/dsh-scope` 与 `@deepseek-ai/dsh-mcp-client` 拥有。插件侧只拿到 `scopeMod.createScope(ctx, scopeKey)` 返回的 `scoped` 句柄,回收时调 `scoped.dispose()`。
167
- - 因此插件**没有 pid、没有 ChildProcess 对象、没有可 kill 的句柄**。在本插件内实现「taskkill 进程树」在技术上不可能——不是没做,是无从下手。
168
-
169
- 结论:本插件无法自己解决这个问题,只能等上游。上游采纳**任一条**(A 的 (1)(3)、B、C)本插件都直接受益:
170
-
171
- - 采纳 A(1)(3):`scoped.dispose()` 从「可能永不返回」变成「有界返回」,本插件的 `retireSharedConnection` 天然跟上,无需改代码。
172
- - 采纳 B:宿主可在建连时开启 `detached`,插件侧零改动。
173
- - 采纳 C:本插件的诊断视图能从「已释放 12.3s 仍在等」升级为「pid 1234 已释放 12.3s 仍在等」,用户可直接去任务管理器处理。
174
-
175
- ## 临时缓解(本侧已做)
176
-
177
- 在上游改动落地前,本插件做了一件事:**把「卡住」从沉默变成可见事实**。这不是修复,是让人能做决定。
178
-
179
- - `lib/workspace-runtime.js` 新增常量 `TEARDOWN_STUCK_MS = 5_000`,与既有的 `DEAD_CONNECTION_AGE_MS = 5_000` 对齐——同一份连接在同一条时间尺度上被判定「已死」与「回收卡住」,两个数字不同会让排障时对不上号。
180
- - 每次回收在模块级记录表里留一条 `{ wsPath, serverName, startedAt, settledAt, elapsedMs, settled, stuck, error }`,按 app root 分桶、每 connKey 只留最近一条(`TEARDOWN_RECORD_MAX = 64` 兜底)。**回收永不返回时,记录里仍有它的出生时间**,于是「已经等了多久」永远算得出来。
181
- - 面板行新增 `teardown` 字段(`null` 或上述读数的投影),于是界面能显示「这条连接已释放 12.3s 仍在等」而不是干等。超龄(`TEARDOWN_RECORD_STALE_MS = 600_000`)的旧结果不再送出——10 分钟前的回收结果对「现在卡不卡」没有解释力。
182
- - 该字段是**纯增量**:沿用已注册的 `dsh-armory#mcpManager/projectConnections` 契约(其 result codec 透传任意结构),不改契约、不改 `lib/typert.js`、不改 `lib/index.js`。
183
-
184
- **用户的正确处置**:面板上看到某条连接 `stuck: true` 且 `elapsedMs` 持续增长时,不要再等——重启宿主。残留的孙进程需要手工清理(Windows:任务管理器按父 pid 找;POSIX:`ps -g <pgid>` 后整组 `kill`)。这份文档的目的就是让这条手工路径最终能被上游的改动取代。
@@ -1,166 +0,0 @@
1
- # 上游提案:Skill 的启用 / 禁用开关
2
-
3
- 这份文档回答一个问题:**为什么「Skill 看板加启停开关」这件事本插件做不到,该由谁做、做成什么样。**
4
-
5
- 它是给宿主(`@deepseek-ai/dsh-skill` 与面板侧 Remote 契约)的上游提案,不是本插件的使用说明。日常使用请看 [README](../README.md) 与 [设计与限制](design.md)。
6
-
7
- - [需求来源](#需求来源)
8
- - [本插件现状](#本插件现状纯只读投影)
9
- - [为什么不能在本插件内做](#为什么不能在本插件内做)
10
- - [建议的上游接口形态](#建议的上游接口形态)
11
- - [插件侧可先做的](#插件侧可先做的待-ui-车道恢复后落地)
12
- - [验收标准](#验收标准上游采纳方案-a-或-b-之后)
13
-
14
- ## 需求来源
15
-
16
- 用户在 Skill 看板里能看到本机全部 skill,但想**临时关掉某个用不上的 skill** 时无处下手:面板上没有任何开关,唯一的「操作」按钮是「打开目录」。用户只能去改配置文件、或者直接把 skill 目录删掉/改名,然后重启宿主。删目录是破坏性的(想再启用就得重新装),改配置文件则需要用户知道宿主把开关存在哪——而这件事连本插件也不知道(见下一节)。
17
-
18
- 期望的行为很朴素:**看板里每一行有个开关,关掉 = 这个 skill 不再进入模型/用户的可用清单,打开 = 恢复。** 不删文件、不改目录。
19
-
20
- ## 本插件现状:纯只读投影
21
-
22
- 结论先行:**本插件对 skill 的能力是纯只读的,「打开目录」不是启停。** 逐条给证据。
23
-
24
- ### 1. 宿主侧只有三个 skill 方法,全部只读
25
-
26
- `lib/typert.js:78-80` 是本插件向宿主注册的**全部** skill 相关 Remote 方法:
27
-
28
- ```js
29
- mv('listSkills', [payloadParam], 'SkillListResult'),
30
- mv('skillDetail', [payloadParam], 'SkillDetailResult'),
31
- mv('revealSkill', [payloadParam], 'NoteResult'),
32
- ```
33
-
34
- 没有第四个。仓库里对 `skillEnable` / `enableSkill` / `disableSkill` / `toggleSkill` / `skillDisable` 全部零命中。
35
-
36
- > 容易看错的一处:`lib/typert.js:45-46` 确实有 `mv('enable', ...)` 与 `mv('disable', ...)`,但那两条属于 **MCP**,不是 skill——它们的宿主实现是 `lib/index.js:946-951` 的 `enable` / `disable`,落到 `setDisabled()` 去写 profile patch(`setManagedMcpDisabled` + `writePatch`)。skill 侧没有对应的写路径。
37
-
38
- ### 2. `revealSkill` 是「在文件夹中显示」,不是启停
39
-
40
- `lib/index.js:1408` 的 `async revealSkill(payload)`,做的事按顺序是:
41
-
42
- ```js
43
- if (shell && typeof shell.showItemInFolder === "function") { shell.showItemInFolder(path); return { note: "已在资源管理器中定位" }; }
44
- if (shell && typeof shell.openExternal === "function") { shell.openExternal(path); return { note: "已交给宿主打开" }; }
45
- const { spawn } = await import("node:child_process");
46
- if (process.platform === "win32") spawn("explorer", ["/select,", path], { detached: true, stdio: "ignore" }).unref();
47
- ```
48
-
49
- 它需要的是一个**磁盘路径**(`payload.path`),路径为空就直接抛「该 skill 没有可打开的文件路径(运行时注册的 skill 不在磁盘上)」。这正是 reveal in folder 的语义——把用户带到文件所在位置,**不改任何状态**。面板上那个按钮的 title 也写着「把 skill 所在目录交给宿主在资源管理器中打开(**只读定位**)」(`lib/client.js:3307`)。
50
-
51
- ### 3. 列表投影里没有 `enabled` 字段
52
-
53
- `lib/index.js:1368` 的 `listSkills` 把宿主行逐字段投影出来,只有这些:
54
-
55
- ```js
56
- name, description, whenToUse?, path?, source, provider, modelInvocable, userInvocable
57
- ```
58
-
59
- 注意 `modelInvocable` / `userInvocable` 是**能力位**(这个 skill 能不能被模型主动调用、能不能被用户 `/名字` 触发),是 skill 自身声明的静态属性,与「用户此刻是否启用它」是两件事。宿主返回的行里没有 `enabled`,本插件也就投影不出来。
60
-
61
- ### 4. 本插件对 skill 注册表只用了两个读方法
62
-
63
- 作用域探针 `lib/index.js:376` 的 `withSkillScope(ctx, payload, run)` 拿的是 `ctx.get("skills")`,整个仓库对它的调用面只有两处:
64
-
65
- - `lib/index.js:1370` `typeof registry.list !== "function"`
66
- - `lib/index.js:1390` `typeof registry.get !== "function"`
67
-
68
- 即 `list()` 与 `get()`。注册表缺席时不报 500,而是返回 `{ available: false, skills: [] }`(`lib/index.js:1383`),看板如实说「不可用」。**这是刻意的设计**:`lib/index.js:1364` 的段注释写得很明白——「数据真源是 `@deepseek-ai/dsh-skill` 的 `ctx.skills` 注册表,本插件只做投影与只读定位,**不发现、不缓存、不写盘**」。
69
-
70
- ### 5. 面板侧同样是纯展示,而且刻意没有伪造开关
71
-
72
- 看板组件 `lib/client.js:3101` 的 `SkillBoard` 里,唯一会改变宿主状态的动作不存在;只有「打开目录」(`lib/client.js:3304-3309`)。过滤条那段注释(`lib/client.js:3152-3155`)已经把这件事钉死了:
73
-
74
- > 宿主 skill 注册表**没有**启用/禁用开关(`listSkills` 只给 `modelInvocable` / `userInvocable` 这类能力位,没有 enable/disable 写入接口),所以这里**不伪造**一个「启用开关」——那会是个点了不生效的假控件。
75
-
76
- 也就是说:看板现在没有开关,不是漏做,是当时就知道做不了而选择不骗人。
77
-
78
- ## 为什么不能在本插件内做
79
-
80
- 分三层,每层是下一层的前提。
81
-
82
- ### 第一层:启停状态**归谁所有**,没有答案
83
-
84
- 一个 skill 的「关掉」是一种**持久状态**,它必须有唯一真源。当前有三种可能的归属,各自都有硬问题:
85
-
86
- | 候选归属 | 问题 |
87
- |---|---|
88
- | 宿主 skill 注册表(推荐) | 注册表是进程内的投影结果,它的输入是磁盘扫描 + 预设作用域。持久状态得存在某份配置里,由注册表读入后过滤——这条链目前**没有公开写入接口**。 |
89
- | 本插件自己存(如 `~/.dsh/skills.disabled.json`) | 插件是「投影者」,不是「所有者」。写一份宿主不读的文件等于自娱自乐;若宿主另有一套状态,两份必然打架。 |
90
- | 用户手改 skill 目录(现状) | 破坏性,且不可撤销而不重装。 |
91
-
92
- 注意第一层还牵出一个**作用域**问题:`withSkillScope`(`lib/index.js:376`)读 skill 时是**带作用域**的——看板没有会话,就借默认预设的 `acquireScope` 拿一份租约,看完释放(`lib/index.js:386-396`)。所以「禁用」是**全局禁用**还是**仅该预设禁用**,是需要宿主回答的语义问题,插件单方面决定必然错一个。
93
-
94
- ### 第二层:就算归宿主,插件也必须调到一个**未经核实存在**的 API
95
-
96
- 本插件能拿到的 `ctx.get("skills")`,在实测环境里能确认存在的只有 `list()` 与 `get()`(见上)。
97
-
98
- 宿主内部的契约文档读不到:本机 `C:\Users\LENOVO\.dsh\profiles\desktop\node_modules\@deepseek-ai` 下只落盘了 `cosmokit` 与 `schemastery` 两个包,`dsh-agent`、`dsh-skill` 等宿主内部包不在这条路径上,无法读到注册表的方法签名。
99
-
100
- 所以「调 `registry.disable(name)`」这件事,**目前既没有文档依据,也没有运行时依据**。以本插件现有的失败哲学(注册表缺席就 `available:false`,绝不 500)来看,直接假设一个方法存在并调用,只会在用户点击时抛一个没人预料到的 TypeError。
101
-
102
- ### 第三层:即便 API 存在,暴露它要新增 RPC 契约,而这条契约**两侧必须同步**
103
-
104
- 这是最硬的一条,也是本次**不实现**的直接原因。
105
-
106
- 本插件的 Remote 契约由**三个必须同步的文件**共同定义:
107
-
108
- 1. `lib/typert.js` —— 宿主侧契约声明。文件头是 `/* Generated for dsh-armory — do not edit. */`,**它是生成物**,手改会在下次生成时被覆盖。
109
- 2. `lib/client.js:5468` 起的 `MCP_CONTRIBUTION` 描述符表 —— 浏览器半的 codec 与描述符(skill 的三条在 `lib/client.js:5524-5526`)。
110
- 3. `lib/index.js` 里带 `@Remote` 的方法 —— 宿主半的实现。
111
-
112
- 这三处维持一个不变式:**契约两侧的方法集合与顺序必须一致**。少一处,客户端调用就是稳定的 404(`lib/client.js:5527` 那段注释记的就是这个坑:工具裁缝「两端契约此前双双缺失,客户端永远 404」)。
113
-
114
- 新增一个 `setSkillEnabled` 意味着:改一个**生成物**、改一张**浏览器半描述符表**、再改宿主半实现——三处任意一处遗漏,面板上就会出现一个点了没反应的按钮。这不是「多加一行」的改动量,而是一次跨契约变更,**不适合被塞进一次以 UI 整理为目标的迭代里**(本次并行车道还在改 `lib/client.js`)。
115
-
116
- ## 建议的上游接口形态
117
-
118
- 三个候选,按推荐度排序。
119
-
120
- ### 方案 A(推荐):宿主提供启停三件套,插件只做 UI
121
-
122
- 宿主侧在 skill 注册表上补齐写入面:
123
-
124
- ```ts
125
- ctx.skills.list(options?) // 每行新增 enabled: boolean
126
- ctx.skills.enable(name, options?)
127
- ctx.skills.disable(name, options?)
128
- ```
129
-
130
- - **代价**:宿主改动(持久化位置、作用域语义都要宿主自己定)。
131
- - **收益**:插件侧**零契约新增**——`enabled` 顺着现有 `listSkills` 投影直接带出来,开关按钮可以复用 MCP 侧已有的 `enable` / `disable` 交互范式(`lib/index.js:946-951` 已经有一套写完 patch 再 reconcile 的成熟写法可参照)。
132
- - **附带好处**:`enabled` 一旦进了投影,「只显示已启用」这类过滤条也顺带可做。
133
-
134
- ### 方案 B(最小):只在清单投影里加只读 `enabled` 字段
135
-
136
- 宿主 `list()` 的每行直接带 `enabled: boolean`,启停仍由用户在宿主的设置页操作。
137
-
138
- - **代价**:宿主改动**极小**(一个只读字段)。
139
- - **收益**:插件立刻能画出「已禁用」的灰显态与对应的指路文案,用户至少知道**为什么这个 skill 不响应**,不会再把它当 bug 报。
140
- - **不足**:用户仍要跳转到设置页,不能在看板里就地操作。作为过渡形态是合适的。
141
-
142
- ### 方案 C(不推荐):插件写一份宿主约定的忽略清单
143
-
144
- 插件写 `~/.dsh/skills.disabled.json`,宿主读 skill 时跳过清单里的名字。
145
-
146
- - **代价**:表面最小——插件自己写文件,宿主加三行读取。
147
- - **问题**:这是**跨进程的隐式约定**。两份独立演进的代码靠一个没人强制校验的文件格式对齐,任何一侧改字段名、改路径、改语义(全局 vs 作用域),另一侧都会**静默失效**——症状是「关了又开着」,用户无法自查。而且它天然与宿主自己将来实现的启停状态冲突。
148
- - **结论:不推荐。** 状态必须由拥有 skill 的那一方持有。
149
-
150
- ## 插件侧可先做的(待 UI 车道恢复后落地)
151
-
152
- 以下是**本次没有做**的部分,记在这里以免变成口头承诺。写清楚原因:本次并行工作中 `lib/client.js` 由另一条车道整文件接管,本车道不碰它。
153
-
154
- 1. **把「只读」明确标出来**:Skill 看板的标题区或详情页顶部加一行说明,让用户第一眼就知道这个看板不管理启停,而不是点遍按钮才发现。
155
- 2. **给一条指路**:文案指向宿主的 skill 设置入口(具体入口待上游确认后写死,现在不确定就不要编一个假路径)。
156
- 3. **详情页「调用策略」区**补一句:`modelInvocable` / `userInvocable` 是 skill 声明的**能力**,不是用户的**开关**——这两个词在中文里太容易混。
157
-
158
- 这三条都是纯文案/展示改动,不碰契约,等 `lib/client.js` 车道释放后即可落地。
159
-
160
- ## 验收标准(上游采纳方案 A 或 B 之后)
161
-
162
- 1. **字段可达**:`listSkills` 返回的每一行都带布尔 `enabled`,且与宿主设置页显示的状态一致(同一 skill 在两边不得出现矛盾)。
163
- 2. **往返一致**:调用禁用后再 `listSkills`,该行 `enabled === false`;重新启用后回到 `true`。中间不要求重启宿主。
164
- 3. **作用域语义明确**:文档写明禁用是全局还是按预设。若按预设,`withSkillScope` 走的默认预设租约(`lib/index.js:386-396`)必须能读到正确的那一份。
165
- 4. **契约三处同步**:`lib/typert.js`、`lib/client.js` 的 `MCP_CONTRIBUTION`、`lib/index.js` 的 `@Remote` 三处方法集合一致——建议加一条测试断言两侧方法名集合相等,这类 404 才不会再次溜过去。
166
- 5. **降级不崩**:宿主未实现写入接口时,看板必须如实显示为只读(沿用现有的 `available:false` 形态),**不得**渲染出点击无效的开关。
@@ -1,105 +0,0 @@
1
- # PRD:dsh-armory 接入 awesome-dsh-plugins 插件市场(增量 PRD)
2
-
3
- > 档位:简单 PRD | 语言:中文 | 版本:v1.1(2026-10-20,Alice;v1.1 并入用户三项拍板:unverified 不进缓存 / 默认序 stars 降序 / skill 类插件市场承接)
4
- > 上游源:`dshworks/awesome-dsh-plugins` 仓库 `data/plugins.json`(13,400,935 字节)+ `data/schema.json`(6,370 字节),updated=2026-10-19。
5
- > 本 PRD 只写需求与验收口径,不写实现方案(架构决策归架构师)。
6
-
7
- ---
8
-
9
- ## 1. 项目信息
10
-
11
- - **Language**: 中文 | **Programming Language**: 纯 JavaScript(Node ≥ 22.15,ESM,零新依赖)| **Project Name**: `dsh_armory_awesome_dsh_plugins`
12
- - **原始需求复述**(用户原话 m03157): 「你看能不能把这个插件市场链接到github上的awesome-dsh-plugins列表,让其全部变为dsh的插件」
13
- - **合理解读**: awesome-dsh-plugins 是 DSH(DeepSeek Harness)宿主生态的插件目录(20,769 条),与现有 codebuddy 源(WorkBuddy/Cordis 生态,zip 包 206 条插件)**不是同一运行时**。「全部变为 dsh 的插件」= 在 dsh-armory 面板新增「DSH 社区插件」市场视图,让用户可浏览并一键安装这些条目到 DSH 宿主。
14
- - **与 prd-workbuddy-parity.md 的关系**: 全新增量功能,不改动既有 P0 范围(R1–R3)与 Non-Goals(X1–X9 继续有效);需求编号自 R11 续编,可复用该 PRD 已建立的机制(R10 排序范式、ETag 更新探测思路、restartNotice)。
15
- - **网络实测基线**(本机 2026-10-20): jsdelivr ✅ / fastly.jsdelivr ✅ / api.github.com ✅;raw.githubusercontent.com ❌、github.com 直连 ❌ 超时——**安装与拉取主路径必须避开 github.com 直连**。
16
- - **并行事项**: 现有「目录投影漏 repository」「installPlugin 假成功」两处 bug 由其他工程师修复中,本 PRD 不以其为前置依赖,验收按修复后的正常行为口径。
17
-
18
- ---
19
-
20
- ## 2. 产品定义
21
-
22
- ### Product Goals(3 个正交目标)
23
-
24
- 1. **内容接入**: 将 awesome-dsh-plugins 作为第二个插件市场源接入 dsh-armory;P0 收录口径 = status=verified ∧ 带 npm 字段(两集合分别 20,264 / 6,404 条,交集落地时实测,量级 ≈ 6 千条),实现浏览/搜索/一键安装到 DSH 宿主。
25
- 2. **大数据可用性**: 13.4MB 全量目录经「一次全量拉取 → 运行时瘦身过滤 → 本地瘦身影射缓存」后压缩到 ≈ 1.5–2MB,面板分页/搜索/筛选全部本地即时响应,永不渲染全量。
26
- 3. **弱网可靠性**: CDN 多源容错(jsdelivr → fastly → api.github.com 兜底)+ 本地缓存降级;在 github.com 直连不可达的实测网络下,浏览与安装主路径(npm spec)不受阻。
27
-
28
- ### User Stories(5 个)
29
-
30
- 1. As a DSH 用户, I want 在 armory 插件面板里浏览并搜索 awesome-dsh-plugins 的 verified 插件, so that 不必自己去翻 13.4MB 的原始列表找插件。
31
- 2. As a DSH 用户, I want 一键安装 npm 可装的插件(`npm:<name>`), so that 不用手敲命令行安装。
32
- 3. As a DSH 用户, I want 看到分类/标签/stars/官方标识/「验证于 dsh x.y.z」徽章, so that 快速判断插件质量与 DSH 版本适配。
33
- 4. As a DSH 用户, I want 断网或全源不可达时仍能浏览上次缓存的目录, so that 弱网只限制安装、不阻塞浏览。
34
- 5. As a DSH 用户, I want broken 条目不可见不可装、unverified 有明确标注, so that 不误装坏插件。
35
-
36
- ---
37
-
38
- ## 3. 技术规范
39
-
40
- ### 3.1 全局约束(继承 parity PRD)
41
-
42
- - 零新依赖;新增 Host Remote 方法必须 `lib/index.js` / `lib/typert.js` / `lib/client.js` 三处同步;缓存与状态写 `~/.dsh`。
43
- - X 约束继承:**无常驻定时器**(X4,联网仅懒触发一次性探测);**不代填凭据**(X9 不涉,awesome 数据无凭据字段);**安装成功必须出现「需重启 DSH 宿主」提示**(沿用现有 restartNotice)。
44
- - 架构决策点(仅陈述需求):awesome 源为**裸 JSON**(非 zip、无 `.codebuddy-plugin/marketplace.json` 结构),现有 `wb-plugin-market.js` zip 管线不适用,**需新增 JSON 读取器**;源注册(`~/.dsh/armory/marketplaces.json` + `wb-registry.js`)的 kind 语义如何扩展由架构师定。
45
-
46
- ### 3.2 Requirements Pool
47
-
48
- > 优先级:P0 = 本迭代必做,P1 = Should,P2 = Could。编号 R11 起续编,与 parity PRD 的 R1–R10 不冲突。
49
-
50
- #### P0
51
-
52
- - **R11|must:DSH 插件源接入 + 数据瘦身口径(数据规模策略,本 PRD 最重要决策)**
53
- - 口径:**一次全量拉取 `data/plugins.json`(13.4MB,CDN 单文件)→ 运行时过滤 status=verified ∧ npm 存在 → 瘦身影射落盘本地缓存**;瘦身影射字段 = name / repo / npm / description(截断)/ category / status / tags / stars / official / verifiedAgainst / lastVerified;预估 ≈ 6 千条 × ~200–300B ≈ **1.5–2MB**(验收上限 3MB)。面板只读瘦身缓存,本地分页(≤50 条/页)+ 搜索。不做 category 限制(category 仅作筛选维度;skill 类去向已拍板,见 R20)。
54
- - 落选备选及理由:分类聚合 + 懒加载(交互复杂、全局搜索体验差);每次拉全量直渲染(13.4MB 内存与渲染不可接受)。
55
- - **源关系(与现有 codebuddy 源)**: **并列视图、不合并混排**——两者运行时(DSH 宿主 vs Cordis)、数据格式(裸 JSON vs zip+plugin.json)、安装链路均不同,合并列表加来源徽章会让用户无法判断装的是哪边的东西。
56
- - 验收:must——面板可浏览/搜索/分页该瘦身子集;断网重开后缓存仍可浏览;缓存文件 ≤ 3MB。
57
- - **R12|must:安装 spec 推导(npm 主路径)**
58
- - npm 字段存在 → `npm:<name>`(优先级最高,实测网络下唯一高成功率路径);走现有宿主 `pluginManager.installBundle(spec, {})` 双参通道;安装成功后出现醒目持久的「需重启 DSH 宿主后生效」提示。
59
- - 验收:must——任选 1 条 npm 插件安装成功且出现重启提示,重启宿主后生效。
60
- - **R13|must:分类与筛选**
61
- - category 5 类(plugin 19,968 / skill 694 / bundle 102 / tool 3 / theme 2)chips 单选筛选;tags 17 枚举(ui/terminal/agents/memory/models/vision/capabilities/channels/notifications/usage/observability/marketplace/devtools/safety/knowledge/interop/fun)多选下拉;搜索框与「只看已安装」复用现有面板控件。
62
- - 验收:must——筛选与搜索结果正确、本地即时响应;skill/theme/tool 类条目正常展示。
63
- - **R14|must:CDN 多源容错与降级**
64
- - 拉取顺序:jsdelivr → fastly.jsdelivr → api.github.com(兜底;其能否承载 13.4MB 单文件由架构师验证,见 OQ2)。失败原因复用现有五枚举 reason 思路扩展(如 no-source / all-sources-failed / stale-cache)。
65
- - 降级表现:全源失败且无缓存 → 空态「源不可达,稍后重试」;全源失败但有缓存 → 正常列表 + 顶部「目录已过期(上次更新 YYYY-MM-DD)」提示。
66
- - 验收:must——屏蔽首源后自动切换次源成功;全源失败时两种降级表现均正确。
67
- - **R15|must:懒触发更新探测(X4 继承)**
68
- - 仅在打开 DSH 插件视图或手动刷新时执行一次性条件探测(复用 `wbPluginUpdateCheck` 的 ETag 五枚举思路);上游更新 → 顶部更新提示,用户确认后拉取并重瘦身。无常驻定时器、不静默更新。
69
- - 验收:must——上游更新后再次打开面板出现更新提示,确认后目录刷新为最新。
70
-
71
- #### P1
72
-
73
- - **R16|should:github 直装路径扩展(依赖 OQ1/OQ6)**
74
- - 无 npm 的 verified 条目 → `github:owner/repo#ref`;monorepo path 条目(`github:owner/repo#ref:path` 形态)在宿主支持度实测确认前**一律不可装**,卡片标注「暂不支持安装」。
75
- - 边界:实测网络 github.com 直连不可达,该路径安装可能失败,必须给出明确失败原因(不得复用假成功行为)。
76
- - 验收:should——网络允许时至少 1 条无 npm 插件安装成功;path 条目不可装但有标注;安装失败有明确错误。
77
- - **R17|should:质量信号透出与状态策略**
78
- - broken(102 条)**永不进瘦身缓存,任何视图不可见不可装**;unverified(403 条)**不进缓存**(✅ 用户已拍板 2026-10-20,安全优先,不再考虑展示标注方案);official=true → 「官方」徽章、featured → 「精选」徽章;卡片显示 stars 与「✓ verified(dsh <verifiedAgainst>)」小徽章,详情页补 lastVerified。
79
- - 验收:should——徽章与验证信息正确展示;broken 在任何筛选/搜索组合下不出现。
80
- - **R18|should:排序(复用 parity R10 范式)**
81
- - 按 stars / added(最新收录)/ 名称本地排序;**默认序 = stars 降序**(✅ 用户已拍板 2026-10-20;featured/official 只做徽章展示,不参与默认序)。
82
- - 验收:should——排序切换后列表正确重排。
83
-
84
- #### P2
85
-
86
- - **R19|could:详情增强** —— evidence 链接、pushedAt / added / starsUpdated / repo 链接全量展示。验收:could——详情字段完整、链接可一键复制。
87
- - **R20|已拍板(✅ 2026-10-20):skill 类由插件市场承接** —— 694 条 skill 类条目保留在 DSH 社区插件视图,不引流技能市场;用 category=skill chips 筛选区分。验收:skill 条目在插件视图可浏览可筛选,技能市场不重复收录。
88
-
89
- ### 3.3 UI Design Draft(文字描述)
90
-
91
- - **入口**: 市场「插件」子 tab 顶部新增来源切换——「CodeBuddy 官方(206)|DSH 社区(awesome)」,并列视图、默认 CodeBuddy,用户选择本地记忆。
92
- - **工具行**(DSH 视图): 搜索框(复用)+ category chips(5 类)+ tags 多选下拉(17 枚举)+ 「只看已安装」开关(复用)+ 排序下拉(R18)。
93
- - **卡片**: 名称 + description 一行截断 + category 徽章 + tags(≤2)+ stars + 「✓ verified (dsh 0.2.x)」徽章(示意)+ 官方/精选徽章 + 右下「安装」按钮;path 条目按钮替换为灰置「暂不支持安装」。
94
- - **详情弹层**: 完整 description、repo、npm 包名、category/tags、stars、pushedAt/added/lastVerified/verifiedAgainst、「安装」按钮。
95
- - **安装按钮状态机**: 未安装[安装] → 安装中[disabled+转圈] → 成功[已安装 ✓ + 顶部持久「需重启 DSH 宿主」提示条] / 失败[红色错误 + 可重试]。
96
- - **容错表现**: 首载骨架屏;全源失败且有缓存 → 列表 + 「目录已过期(上次更新 YYYY-MM-DD)」提示条;无缓存 → 空态引导重试。
97
-
98
- ### 3.4 Open Questions(需架构师/用户拍板)
99
-
100
- 1. **monorepo path 条目**: `github:owner/repo#ref:path` 形态的宿主 installBundle 支持度未验证——需架构师选 1 个真实条目实测后解锁 R16。
101
- 2. **13MB 源缓存策略**: 复用 ETag 条件请求(推荐,避免每次重拉 13.4MB)vs 每次全量拉取后瘦身覆盖;上游 CDN 是否输出稳定 ETag 需实测。api.github.com 兜底对 13.4MB 单文件的可行性(Contents API 有体积上限)也需验证。
102
- 3. **unverified 403 条**: ✅ 已拍板(2026-10-20)——**不进缓存**(安全优先);broken 102 条按「直接不进缓存」写入本 PRD。
103
- 4. **默认排序**: ✅ 已拍板(2026-10-20)——**stars 降序**为默认序;featured/official 仅作徽章展示,不参与默认序。
104
- 5. **skill 类 694 条去向**: ✅ 已拍板(2026-10-20)——**插件市场承接**,category=skill 筛选区分,不引流技能市场。
105
- 6. **无 npm 条目的 ref 取值**: 默认分支、固定 tag,还是与 verifiedAgainst 版本映射——影响 R16 spec 的正确性。
@@ -1,128 +0,0 @@
1
- # PRD:dsh-armory WorkBuddy 市场对标(增量 PRD)
2
-
3
- > 档位:简单 PRD | 语言:中文 | 版本:v1.0(2026-10-20,Alice)
4
- > 唯一权威输入:`docs/workbuddy-market-gap-analysis.md`。本 PRD 只写「要在 dsh-armory 上新增/修改什么」,不重复报告论证过程。
5
- > 范围 = 报告第三节差距清单(G1–G11)+ 第五节落地顺序(三批推进);排除项 X1–X9 全部列为 Non-Goals,禁止进入需求池。
6
-
7
- ---
8
-
9
- ## 1. 项目信息
10
-
11
- - **Language**: 中文(与用户语言一致)
12
- - **Programming Language**: 纯 JavaScript(无构建步骤、无 TypeScript、无 UI 框架),Node ≥ 22.15,ESM。
13
- - UI:`lib/client.js` 内手写 Web 面板(注入 DSH Web GUI)
14
- - 市场数据层:`lib/wb-market.js`(WB 连接器/技能)、`lib/wb-zip.js`(zip 下载解压,零新依赖)、`lib/expert-market.js`(专家)
15
- - Host Remote 契约三件套:`lib/index.js` + `lib/typert.js` + `lib/client.js`(新增 Remote 方法必须三处同步,漏一处会静默 404)
16
- - **Project Name**: `dsh_armory_workbuddy_parity`
17
- - **原始需求复述**: 对标 WorkBuddy 市场体系(已卸载,数据目录 `C:\Users\LENOVO\.workbuddy`),在 dsh-armory 现有「市场」标签页(连接器/技能/内置 MCP 三类)之上,按差距报告结论补齐**当前技术条件下可实现**的差距:三批推进(正确性补丁 → 市场扩展 → 基础设施),并明确不做的边界(X1–X9)。
18
-
19
- ---
20
-
21
- ## 2. 产品定义
22
-
23
- ### Product Goals
24
-
25
- 1. **正确性优先**:修复连接器「残缺安装」——解析 `auth_injection_rules`,让 tencent-docs / ima-mcp 等连接器装完即真正可用(G2),同时把已有连接器数据(visible_in 标签、示例问法、版本)利用起来(G3/G5)。
26
- 2. **补齐最大内容品类与可用性**:新增「插件」市场子 tab(依托多市场注册表接入 codebuddy-plugins-official,150+ 插件,一次装齐技能+命令+钩子+MCP 组合包),并以「令牌引导安装」解锁 80+ 条需认证连接器(G7/G1/G10/G4)。
27
- 3. **建立可管理的基础设施**:本地安装状态与来源锁可查可筛、目录打开时自动保鲜提示、列表排序对齐技能侧体验(G6/G9/G8/G11)。
28
-
29
- ### User Stories
30
-
31
- 1. **As a** DSH 用户, **I want** 在 armory 里浏览并安装 codebuddy-plugins-official 的插件(如官方 docx/pptx 技能包、security-scan), **so that** 一次装齐「技能+命令+钩子+MCP」组合包,而不必逐个装 MCP 或技能。
32
- 2. **As a** DSH 用户, **I want** 安装 tencent-docs / ima-mcp 等连接器时自动应用其 token 注入规则, **so that** 装完即可用,而不是残缺安装后手工补 token。
33
- 3. **As a** DSH 用户, **I want** 安装 auth_mode=token 的连接器前自行填入令牌, **so that** 80+ 条需认证连接器在 armory 里真正可用。
34
- 4. **As a** DSH 用户, **I want** 看到连接器的部署/档位标签、版本与示例问法, **so that** 快速判断哪些连接器适合我,并直接复制示例 prompt 试用。
35
- 5. **As a** DSH 用户, **I want** 在列表中看到哪些连接器/插件已安装并按状态全量筛选, **so that** 避免重复安装、便于管理已装内容。
36
-
37
- ---
38
-
39
- ## 3. 技术规范
40
-
41
- ### 3.1 全局技术约束
42
-
43
- - **零新依赖**贯穿全程(zip 下载解压继续走 `lib/wb-zip.js`)。
44
- - 新增 Host Remote 方法必须 `lib/index.js` / `lib/typert.js` / `lib/client.js` **三处同步**,漏一处静默 404。
45
- - 注册表与本地状态文件均写入 `~/.dsh`(armory 已有该目录写入能力)。
46
- - 批次顺序与依赖:批次一独立先行;批次二内 G1 依赖 G7(不为插件源硬编码 URL)、G10 内嵌于 G1;批次三内 G6 与 G9 共用同一份本地状态文件设计。
47
-
48
- ### 3.2 Requirements Pool
49
-
50
- > 优先级:P0 = Must have(本迭代必做),P1 = Should have,P2 = Could have。每条映射差距编号与批次。
51
-
52
- #### P0 —— 批次一:正确性补丁(1 次小迭代,零新依赖,风险最低)
53
-
54
- - **R1|G2|批次一|must:解析并应用 `auth_injection_rules`**
55
- - 解析 connectors.json 顶层 `auth_injection_rules`(已证实 2 条:tencent-docs、ima-mcp,均双 token);安装/预览时应用到作用域合并逻辑,或在预览/安装界面明示「该连接器需要 N 个 token」。
56
- - **验收标准**:must——预览界面能显示注入规则条数与用途;安装 tencent-docs 后 profile 的 mcpServers 配置不再残缺(含 token 占位或注入说明),配合 R7 可完成完整认证配置。
57
- - **R2|G3|批次一|must:`visible_in` 标签展示 + 本地筛选**
58
- - 每条连接器解析 `visible_in` 数组(internal / iOA / plan:ultimate / plan:exclusive / selfhosted / cloudhosted),卡片与详情展示标签;筛选栏支持按标签本地过滤。
59
- - **边界**:只做数据标签展示 + 本地筛选,**不做**渠道/档位准入逻辑(X2);无 `visible_in` 的条目正常默认展示。
60
- - **验收标准**:must——含 visible_in 的条目显示标签;按标签筛选结果正确;不含标签条目不受影响。
61
- - **R3|G5|批次一|must:连接器元数据增强(examples 优先)**
62
- - 解析并展示 `examples_zh` / `examples_en`(可一键复制的示例问法)、`version`、`minWorkbuddyVersion`(版本仅作元数据新鲜度提示)。
63
- - **验收标准**:must——连接器详情可见中英示例问法并可一键复制、可见 version;could——zip 内 `icons/` 图标利用为可选加分项,不阻塞验收。
64
-
65
- #### P1 —— 批次二:市场扩展(核心增量)
66
-
67
- - **R4|G7|批次二|must:多市场注册表**
68
- - 借鉴 WB `known_marketplaces.json` schema(每项:manifestName / type(zip/directory)/ source / autoUpdate / isBuiltIn),把连接器源从硬编码单 URL 改为注册表配置(存 `~/.dsh`);G1 插件源直接接入注册表,不再新增硬编码 URL。
69
- - **验收标准**:must——连接器源可在注册表中配置并生效;新增一个市场源无需改代码。
70
- - **R5|G1|批次二|must:插件市场子 tab**
71
- - 新增「插件」子 tab:经 R4 注册表下载 codebuddy-plugins-official zip → 解析 marketplace.json → 列表/详情展示组成(skills / commands / agents / hooks / mcpServers / lspServers 数量或清单)、category / keywords / author → 安装经既有宿主通道 `remote.pluginManager.installBundle(targetSpec, {})`(严格双参调用,注意 `lib/client.js:3577` 已记录的踩坑注释)。
72
- - **边界与必写提示**:① 安装成功后**必须明确提示「需重启 DSH 宿主才生效」**(宿主 installBundle 机制决定,是与 WB「装完即用」唯一无法抹平的差距,提示须醒目持久);② `lspServers` 字段仅展示、不承诺安装后生效(X7);③ oauth/oneid 相关连接器不代登录、仅标注(X3)。
73
- - **验收标准**:must——150+ 插件可浏览可搜索;至少 1 个插件(如官方 docx 包)安装成功且出现重启提示,重启宿主后生效。
74
- - **R6|G10|批次二|must(随 R5):插件依赖提示**
75
- - 解析插件条目的依赖声明,安装前提示依赖清单,用户确认后一并安装(marketplace.json 中 dependencies 的具体表达形式在解析真实 zip 时确认,见 Open Questions)。
76
- - **验收标准**:must——安装含依赖的插件(如 security-scan 类)时先列出依赖并需确认;无依赖插件不弹确认。
77
- - **R7|G4|批次二|must:令牌引导安装**
78
- - 针对已证实存在的 73 条 auth_mode=token + 8 条 oauth 连接器(现仅拦截不引导:`lib/wb-market.js:128-129` 拒装、`:166` authMode 仅透传展示):安装前弹出令牌输入 → 替换 mcpServers 的 env/header 占位符 → 写入 profile mcpServers;并按 auth_mode 区分「可自填 token」与「oauth/oneid 不支持代注入、需自备凭据」的标注。
79
- - **边界**:只做**用户自填简单 token**(env/header 占位符替换);WB 的 mcp-oauth / oneid-token 双 token 代注入机制整条链路**不做**(X9)。
80
- - **验收标准**:must——任选 1 条 auth_mode=token 连接器,输入令牌安装后认证可用;oauth 连接器显示明确的「不支持代注入」标注而非静默拒装。
81
-
82
- #### P2 —— 批次三:基础设施
83
-
84
- - **R8|G6+G9|批次二→批次三|must:连接器/插件已安装状态 + 来源锁(同一份本地状态文件)**
85
- - 安装连接器/插件时同步写一份本地状态文件(存 `~/.dsh`):scope / installPath / version / dependencies / installedAt / lastUpdated + 上游来源锁(来源市场、版本,参考 WB `upstream-skills-lock.json` 机制,字段细节实现前补查);列表支持「已安装」**全量**筛选(非当前页,对齐技能侧 `skillOnlyInstalled` 但做得更完整)。
86
- - **验收标准**:must——安装后状态文件可查(含来源版本);「已安装」筛选在全量范围内正确;could——卸载/更新时同步维护该文件为加分项。
87
- - **R9|G8|批次三|should:懒触发 ETag 更新检查**
88
- - 打开市场页时后台比对上游 ETag(复用 `lib/wb-market.js:83` 既有 ETag 缓存),有更新则提示,由用户手动确认更新。
89
- - **边界**:不做静默 autoUpdate、不做常驻定时器(X4)。
90
- - **验收标准**:should——上游目录变更后再次打开市场页出现更新提示;确认后目录刷新为最新。
91
- - **R10|G11|批次三|could:连接器/内置目录排序**
92
- - 本地排序(如按名称 / auth_mode / type),对齐技能侧已有排序体验;因 WB 本体 UI 无排序证据,仅作体验补全、不作为对标宣传点。
93
- - **验收标准**:could——排序切换后列表正确重排。
94
-
95
- ### 3.3 UI Design Draft
96
-
97
- - **市场 tab 顶部**:现有「连接器 / 技能 / 内置 MCP」子 tab 增加「**插件**」(第四个子 tab)。
98
- - **连接器子 tab**:
99
- - 卡片:名称 + 现有信息之外,新增标签行(`visible_in` 标签以小徽章展示,如 `selfhosted`、`plan:ultimate`)、版本号(v1.0.1)与 auth_mode 标注(如 `token`)。
100
- - 筛选栏(现有搜索/分页旁):新增「已安装」开关(全量筛选)+ `visible_in` 标签多选筛选 + 排序下拉(R10)。
101
- - 详情/预览面板:新增「示例问法」区块(中文/英文并列或切换,每条带一键复制按钮);新增注入规则提示行(「该连接器需要 N 个 token」及用途说明)。
102
- - **令牌输入弹窗**(R7):模态弹窗,列出该连接器全部 env/header 占位符(键名 + 用途说明),每行一个输入框(内容遮罩显示);底部「确认安装 / 取消」。oauth/oneid 条目的弹窗改为说明页:标注「需自备凭据,DSH 不支持代登录」。
103
- - **插件子 tab**(R5/R6):
104
- - 列表:插件名 + author + category/keywords 标签 + 组成 chips(如 `skills×3`、`commands×5`、`hooks×2`、`MCP×1`、`LSP(仅展示)`);支持搜索。
105
- - 详情面板:完整组成清单、依赖清单、keywords/author;「安装」按钮。
106
- - **重启提示**:安装成功 toast 之外,页面顶部出现醒目持久提示条「已安装,需重启 DSH 宿主后生效」,直到用户重启前不消失;lspServers 条目旁固定标注「仅展示,DSH 暂不托管 LSP」。
107
- - **更新提示**(R9):检测到上游更新时,市场页顶部出现轻量提示条「目录有更新」,点击确认后刷新;无更新时无感知。
108
-
109
- ### 3.4 Open Questions
110
-
111
- 1. **插件市场源与注册表已实测确认**(2026-10-20 主理人核查):注册表文件实为 `C:\Users\LENOVO\.workbuddy\plugins\known_marketplaces.json`(7 个市场,schema:manifestName / type(zip|directory) / source{path,url} / installLocation / description / lastUpdated / autoUpdate / isBuiltIn)。`codebuddy-plugins-official` 源 = `https://download.codebuddy.cn/plugin-marketplace/codebuddy-plugins-official-4b9366ac-f7e5-4cef-b60a-99a5229ce2b2.zip`;其 zip 内清单在 `.codebuddy-plugin\marketplace.json`(非根目录),插件条目在 `external_plugins\<plugin>\.codebuddy-plugin\plugin.json`(实测含 name/description/author/repository/homepage/license 字段,未见 dependencies 字段——条目级依赖表达形式以清单文件实测为准)。R4/R5 按此实现。
112
- 2. **auth_injection_rules 结构已实测**(同日核查):connectors.json 顶层键 = ["name","description","owner","auth_injection_rules","connectors"];规则条目 = {id, when:{active_in:[...]}, applies_to_connectors:[...], requires_connected_connectors:[...], timing:"request", inject:[{from_connector, token_type:"mcp-oauth"|"oneid-token", header, value_template:"Bearer ${access_token}"}]},实测仅 2 条(tencent-docs-dual-token、ima-mcp-dual-token),均为代注入机制 → 归 X9 Non-Goals。R1 相应收窄为:**解析该数组并在预览/详情展示「存在 N 条 token 注入规则(依赖 WB 账号体系,DSH 不代注入)」提示**,不实现注入逻辑;connector 条目实测键 = id,name,name_en,description,description_zh,description_en,source,auth_mode,type,examples_zh,examples_en,visible_in,与 R2/R3 字段完全吻合。
113
- 3. **marketplace.json 条目级 dependencies 的具体表达形式待解析真实 zip 时确认**(插件间依赖已在 WB `installed_plugins.json` v2 中证实存在,影响 R6 的解析实现)。
114
- 4. **`upstream-skills-lock.json` 字段级结构未逐项验证**(文件存在于 WB 数据目录、28971 字节),R8 的来源锁字段可在实现前参照该文件对齐。
115
-
116
- ### 3.5 Non-Goals(排除项 X1–X9,禁止进入需求池)
117
-
118
- | # | 排除项 | 一句话理由 |
119
- |---|---|---|
120
- | X1 | 多账号隔离(WB 的 5 个 user-uuid 目录) | DSH 宿主无多账号概念,armory 是单用户本地工具,`~/.dsh` 单命名空间与宿主会话模型冲突 |
121
- | X2 | `visible_in` 的渠道/档位准入逻辑(iOA 渠道、付费档位控制) | armory 不是分发渠道方;G3 只做数据标签展示与本地筛选 |
122
- | X3 | server-side / oauth / oneid-token 连接器的服务端代登录 | 依赖 WB 闭源客户端与账号服务,DSH 侧无对应服务端;极限是安装时按 auth_mode 标注 |
123
- | X4 | WB 式静默自动更新(autoUpdate) | 静默改写用户环境风险高;G8 只做「检查 + 提示」 |
124
- | X5 | WB 内置市场内容搬运 | 内容嵌在闭源本体运行时内;armory 专家市场已独立实现同类能力 |
125
- | X6 | tabbit-browser 第三方浏览器技能市场 | 强绑定 tabbit 生态运行时,DSH 侧有独立浏览器技能体系 |
126
- | X7 | `lspServers` 的 DSH 原生 LSP 托管 | DSH 宿主无 LSP 托管运行时;G1 中仅展示该字段、不承诺生效 |
127
- | X8 | 企微技能包(wecom-unified)市场 | 强绑定企业微信场景;仅其「技能锁」工程机制纳入 G9 |
128
- | X9 | WB 的 mcp-oauth / oneid-token 双 token 代注入机制 | 依赖 WB 账号体系与闭源服务端;G4 只做用户自填简单 token |