dsh-vscode-mode 0.12.0 → 0.13.1

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 (71) hide show
  1. package/README.md +148 -112
  2. package/assets/shell/dsh-open.cs +65 -149
  3. package/assets/shell/dsh-open.mjs +241 -0
  4. package/assets/shell/dsh-open.ps1 +28 -47
  5. package/assets/shell/dsh-open.sh +25 -66
  6. package/lib/client.js +30185 -29711
  7. package/lib/client.js.map +1 -1
  8. package/lib/index.js +3889 -2336
  9. package/lib/index.js.map +1 -1
  10. package/package.json +9 -8
  11. package/skills/dsh-vscodemode-mcp/SKILL.md +94 -132
  12. package/src/childEnv.ts +19 -0
  13. package/src/client/addToConversation.ts +4 -2
  14. package/src/client/ai/inlineProvider.ts +1 -8
  15. package/src/client/commandBridge.ts +6 -2
  16. package/src/client/externalOpen.ts +40 -19
  17. package/src/client/index.ts +17 -15
  18. package/src/client/keybindings.ts +18 -1
  19. package/src/client/monaco/loader.ts +3 -1
  20. package/src/client/openFlow.ts +36 -28
  21. package/src/client/openReceipt.ts +64 -0
  22. package/src/client/panelCommands.ts +101 -0
  23. package/src/client/pdf/pdfLoader.ts +4 -2
  24. package/src/client/receiptState.ts +120 -0
  25. package/src/client/rpc.ts +30 -4
  26. package/src/client/shortcutsOfficial.ts +39 -4
  27. package/src/client/sidebarBridge.ts +7 -0
  28. package/src/client/styles/mcp.css +1 -19
  29. package/src/client/ui/DiffBadge.ts +2 -1
  30. package/src/client/ui/EditorView.ts +237 -55
  31. package/src/client/ui/McpSettings.ts +32 -7
  32. package/src/client/ui/OfficialSideTab.ts +5 -2
  33. package/src/client/ui/QuickOpen.ts +10 -9
  34. package/src/client/ui/commandCatalog.ts +5 -4
  35. package/src/client/useOpenReceipt.ts +128 -0
  36. package/src/compat.ts +5 -3
  37. package/src/dap/manager.ts +46 -13
  38. package/src/devForm.ts +252 -92
  39. package/src/externalHandoff.ts +93 -52
  40. package/src/fileOpenSettings.ts +1 -24
  41. package/src/hostImport.ts +19 -0
  42. package/src/httpGuard.ts +101 -0
  43. package/src/index.ts +106 -97
  44. package/src/integrate.ts +56 -50
  45. package/src/lsp/transport.ts +84 -127
  46. package/src/mcp.ts +58 -9
  47. package/src/mcpIsolation.ts +211 -102
  48. package/src/mcpProject.ts +220 -133
  49. package/src/mcpRuntime.ts +343 -0
  50. package/src/openBridge.ts +69 -0
  51. package/src/openInbox.ts +207 -0
  52. package/src/paths.ts +8 -5
  53. package/src/processTree.ts +63 -0
  54. package/src/reveal.ts +5 -3
  55. package/src/routes.ts +156 -154
  56. package/src/rpc.ts +14 -13
  57. package/src/shared/appUrl.ts +19 -0
  58. package/src/shared/externalOpen.ts +71 -0
  59. package/src/shared/integration.ts +2 -0
  60. package/src/shared/keybindings.ts +8 -5
  61. package/src/shared/mcp.ts +4 -1
  62. package/src/shared/rpc.ts +7 -4
  63. package/src/shared/svn.ts +6 -8
  64. package/src/svnPlanTool.ts +51 -0
  65. package/src/unityBridge.ts +26 -3
  66. package/unity/com.dsh.editor/Editor/DshCodeEditor.cs +99 -203
  67. package/unity/com.dsh.editor/Editor/DshOpenWorker.cs +166 -0
  68. package/unity/com.dsh.editor/Editor/DshOpenWorker.cs.meta +2 -0
  69. package/unity/com.dsh.editor/README.md +16 -25
  70. package/unity/com.dsh.editor/package.json +2 -2
  71. package/src/client/ui/ShortcutSettings.ts +0 -294
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-vscode-mode",
3
- "version": "0.12.0",
3
+ "version": "0.13.1",
4
4
  "description": "DSH 上的类 VSCode 编码体验:Monaco 编辑器(文件页签/QuickOpen/状态栏,可驻 DSH 0.1.5+ 官方右侧 Sidebar 与对话同屏)+ 命令栏(Ctrl+Shift+P)与可扩展指令系统 + VS Code 兼容代码片段(.code-snippets 全局/项目,IntelliSense 展开)+ Agent 编辑差异审查(整文件差异/采纳/拒绝/归档/回滚)+ SVN 集成(侧栏变更面板/日志弹窗/并排差异)+ 语言服务器(LSP)智能(跳转定义/引用/hover/大纲 + VSIX 扩展市场安装),状态持久化到工作区旁车",
5
5
  "icon": "assets/icon.svg",
6
6
  "keywords": [
@@ -48,12 +48,12 @@
48
48
  "@vscode/ripgrep": "^1.18.0"
49
49
  },
50
50
  "peerDependencies": {
51
- "@deepseek-ai/dsh-client-ui-slots": ">=0.0.1-rc.1 <0.1.0 || >=0.1.0-rc.1 <0.2.0-0 || >=0.1.1-0 <0.2.0-0 || >=0.1.2-0 <0.2.0-0 || >=0.1.3-0 <0.2.0-0 || >=0.1.4-0 <0.2.0-0 || >=0.1.5-0 <0.2.0-0 || >=0.1.6-0 <0.2.0-0",
52
- "@deepseek-ai/dsh-settings": ">=0.1.0-rc.1 <0.2.0-0 || >=0.1.1-0 <0.2.0-0 || >=0.1.2-0 <0.2.0-0 || >=0.1.3-0 <0.2.0-0 || >=0.1.4-0 <0.2.0-0 || >=0.1.5-0 <0.2.0-0 || >=0.1.6-0 <0.2.0-0",
53
- "@deepseek-ai/dsh-client-ui-settings": ">=0.1.0-rc.1 <0.2.0-0 || >=0.1.1-0 <0.2.0-0 || >=0.1.2-0 <0.2.0-0 || >=0.1.3-0 <0.2.0-0 || >=0.1.4-0 <0.2.0-0 || >=0.1.5-0 <0.2.0-0 || >=0.1.6-0 <0.2.0-0",
54
- "@deepseek-ai/dsh-client-ui-primitives": ">=0.1.0-rc.1 <0.2.0-0 || >=0.1.1-0 <0.2.0-0 || >=0.1.2-0 <0.2.0-0 || >=0.1.3-0 <0.2.0-0 || >=0.1.4-0 <0.2.0-0 || >=0.1.5-0 <0.2.0-0 || >=0.1.6-0 <0.2.0-0",
55
- "@deepseek-ai/dsh-llm": ">=0.0.1-rc.1 <0.1.0 || >=0.1.0-rc.1 <0.2.0-0 || >=0.1.1-0 <0.2.0-0 || >=0.1.2-0 <0.2.0-0 || >=0.1.3-0 <0.2.0-0 || >=0.1.4-0 <0.2.0-0 || >=0.1.5-0 <0.2.0-0 || >=0.1.6-0 <0.2.0-0",
56
- "@deepseek-ai/dsh-tools": ">=0.0.1-rc.1 <0.1.0 || >=0.1.0-rc.1 <0.2.0-0 || >=0.1.1-0 <0.2.0-0 || >=0.1.2-0 <0.2.0-0 || >=0.1.3-0 <0.2.0-0 || >=0.1.4-0 <0.2.0-0 || >=0.1.5-0 <0.2.0-0 || >=0.1.6-0 <0.2.0-0",
51
+ "@deepseek-ai/dsh-client-ui-slots": ">=0.0.1-rc.1 <0.1.0 || >=0.1.0-rc.1 <0.2.0-0 || >=0.1.1-0 <0.2.0-0 || >=0.1.2-0 <0.2.0-0 || >=0.1.3-0 <0.2.0-0 || >=0.1.4-0 <0.2.0-0 || >=0.1.5-0 <0.2.0-0 || >=0.1.6-0 <0.2.0-0 || >=0.1.7-0 <0.2.0-0",
52
+ "@deepseek-ai/dsh-settings": ">=0.1.0-rc.1 <0.2.0-0 || >=0.1.1-0 <0.2.0-0 || >=0.1.2-0 <0.2.0-0 || >=0.1.3-0 <0.2.0-0 || >=0.1.4-0 <0.2.0-0 || >=0.1.5-0 <0.2.0-0 || >=0.1.6-0 <0.2.0-0 || >=0.1.7-0 <0.2.0-0",
53
+ "@deepseek-ai/dsh-client-ui-settings": ">=0.1.0-rc.1 <0.2.0-0 || >=0.1.1-0 <0.2.0-0 || >=0.1.2-0 <0.2.0-0 || >=0.1.3-0 <0.2.0-0 || >=0.1.4-0 <0.2.0-0 || >=0.1.5-0 <0.2.0-0 || >=0.1.6-0 <0.2.0-0 || >=0.1.7-0 <0.2.0-0",
54
+ "@deepseek-ai/dsh-client-ui-primitives": ">=0.1.0-rc.1 <0.2.0-0 || >=0.1.1-0 <0.2.0-0 || >=0.1.2-0 <0.2.0-0 || >=0.1.3-0 <0.2.0-0 || >=0.1.4-0 <0.2.0-0 || >=0.1.5-0 <0.2.0-0 || >=0.1.6-0 <0.2.0-0 || >=0.1.7-0 <0.2.0-0",
55
+ "@deepseek-ai/dsh-llm": ">=0.0.1-rc.1 <0.1.0 || >=0.1.0-rc.1 <0.2.0-0 || >=0.1.1-0 <0.2.0-0 || >=0.1.2-0 <0.2.0-0 || >=0.1.3-0 <0.2.0-0 || >=0.1.4-0 <0.2.0-0 || >=0.1.5-0 <0.2.0-0 || >=0.1.6-0 <0.2.0-0 || >=0.1.7-0 <0.2.0-0",
56
+ "@deepseek-ai/dsh-tools": ">=0.0.1-rc.1 <0.1.0 || >=0.1.0-rc.1 <0.2.0-0 || >=0.1.1-0 <0.2.0-0 || >=0.1.2-0 <0.2.0-0 || >=0.1.3-0 <0.2.0-0 || >=0.1.4-0 <0.2.0-0 || >=0.1.5-0 <0.2.0-0 || >=0.1.6-0 <0.2.0-0 || >=0.1.7-0 <0.2.0-0",
57
57
  "cordis": ">=4.0.0-rc <5",
58
58
  "react": "^18.2.0",
59
59
  "react-dom": "^18.2.0",
@@ -67,6 +67,7 @@
67
67
  "react": "^18.3.1",
68
68
  "react-dom": "^18.3.1",
69
69
  "schemastery": "3.18.0",
70
+ "semver": "^7.8.5",
70
71
  "tsdown": "^0.22.14",
71
72
  "typescript": "^5.9.0",
72
73
  "vitest": "^3.2.4"
@@ -75,7 +76,7 @@
75
76
  "build": "tsdown",
76
77
  "build:client": "tsdown",
77
78
  "typecheck": "tsc -p tsconfig.json --noEmit",
78
- "test": "vitest run",
79
+ "test": "vitest run && node --test tests/nativeOpen.test.mjs",
79
80
  "prebuild": "node -e \"fs.rmSync('lib',{recursive:true,force:true})\"",
80
81
  "prepare": "npm run prebuild && npm run build",
81
82
  "pack": "npm run prebuild && npm run build && npm pack",
@@ -1,46 +1,37 @@
1
1
  ---
2
2
  name: dsh-vscodemode-mcp
3
- description: 配置、新增、启用或排查 dsh-vscode-mode 插件(VSCodeMode 设置页)的 MCP 服务——全局「我的 MCP」与项目级 `.mcp.json` 两种作用域、stdio / streamable-http 传输写法、`mcp.*` RPC 方法、serverName 命名与冲突规则、项目 MCP 的工作区隔离边界,以及连接失败/工具数为 0 等故障的定位路径。当用户提到「加个 MCP」「配置 MCP」「MCP 连不上」「项目 MCP」「.mcp.json」「VSCodeMode 的 MCP 管理」时加载本技能。
4
- whenToUse: 用户要求新增/修改/启停/排查 dsh-vscode-mode 的 MCP 服务配置时;或需要判断某个 MCP 工具为何在对话中不可见/被拒绝时。
3
+ description: 配置、新增、启用或排查 dsh-vscode-mode 的 MCP 服务:全局「我的 MCP」、项目 .mcp.json、agent 连接实例、工具/资源/指令隔离、mcp.* RPC、命名冲突与连接状态。
4
+ whenToUse: 用户要求新增、修改、启停或排查 VSCodeMode MCP 配置,或判断项目工具、资源、服务器指令为何不可见或被拒绝时。
5
5
  ---
6
6
 
7
- # dsh-vscodemode-mcp — dsh-vscode-mode 的 MCP 配置与使用
7
+ # dsh-vscodemode-mcp — MCP 配置与使用
8
8
 
9
- 本技能覆盖 **dsh-vscode-mode 插件**(DSH 上的类 VSCode 编码体验)提供的 MCP 可视化管理能力。
10
- 它管理的是 `@deepseek-ai/dsh-mcp-client` 的 loader 条目,连接由 DSH Host 维护,**不在浏览器侧连接 MCP**。
9
+ 本技能覆盖 dsh-vscode-mode 的 MCP 管理面。连接由 DSH Host 内的官方 `@deepseek-ai/dsh-mcp-client` 插件维护;浏览器只管理配置。
11
10
 
12
- ## 1. 两种作用域(先判断用户要哪一种)
11
+ ## 1. 先确定作用域
13
12
 
14
- | | 全局「我的 MCP」 | 项目 MCP |
13
+ | 项目 | 全局「我的 MCP」 | 项目 MCP |
15
14
  |---|---|---|
16
- | 配置位置 | profile 的 loader 树(`~/.dsh/profiles/<profile>/cordis.yml`) | 项目根 `.mcp.json` 的 `mcpServers` |
17
- | 生效范围 | 该 profile 下所有工作区 | 仅该 workspace(+ 其子路径可匹配到的工作区) |
18
- | 随仓库共享 | 否 | 是(`.mcp.json` 入库即团队共享) |
19
- | 持久真相 | loader 条目 | `.mcp.json` 文件(重启后由插件 reconcile 恢复激活) |
20
- | 受工作区隔离约束 | 否 | **是**(见 §5) |
15
+ | 持久配置 | profile loader 树 | 已注册 workspace 根目录的 `.mcp.json` |
16
+ | 生效范围 | profile 内所有工作区 | cwd 匹配该 workspace 的 agent |
17
+ | 运行方式 | 原有全局 loader entry | `agent.ctx.plugin(officialMcp, config)` |
18
+ | 连接实例 | 每个启用的全局 entry 一份 | 每个活动 agent、每个启用服务一份 |
19
+ | 没有活动 agent | 仍可运行 | 仅保存配置,`configured`、`instanceCount: 0` |
20
+ | 团队共享 | 否 | `.mcp.json` 可以随仓库共享 |
21
21
 
22
- 判断规则:
23
- - 用户说「全局都能用」「所有项目都要」→ 全局。
24
- - 用户说「这个项目需要」「跟仓库一起提交」→ 项目级。
25
- - 不确定且该 MCP 只在某个仓库有意义 → 默认项目级。
22
+ 用户说「全局都能用」选全局;说「这个项目需要」「跟仓库一起提交」选项目级。工作区匹配使用最长父路径;Windows 路径忽略大小写,POSIX 路径区分大小写。
26
23
 
27
- ## 2. UI 路径
24
+ ## 2. UI 与连接语义
28
25
 
29
- 设置 → **VSCodeMode** → **「MCP 管理」** Tab,其下三个子页签:
26
+ 设置 → VSCodeMode → MCP 管理,包含「我的 MCP」「项目 MCP」「MCP 市场」(占位)。项目服务在所选项目内管理。
30
27
 
31
- - **我的 MCP** — 全局服务列表(可查看状态/工具、添加、刷新 ⟳、启用/禁用、删除 ⌫)。
32
- - **项目 MCP** — 先选项目(带路径与 MCP 计数),再管理该项目的服务。
33
- - **MCP 市场** — 占位,暂未接入。
28
+ 保存项目服务会写入 `.mcp.json` 并更新匹配的活动 agent;没有活动 agent 时保存为待挂载配置。启用不会凭空创建会话。每个新 agent 的串行 `agent/created` 监听会读取最新配置并等待其官方插件启动;插件装配时也会接入已经存在的 agent。
34
29
 
35
- 「+ 添加全局 MCP」按钮在子页签栏右侧;项目 MCP 的新增入口在所选项目的分组内。
36
- 表单字段:名称 / 传输方式 / (stdio)命令·参数·工作目录 / (streamable-http)URL·请求头。
37
- 按钮文案为「保存并连接」——**保存后会立刻尝试连接**。
30
+ 禁用、删除、配置变更、刷新重连及 agent/插件卸载都会释放相应 fiber 和连接。仅刷新未变化的项目列表不会重复连接。主机代码更新需要重启 DSH 才能加载新实现。
38
31
 
39
- ## 3. 配置写法
32
+ ## 3. 项目配置
40
33
 
41
- ### 3.1 项目级 `.mcp.json`
42
-
43
- 对齐 Claude Code / Cursor 格式,存于项目根:
34
+ `.mcp.json` 示例:
44
35
 
45
36
  ```json
46
37
  {
@@ -52,26 +43,26 @@ whenToUse: 用户要求新增/修改/启停/排查 dsh-vscode-mode 的 MCP 服
52
43
  "remote-tools": {
53
44
  "url": "http://localhost:3000/mcp",
54
45
  "headers": { "Authorization": "Bearer <token>" },
55
- "toolCallTimeoutMs": 60000
46
+ "toolCallTimeoutMs": 60000,
47
+ "reconnect": { "enabled": true }
56
48
  }
57
49
  }
58
50
  }
59
51
  ```
60
52
 
61
- 规则(`src/mcpProject.ts` 的 `configFromDef`):
62
- - **有 `url` → `streamable-http`;无 `url` → `stdio`**。传输方式由字段形态自动判定,不写 `transport`。
63
- - stdio 字段:`command`(必填)、`args`(数组)、`cwd`、`env`(键值对象)。
64
- - http 字段:`url`(必填,须 `http(s)://`)、`headers`(键值对象)。
65
- - 可选:`toolCallTimeoutMs`(数字)。
66
- - `disabled: true` = 停用(由启停开关维护;**保存时不会写这个键,保存恒为启用**)。
67
- - 顶层可有其他字段,写回时保留;`mcpServers` 之外的未知字段不丢。
68
-
69
- ### 3.2 全局(等价配置形)
53
+ - 有字符串 `url` 采用 `streamable-http`;其余采用 `stdio`。文件不用写 `transport`。
54
+ - stdio:`command` 必填;可填 `args`、`cwd`、`env`。未指定 `cwd` 时,以项目根作为进程工作目录。
55
+ - HTTP:`url` 必须以 `http://` 或 `https://` 开头,可填 `headers`。
56
+ - 高级参数:`toolCallTimeoutMs`、`failOnStartupError`、`maxInstructionBytes`、`reconnect`。
57
+ - `disabled: true` 保持配置但不创建连接;保存表单会恢复启用。重启和 reconcile 都尊重 `disabled`。
58
+ - 保存保留文档顶层、其他服务器及当前服务器的未知字段和未编辑高级参数;切换传输方式会移除旧传输专属字段。
59
+ - `env` / `headers` 在返回值中使用 `••••••` 脱敏。原键的未修改掩码保存时恢复真实值;新键只有掩码而没有原值时会拒绝保存,需填写凭据。
60
+ - 只有 `ENOENT` 表示配置不存在。权限、IO、JSON 或结构错误会报告 `fileError`,保留最后有效配置与活动连接,不覆盖损坏文件。
70
61
 
71
- 全局条目等价于 profile loader 里的一行 `@deepseek-ai/dsh-mcp-client`:
62
+ 全局配置继续使用官方 loader entry:
72
63
 
73
64
  ```yaml
74
- - id: <条目 id>
65
+ - id: mcp-example
75
66
  name: '@deepseek-ai/dsh-mcp-client'
76
67
  config:
77
68
  serverName: codegraph
@@ -80,102 +71,73 @@ whenToUse: 用户要求新增/修改/启停/排查 dsh-vscode-mode 的 MCP 服
80
71
  args: ['serve', '--mcp']
81
72
  ```
82
73
 
83
- 但 **优先用设置页 UI 或 `mcp.save` RPC**,不要手改 profile 的 `cordis.yml`:UI/RPC 会同步触发 loader 热更新与冲突校验,手改容易产生重复条目。
74
+ 优先使用设置页或 RPC,保证冲突检查和运行状态同步。
84
75
 
85
- ### 3.3 两个最小可用示例
76
+ ## 4. RPC
86
77
 
87
- stdio(本地命令行 MCP,如 CodeGraph):
88
- ```
89
- 名称: codegraph
90
- 传输方式: stdio
91
- 命令: codegraph
92
- 参数: serve --mcp --path D:/Work/MyProject
93
- ```
78
+ `POST /edrv/rpc`,请求 `{ "method": "...", "args": { ... } }`,返回 `{ ok: true, ... }` 或 `{ ok: false, error }`。
94
79
 
95
- streamable-http(远端/已启动的 HTTP MCP):
96
- ```
97
- 名称: remote-tools
98
- 传输方式: streamable-http
99
- URL: http://localhost:3000/mcp
100
- 请求头: Authorization=Bearer <token>
101
- ```
80
+ | 方法 | 入参 | 成功返回字段 |
81
+ |---|---|---|
82
+ | `mcp.list` | `{}` | `servers` |
83
+ | `mcp.save` | `{ config }` | `server` |
84
+ | `mcp.remove` | `{ id }` | 无 |
85
+ | `mcp.toggle` | `{ id, enabled }` | `server` |
86
+ | `mcp.refresh` | `{ id }` | `server` |
87
+ | `mcp.projects` | `{}` | `projects` |
88
+ | `mcp.projectSave` | `{ workspacePath, serverName, config }` | `project` |
89
+ | `mcp.projectRemove` | `{ workspacePath, serverName }` | `project` |
90
+ | `mcp.projectToggle` | `{ workspacePath, serverName, enabled }` | `project` |
91
+ | `mcp.projectRefresh` | `{ workspacePath, serverName }` | `project` |
102
92
 
103
- ## 4. RPC 方法表(UI 之外的程序化入口)
93
+ `MpcServer` 包括 `id`、`serverName`、`enabled`、`transport`、脱敏 `config`、`status`、`instanceCount`、`toolCount`、`tools[]`、可选 `error`。项目视图另有 `workspacePath`、`title`、`source: 'project'`,以及可选 `missingDir`、`fileError`。
104
94
 
105
- 路由:`POST /edrv/rpc`,报文体 `{ "method": "...", "args": { ... } }`,返回 `{ ok: true, ... }` 或 `{ ok: false, error }`。
95
+ ## 5. 命名与迁移
106
96
 
107
- | 方法 | 入参 | 返回 |
108
- |---|---|---|
109
- | `mcp.list` | `{}` | `{ servers: MpcServer[] }` |
110
- | `mcp.save` | `{ config: MpcConfig }` | `{ server: MpcServer }` |
111
- | `mcp.remove` | `{ id }` | `{}` |
112
- | `mcp.toggle` | `{ id, enabled }` | `{ server: MpcServer }` |
113
- | `mcp.refresh` | `{ id }` | `{ server: MpcServer }` |
114
- | `mcp.projects` | `{}` | `{ projects: MpcProject[] }` |
115
- | `mcp.projectSave` | `{ workspacePath, serverName, config }` | `{ project: MpcProject }` |
116
- | `mcp.projectRemove` | `{ workspacePath, serverName }` | `{ project: MpcProject }` |
117
- | `mcp.projectToggle` | `{ workspacePath, serverName, enabled }` | `{ project: MpcProject }` |
118
- | `mcp.projectRefresh` | `{ workspacePath, serverName }` | `{ project: MpcProject }` |
119
-
120
- `MpcServer` 关键字段:`id`、`serverName`、`enabled`、`transport`、`config`、`status`、`toolCount`、`tools[]`。
121
- `MpcProject` 关键字段:`workspacePath`、`title`、`servers[]`、`source: 'project'`,异常时带 `missingDir` 或 `fileError`。
122
-
123
- ## 5. 命名规则与冲突(最容易踩的坑)
124
-
125
- - **`serverName` 规则**:`/^[A-Za-z0-9_-]{1,32}$/` —— 字母、数字、**下划线**、连字符,最多 32 位。
126
- ⚠️ 注意与技能名的区别:**MCP 的 `serverName` 允许下划线**,而 DSH 技能名不允许(技能名须 kebab-case)。
127
- - **唯一性**:同一个 `serverName` 在**全局与所有项目之间**全局唯一。重复时保存直接失败:
128
- `serverName "xxx" 已被另一个 MCP 使用(全局或其他项目),请换一个名称`。
129
- - **模型侧工具名**:`mcp__<serverName>__<原始工具名>`。这段前缀是隔离与可见性机制的判定依据,不要手工改 `serverName` 来"重命名工具"——那会生成一整套新工具名。
130
- - **项目条目 id**:`vsm-mcp.<workspaceHash>.<serverName>`(旧版残留为 `vsm-mcp:<hash>:<name>`,插件识别但不显示为全局 MCP)。
131
- - 项目 MCP 只接受**已注册为 DSH workspace** 的路径,否则报 `项目未注册为 DSH workspace,不能管理项目 MCP`。
132
-
133
- ## 6. 作用域隔离(项目 MCP 的边界)
134
-
135
- 项目 MCP 的连接由 Host 维护,但**每个 agent 只继承当前工作区的项目工具**:
136
-
137
- - 机制:`tools.restrict({ deny })` 控制模型可见性 + `tools.guard()` 在执行层兜底拒绝。
138
- - 当前对话工作区 ≠ 条目所属工作区时,调用被拒,返回:
139
- `已拒绝:当前对话工作区不能使用其他项目的 MCP`
140
- - 会话没有落在任何已注册工作区时:
141
- `项目 MCP 需要在已注册工作区的对话中使用`
142
- - **全局 MCP 不受此限制**,在任何会话都可用。
143
- - 工作区匹配按**最长父路径**:在子目录会话里也能用到父级工作区的项目 MCP。
144
-
145
- 排查「工具有但没有出现在模型面前」时,先看当前会话 cwd 是否落在目标 workspace 内。
146
-
147
- ## 7. 故障排查
148
-
149
- | 症状 | 原因与处理 |
97
+ - `serverName` 使用 `/^[A-Za-z0-9_-]{1,32}$/`;允许下划线,与技能名规则不同。
98
+ - VSCodeMode 管理的全局和所有项目配置之间名称唯一,包括未启用和没有活动 agent 的配置。官方 MCP 还会拒绝同一个 agent scope 中的重复名称。
99
+ - 工具名是 `mcp__<serverName>__<原始工具名>`。共享资源工具通过 `arguments.server` 选择服务器。
100
+ - 项目稳定 id 仍是 `vsm-mcp.<workspaceHash>.<serverName>`,用于 UI/RPC 身份;0.13 起不再代表全局 loader entry。
101
+ - 启动迁移会先删除旧的 `vsm-mcp.*` 和 `vsm-mcp:*` 全局项目条目及其连接,再在 agent scope 挂载。不要手动将项目条目改成其他 id。
102
+ - 项目写操作只接受已注册 workspace,目标固定为该根目录的 `.mcp.json`。
103
+
104
+ ## 6. 工具、资源和指令隔离
105
+
106
+ 官方 0.1.7-rc.2 的 MCP Config 没有关闭资源/指令的开关;官方注册随 `scopeOf(ctx)` 归属。因此项目连接必须直接在 `agent.ctx` 挂载,不能全局连接后只隐藏工具。
107
+
108
+ 子 agent 会继承父 scope;跨 cwd 子会话还有三层处理:
109
+
110
+ - `tools.restrict({ deny })` 隐藏继承的外项目工具;执行 guard 同时检查所有项目工具命名空间。
111
+ - `list_mcp_resources`、`list_mcp_resource_templates`、`read_mcp_resource` 在执行前检查 `arguments.server` 归属,不能通过共享工具访问父工作区资源。
112
+ - 子 scope 为外项目 `mcp:<serverName>` 注册空指令段;通过公开 `system-prompt/assemble` waterfall 按实际查看 scope 过滤官方资源服务器列表,保留全局和独立 agent preset 的纯资源服务。不会改动 scope parent 链。官方 0.1.7-rc.2 列表格式已验证;外项目存在且列表格式无法识别时,安全隐藏该提示列表,执行 guard 仍独立生效。
113
+
114
+ 全局 MCP 保持可用。工作区外的 agent 不能访问任何项目 MCP。拒绝文案:
115
+
116
+ - `已拒绝:当前对话工作区不能使用其他项目的 MCP`
117
+ - `项目 MCP 需要在已注册工作区的对话中使用`
118
+
119
+ ## 7. 状态与排错
120
+
121
+ | 状态/现象 | 含义与处理 |
150
122
  |---|---|
151
- | 卡片状态 `错误`,提示 `MCP 插件未正常运行` | loader 条目 fiber 未进入 ACTIVE。看 Host 启动日志中该条目的报错;多半是 `command` 不存在或启动即退出 |
152
- | 状态 `连接中` 一直不变 | 服务端未响应 MCP 握手。核对 stdio 的 `command`/`args`/`cwd`,或 HTTP 的 URL 可达性 |
153
- | 已连接但 `toolCount` 为 0 | 服务端 `tools/list` 返回空,或工具同步失败。先用该 MCP 的 CLI 自行验证能列出工具 |
154
- | `stdio MCP 必须填写 command` | 表单「命令」为空 |
155
- | `HTTP MCP 必须填写 http(s) URL` | 「URL」为空或不是 `http(s)://` 开头 |
156
- | `serverName 只能包含字母、数字、下划线和连字符(最多 32 位)` | 名称含空格/点/中文或超长 |
157
- | `serverName "x" 已被另一个 MCP 使用…` | 见 §5 唯一性 |
158
- | 设置页里 `env` / `headers` 的值显示成 `••••••` | **这是脱敏掩码**(`publicConfig`)。读回的是掩码而非真实值,**不要把它原样回写保存**,否则会用掩码覆盖真实凭据 |
159
- | `.mcp.json 解析失败:…` | 文件不是合法 JSON;插件只报错、**不覆盖**该文件,修好 JSON 后刷新 |
160
- | `.mcp.json 的 mcpServers 必须是对象` | `mcpServers` 写成了数组/标量 |
161
- | 项目显示「目录缺失」 | workspace 路径已不存在(项目被移动/删除) |
162
-
163
- ## 8. 边界与注意事项
164
-
165
- - **删除/启停走设置页或 RPC**,不要手删 profile 树里的条目——插件按条目 id(`vsm-mcp.*`)识别项目 MCP,手工改名会让它被当成全局条目。
166
- - **项目 MCP 的持久真相是 `.mcp.json`**:重启后插件按文件内容 reconcile 该项目的激活状态(文件里有的激活、文件里没有的停用)。
167
- - 项目 MCP 的写操作固定写入 `<workspacePath>/.mcp.json`,不会写到别处。
168
- - 全局 MCP 由 loader 落盘到 profile 树,重启自动恢复。
169
- - MCP 条目本身是 loader 条目,**HMR/热更新会重建连接**;调试连接问题时可先 `mcp.refresh`(等价于用当前 config 重新 `loader.update`)。
170
- - 本技能只覆盖 dsh-vscode-mode 的 MCP 管理面。若问题出在 MCP 服务端自身(协议实现、鉴权、工具 schema),那不是本插件的问题,应转向该服务端的调试手段。
171
-
172
- ## 9. 关键源码位置
173
-
174
- 需要在插件仓库里核对行为时(按需读取,不要一次全开):
175
-
176
- - `src/mcp.ts` — 全局 MCP 的 list/save/remove/toggle/refresh,`validateConfig`,`publicConfig` 脱敏,`toolsOf` 工具前缀。
177
- - `src/mcpProject.ts` — 项目 `.mcp.json` 读写、`configFromDef`、reconcile、条目 id 生成。
178
- - `src/mcpIsolation.ts` — 工作区隔离(restrict + guard)与两条拒绝文案。
179
- - `src/shared/mcp.ts` — 共享契约(`MpcConfig` / `MpcServer` / `MpcProject`)。
180
- - `src/shared/rpc.ts` — `mcp.*` 方法的入参与返回形状。
181
- - `src/client/ui/McpSettings.ts` — 设置页 UI(三个子页签与表单字段映射)。
123
+ | `configured` | 配置已保存,没有已挂载实例;打开匹配工作区会话后挂载 |
124
+ | `disabled` | 配置已停用,不建立项目连接 |
125
+ | `connecting` | 官方插件还在等待启动完成;检查命令、cwd、URL 和握手 |
126
+ | `unverified` | fiber 已激活,但官方没有公开连接状态证据;不能宣称已连接 |
127
+ | `error` | 插件启动或挂载失败;读 `error` 与 Host 日志 |
128
+ | `instanceCount: N` | N 个挂载的官方插件 fiber,**不等于 N 个确认存活的网络连接** |
129
+ | `toolCount: 0` | 可能是合法的纯资源服务器,也可能启动失败或工具同步失败;结合资源调用和日志判断 |
130
+ | 文件读取/解析错误 | 修复源文件后刷新;最后有效实例保持运行 |
131
+ | 工作区不匹配 | 检查 agent cwd 和最长父路径匹配结果 |
132
+
133
+ `failOnStartupError` 默认 false:初次连接失败时 fiber 仍可能 ACTIVE。工具数和 ACTIVE 都不能单独证明连接成功;只有实际成功调用等公开证据才能确认。当前管理面保守使用 `unverified`。
134
+
135
+ ## 8. 维护位置与验证
136
+
137
+ - `src/mcpRuntime.ts`:agent → fibers 台账、官方插件加载、串行创建、迁移与卸载。
138
+ - `src/mcpProject.ts`:源配置读取、保留字段、命名检查及 reconcile。
139
+ - `src/mcpIsolation.ts`:继承工具限制、资源执行 guard、指令和资源列表覆盖。
140
+ - `src/mcp.ts`:全局 loader CRUD、脱敏与诚实状态。
141
+ - `src/shared/mcp.ts`:共享配置和状态契约。
142
+
143
+ 集成时先 `installIsolation(ctx)`,再 `await installMcpRuntime(ctx)`。UI 需支持 `configured`、`unverified` 和实例数。最小回归只运行 MCP 相关测试;可设置 `DSH_MCP_HOST_ROOT` 为安装的 harness 根来运行真实官方模块与本地 HTTP MCP 测试,未设置时该集成用例跳过。
@@ -0,0 +1,19 @@
1
+ /** 外部进程环境:参照官方 dsh-subprocess.scrubbedParentEnv。 */
2
+ const SENSITIVE_ENV = /KEY|PASSWORD|SECRET|TOKEN/i
3
+
4
+ /**
5
+ * 清理父进程隐式凭据及 DSH 身份,再合并用户显式配置;保留 Electron Node 模式。
6
+ * @public
7
+ * @author ddj 2026年09月28号
8
+ * @param explicit 用户显式配置的环境变量,可有意传递凭据
9
+ * @param parent 父环境,默认当前进程;测试可注入
10
+ * @returns 新环境对象,不修改任何输入
11
+ */
12
+ export function childEnv(explicit: NodeJS.ProcessEnv = {}, parent: NodeJS.ProcessEnv = process.env): NodeJS.ProcessEnv {
13
+ const env: NodeJS.ProcessEnv = {}
14
+ for (const [key, value] of Object.entries(parent)) {
15
+ if (value === undefined || SENSITIVE_ENV.test(key) || key.toUpperCase().startsWith('DSH_')) continue
16
+ env[key] = value
17
+ }
18
+ return { ...env, ...explicit }
19
+ }
@@ -6,6 +6,8 @@
6
6
  * 作者 ddj 2026-08-25
7
7
  */
8
8
 
9
+ import { appUrl } from '../shared/appUrl.js'
10
+
9
11
  /** 文件引用可选的行区间(选中内容用)。 */
10
12
  export interface RefRange {
11
13
  startLine: number
@@ -320,7 +322,7 @@ function rpcOk(res: unknown): { ok: boolean; value?: unknown } {
320
322
  * @returns 新会话 id(失败空串)
321
323
  */
322
324
  async function apiSessionCreate(cwd: string | undefined): Promise<string> {
323
- const res = await fetch(SESSION_CREATE_PATH, {
325
+ const res = await fetch(appUrl(SESSION_CREATE_PATH), {
324
326
  method: 'POST',
325
327
  headers: { 'content-type': 'application/json' },
326
328
  body: JSON.stringify({
@@ -347,7 +349,7 @@ async function apiSessionCreate(cwd: string | undefined): Promise<string> {
347
349
  export async function apiSessionPrompt(sessionId: string, text: string): Promise<boolean> {
348
350
  try {
349
351
  const payload = { sessionId, mode: 'queue', content: [{ type: 'text', text }] }
350
- const res = await fetch(SESSION_PROMPT_PATH, {
352
+ const res = await fetch(appUrl(SESSION_PROMPT_PATH), {
351
353
  method: 'POST',
352
354
  headers: { 'content-type': 'application/json' },
353
355
  body: JSON.stringify({
@@ -9,7 +9,6 @@
9
9
  * 作者 ddj
10
10
  */
11
11
  import { rpc } from '../rpc.js'
12
- import { RPC_PATH } from '../../shared/rpc.js'
13
12
  import {
14
13
  AI_PREFIX_MAX, AI_SUFFIX_MAX, inlineWorth, trimInlineWindow,
15
14
  } from '../../shared/ai.js'
@@ -145,13 +144,7 @@ export function registerAiInline(monaco) {
145
144
  inFlight = controller
146
145
  emitAiStatus('busy')
147
146
  try {
148
- const res = await fetch(RPC_PATH, {
149
- method: 'POST',
150
- headers: { 'content-type': 'application/json' },
151
- body: JSON.stringify({ method: 'edrv.ai.inline', args: { path, prefix: trimmed.prefix, suffix: trimmed.suffix } }),
152
- signal: controller.signal,
153
- })
154
- const data = await res.json()
147
+ const data = await rpc('edrv.ai.inline', { path, prefix: trimmed.prefix, suffix: trimmed.suffix }, controller.signal)
155
148
  if (mySeq !== seq || !data?.ok || typeof data.text !== 'string' || !data.text) {
156
149
  cache.set(key, { items: [], at: Date.now() })
157
150
  if (data?.note) emitAiStatus('error', { note: String(data.note) })
@@ -7,6 +7,8 @@
7
7
  * React 状态,因此指令系统可在编辑器挂载前装配、卸载后仍安全。
8
8
  * 作者 ddj 2026年09月10号 / 2026年10月
9
9
  */
10
+ import { setPanelRoute } from './panelCommands.js'
11
+ import type { PanelRoute } from './panelCommands.js'
10
12
  import { BRIDGE_COMMANDS, EDITOR_COMMANDS, showCommandsDef } from './ui/commandCatalog.js'
11
13
  import { createCommandRegistry } from './commandRegistry.js'
12
14
  import type { CommandRegistry } from './commandRegistry.js'
@@ -25,12 +27,14 @@ export interface CommandBridge {
25
27
 
26
28
  /**
27
29
  * 创建指令执行桥:注册内置指令 + 命令栏自身,注入命令栏执行器。
28
- * @author ddj 2026年09月10号
30
+ * @author ddj 2026年09月28号
31
+ * @param panelRoute 当前会话读取器与已有侧栏打开能力(首次打开命令使用)
29
32
  * @returns 指令执行桥句柄
30
33
  */
31
- export function createCommandBridge(): CommandBridge {
34
+ export function createCommandBridge(panelRoute?: PanelRoute): CommandBridge {
32
35
  const registry = createCommandRegistry()
33
36
  const disposers: Array<() => void> = []
37
+ if (panelRoute) disposers.push(setPanelRoute(panelRoute))
34
38
  const palette = showCommandsDef(() => openCommandPalette('command'))
35
39
  for (const command of [...EDITOR_COMMANDS, ...BRIDGE_COMMANDS]) {
36
40
  disposers.push(registry.register(command))
@@ -7,6 +7,7 @@
7
7
  */
8
8
  import { EDRV_PARAM_KEYS, parseOpenParams } from '../shared/externalOpen.js'
9
9
  import { openDeepLink } from './openFlow.js'
10
+ import { disposeOpenWait } from './openReceipt.js'
10
11
  import { rpc } from './rpc.js'
11
12
  import { log } from './log.js'
12
13
 
@@ -31,22 +32,23 @@ export interface ExtOpenOptions {
31
32
  * @param ctx 客户端根上下文(sessions/workspaces 服务)
32
33
  * @param options 可注入选项
33
34
  */
34
- export function setupExtOpen(ctx: unknown, options: ExtOpenOptions = {}): void {
35
- startPresencePoll(ctx)
35
+ export function setupExtOpen(ctx: unknown, options: ExtOpenOptions = {}): () => void {
36
+ const dispose = startPresencePoll(ctx)
36
37
  const search = options.search ?? (typeof location !== 'undefined' ? location.search : '')
37
38
  const params = parseOpenParams(search)
38
- if (!params) return
39
+ if (!params) return dispose
39
40
  const referrer = options.referrer ?? (typeof document !== 'undefined' ? document.referrer : '')
40
41
  const origin = options.origin ?? (typeof location !== 'undefined' ? location.origin : '')
41
42
  if (!referrerAllowed(referrer, origin)) {
42
43
  log.warn('已忽略跨源深链请求(referrer=' + referrer + ')')
43
- return
44
+ return dispose
44
45
  }
45
46
  stripCurrentUrl()
46
47
  void openDeepLink(ctx, params).catch((error) => {
47
48
  log.warn('深链打开失败:' + String(error))
48
49
  toastDom('深链打开失败:' + String((error as Error)?.message ?? error))
49
50
  })
51
+ return dispose
50
52
  }
51
53
 
52
54
  /**
@@ -55,23 +57,42 @@ export function setupExtOpen(ctx: unknown, options: ExtOpenOptions = {}): void {
55
57
  * @author ddj 2026年09月08号
56
58
  * @param ctx 客户端根上下文
57
59
  */
58
- function startPresencePoll(ctx: unknown): void {
59
- if (typeof window === 'undefined') return
60
- const marker = window as { __edrvExtPoll?: boolean }
61
- if (marker.__edrvExtPoll) return
62
- marker.__edrvExtPoll = true
60
+ function startPresencePoll(ctx: unknown): () => void {
61
+ if (typeof window === 'undefined') return () => {}
62
+ const marker = window as unknown as { __edrvExtPoll?: () => void }
63
+ if (typeof marker.__edrvExtPoll === 'function') marker.__edrvExtPoll()
64
+ const clientId = crypto.randomUUID()
65
+ let disposed = false
66
+ let polling = false
63
67
  let executing = false
64
- window.setInterval(() => {
65
- if (executing) return
68
+ /** @private @author ddj 2026年09月28号 Open all requested paths before acknowledging this exact lease. */
69
+ async function execute(open: { paths: string[]; line?: number; column?: number; token: string; leaseId: string }): Promise<void> {
66
70
  executing = true
67
- void rpc('edrv.external.pending', {})
68
- .then(async (result) => {
69
- if (!result.ok || !result.open) return
70
- await openDeepLink(ctx, { paths: result.open.paths, line: result.open.line, column: result.open.column })
71
- })
72
- .catch(() => { /* 旧版 host 无此方法/离线:忽略 */ })
73
- .finally(() => { executing = false })
74
- }, HANDOFF_POLL_MS)
71
+ try {
72
+ const success = await openDeepLink(ctx, open)
73
+ if (!disposed) await rpc('edrv.external.ack', { token: open.token, leaseId: open.leaseId, clientId, success, error: success ? undefined : '编辑器打开失败或取消' })
74
+ } finally { executing = false }
75
+ }
76
+ /** @private @author ddj 2026年09月28号 Poll while opening to renew the lease without accepting a second request. */
77
+ async function tick(): Promise<void> {
78
+ if (disposed || polling) return
79
+ polling = true
80
+ try {
81
+ const result = await rpc('edrv.external.pending', { clientId, busy: executing })
82
+ if (!disposed && result.ok && result.open && !executing) void execute(result.open).catch(() => {})
83
+ } finally { polling = false }
84
+ }
85
+ const timer = window.setInterval(() => { void tick().catch(() => {}) }, HANDOFF_POLL_MS)
86
+ /** @private @author ddj 2026年09月28号 Stop heartbeats and fail outstanding editor waits on plugin disposal. */
87
+ function dispose(): void {
88
+ disposed = true
89
+ window.clearInterval(timer)
90
+ disposeOpenWait()
91
+ if (marker.__edrvExtPoll === dispose) delete marker.__edrvExtPoll
92
+ }
93
+ marker.__edrvExtPoll = dispose
94
+ void tick().catch(() => {})
95
+ return dispose
75
96
  }
76
97
 
77
98
  /**
@@ -32,7 +32,7 @@ import { openEditorView } from './events.js'
32
32
  import { setupExtOpen } from './externalOpen.js'
33
33
  import { SettingsContext } from './settingsContext.js'
34
34
  import { SIDEBAR_PLUGIN, registerSlotSafely, settingsBridge } from './compat.js'
35
- import { detectSidebarService, installSideEditor, setEnsureSideEditor, SIDEBAR_INSTALL_CMD } from './sidebarBridge.js'
35
+ import { detectSidebarService, hasSideRoute, installSideEditor, routeSideEditor, setEnsureSideEditor, SIDEBAR_INSTALL_CMD } from './sidebarBridge.js'
36
36
  import { detectOfficial, installOfficial, registerOfficialFileClaim, OFFICIAL_TAB_KIND, OFFICIAL_TAB_TITLE, isEditorTabActive, restoreEditorTab, buildFileAddress } from './officialSidebar.js'
37
37
  import { setNativeCsv, setNativeOpenHandler, resetNativeOpen } from './nativeOpenStore.js'
38
38
  import { SideEditorTab } from './ui/SideEditorTab.js'
@@ -56,7 +56,6 @@ import type { CommandBridge } from './commandBridge.js'
56
56
  import { REGISTRY_GLOBAL } from './commandGlobals.js'
57
57
  import { awaitShortcutsService, bindCatalogChords, detectShortcuts, installLegacyKeys, migrateLegacyKeybindings, registerOfficialShortcuts } from './shortcutsOfficial.js'
58
58
  import { SHORTCUT_PROFILES, defaultKeybindings, normalizeKeybindings } from '../shared/keybindings.js'
59
- import { ShortcutSettings } from './ui/ShortcutSettings.js'
60
59
  import { sidebarMinApply } from './sidebarMin.js'
61
60
  import { editorLimitApply } from './editorLimit.js'
62
61
  import { log } from './log.js'
@@ -88,7 +87,13 @@ let commandsBridge: CommandBridge | null = null
88
87
  */
89
88
  function setupCommands(ctx: any): CommandBridge {
90
89
  if (commandsBridge) return commandsBridge
91
- const bridge = createCommandBridge()
90
+ /** @author ddj 2026年09月28号 @returns Current session/workspace for command ownership. */
91
+ const readScope = () => readSessionScope(ctx)
92
+ /** @author ddj 2026年09月28号 @returns Whether an installed sidebar can mount the editor. */
93
+ const canOpen = () => hasSideRoute()
94
+ /** @author ddj 2026年09月28号 @returns Whether the current editor route accepted the request. */
95
+ const open = () => routeSideEditor(null, false)
96
+ const bridge = createCommandBridge({ readScope, canOpen, open })
92
97
  commandsBridge = bridge
93
98
  ctx.provide(REGISTRY_GLOBAL, bridge.registry)
94
99
  // 无条件镜像到 window(DSH 不创建 window.dsh,旧条件式赋值是死代码 → 命令栏空表)
@@ -143,7 +148,7 @@ export function apply(ctx: any): void {
143
148
  const sessions = ctx.get('sessions')
144
149
 
145
150
  // 外部深链落地:Windows 右键菜单 / Unity 外部编辑器 → 浏览器 URL 参数 → 打开规则路由
146
- setupExtOpen(ctx)
151
+ ctx.effect(() => setupExtOpen(ctx), 'vscode-mode: external open')
147
152
 
148
153
  // Monaco 加载时机:不再 DSH 启动即预热,改为进入会话界面(当前会话出现)后再后台加载,
149
154
  // 用户点开「文件编辑」页签即用;空闲时仍由 EditorView 挂载兜底加载。
@@ -258,7 +263,7 @@ export function apply(ctx: any): void {
258
263
  return sidebar ? registry.register(sidebar) : undefined
259
264
  }, 'vscode-mode: sidebar file opener')
260
265
  // 官方侧边栏正文组件装配(页类型正文;兼作 file 认领转发失败时的兜底正文)
261
- const officialRenderTab = (props: Record<string, unknown>) => React.createElement(OfficialSideTab, Object.assign({}, props, { schedule, addToConversation, sidebarPanels, outlineSources, fileMenuItems, fileOpeners: registry, sessions }))
266
+ const officialRenderTab = (props: Record<string, unknown>) => React.createElement(OfficialSideTab, Object.assign({}, props, { schedule, addToConversation, sidebarPanels, outlineSources, fileMenuItems, fileOpeners: registry, sessions, readSessionScope: () => readSessionScope(ctx) }))
262
267
  /** 官方 file 地址认领同步:自动/VSCodeMode 档认领(转发进单一编辑器页签),其余交官方查看器。 */
263
268
  const syncFileClaim = (): void => {
264
269
  const official = officialService
@@ -569,16 +574,13 @@ export function apply(ctx: any): void {
569
574
  id: 'vscode-mode',
570
575
  order: 30,
571
576
  label: 'VSCodeMode',
572
- }, () => React.createElement(SettingsContext.Provider, { value: bridge.scope() }, React.createElement(McpSettings, { openerRegistry: registry, compatSummary })))
573
-
574
- // 独立「快捷键」设置区(与 VSCodeMode 平级):官方快捷键子页 + 插件快捷键子页。
575
- // 服务经 getShortcuts 运行时探测(自愈),组件内部自带就绪重试。
576
- registerSlotSafely(ctx, {
577
- name: 'settings.section',
578
- id: 'vscode-mode-shortcuts',
579
- order: 31,
580
- label: '快捷键',
581
- }, () => React.createElement(ShortcutSettings, { getShortcuts: () => detectShortcuts(ctx) }))
577
+ }, () => React.createElement(SettingsContext.Provider, { value: bridge.scope() }, React.createElement(McpSettings, {
578
+ openerRegistry: registry,
579
+ compatSummary,
580
+ // 快捷键统一走 DSH 官方配置:通用页只提供「打开官方快捷键配置」入口。
581
+ // 服务运行时探测(自愈):旧版 DSH 无该服务时按钮给出手动打开方式。
582
+ getShortcuts: () => detectShortcuts(ctx),
583
+ })))
582
584
 
583
585
  // 卸载收尾(G4,DSH 0.1.6-alpha.2 起支持运行时卸载/重载):
584
586
  // 注销挂在存活的 window.monaco 上的全部 Monaco provider 并复位模块状态。
@@ -32,6 +32,22 @@ const MODIFIERS: Record<string, 'ctrl' | 'shift' | 'alt' | 'meta'> = {
32
32
 
33
33
  /** 弦表(官方 catalog 派生:id → 显示键位弦)。官方目录就绪前为空(tooltip 走各自的兜底文案)。 */
34
34
  let current: Record<string, string> = {}
35
+ let currentAria: Record<string, string | undefined> = {}
36
+
37
+ /**
38
+ * Read official accessibility metadata without deriving it from visual keycaps.
39
+ * @author ddj 2026年09月28号
40
+ * @param id Command identifier.
41
+ * @returns Official single-key ARIA shortcut, absent for two-key chords.
42
+ */
43
+ export function ariaOf(id: string): string | undefined { return currentAria[id] }
44
+
45
+ /**
46
+ * Replace accessibility metadata from the official catalog.
47
+ * @author ddj 2026年09月28号
48
+ * @param aria Complete catalog ARIA map, applied before display notification.
49
+ */
50
+ export function applyKeyAria(aria: Record<string, string | undefined>): void { currentAria = { ...aria } }
35
51
  const listeners = new Set<() => void>()
36
52
 
37
53
  /**
@@ -162,7 +178,8 @@ export function formatChord(binding: Binding): string {
162
178
  * @param bindings 解析键位或键位数组(null/空 = 未绑定,永不命中)
163
179
  * @returns 是否命中
164
180
  */
165
- export function matchEvent(e: { ctrlKey?: boolean; metaKey?: boolean; shiftKey?: boolean; altKey?: boolean; key?: string }, bindings: Binding | Binding[] | null): boolean {
181
+ export function matchEvent(e: { ctrlKey?: boolean; metaKey?: boolean; shiftKey?: boolean; altKey?: boolean; key?: string; isComposing?: boolean; keyCode?: number; defaultPrevented?: boolean; getModifierState?: (key: string) => boolean }, bindings: Binding | Binding[] | null): boolean {
182
+ if (e.defaultPrevented || e.isComposing || e.keyCode === 229 || e.key === 'Dead' || e.getModifierState?.('AltGraph')) return false
166
183
  const list = Array.isArray(bindings) ? bindings : bindings ? [bindings] : []
167
184
  for (const binding of list) {
168
185
  if (Boolean(e.ctrlKey || e.metaKey) !== (binding.ctrl || binding.meta)) continue