dsh-armory 2.0.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,530 +0,0 @@
1
- # dsh-armory「WorkBuddy 市场对标」系统设计与任务分解
2
-
3
- > 设计:Bob(Architect)| 日期:2026-10-20 | 输入:`docs/prd-workbuddy-parity.md`(v1.0,Alice)+ 主理人核实事实包
4
- > 约束基线:纯 JavaScript ESM、零新依赖、Node ≥ 22.15、无构建步骤、Host Remote 契约三件套同步、增量项目(非绿地)。
5
-
6
- ---
7
-
8
- # Part A:系统设计
9
-
10
- ## 1. 实现方案(Implementation Approach)
11
-
12
- ### 1.1 核心技术难点与对策
13
-
14
- | # | 难点 | 对策 |
15
- |---|------|------|
16
- | D1 | 连接器「残缺安装」:`serverInstallability`(`lib/wb-market.js:118-133`)把 env/header 带 token 的条目一律拒装,80+ 条 auth_mode=token 连接器装不了 | 把「需要 token」从**拒装理由**改造成**可装但需引导**的独立判定:占位符扫描(env/header 值中形如 `${access_token}`、`Bearer ${token}` 的模板槽位)→ 用户自填替换 → 写入 profile。oauth/oneid 类照旧不代注入,但由「静默拒装」升级为「明示不可代注入」(R7 边界) |
17
- | D2 | `auth_injection_rules` 是 WB 账号体系代注入(X9),DSH 不做注入 | 严格收窄为**只解析、只展示**:读顶层 `auth_injection_rules` 数组,按 `applies_to_connectors` 关联到连接器条目,UI 显示「存在 N 条 token 注入规则(依赖 WB 账号体系,DSH 不代注入)」(R1) |
18
- | D3 | 连接器源硬编码单 URL(`lib/wb-market.js:27`),插件源要接进来但不能新增硬编码 | 抽出**多市场注册表** `~/.dsh/armory/marketplaces.json`(借鉴 WB `known_marketplaces.json` schema),连接器 zip 与 codebuddy-plugins-official 都从注册表取源;新增市场 = 新增一条注册表记录,不改代码(R4) |
19
- | D4 | 插件 zip 结构特殊:清单在 `.codebuddy-plugin\marketplace.json`(非根目录),插件条目在 `external_plugins\<name>\.codebuddy-plugin\plugin.json`,条目无 dependencies 字段 | `ZipReader.listByPrefix`(`lib/wb-zip.js:103`)已支持前缀枚举,按实测路径解析;dependencies 做**防御性解析**(字段不存在 = 无依赖,不猜格式;实现时若真实 zip 中出现依赖表达形式再按实测兼容)(R5/R6) |
20
- | D5 | 插件安装走宿主通道,装完不生效要重启 | 客户端直接调 `pluginManager.installBundle(targetSpec, {})`(严格双参,`lib/client.js:3577` 已记录踩坑注释),安装成功后渲染**持久醒目重启提示条**,重启前不消失(R5) |
21
- | D6 | 已安装状态分散(技能=目录即记录、连接器=profile 内 mcpServers、插件=宿主 bundle 列表),无法全量筛选 | 新建统一**本地安装状态 + 来源锁**文件 `~/.dsh/armory/install-state.json`,三类安装写链在各自收尾点同步登记;列表侧以该文件做「已安装」全量筛选(R8) |
22
- | D7 | ETag 缓存已有(`lib/wb-market.js:83` 起,`meta.json` 记 ETag),不能发明平行机制 | R9 复用既有 `loadConnectorZip` 的 304 探测链路,插件市场按同一模式实现(zip 缓存 + meta 记 ETag),只加一个轻量「上游是否有更新」的只读检查方法 |
23
-
24
- ### 1.2 框架与库选型
25
-
26
- - **零新依赖**(PRD 硬约束)。全部能力用既有栈覆盖:
27
- - ZIP 解压:`lib/wb-zip.js` 的 `ZipReader`(自研 zlib DEFLATE,store/deflate 双支持、大小写不敏感、前缀枚举)——插件 zip 与连接器 zip 同一读取器;
28
- - 网络下载:`fetch` + UA + 大小上限(沿用 `wb-market.js:52` `fetchWithLimits` 模式);
29
- - JSON/原子写盘:`node:fs/promises` + staging-rename 原子替换(沿用 `wbSkillInstall:326-349` 的 staging 模式);
30
- - YAML patch:`yaml@2.9.0`(既有 expert-market 写链,本项目不改 patch 写链,连接器安装仍走 `applyManagedImport/applyWorkspaceImport`);
31
- - 契约校验:`zod@4.4.3`(typert codec,`$any` 宽容度,沿用 wb* 系列)。
32
- - **架构模式**:沿用 dsh-armory 现有「Host 数据层(lib/*.js 纯函数模块)+ Host Remote 门面(index.js McpManager 方法)+ typert 契约 + client.js 手写 Web 面板」三层结构。本设计不发明平行体系,只做**增量扩展**:新逻辑进新文件,改动落在既有扩展点上。
33
-
34
- ### 1.3 模块划分(增量)
35
-
36
- | 模块 | 性质 | 职责 |
37
- |------|------|------|
38
- | `lib/wb-registry.js` | **新增** | 多市场注册表读写(R4):内置默认项 + 用户自定义项的合并投影、增删改、ETag/lastUpdated 维护 |
39
- | `lib/wb-install-state.js` | **新增** | 本地安装状态 + 来源锁读写(R8):`~/.dsh/armory/install-state.json` 的 record/forget/list,原子写 |
40
- | `lib/wb-auth-rules.js` | **新增** | `auth_injection_rules` 解析与关联(R1)+ mcpServers 占位符扫描与 token 替换(R7 的纯函数部分) |
41
- | `lib/wb-plugin-market.js` | **新增** | 插件市场(R5/R6):按注册表源下载 zip、解析 marketplace.json/plugin.json、目录投影、组成统计、依赖提示、installSpec 计算、ETag 缓存 |
42
- | `lib/wb-market.js` | **修改** | 连接器源改走注册表(R4);目录投影补 `visible_in/version` 字段(R2/R3);`serverInstallability` 拆 token 判定(R7 改造点);`wbConnectorInstall` 支持 tokens + 登记状态 |
43
- | `lib/index.js` | **修改** | 新增 Remote 方法(插件/注册表/状态/更新提示);`wbConnectorInstall`/`wbSkillInstall` 收尾点登记安装状态 |
44
- | `lib/typert.js` | **修改** | 新增 Remote 方法契约声明(三件套之一) |
45
- | `lib/client.js` | **修改** | 市场面板扩展:插件子 tab、连接器标签/示例/规则提示/令牌弹窗/已安装筛选/排序、重启提示条、更新提示条;契约数组同步(三件套之一) |
46
- | `lib/wb-zip.js` / `lib/expert-market.js` | 不改 | 只复用(ZipReader / recent.json 模式参考) |
47
-
48
- ## 2. 文件列表(File List)
49
-
50
- ```
51
- dsh-armory/
52
- ├─ lib/
53
- │ ├─ index.js # 修改:新增 Remote 方法 + 安装收尾登记状态
54
- │ ├─ typert.js # 修改:契约声明三件套之一
55
- │ ├─ client.js # 修改:市场面板 UI 扩展 + 契约数组三件套之一
56
- │ ├─ wb-registry.js # 新增:多市场注册表(R4)
57
- │ ├─ wb-install-state.js # 新增:安装状态 + 来源锁(R8)
58
- │ ├─ wb-auth-rules.js # 新增:注入规则解析 + token 占位符(R1/R7)
59
- │ ├─ wb-plugin-market.js # 新增:插件市场目录/详情/安装规格(R5/R6)
60
- │ ├─ wb-market.js # 修改:注册表化 + 元数据增强 + 可装性改造(R2/R3/R4/R7)
61
- │ └─ wb-zip.js # 不改(复用)
62
- ├─ test/
63
- │ ├─ wb-registry.mjs # 新增:注册表读写/默认项/防御解析
64
- │ ├─ wb-install-state.mjs # 新增:状态登记/全量筛选投影/原子写
65
- │ ├─ wb-auth-rules.mjs # 新增:规则关联 + 占位符扫描/替换真值表
66
- │ ├─ wb-plugin-market.mjs # 新增:构造样例 zip 验证清单解析/依赖提示/组成统计
67
- │ ├─ wb-connector-meta.mjs # 新增:visible_in/examples/version 投影 + 可装性改造回归
68
- │ ├─ wb-install-flow.mjs # 新增:令牌替换写链 + 状态登记联动(替身宿主,不联网)
69
- │ ├─ wb-update-hint.mjs # 新增:ETag 更新提示判定
70
- │ └─ run-all.mjs # 修改:挂载上述 7 个新套件
71
- └─ docs/
72
- ├─ system_design.md # 本文档
73
- ├─ sequence-diagram.mermaid # 时序图抽取
74
- └─ class-diagram.mermaid # 类图抽取
75
- ```
76
-
77
- ## 3. 数据结构与接口(Data Structures and Interfaces)
78
-
79
- ```mermaid
80
- classDiagram
81
- %% ── 数据模型 ──────────────────────────────────────
82
- class MarketplaceEntry {
83
- +string id
84
- +string kind %% "connectors" | "plugins"
85
- +string manifestName
86
- +string type %% "zip" | "directory"
87
- +object source %% {url, path}
88
- +string manifestPath
89
- +boolean autoUpdate
90
- +boolean isBuiltIn
91
- +string description
92
- +number addedAt
93
- +number lastUpdated
94
- }
95
- class InstallStateItem {
96
- +string kind %% "connector"|"skill"|"plugin"
97
- +string key
98
- +string name
99
- +string scope
100
- +string installPath
101
- +string version
102
- +string[] dependencies
103
- +object source %% {marketplace,url,kind} 来源锁
104
- +number installedAt
105
- +number lastUpdated
106
- +object filesDigest
107
- }
108
- class ConnectorRow {
109
- +string id
110
- +string name
111
- +string authMode
112
- +string version
113
- +string[] visibleIn
114
- +string[][] examplesZhEn
115
- +number tokenSlots
116
- +number injectionRules
117
- +boolean installable
118
- +boolean needsTokens
119
- +string blockedReason
120
- }
121
- class PluginRow {
122
- +string name
123
- +string description
124
- +object author
125
- +string[] keywords
126
- +string category
127
- +object composition %% {skills,commands,agents,hooks,mcpServers,lspServers}
128
- +string[] dependencies
129
- +string installSpec
130
- +boolean installed
131
- }
132
-
133
- %% ── Host 数据层 ──────────────────────────────────
134
- class MarketRegistry {
135
- -string registryPath()
136
- +readRegistry()$ Promise~RegistryDoc~
137
- +upsertMarketplace(entry)$ Promise~RegistryDoc~
138
- +removeMarketplace(id)$ Promise~RegistryDoc~
139
- +sourceFor(kind)$ Promise~MarketplaceEntry~
140
- }
141
- class InstallStateStore {
142
- -string statePath()
143
- +record(item)$ Promise~void~
144
- +forget(kind, key)$ Promise~void~
145
- +listInstalled(kind)$ Promise~InstallStateItem[]~
146
- }
147
- class AuthRules {
148
- +parseInjectionRules(indexJson)$ InjectionRule[]
149
- +rulesFor(rules, connectorId)$ number
150
- +scanTokenSlots(mcpServers)$ TokenSlot[]
151
- +applyTokens(mcpServers, tokens)$ object
152
- }
153
- class ConnectorMarket {
154
- +wbConnectorCatalog()$ Promise~CatalogResult~
155
- +wbConnectorPreview(connectorId)$ Promise~PreviewResult~
156
- +wbConnectorCachedCount()$ Promise~object~
157
- -loadConnectorZip() Promise~ZipReader~
158
- -serverInstallability(name, raw)$ CheckResult
159
- }
160
- class PluginMarket {
161
- +wbPluginCatalog()$ Promise~PluginListResult~
162
- +wbPluginDetail(payload)$ Promise~PluginRow~
163
- +wbPluginUpdateCheck()$ Promise~UpdateHint~
164
- -loadPluginZip(entry) Promise~ZipReader~
165
- }
166
- class ZipReader {
167
- +extract(name) Buffer
168
- +has(name) boolean
169
- +listByPrefix(prefix) string[]
170
- }
171
-
172
- %% ── Host Remote 门面 ─────────────────────────────
173
- class McpManagerRemote {
174
- +wbMarketRegistry()$ Promise~RegistryDoc~
175
- +wbMarketRegistryUpsert(payload)$ Promise~RegistryDoc~
176
- +wbPluginCatalog(payload)$ Promise~PluginListResult~
177
- +wbPluginDetail(payload)$ Promise~PluginRow~
178
- +wbPluginUpdateCheck()$ Promise~UpdateHint~
179
- +wbInstallState()$ Promise~InstallStateDoc~
180
- +wbInstallStateRecord(payload)$ Promise~void~
181
- +wbInstallStateForget(payload)$ Promise~void~
182
- +wbConnectorInstall(payload)$ Promise~NoteResult~
183
- +wbSkillInstall(payload)$ Promise~NoteResult~
184
- }
185
-
186
- %% ── 客户端 ───────────────────────────────────────
187
- class WorkBuddyMarket {
188
- -state tab %% connector|skill|plugin
189
- -state installedIndex
190
- -state updateHint
191
- +render() void
192
- }
193
- class ConnectorTab {
194
- -state visibleInFilter
195
- -state onlyInstalled
196
- -state sortKey
197
- -state tokenModal
198
- +openTokenModal(slots) void
199
- +confirmInstall(tokens) Promise~void~
200
- }
201
- class PluginTab {
202
- -state query
203
- -state restartBanner
204
- +installPlugin(row) Promise~void~
205
- }
206
- class TokenModal {
207
- +string[] slots
208
- +confirm() void
209
- +cancel() void
210
- }
211
-
212
- McpManagerRemote ..> ConnectorMarket : 转发
213
- McpManagerRemote ..> PluginMarket : 转发
214
- McpManagerRemote ..> MarketRegistry : 转发
215
- McpManagerRemote ..> InstallStateStore : 转发
216
- ConnectorMarket --> ZipReader : 读取 zip
217
- PluginMarket --> ZipReader : 读取 zip
218
- PluginMarket --> MarketRegistry : 取源
219
- ConnectorMarket --> AuthRules : 规则解析/占位符
220
- ConnectorMarket --> InstallStateStore : 安装登记(经 index.js)
221
- PluginMarket --> InstallStateStore : 安装登记(经 index.js)
222
- WorkBuddyMarket *-- ConnectorTab
223
- WorkBuddyMarket *-- PluginTab
224
- ConnectorTab *-- TokenModal
225
- McpManagerRemote ..> WorkBuddyMarket : call()/installBundle
226
- ```
227
-
228
- ## 4. 程序调用流(Program Call Flow)
229
-
230
- ```mermaid
231
- sequenceDiagram
232
- autonumber
233
- participant U as 用户
234
- participant P as WorkBuddyMarket (client.js)
235
- participant R as McpManagerRemote (index.js)
236
- participant PM as PluginMarket
237
- participant CM as ConnectorMarket
238
- participant Reg as MarketRegistry
239
- participant St as InstallStateStore
240
- participant H as 宿主 pluginManager
241
-
242
- rect rgb(240,248,255)
243
- Note over U,H: 插件市场(R4/R5/R6/R8)
244
- U->>P: 切到「插件」子 tab
245
- P->>R: call('wbPluginCatalog')
246
- R->>Reg: sourceFor("plugins")
247
- Reg-->>R: codebuddy-plugins-official(zip 源)
248
- R->>PM: wbPluginCatalog()
249
- PM->>PM: 下载 zip(ETag 304 探测,缓存 ~/.dsh/armory/plugin-market/)
250
- PM->>PM: 解析 .codebuddy-plugin/marketplace.json + external_plugins/*/plugin.json(dependencies 缺失=无依赖)
251
- PM-->>P: 150+ 插件行(组成 chips / keywords / author / installSpec)
252
- U->>P: 点开插件详情 → 依赖清单展示
253
- U->>P: 确认安装
254
- P->>P: pluginManager.installBundle(targetSpec, {}) ←严格双参
255
- H-->>P: restart-required
256
- P->>R: call('wbInstallStateRecord', {kind:'plugin', source锁})
257
- R->>St: record(item)
258
- P->>U: 醒目持久提示「需重启 DSH 宿主才生效」
259
- end
260
-
261
- rect rgb(255,248,240)
262
- Note over U,H: 连接器浏览 + 令牌引导安装(R1/R2/R3/R7/R8)
263
- U->>P: 打开「连接器」子 tab
264
- P->>R: call('wbConnectorCatalog')
265
- R->>Reg: sourceFor("connectors")
266
- R->>CM: wbConnectorCatalog()
267
- CM->>CM: 注册表源替换原硬编码 URL,ETag 链路不变
268
- CM->>CM: 投影 visible_in/version/examples + 注入规则计数 + token 槽位扫描
269
- CM-->>P: 目录行(含 needsTokens/injectionRules)
270
- U->>P: 安装 auth_mode=token 连接器
271
- P->>U: 令牌弹窗(每个 env/header 占位符一行,遮罩输入)
272
- U->>P: 填入 token → 确认安装
273
- P->>R: call('wbConnectorInstall', {connectorId, tokens, mode, wsPath})
274
- R->>CM: wbConnectorPreview() → AuthRules.applyTokens() 替换占位符
275
- R->>R: applyManagedImport/applyWorkspaceImport(既有准入+patch 写链)
276
- R->>St: record({kind:'connector', version, source锁})
277
- R-->>P: 安装完成 + note
278
- end
279
-
280
- rect rgb(245,255,245)
281
- Note over U,H: 保鲜提示(R9)
282
- U->>P: 打开市场页
283
- P->>R: call('wbPluginUpdateCheck')
284
- R->>PM: 对注册表各源 if-none-match 探测
285
- PM-->>P: {marketplaces:[{id,updated}]}
286
- alt 有更新
287
- P->>U: 顶部轻提示「目录有更新」
288
- U->>P: 确认 → 重拉 wbPluginCatalog / wbConnectorCatalog
289
- else 无更新
290
- P->>U: 无感知
291
- end
292
- end
293
- ```
294
-
295
- ## 5. Anything UNCLEAR(不明确项与假设)
296
-
297
- 1. **插件 installSpec 的最终取值**(假设待实测修正):WB 插件条目含 repository/homepage 字段,DSH 的 `installBundle(spec, {})` 执行的是 npm spec 安装。设计假设「插件条目的可装 spec = 其 npm 包 spec(优先 repository 推导,其次 registry 记录);不可推导的条目标注不可装、仅展示」。真实 zip 拿到后以实测为准,若插件本体不是 npm 包形态,则安装通道需降级为「下载 zip 解压到宿主可发现目录」(此时 T03 内修正,不影响整体结构)。
298
- 2. **marketplace.json 条目级 dependencies 表达形式**(PRD Open Question 3,事实包第 2 条确认 plugin.json 实测无该字段):设计按「字段不存在 = 无依赖」防御性解析;R6 的依赖提示实现为「解析到什么就展示什么」,真实依赖样例(如 security-scan 类)解析格式在 T03 实测时补齐。
299
- 3. **R9 的「有更新」判定口径**:连接器侧沿用 meta.json 的 ETag(304=无更新、200=有更新);插件侧按同一模式新增 plugin-market/meta.json。口径 = ETag 与上次落盘不一致。若上游对 HEAD/条件请求返回不稳定,允许降级为「内容哈希比对」但不多做请求。
300
- 4. **wbConnectorCachedCount 不受影响**:仍只读磁盘缓存、零网络;插件 chip 计数不在本期范围(PRD 未要求)。
301
- 5. **假设**宿主 `installBundle` 对已装同名包会返回 restart-required(与 client.js:3588 注释一致),UI 重启提示条以此为触发;若宿主未来改为热生效,提示条随之自然消失,不需要改设计。
302
-
303
- ---
304
-
305
- # Part B:任务分解
306
-
307
- ## 6. Required Packages(无新增依赖)
308
-
309
- **零新依赖**——本迭代不向 `package.json` 添加任何运行时依赖(PRD 3.1 硬约束)。既有依赖及用途:
310
-
311
- ```
312
- - yaml@2.9.0: profile patch / 专家市场 YAML 读写(本项目沿用既有写链,不新增调用面)
313
- - smol-toml@^1.8.0: 既有 TOML 解析(本项目不新增使用)
314
- - zod@4.4.3: typert strict codec 的 schema 载体(新 Remote 方法沿用 $any 宽容度,与 wb* 系列一致)
315
- - Node 内置: node:zlib(ZipReader 解 DEFLATE)、node:fs/promises(原子写盘)、fetch(带上限下载)——经 lib/wb-zip.js 与 wb-market.js 既有模式复用
316
- ```
317
-
318
- ## 7. 任务列表(按依赖排序,共 5 个任务)
319
-
320
- | 任务 ID | 任务名 | Source Files | Dependencies | Priority |
321
- |---------|--------|--------------|--------------|----------|
322
- | **T01** | **共享数据层基础设施**(本项目「项目基础设施」落地形态:注册表读取器 + 安装状态/来源锁读写器 + 注入规则/占位符纯函数库 + 测试基建扩展) | `lib/wb-registry.js`(新)、`lib/wb-install-state.js`(新)、`lib/wb-auth-rules.js`(新)、`test/wb-registry.mjs`(新)、`test/wb-install-state.mjs`(新)、`test/wb-auth-rules.mjs`(新)、`test/run-all.mjs`(挂载 3 套件) | — | P0 |
323
- | **T02** | **批次一:连接器正确性补丁**(R1 注入规则展示 / R2 visible_in 标签与筛选 / R3 元数据增强;含 R7 改造点预备——可装性判定拆出 needsTokens,但令牌弹窗接线归 T04) | `lib/wb-market.js`、`lib/typert.js`、`lib/client.js`(连接器 tab:标签徽章、版本、示例问法一键复制、规则提示行、visible_in 多选筛选)、`test/wb-connector-meta.mjs`(新) | T01 | P0 |
324
- | **T03** | **批次二:多市场注册表接入 + 插件市场子 tab**(R4 连接器源注册表化 + R5 插件浏览/安装 + R6 依赖提示;安装走 installBundle 双参 + 持久重启提示) | `lib/wb-plugin-market.js`(新)、`lib/index.js`(新 Remote 方法 + 插件链路)、`lib/typert.js`、`lib/client.js`(插件 tab + 重启提示条)、`test/wb-plugin-market.mjs`(新) | T01 | P0 |
325
- | **T04** | **令牌引导安装 + 安装状态落地**(R7 令牌弹窗接线与占位符替换写链 + R8 三类安装登记与「已安装」全量筛选) | `lib/wb-market.js`、`lib/index.js`(wbConnectorInstall/wbSkillInstall 收尾登记)、`lib/client.js`(令牌弹窗 TokenModal + 已安装筛选 + 状态查询接 wbInstallState)、`test/wb-install-flow.mjs`(新) | T01、T02、T03 | P0 |
326
- | **T05** | **批次三:保鲜提示 + 排序 + 集成收尾**(R9 ETag 更新提示条 + R10 连接器排序 + run-all 全量挂载 + 契约一致性终验) | `lib/wb-plugin-market.js`(wbPluginUpdateCheck)、`lib/wb-market.js`、`lib/typert.js`、`lib/client.js`(更新提示条 + 排序下拉)、`test/wb-update-hint.mjs`(新)、`test/run-all.mjs`(挂满 7 套件) | T01、T03、T04 | P1 |
327
-
328
- > 任务数 5(硬上限内);每任务 ≥3 文件;T01/T02/T03 可并行推进(仅 T04 需要 T02 的 UI 骨架与 T03 的注册表就位),避免长线性链。
329
-
330
- ## 8. Shared Knowledge(跨任务共享知识)
331
-
332
- ### 8.1 Host Remote 契约三件套同步清单(本次预计新增 8 个方法)
333
-
334
- 每新增一个 Remote 方法,**三处落点缺一即静默 404**(历史事故:toolTailor、installInfo):
335
-
336
- | Remote 方法 | ① lib/typert.js | ② lib/client.js 契约数组 | ③ lib/index.js McpManager |
337
- |---|---|---|---|
338
- | `wbMarketRegistry` | `TYPERT.invocations` 追加 `mv('wbMarketRegistry', [], 'RegistryResult')` | `REMOTE_CONTRACT` 数组(约 :7464 区域)追加同 id 条目 | `_wbMarketRegistry_decorators = [Remote("wbMarketRegistry")]`(~:931 区域)+ `async wbMarketRegistry()` |
339
- | `wbMarketRegistryUpsert` | `mv('wbMarketRegistryUpsert', [payloadParam], 'RegistryResult')` | 同上区域追加 | decorators + async 方法(参数校验后写注册表) |
340
- | `wbPluginCatalog` | `mv('wbPluginCatalog', [payloadParam], 'WbPluginListResult')` | 同上区域追加 | decorators + async 方法(转发 wb-plugin-market.js) |
341
- | `wbPluginDetail` | `mv('wbPluginDetail', [payloadParam], 'WbPluginDetailResult')` | 同上区域追加 | decorators + async 方法 |
342
- | `wbPluginUpdateCheck` | `mv('wbPluginUpdateCheck', [], 'WbUpdateHintResult')` | 同上区域追加 | decorators + async 方法 |
343
- | `wbInstallState` | `mv('wbInstallState', [], 'InstallStateResult')` | 同上区域追加 | decorators + async 方法 |
344
- | `wbInstallStateRecord` | `mv('wbInstallStateRecord', [payloadParam], 'NoteResult')` | 同上区域追加 | decorators + async 方法(插件安装后客户端回写登记) |
345
- | `wbInstallStateForget` | `mv('wbInstallStateForget', [payloadParam], 'NoteResult')` | 同上区域追加 | decorators + async 方法 |
346
-
347
- **改造(非新增)**的既有方法:`wbConnectorInstall`(payload 增加 `tokens`;内部经 `AuthRules.applyTokens` 替换占位符,收尾 `InstallStateStore.record`)、`wbSkillInstall`(收尾 `record`)。三件套签名不变(payload 仍为 `$any`),只改 lib/index.js / lib/wb-market.js 实现。
348
- **守护**:`test/contract-consistency.mjs` 运行时解析三份源码做差集,新增方法漏落点该套件直接红——每个任务收尾必须跑 `node ./test/run-all.mjs`。
349
-
350
- ### 8.2 测试入口
351
-
352
- ```
353
- node ./test/run-all.mjs # 全量(npm test 同义),子进程逐套件、非零即败
354
- node --import ./test/loader.mjs test/<suite>.mjs # 单套件(loader 重定向 yaml/smol-toml/zod 宿主副本)
355
- ```
356
-
357
- 新增 7 个套件全部挂入 `test/run-all.mjs` 的 `SUITES` 白名单(该文件显式白名单是唯一安全入口,禁止用 `node --test` 自动发现)。新套件纪律:不联网(fetch 替换为必抛函数)、不碰真实 profile、zip 类用构造样例。
358
-
359
- ### 8.3 `~/.dsh` 新增文件命名约定与 JSON schema 概要
360
-
361
- 新文件统一收进 `~/.dsh/armory/`(与既有 `~/.dsh/wb-market/` 缓存区并列、互不侵入):
362
-
363
- ```jsonc
364
- // ~/.dsh/armory/marketplaces.json —— 多市场注册表(R4)
365
- {
366
- "version": 1,
367
- "marketplaces": [{
368
- "id": "codebuddy-plugins-official", // 稳定键
369
- "kind": "plugins", // "connectors" | "plugins"(一个 kind 可多条,sourceFor 取第一条)
370
- "manifestName": "codebuddy-plugins-official",
371
- "type": "zip", // "zip" | "directory"
372
- "source": { "url": "https://download.codebuddy.cn/plugin-marketplace/codebuddy-plugins-official-4b9366ac-f7e5-4cef-b60a-99a5229ce2b2.zip", "path": "" },
373
- "manifestPath": ".codebuddy-plugin/marketplace.json", // 实测:清单在 zip 内该路径,非根目录
374
- "autoUpdate": false, // DSH 永不做静默更新(X4),字段仅记录 WB 语义
375
- "isBuiltIn": true,
376
- "description": "CodeBuddy 官方插件市场",
377
- "addedAt": 0, "lastUpdated": 0
378
- }]
379
- }
380
- // 内置默认两条(代码内声明,首读时落盘):connectors → 现有 connectors-config.zip;plugins → codebuddy-plugins-official
381
-
382
- // ~/.dsh/armory/install-state.json —— 安装状态 + 来源锁(R8;取 WB upstream-skills-lock.json 之精神)
383
- {
384
- "version": 1,
385
- "items": [{
386
- "kind": "connector", // "connector" | "skill" | "plugin"
387
- "key": "tencent-docs", // connector=id;skill=namespace__slug;plugin=插件名
388
- "name": "腾讯文档",
389
- "scope": "managed", // "managed" | "workspace:<path>"
390
- "installPath": "", // plugin/skill 有值;connector 空串(落 profile)
391
- "version": "1.0.1",
392
- "dependencies": [],
393
- "source": { "marketplace": "workbuddy-connectors", "url": "https://static.workbuddy.cn/...", "kind": "zip" },
394
- "installedAt": 0, "lastUpdated": 0,
395
- "filesDigest": { "相对路径": "sha256" } // 仅 plugin/skill 记关键文件;connector 不记
396
- }]
397
- }
398
- ```
399
-
400
- 缓存区(非状态、可随时删):`~/.dsh/armory/plugin-market/plugin.zip` + `meta.json`(`{etag, fetchedAt}`),与 `~/.dsh/wb-market/connectors-config.zip` + `meta.json` 同构。
401
-
402
- ### 8.4 ETag 缓存复用点
403
-
404
- - 连接器:`lib/wb-market.js:83`(`loadConnectorZip` 的 `if (meta.etag)` 304 探测分支)**原样保留**,仅把 `CONNECTOR_ZIP_URL`(:27)的取值改为「注册表 `sourceFor("connectors")` 命中项的 url,缺注册表回落硬编码默认」。
405
- - 插件:`lib/wb-plugin-market.js` 新增同构 `loadPluginZip`(zip 落盘 + meta 记 ETag + 304 探测),复用 `ZipReader`。
406
- - R9:`wbPluginUpdateCheck` 只做条件请求探测、绝不落新包;确认更新 = 重新调 catalog(其内部才会换包)。
407
- - **R9 `wbPluginUpdateCheck` 返回契约(`reason` 枚举值域,Run4b 实测 7 分支后由 Bob 补钉)**:
408
-
409
- | reason | 触发条件 | ok | hasUpdate | 客户端 UI 文案(`lib/client.js` 更新提示按钮态) |
410
- |---|---|---|---|---|
411
- | `no-source` | 注册表中无 `kind:"plugins"` 市场源 | false | false | 「未配置插件市场」→ 提示去配置市场源(配置类动作) |
412
- | `probe-failed` | **有源**但本次探测失败:网络异常/超时,或 HTTP 非 ok(含 5xx/4xx);探测失败不改变既有缓存 | false | false | 「暂时连不上,稍后重试」→ 仅提示重试,**不得**引导用户重新配置市场(重试类动作) |
413
- | `no-cache` | 有源 + 本地无缓存(首次使用) | true | true | 触发一次完整 catalog 拉取(视同「有更新」引导刷新) |
414
- | `up-to-date` | 有源 + 有缓存 + 探测成功(304,或 200 且 ETag 与上次落盘一致) | true | false | 无感知(不渲染提示条) |
415
- | `update-available` | 有源 + 有缓存 + 探测成功且 ETag 与上次落盘不一致(200 换包) | true | true | 顶部轻提示「目录有更新」+ 确认按钮(确认后重拉 catalog) |
416
-
417
- 判定顺序(实现与测试都用同一顺序,避免分支重叠):`no-source`(先查注册表)→ `probe-failed`(fetch 抛错或 `!response.ok`)→ `no-cache`(本地无 zip/meta)→ `up-to-date` / `update-available`(比对 ETag)。`error` 字段只在 `ok:false` 时携带原始错误文本,供调试展示;**UI 分支只看 `reason`,不看 `error`**。
418
-
419
- ### 8.5 auth_mode 行为改造点
420
-
421
- - `lib/wb-market.js:118-133`(`serverInstallability`):把 :128-129 的「env/header 带 token → 拒装」改为**拆分输出**——`tokenSlots`(`AuthRules.scanTokenSlots` 扫描的占位符清单)+ `needsTokens` 布尔;其余拒装理由(preAuth、WorkBuddy 专属 command、未知传输)保持从严不动。auth_mode=token 且占位符可替换 → `installable=true, needsTokens=true`,UI 引导填 token;oauth/oneid → `installable=false, blockedReason="不支持代注入,需自备凭据(DSH 不代登录)"`(明示而非静默)。
422
- - `lib/wb-market.js:166`(`authMode` 仅透传):升级为目录行的一等字段,驱动徽章(`token` 绿 / `oauth` 灰标注)与弹窗分流。
423
- - `auth_injection_rules`:**只解析展示,不注入**(X9)。规则计数经 `AuthRules.rulesFor` 关联到条目,详情面板显示「存在 N 条 token 注入规则(依赖 WB 账号体系,DSH 不代注入)」。
424
-
425
- ### 8.6 其他跨任务约定
426
-
427
- - profile patch 顶层条目永远是补丁指令;若任何链路需要往 patch 新增行,必须 `- insert:` 包装(本项目连接器安装走既有 `applyManagedImport`,不直接改 patch;插件安装走宿主 installBundle,同样不直接改 patch)。
428
- - 插件安装成功后的「需重启 DSH 宿主才生效」提示条:持久渲染于插件 tab 顶部,直至用户重启宿主(组件卸载即消失,重启后 bundle 列表生效),不做 localStorage 持久化(重启即失效,符合语义)。
429
- - 所有列表筛选(已安装 / visible_in 多选 / 排序)均为**全量本地筛选**,不受当前页影响——与技能侧 `skillOnlyInstalled`(仅当前页)的差异要在标签文案写清。
430
- - UI 文案全部走 `t()` 并同步 en-US 字典(`i18n-coverage.mjs` 会抓新增漏键)。
431
-
432
- ### 8.7 既有回归套件随 R7 演进的迁移规约(Bob 签发,供严过关改测用)
433
-
434
- > 背景:R7 已把 `serverInstallability(serverName, rawServer, authMode)`(`lib/wb-market.js:190`)的语义从「鉴权一律拒装」改为「token+可替换槽位 → 放行(`needsTokens=true`),oauth/oneid → 拒装并明示」。`test/auth-gate-regression.mjs` 第 2/3 节 6 条断言基于旧语义,部署新副本后必然变红。本节签发目标形态,**只改测试、不动 lib/**。
435
-
436
- #### 8.7.1 断言 2c/2d/2e 的目标形态(鉴权闸门「不得误放」的可判定形式)
437
-
438
- 采用方案「放行必须伴随可替换槽位」并**加严**:单凭 `needsTokens===true` 不足以证安全(实现上 `needsTokens` 即 `hasReplaceableSlots`,二者恒等价,只断言它是同义反复),必须同时断言**槽位清单非空且每个槽位可判定**(有键名),并把放行集合与拒装集合的**互斥完备性**钉死(auth_mode 非空的 95 条,每条要么进放行集、要么进拒装集,不允许第三态)。可直接照抄的断言文本:
439
-
440
- ```js
441
- // 2c(替换 :248):auth_mode 非空的条目,installable===true 的必须同时满足
442
- // needsTokens===true 且 tokenSlots 非空(放行必须伴随可替换槽位,否则即误放);
443
- // installable===false 的必须 blockedReason 非空。两集合互斥且并集 = 全部 95 条。
444
- const released = authed.filter((row) => row?.installable === true);
445
- const rejected = authed.filter((row) => row?.installable === false);
446
- ok("2c 鉴权条目两态互斥完备:放行 ⇒ needsTokens 且 tokenSlots 非空;拒装 ⇒ blockedReason 非空",
447
- released.every((row) => row?.needsTokens === true && Array.isArray(row?.tokenSlots) && row.tokenSlots.length > 0)
448
- && rejected.every((row) => String(row?.blockedReason || "").length > 0)
449
- && released.length + rejected.length === authed.length,
450
- `放行=${released.length} 拒装=${rejected.length} 共=${authed.length}`);
451
-
452
- // 2d(替换 :249):oauth / oneid-token 条目必须拒装且 blockedReason 指明「不代注入/自备凭据」语义
453
- // (R7 新文案;不再要求 95 条全部拒装——token+槽位型按 R7 放行)。
454
- const oauthRows = authed.filter((row) => /^(oauth|oneid-token|mcp-oauth)$/i.test(String(row?.authMode || "").trim()));
455
- ok("2d oauth/oneid 条目全部拒装且 blockedReason 含「不代注入」",
456
- oauthRows.every((row) => row?.installable === false && /不代注入|自备凭据|自备凭据/.test(String(row?.blockedReason || ""))),
457
- `oauth/oneid 条数=${oauthRows.length}`);
458
-
459
- // 2e(替换 :250):判定域收窄到 R7 真正变更的语义域 —— token 型条目不得出现旧文案
460
- // 「DSH 无法自动注入」(防旧写法复活;实测 token 型含旧文案 = 0 条,作为防回归针有意义)。
461
- const tokenRows = authed.filter((row) => /^token$/i.test(String(row?.authMode || "").trim()));
462
- ok("2e token 型条目旧拒装文案「DSH 无法自动注入」不得复活",
463
- tokenRows.every((row) => !String(row?.blockedReason || "").includes("DSH 无法自动注入")),
464
- `token 型条数=${tokenRows.length}`);
465
- // 2e 补充(从严区守卫):server-side / mcp 型条目必须拒装且 blockedReason 非空(不得误放)。
466
- // 从严区措辞不钉死具体字符串(可随文案微调),仅断言非空;旧文案在这两个分支是合法保留项。
467
- const strictRows = authed.filter((row) => /^(server-side|mcp)$/i.test(String(row?.authMode || "").trim()));
468
- ok("2e 从严区(server-side/mcp)条目全部拒装且 blockedReason 非空",
469
- strictRows.every((row) => row?.installable === false && String(row?.blockedReason || "").length > 0),
470
- `server-side/mcp 条数=${strictRows.length}`);
471
- ```
472
-
473
- **修订说明(2e v2)**:v1 对全部 authed 条目断言旧文案不得出现,但 R7 从未要求改 server-side/mcp 分支的措辞(§8.7:421「保持从严不动」),实测 316 条目录、authed=95 中含旧文案的 13 条全部落在 server-side(12)+ mcp(1),token 型为 0 条——v1 按现写法要么必须去改 `lib/wb-market.js:212/:215`(违反 §8.7:434「只改测试、不动 lib/」),要么测试恒红,不可判定。v2 把判定域收窄:正向防回归针只挂在 token 型(R7 真正变更的语义域),从严区改为「拒装 + blockedReason 非空」的守卫式断言。
474
-
475
- 说明:2c 的互斥完备性比原断言**更强**——旧断言只证「全部拒装」,新断言同时证「放行有据、拒装有因、无第三态」。若 `authed.length===0`(上游目录变更)整节照旧走 SKIP 分支,不受影响。
476
-
477
- #### 8.7.2 断言 3b/3c/3d 的目标形态(装链顺序不变式:先替换、再探测、后落盘)
478
-
479
- 确认你的候选方向正确,按 R7 实际落点(`lib/index.js:1496-1504`)逐字定针。**关键加严**:除四针齐全外,必须断言 `applyTokens` 与 `probeMcpServersAuthorization` 都消费的是 `preview.mcpServers`(而不是中间变量重命名绕针),且探测严格晚于替换、json 组装严格晚于探测——探测早于替换 = 探的是未替换旧凭据(白探),json 早于探测 = 可能把未探测的配置落盘(违背「绝不写一半」)。可直接照抄:
480
-
481
- ```js
482
- // 3b/3c/3d(替换 :273-282)。四针与 lib/index.js:1496-1504 逐字一致:
483
- const PREVIEW_CALL = "const preview = await wbConnectorPreview(connectorId);"; // :1496
484
- const APPLY_CALL = "const { mcpServers, missingSlots } = applyTokens(preview.mcpServers, tokens);"; // :1499
485
- const PROBE_CALL = "await probeMcpServersAuthorization(mcpServers);"; // :1503
486
- const JSON_LINE = "const json = { mcpServers };"; // :1504
487
- const atPreview = source.indexOf(PREVIEW_CALL);
488
- const atApply = source.indexOf(APPLY_CALL);
489
- const atProbe = source.indexOf(PROBE_CALL);
490
- const atJson = source.indexOf(JSON_LINE);
491
- ok("3b 源码针齐全:preview / applyTokens / 探测 / json 组装四行均在场",
492
- atPreview >= 0 && atApply >= 0 && atProbe >= 0 && atJson >= 0,
493
- `preview@${atPreview} apply@${atApply} probe@${atProbe} json@${atJson}`);
494
- // 3c:替换在 preview 之后,探测在替换之后(探的必须是替换后的 mcpServers——变量名相同即证,
495
- // 因为 PROBE_CALL 消费 mcpServers 而 APPLY_CALL 是唯一对 mcpServers 的解构赋值行)。
496
- ok("3c 装链顺序:preview < applyTokens < 探测",
497
- atPreview >= 0 && atApply > atPreview && atProbe > atApply,
498
- `preview@${atPreview} < apply@${atApply} < probe@${atProbe}`);
499
- // 3d:json 组装在探测之后(未探测的配置绝不落盘)。missingSlots 回传行(return {...result, missingSlots})
500
- // 无需单独钉——它是 3b APPLY_CALL 的解构伴生, APPLY 针在场即证链路存在。
501
- ok("3d 装链顺序:探测 < json 组装(绝不写未探测的配置)",
502
- atJson >= 0 && atProbe < atJson,
503
- `probe@${atProbe} < json@${atJson}`);
504
- ```
505
-
506
- #### 8.7.3 第 0 节哈希预检的去留裁定
507
-
508
- **保留哈希预检,采用你给的规程 ①(部署后重钉),但不由严过关改测时顺手改**——重钉是**主理人的部署职责**,不是测试作者职责,两者混在一起会让「哈希对不上」既可能是真漂移也可能是忘了重钉,失去诊断价值。规程如下:
509
-
510
- 1. **重钉规程(主理人执行)**:每次向 `~/.dsh/profiles/desktop/node_modules/dsh-armory/lib/` 部署新副本后,由主理人计算三文件 SHA-256(`certutil -hashfile <path> SHA256` 或等价),**逐字更新 `EXPECTED_SHA256` 表**并同步 `.r4b-recheck.mjs` 等部署核查脚本;更新动作必须出现在部署记录里(哪个会话、何时、基于哪次 Run),可追溯。
511
- 2. **哈希不一致时的处置**:维持现有中止语义(⛔ 不做后续断言)——「测了个并行会话的中间态」的风险仍在,这是这层守卫的原始价值,不能丢。
512
- 3. **不采纳方案 ②(改符号一致性断言)的原因**:符号在场证明的是「模块存在」,证明不了「逐字节未被并行会话改写」——中间态检测恰恰需要字节级锚点;且三件套内容随 R7 持续演进,符号清单会变成第二个需要维护的漂移面,成本不降反升。
513
- 4. **补充**:哈希预检对象从 `wb-market.js / index.js / client.js` 三件**扩到含新增模块**——`EXPECTED_SHA256` 增补 `wb-auth-rules.js`、`wb-install-state.js`、`wb-plugin-market.js` 三行(由主理人部署时一并重钉),防「新模块被并行会话单独改写」的中间态漏检。
514
-
515
- ## 9. 任务依赖图(Task Dependency Graph)
516
-
517
- ```mermaid
518
- graph TD
519
- T01["T01 共享数据层基础设施<br/>wb-registry / wb-install-state / wb-auth-rules<br/>+ 3 套件 + run-all 挂载"]
520
- T02["T02 连接器正确性补丁(R1/R2/R3)<br/>wb-market / typert / client / wb-connector-meta"]
521
- T03["T03 注册表接入 + 插件市场(R4/R5/R6)<br/>wb-plugin-market / index / typert / client / wb-plugin-market 测试"]
522
- T04["T04 令牌引导 + 安装状态(R7/R8)<br/>wb-market / index / client / wb-install-flow"]
523
- T05["T05 保鲜提示 + 排序 + 收尾(R9/R10)<br/>wb-plugin-market / wb-market / typert / client / wb-update-hint"]
524
-
525
- T01 --> T02
526
- T01 --> T03
527
- T02 --> T04
528
- T03 --> T04
529
- T04 --> T05
530
- ```