@omdp/dsh-connector 0.3.2 → 0.3.3

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.
Files changed (3) hide show
  1. package/README.md +60 -12
  2. package/index.js +112 -36
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -1,12 +1,13 @@
1
1
  # @omdp/dsh-connector
2
2
 
3
- **MCP 服务器 + 用户 Skills + 魔搭市场浏览三合一设置页**(`v0.3.0`)。适合需要在 DSH 里频繁增删改 MCP server / skills、又不想手改 `cordis.patch.yml` 的用户。
3
+ **MCP 服务器 + 用户 Skills + 魔搭市场浏览三合一设置页**(`v0.3.3`)。适合需要在 DSH 里频繁增删改 MCP server / skills、又不想手改 `cordis.patch.yml` 的用户。
4
4
 
5
5
  ## Requirements
6
6
 
7
7
  - DeepSeek Harness 带 `web` profile GUI(`npx @deepseek-ai/dsh web`)
8
8
  - Node.js `^22.19` 或 `>=24`
9
- - `@deepseek-ai/cordis` `4.0.1` / `4.0.2`(已实测版本;插件无 peer 声明,唯一 DSH 硬依赖是 `ctx.webServer`)
9
+ - `@deepseek-ai/schemastery` `3.18.1` / `3.18.2` / `3.18.4`(peer 枚举,供工具过滤的 Config 声明用;无则过滤静默全放行)
10
+ - 已实测 DSH **0.1.5-rc.1**(源码级核查,2026-09-10)、**0.1.5-rc.2** 与 **0.1.6-alpha.1**(源码级 + 运行时冒烟:`/connector/api/*` 正常服务、设置页渲染,2026-09-15,见 `docs/plugin-compatibility.md`)、**0.1.7-rc.1**(0.3.3 适配:修掉 import 期崩溃 + 迁移到 settings 托管配置,2026-09-24)
10
11
 
11
12
  ## Overview
12
13
 
@@ -14,7 +15,7 @@
14
15
  合并到 DSH Web UI 的同一个设置页(设置页标签:**Connector**)。
15
16
 
16
17
  - **MCP**:读取/编辑 `profiles/web/cordis.patch.yml` 中的 `mcp-*` 块(结构化表单)。保存后**重启 `dsh` 生效**。
17
- - **工具过滤(0.3.0 新增)**:每台 MCP server 卡片下可勾选放行的工具(`mcp__<server>__<raw>` 公开名按 `__` 切分回 raw 名)。规则存 `settings.yaml` 的 `connector.toolFilters`(`{<serverName>: {allow: [...]}}`),**无配置 = 全量放行**;保存后新会话即生效、无需重启。生效三件套:`systemPrompt.tools(provider)` 隐藏 schema + `ctx.tools.guard` 执行期硬拦截(做法参照 `hyqhyq3/dsh-mcp-manager`)。典型场景:tinyfish 这类 15 个工具只留 `search`/`fetch_content` 两个免费工具。
18
+ - **工具过滤(0.3.0 新增)**:每台 MCP server 卡片下可勾选放行的工具(`mcp__<server>__<raw>` 公开名按 `__` 切分回 raw 名)。规则存本插件自己的配置(0.1.7+ 为 profile 条目 `connector` 行的 `config.toolFilters`;rc.x 为 `settings.yaml` 的 `connector.toolFilters`,双后端自动判别),**无配置 = 全量放行**;保存后新会话即生效、无需重启。生效三件套:`systemPrompt.tools(provider)` 隐藏 schema + `ctx.tools.guard` 执行期硬拦截(做法参照 `hyqhyq3/dsh-mcp-manager`)。典型场景:tinyfish 这类 15 个工具只留 `search`/`fetch_content` 两个免费工具。
18
19
  - **Skills**:列出/查看/编辑/删除 `~/.dsh/skills` 下的 `SKILL.md`。保存**即时生效**(filesystem provider 自动重新发现)。
19
20
  - **市场探索(0.2.0 新增)**:只读浏览魔搭社区 [Skills 中心](https://modelscope.cn/skills) 与 [MCP 广场](https://modelscope.cn/mcp)(匿名 OpenAPI,无需密钥)。
20
21
  - 列表/详情:名称、作者、分类、下载/浏览数、认证标识(Hosted 官方托管 / 已认证)
@@ -144,7 +145,7 @@ pnpm remove @omdp/dsh-connector
144
145
  - 列出当前 `cordis.patch.yml` 里的 `mcp-*` 服务器
145
146
  - 「编辑」改名称/传输/URL/命令/参数/Header;「删除」移除;「+ 添加」新建
146
147
  - 每台 server 卡片下有**工具过滤**多选(chips):勾选即放行,未勾选的工具模型不可见、调用被拒;「清除」回到全量放行
147
- - 保存后提示**重启 dsh** 才会真正加载新的 MCP server(工具过滤规则除外:存 settings,**新会话即生效**)
148
+ - 保存后提示**重启 dsh** 才会真正加载新的 MCP server(工具过滤规则除外:存**本插件的 profile 条目配置**,**新会话即生效**)
148
149
  2. **Skills** 区:
149
150
  - 列出 `~/.dsh/skills` 下的用户技能
150
151
  - 「编辑」改 frontmatter 与正文;「删除」移除目录;「+ 新建」创建
@@ -163,7 +164,7 @@ pnpm remove @omdp/dsh-connector
163
164
  | 设置页 | client half 注册 `settings.section` 槽位("Connector" 页签;client factory 须 `exports.inject = ['slots']`,否则 fiber 在 slots 就绪前跑 apply 会静默丢注册) |
164
165
  | 跨边界调用 | client 用 `fetch('/connector/api/...')`,host 用 `ctx.webServer.register` 接收(安装包走 HTTP) |
165
166
  | MCP 持久化 | 文本块级提取并替换 `cordis.patch.yml` 中含 `mcp-` 的 insert 块,**保留 `!!js` 表达式与 env 块原样**(preserve 桶) |
166
- | 工具过滤 | 规则存 settings `connector` 命名空间(`toolFilters`);`systemPrompt.tools(provider)` 滤 schema + `tools.guard` 硬拦截;`dsh-mcp-client` Config 封闭,规则**不能**写进 mcp 行 config |
167
+ | 工具过滤 | 规则存本插件 profile 条目(`connector` 行)的 `config.toolFilters`,经 `Config` 的 `.volatile()` 字段由 DSH settings 服务托管(0.1.7+);`systemPrompt.tools(provider)` 滤 schema + `tools.guard` 硬拦截;`dsh-mcp-client` Config 封闭,规则**不能**写进 mcp 行 config |
167
168
  | Skill 持久化 | 直接读写 `~/.dsh/skills/<name>/SKILL.md` |
168
169
 
169
170
  ## 已知限制
@@ -181,7 +182,7 @@ pnpm remove @omdp/dsh-connector
181
182
  | 问题 | 原因 / 解决 |
182
183
  |---|---|
183
184
  | 设置页看不到 Connector 标签 | bundle 未挂载:确认 `dsh.profile.bundles` 含 `@omdp/dsh-connector`,重启 dsh;仍无则检查 client.js 尾部 `exports.inject = ['slots']` 是否在(缺了会静默丢注册) |
184
- | 工具过滤不生效(模型仍能看到/调用) | 过滤规则只对**新会话**生效(当前会话的 schema 已下发);确认 settings.yaml 有 `connector.toolFilters` 且 serverName 拼写与 patch 一致 |
185
+ | 工具过滤不生效(模型仍能看到/调用) | 过滤规则只对**新会话**生效(当前会话的 schema 已下发);确认规则落盘位置正确——0.1.7+ 看 profile 条目 `connector` 的 `config.toolFilters`,rc.x 看 `~/.dsh/settings.yaml` 的 `connector.toolFilters`;再确认 serverName 拼写与 patch 一致 |
185
186
  | 保存 MCP 被拒(HTTP 400) | 配置不合法(transport/serverName/command/url 校验失败),按提示修正——插件不会写入坏配置 |
186
187
  | MCP server 保存后不生效 | 需**重启 dsh**(`dsh-mcp-client` 静态加载) |
187
188
  | `/connector/api/*` 404 | client/host 边界异常:确认插件 host 半边已加载(重启),浏览器强刷缓存 |
@@ -228,9 +229,9 @@ MIT License。安全问题请通过 GitHub Issues 私密报告(https://github.
228
229
 
229
230
  | 数据 | 访问方式 | 说明 |
230
231
  |---|---|---|
231
- | `profiles/web/cordis.patch.yml` | **读写** | MCP 块的结构化编辑(保留 `!!js`/env 原样) |
232
+ | `profiles/web/cordis.patch.yml` | **读写** | MCP 块的结构化编辑(保留 `!!js`/env 原样);0.1.7+ 工具过滤规则也由 DSH settings 服务写回本行 `config.toolFilters` |
232
233
  | `~/.dsh/skills/**/SKILL.md` | **读写** | 用户技能文件的查看/编辑/删除/新建;「记录来源」会写 `source`/`sourceUpdated` frontmatter |
233
- | `~/.dsh/settings.yaml` 等 | 只读 | 不主动读写 |
234
+ | `~/.dsh/settings.yaml` 等 | rc.x 经 settings 服务读 | 0.1.7+ 不再直接读写该文件(DSH 已改为 profile 条目托管) |
234
235
  | HTTP `/connector/api/*` | 本机监听 | 与 DSH GUI 同源,无额外鉴权 |
235
236
  | 魔搭 `modelscope.cn/openapi/v1` | **只读外部** | 市场浏览代理(匿名);结果仅存进程内存(30 分钟 TTL),**不写文件、不落盘** |
236
237
  | 环境变量 | 只读引用 | 只读 `process.env.*`,不持久化 |
@@ -241,10 +242,44 @@ MIT License。安全问题请通过 GitHub Issues 私密报告(https://github.
241
242
 
242
243
  本插件采用**抗崩溃架构**,DSH 更新时不会导致 DSH 崩溃(硬保证)。
243
244
 
244
- - **纯静态依赖**:只 `import node:*` + `yaml`(唯一第三方依赖,版本 `^2.9.0`),**零 `@deepseek-ai/*` 依赖**(`@deepseek-ai/schemastery` 仅 peer 声明,供工具过滤的 settings schema 用,无则过滤静默全放行)。
245
+ - **纯静态依赖**:只 `import node:*` + `yaml`(`^2.9.0`);`jsdom`(`^24.1.3`)**只在魔搭 WAF 挑战求解时动态 `import()`**,不在模块顶层加载(见下方 0.1.7 说明)。`@deepseek-ai/schemastery` 仅 peer 声明(供工具过滤的 Config 声明用),且用 `typeof field.volatile === 'function'` 守卫,具备则走 0.1.7+ 托管配置。
245
246
  - **唯一的 DSH 硬依赖**:`ctx.webServer`(`inject: ['webServer']`),用于注册 `/connector/api/*` HTTP 路由。
246
247
  - **失败隔离**:webServer 不可用/变化时插件**干净失败不加载**,DSH 照常运行;内部多处 try/catch 防御。
247
248
 
249
+ ### 为什么 jsdom 必须懒加载(0.3.3 修复)
250
+
251
+ 0.3.2 在模块顶层 `import { JSDOM, VirtualConsole } from 'jsdom'`,这会在插件 import 期就拉起
252
+ `jsdom → whatwg-url → tr46` 依赖图,而 `tr46/index.js` 第 3 行是 `require("punycode/")`。
253
+ DSH `0.1.7-rc.1` 的解析路由(`dsh-app-boot` `ResolutionRouter.routeScoped`)对 `punycode/`
254
+ 这种**带子路径的内置模块名**会先切出裸名 `punycode`,再用
255
+ `createRequire(...).resolve.paths("punycode")` 求查找路径 —— Node 对裸内置模块名返回
256
+ `null`,而该处循环没有兜底,直接抛:
257
+
258
+ ```
259
+ TypeError: createRequire.resolve.paths is not a function or its return value is not iterable
260
+ at ResolutionRouter.routeScoped (…/dsh-app-boot/lib/index.js:1414:58)
261
+ ```
262
+
263
+ 后果不是"市场功能不可用",而是**整个插件的 import 失败**:fiber 建不起来,设置页那行永远停在
264
+ 「已安装,重启后生效」,`/connector/api/*` 全部 404(重启也不会好,因为抛错是确定性的)。
265
+ 修复即把 jsdom 挪进 `loadJsdom()`,只在真正要解 WAF 挑战时才 `await import('jsdom')`,
266
+ 失败范围收敛到该功能本身。
267
+
268
+ > ⚠️ **诚实说明:这是缓解,不是根治。** DSH 的解析拦截(`PluginPackages` 安装的
269
+ > `installRuntimeInterception`)在**进程生命周期内常驻**(只在 `ctx.effect` 清理时 `dispose()`),
270
+ > 所以懒加载只是把同一个 `TypeError` **推迟到首次解 WAF 挑战时**,并非消除:
271
+ >
272
+ > | | 0.3.2(顶层静态 import) | 0.3.3(懒加载) |
273
+ > |---|---|---|
274
+ > | 插件 import | ❌ 崩溃 ⇒ 设置页死的、全部路由 404 | ✅ 干净 |
275
+ > | 设置页 / MCP / Skills / 工具过滤 | ❌ 全废 | ✅ 恢复 |
276
+ > | 魔搭市场浏览(走 WAF 解) | ❌(插件都没起来) | ⚠️ 首次解挑战仍会抛,但被 `marketError` 包成 **502 + 明确 message**,不挂起 |
277
+ > | 爆炸半径 | 整个插件 | 单个功能分支 |
278
+ >
279
+ > 实测该 WAF 挑战当前**未下发**(`PUT /api/v1/dolphin/mcpServers` 返回 HTTP 200、116103 字节、
280
+ > 无 `acw_sc__v2`/`aliyunwaf`),故该路径处于休眠。**根治仍应由 DSH 在 `dsh-app-boot` 的该循环
281
+ > 补 `?? []` 兜底**(同文件 `packageDirFromAnchor` 是有兜底的,新增 `routeScoped` 时漏了)。
282
+
248
283
  | 场景 | 崩溃? |
249
284
  |---|---|
250
285
  | DSH 小更新/补丁 | ✅ 不会崩 |
@@ -252,6 +287,19 @@ MIT License。安全问题请通过 GitHub Issues 私密报告(https://github.
252
287
  | yaml 版本 | ✅ 独立 npm 包,不受 DSH 更新影响 |
253
288
  | 魔搭市场不可达 | ✅ 市场接口报 502,本地 MCP/skill 管理不受影响 |
254
289
 
255
- **最后验证**:DSH `0.1.0-rc.8`(2026-08-20);0.2.0 市场功能以 `node --check` +
256
- 真实 HTTP 集成测试通过(11 项:skills/mcp 列表与详情、证书/Hosted 标识、安装命令、
257
- 记录来源回写、更新判定),未改动 DSH 实例。当前 npm 版本 `0.3.0`。
290
+ **最后验证**:DSH `0.1.7-rc.1`(2026-09-24,本轮修复:模块 import 期零抛错,`node --check` + 实际
291
+ `import()` 冒烟通过;工具过滤读写改用 0.1.7 的 `Config`+`.volatile()`+`settings.replace()`)。历史:
292
+ DSH `0.1.0-rc.8`(2026-08-20);0.2.0 市场功能以 `node --check` + 真实 HTTP 集成测试通过(11 项:
293
+ skills/mcp 列表与详情、证书/Hosted 标识、安装命令、记录来源回写、更新判定),未改动 DSH 实例。
294
+
295
+ ## 变更记录
296
+
297
+ - **0.3.3**(2026-09-24):**适配 DSH 0.1.7-rc.1**。
298
+ 1. **修复 import 期崩溃**:`jsdom` 由顶层静态 import 改为按需 `await import()`(原因见上方
299
+ 「为什么 jsdom 必须懒加载」)——0.3.2 在 0.1.7 上整个插件 import 失败、设置页永远显示
300
+ 「已安装,重启后生效」、API 全 404。
301
+ 2. **工具过滤迁到 0.1.7 托管配置**:导出 `Config = z.object({ toolFilters: <dict>.volatile() })`,
302
+ 读取走注入的 `config.toolFilters` 活 Ref、写入走 `settings.replace(entryId, …)`;同时对
303
+ 0.1.5/0.1.6 rc.x 保留老的 `settings.register/get/update` 后端(`typeof volatile === 'function'`
304
+ 守卫自动分流),**新旧两代均可用**。
305
+ 3. 补 `@deepseek-ai/schemastery` peer 枚举 `3.18.4`(0.1.7 自带版本)。
package/index.js CHANGED
@@ -26,8 +26,16 @@ import { spawnSync } from 'node:child_process'
26
26
  import { homedir } from 'node:os'
27
27
  import { join, dirname } from 'node:path'
28
28
  import { parse as parseYaml, stringify as stringifyYaml } from 'yaml'
29
- import { JSDOM, VirtualConsole } from 'jsdom'
30
29
  import z from '@deepseek-ai/schemastery'
30
+ // NOTE: `jsdom` is deliberately NOT imported at module scope. A static import
31
+ // pulls in jsdom -> whatwg-url -> tr46, and tr46/index.js:3 does
32
+ // `require("punycode/")`. Under DSH 0.1.7's ResolutionRouter that subpath form
33
+ // resolves `createRequire(...).resolve.paths("punycode")` to null, which the
34
+ // router iterates without a guard (dsh-app-boot/lib/index.js:1414) and throws
35
+ // `TypeError: createRequire.resolve.paths is not a function or its return value
36
+ // is not iterable` — killing this whole plugin's import (fiber never created,
37
+ // UI shows "已安装,重启后生效"). Only the ModelScope WAF challenge needs jsdom,
38
+ // so it is loaded lazily there and the failure is local to that feature.
31
39
 
32
40
  const API_PREFIX = '/connector/api'
33
41
  const SKILL_NAME = /^[a-z0-9]+(?:-[a-z0-9]+)*$/
@@ -41,12 +49,14 @@ const TRANSPORTS = new Set(['stdio', 'streamable-http'])
41
49
  // patch file itself (NUL, control chars). Tab/newline are already rejected by
42
50
  // the single-token rules; this catches the rest defensively.
43
51
  const CONTROL_CHARS = /[\x00-\x1f\x7f]/
52
+ // 0.1.7 移除了 settings.register/get/update;`settings` 服务本身仍需存在(本插件的
53
+ // Config 条目才会进入组合树并被托管),且老版本(0.1.5/0.1.6 rc.x)仍走它的
54
+ // register/get/update 老 API,所以 inject 保留。
44
55
  export const inject = ['webServer', 'settings']
45
56
 
46
57
  // ── MCP 工具过滤(tool-filter, 0.3.0 新增)────────────────────────────
47
58
  // dsh-mcp-client 的 Config schema 是封闭的:未知键会拒收并导致下次启动失败,
48
- // 所以过滤规则不能写进 mcp-* 行的 config,只能放在 connector 自己的 settings
49
- // namespace(settings.yaml `connector:` 节)里:
59
+ // 所以过滤规则不能写进 mcp-* 行的 config,只能放在 connector 自己的配置里:
50
60
  // connector:
51
61
  // toolFilters:
52
62
  // <serverName>:
@@ -56,25 +66,57 @@ export const inject = ['webServer', 'settings']
56
66
  // 1. systemPrompt.tools(provider) —— 提示词里的 schema 列表先过滤,模型看不到被滤掉的工具;
57
67
  // 2. ctx.tools.guard —— 执行期硬拦截(guard 返回 reason 即拒收),补 provider 漏网;
58
68
  // 3. UI 多选框 —— 见 client.js ServerForm 的工具过滤区 + GET /api/mcp/tools/:serverName。
59
- // 无配置 = 全量放行(零回归);guard 是同步函数,读的是 apply 时缓存 + watch 刷新的快照。
69
+ // 无配置 = 全量放行(零回归);guard 是同步函数,每次调用现读最新值。
70
+ //
71
+ // 配置后端是双版本的(同 dsh-key-fallback 的做法):
72
+ // · 0.1.7+:DSH 用 settings 服务托管插件配置 —— 本插件导出带 `.volatile()` 的
73
+ // `Config`,apply(ctx, config) 拿到的 config.toolFilters 是一个活 Ref,读写走
74
+ // `settings.replace(entryId, ...)`;
75
+ // · 0.1.5/0.1.6(rc.x):无 `.volatile()`,`Config` 退化为 undefined,改用老的
76
+ // `settings.register(ns, schema)` + `settings.get(ns)` + `settings.update(ns, ...)`。
77
+ // 两条路都在 apply 时判别一次,之后读写自动分流;都没有时静默全放行。
78
+ // Profile entry id of this plugin (the `id:` in the bundle's cordis.patch.yml,
79
+ // which the user's own profile patch keeps unless they override the row). The
80
+ // settings service addresses config by entry id, not by settings namespace.
60
81
  const CONNECTOR_SETTINGS_NS = 'connector'
82
+ const CONNECTOR_ENTRY_ID = 'connector'
61
83
  const ToolFilterSchema = z.object({
62
84
  toolFilters: z.dict(z.object({ allow: z.array(String).default([]) })).default({}),
63
85
  })
64
- function readToolFilters(ctx) {
86
+ // DSH 0.1.7+ hands a `.volatile()` field to apply() as a live Ref; 0.1.6 and older
87
+ // lack `volatile`, so Config degrades to undefined and the legacy path is used.
88
+ const _toolFiltersField = z.dict(z.object({ allow: z.array(String).default([]) })).default({})
89
+ export const Config = (typeof _toolFiltersField.volatile === 'function')
90
+ ? z.object({ toolFilters: _toolFiltersField.volatile() })
91
+ : undefined
92
+ // 后端状态:二选一(0.1.7+ 用 _filterRef;rc.x 用 _legacyScope/_legacySvc),都用空表示全放行。
93
+ let _filterRef = null // 0.1.7+: config.toolFilters 这个活 Ref
94
+ let _legacyScope = null // rc.x: settings.register(...) 返回的 scope
95
+ let _legacySvc = null // rc.x: settings 服务(读用 get,写用 update)
96
+ function readToolFilters() {
65
97
  try {
66
- const settings = ctx.get('settings')
67
- const value = settings ? settings.get(CONNECTOR_SETTINGS_NS) : undefined
68
- const filters = value && typeof value === 'object' ? value.toolFilters : undefined
69
- if (!filters || typeof filters !== 'object' || Array.isArray(filters)) return {}
70
- const out = {}
71
- for (const [server, rule] of Object.entries(filters)) {
72
- const allow = rule && Array.isArray(rule.allow) ? rule.allow.filter((t) => typeof t === 'string' && t.length > 0) : []
73
- out[server] = { allow }
98
+ // 优先 0.1.7+ 的活 Ref(.get() 每次返回最新值)
99
+ if (_filterRef) {
100
+ const value = _filterRef.get()
101
+ return normalizeToolFilters(value)
102
+ }
103
+ // rc.x:老的 settings.get(ns) —— 每次现读,外部编辑同样立即生效
104
+ if (_legacySvc && typeof _legacySvc.get === 'function') {
105
+ const value = _legacySvc.get(CONNECTOR_SETTINGS_NS)
106
+ return normalizeToolFilters(value && value.toolFilters)
74
107
  }
75
- return out
108
+ return {}
76
109
  } catch { return {} }
77
110
  }
111
+ function normalizeToolFilters(filters) {
112
+ if (!filters || typeof filters !== 'object' || Array.isArray(filters)) return {}
113
+ const out = {}
114
+ for (const [server, rule] of Object.entries(filters)) {
115
+ const allow = rule && Array.isArray(rule.allow) ? rule.allow.filter((t) => typeof t === 'string' && t.length > 0) : []
116
+ out[server] = { allow }
117
+ }
118
+ return out
119
+ }
78
120
  // 公开名 `mcp__<server>__<raw>` 反解回 (server, raw):注意 serverName 本身可含
79
121
  // 下划线,所以按 `mcp__` 前缀 + __ 分段取“第一段”为 server,余下 join 回 raw。
80
122
  function splitPublicName(publicName) {
@@ -540,8 +582,17 @@ let wafCookie = null // { value, at }
540
582
  // Solve the acw_sc__v2 cookie by executing the challenge page in jsdom. The
541
583
  // challenge HTML arrives as the body of the WAF-blocked request itself (the
542
584
  // SPA shell on /mcp is NOT a challenge page and yields no cookie).
585
+ // jsdom is imported lazily on purpose — see the note next to the import list.
586
+ let _jsdom = null
587
+ async function loadJsdom() {
588
+ if (_jsdom) return _jsdom
589
+ const mod = await import('jsdom')
590
+ _jsdom = mod.default && mod.default.JSDOM ? mod.default : mod
591
+ return _jsdom
592
+ }
543
593
  async function solveWafFromChallenge(html) {
544
594
  if (wafCookie && Date.now() - wafCookie.at < WAF_TTL) return wafCookie.value
595
+ const { JSDOM, VirtualConsole } = await loadJsdom()
545
596
  const vc = new VirtualConsole()
546
597
  vc.on('jsdomError', () => {}) // jsdom navigation is not implemented — that is expected here
547
598
  const dom = new JSDOM(html, {
@@ -922,23 +973,37 @@ async function readBody(req) {
922
973
  }
923
974
  }
924
975
 
925
- export function apply(ctx) {
926
- // ── 工具过滤三件套之 (0):settings namespace 注册 + 快照缓存 ──
927
- // settings 经声明式 inject 依赖保证就绪(见上方 export const inject),
928
- // 这里同步拿服务;万一缺失不炸,过滤保持全放行(零回归)。
976
+ export function apply(ctx, config) {
977
+ // ── 工具过滤三件套之 (0):判定配置后端 + 快照缓存 ──
978
+ // 0.1.7+:config.toolFilters 是 volatile Ref(.get() 每次返回最新值,编辑热生效)。
979
+ // rc.x:无 volatile,退回老的 settings.register/get/update(0.3.0 的原始实现)。
980
+ // 两者皆无 → 恒 {} 全放行(零回归)。
929
981
  // 注:cordis 的 ctx.inject(deps, cb) 回调按 (ctx, config) 调用,不是服务展开,
930
982
  // 不能拿回调参数当 service 用 —— 0.3.0 曾因此 register 从未落地。
931
- const settings = ctx.get('settings')
932
- let toolFilters = {}
933
- const refreshFilters = () => { toolFilters = readToolFilters(ctx) }
934
- if (settings) {
935
- try {
936
- const scope = settings.register(CONNECTOR_SETTINGS_NS, ToolFilterSchema, { base: { toolFilters: {} } })
937
- refreshFilters()
938
- ctx.effect(() => scope.watch(() => refreshFilters()), 'connector: watch tool filters')
939
- } catch (error) {
940
- console.error('[dsh-connector] register tool filter settings failed:', error?.message ?? error)
983
+ const currentFilters = () => {
984
+ try { return readToolFilters() } catch { return {} }
985
+ }
986
+ try {
987
+ const field = config && config.toolFilters
988
+ if (field && typeof field.get === 'function') {
989
+ _filterRef = field
990
+ ctx.effect(() => () => { _filterRef = null }, 'connector: release tool filter ref')
991
+ } else {
992
+ // ── rc.x 老后端:注册 settings namespace(本插件自己的两段式配置)──
993
+ const settings = ctx.get('settings')
994
+ if (settings && typeof settings.register === 'function') {
995
+ const scope = settings.register(CONNECTOR_SETTINGS_NS, ToolFilterSchema, { base: { toolFilters: {} } })
996
+ _legacySvc = settings
997
+ _legacyScope = scope
998
+ ctx.effect(() => () => { _legacySvc = null; _legacyScope = null }, 'connector: release legacy tool filter scope')
999
+ // scope 存在时用它的 watch 做热刷新(0.3.0 的行为);读仍走 settings.get。
1000
+ if (scope && typeof scope.watch === 'function') {
1001
+ ctx.effect(() => scope.watch(() => {}), 'connector: watch tool filters')
1002
+ }
1003
+ }
941
1004
  }
1005
+ } catch (error) {
1006
+ console.error('[dsh-connector] tool filter config unavailable:', error?.message ?? error)
942
1007
  }
943
1008
 
944
1009
  // ── 三件套之 (1):prompt 层过滤 —— assemble waterfall 真替换 ──
@@ -950,7 +1015,7 @@ export function apply(ctx) {
950
1015
  ctx.on('system-prompt/assemble', async (_assembly, _context, next) => {
951
1016
  const assembled = await next()
952
1017
  if (!assembled || !Array.isArray(assembled.tools)) return assembled
953
- const kept = assembled.tools.filter((t) => isToolAllowed(toolFilters, t && t.name))
1018
+ const kept = assembled.tools.filter((t) => isToolAllowed(currentFilters(), t && t.name))
954
1019
  if (kept.length === assembled.tools.length) return assembled
955
1020
  return { ...assembled, tools: kept }
956
1021
  }, { global: true })
@@ -961,7 +1026,7 @@ export function apply(ctx) {
961
1026
  if (tools && typeof tools.guard === 'function') {
962
1027
  ctx.effect(() => tools.guard((exec) => {
963
1028
  if (!exec || typeof exec.name !== 'string') return undefined
964
- if (isToolAllowed(toolFilters, exec.name)) return undefined
1029
+ if (isToolAllowed(currentFilters(), exec.name)) return undefined
965
1030
  const split = splitPublicName(exec.name)
966
1031
  return `connector: 工具 ${exec.name} 已被过滤(server "${split ? split.server : '?'}” 的 allow 列表未包含它)`
967
1032
  }), 'connector: mcp tool filter guard')
@@ -1058,14 +1123,14 @@ export function apply(ctx) {
1058
1123
  } catch (error) {
1059
1124
  return json(res, 500, { error: String(error?.message ?? error).slice(0, 200) })
1060
1125
  }
1061
- const filters = readToolFilters(ctx)
1126
+ const filters = readToolFilters()
1062
1127
  const rule = filters[serverName]
1063
1128
  return json(res, 200, { server: serverName, tools: names.sort(), allow: rule ? rule.allow : [] })
1064
1129
  }
1065
1130
 
1066
1131
  // GET /api/mcp/filters — 全部 server 的过滤规则(UI 批量展示用)
1067
1132
  if (req.method === 'GET' && path === API_PREFIX + '/mcp/filters') {
1068
- return json(res, 200, { filters: readToolFilters(ctx) })
1133
+ return json(res, 200, { filters: readToolFilters() })
1069
1134
  }
1070
1135
 
1071
1136
  // PUT /api/mcp/filters — 保存过滤规则 → settings.yaml `connector:` 节
@@ -1092,13 +1157,24 @@ export function apply(ctx) {
1092
1157
  }
1093
1158
  try {
1094
1159
  const settings = ctx.get('settings')
1095
- if (!settings) return json(res, 503, { error: 'settings service unavailable' })
1096
- await settings.update(CONNECTOR_SETTINGS_NS, { toolFilters: clean })
1160
+ if (_filterRef) {
1161
+ // 0.1.7+:settings 服务托管配置。replace() 先清空本 entry 的全部 volatile
1162
+ // 字段再写入本节:toolFilters 是唯一 volatile 字段,所以等价于「整节覆盖」,
1163
+ // 空 allow 的 server 已被上面清掉。
1164
+ if (!settings || typeof settings.replace !== 'function') {
1165
+ return json(res, 503, { error: 'settings service unavailable (DSH 0.1.7+ required to persist tool filters)' })
1166
+ }
1167
+ await settings.replace(CONNECTOR_ENTRY_ID, { toolFilters: clean })
1168
+ } else if (_legacySvc && typeof _legacySvc.update === 'function') {
1169
+ // rc.x:老的命名空间写入(settings.yaml `connector:` 节)
1170
+ await _legacySvc.update(CONNECTOR_SETTINGS_NS, { toolFilters: clean })
1171
+ } else {
1172
+ return json(res, 503, { error: 'settings service unavailable; cannot persist tool filters' })
1173
+ }
1097
1174
  } catch (error) {
1098
1175
  return json(res, 500, { error: String(error?.message ?? error).slice(0, 300) })
1099
1176
  }
1100
- refreshFilters()
1101
- return json(res, 200, { ok: true, filters: readToolFilters(ctx) })
1177
+ return json(res, 200, { ok: true, filters: readToolFilters() })
1102
1178
  }
1103
1179
 
1104
1180
  // GET /api/skills — list
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@omdp/dsh-connector",
3
- "version": "0.3.2",
3
+ "version": "0.3.3",
4
4
  "description": "Unified DeepSeek Harness connector: edit MCP servers (cordis.patch.yml), user skills (~/.dsh/skills) and browse the ModelScope MCP/Skills marketplaces from one Web UI settings page.",
5
5
  "type": "module",
6
6
  "main": "index.js",
@@ -46,6 +46,6 @@
46
46
  "jsdom": "^24.1.3"
47
47
  },
48
48
  "peerDependencies": {
49
- "@deepseek-ai/schemastery": "3.18.1 || 3.18.2"
49
+ "@deepseek-ai/schemastery": "3.18.1 || 3.18.2 || 3.18.4"
50
50
  }
51
51
  }