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
@@ -1,180 +1,137 @@
1
- /**
2
- * dsh-vscode-mode host — LSP 语言服务器进程传输层。
3
- * 基于 node:child_process.spawn(stdio pipe 双向流)。刻意不用 DSH ctx.subprocess
4
- * 抽象:LSP 需要持续写 stdin + 流式读 stdout,而该抽象 stdin 不可写、输出为一次性收集。
5
- * 作者 ddj 2026-08-27
6
- */
7
- import { spawn, type ChildProcess } from 'node:child_process'
1
+ /** LSP 双向 stdio 传输;外部进程使用清理后的环境及独立进程组。 */
2
+ import { spawn } from 'node:child_process'
3
+ import type { ChildProcess, ChildProcessWithoutNullStreams } from 'node:child_process'
8
4
  import { createFrameParser } from './jsonrpc.js'
5
+ import { childEnv } from '../childEnv.js'
6
+ import { killTree, stopTree } from '../processTree.js'
9
7
 
10
8
  const STDERR_CAP = 256 << 10
11
-
12
9
  export interface TransportSpec {
13
10
  argv: string[]
14
11
  cwd?: string
15
12
  env?: Record<string, string>
16
13
  }
17
-
18
14
  export interface Transport {
19
- /** 写一行帧到 stdin(自动加换行)。 */
20
15
  write(chunk: Buffer): boolean
21
- /** 推送 stdout 解析出的原始消息(内部由帧解析器调用)。 */
22
16
  onMessage: ((message: unknown) => void) | null
23
- /** stderr 收集缓冲(有界,供诊断)。 */
24
17
  stderr(): string
25
- /** 进程退出/崩溃回调(code/signal)。 */
26
18
  onExit: ((code: number | null, signal: string | null) => void) | null
27
- /** 优雅关闭后强杀(Windows 下连带进程树)。 */
28
19
  dispose(): void
29
- /** 是否仍存活。 */
30
20
  readonly alive: boolean
31
21
  readonly pid: number | undefined
32
22
  }
33
-
34
23
  const running = new Set<ChildProcess>()
35
24
 
36
- /** 当前仍在运行的 LSP 子进程(供 disposeAll / 诊断)。 */
37
- export function liveChildren(): ReadonlySet<ChildProcess> {
38
- return running
39
- }
25
+ /** 当前仍归本模块管理的进程。 */
26
+ export function liveChildren(): ReadonlySet<ChildProcess> { return running }
40
27
 
41
28
  /**
42
- * 启动语言服务器进程。
43
- * @author ddj 2026年08月27号
44
- * @param spec 启动参数(argv 数组,不经 shell,Windows 引号安全)
45
- * @param onMessage stdout 消息回调
46
- * @param logger 诊断日志(可选)
47
- * @returns 传输句柄
29
+ * 建立传输状态与回收路径;根进程退出也立即回收剩余进程组。
30
+ * @private
31
+ * @author ddj 2026年09月28号
32
+ * @param child 新建子进程
33
+ * @param onMessage 协议消息回调
34
+ * @param logger 诊断回调
35
+ * @returns 传输句柄与错误缓冲写入器
48
36
  */
49
- export function spawnServer(spec: TransportSpec, onMessage: (message: unknown) => void, logger?: (line: string) => void): Transport {
50
- const { argv, cwd, env } = spec
51
- const child = spawn(argv[0], argv.slice(1), {
52
- cwd,
53
- env: { ...process.env, ...env },
54
- stdio: ['pipe', 'pipe', 'pipe'],
55
- windowsHide: true,
56
- })
57
- running.add(child)
58
- let stderrBuf = Buffer.alloc(0)
37
+ function makeTransport(child: ChildProcessWithoutNullStreams, onMessage: (message: unknown) => void, logger?: (line: string) => void) {
59
38
  let alive = true
60
- const parser = createFrameParser()
61
-
62
- child.stderr.on('data', (chunk: Buffer) => {
63
- if (stderrBuf.length < STDERR_CAP) {
64
- stderrBuf = Buffer.concat([stderrBuf, chunk], Math.min(stderrBuf.length + chunk.length, STDERR_CAP))
65
- }
66
- logger?.(chunk.toString('utf8'))
67
- })
68
-
69
- let exited = false
70
- const emitExit = (code: number | null, signal: string | null): void => {
71
- if (exited) return
72
- exited = true
73
- alive = false
74
- running.delete(child)
75
- transport.onExit?.(code, signal)
76
- }
77
- child.on('error', (error) => {
78
- logger?.('spawn error: ' + String(error))
79
- emitExit(null, null)
80
- })
81
- child.on('exit', (code, signal) => emitExit(code, signal))
82
- child.on('close', (code, signal) => emitExit(code, signal))
83
-
39
+ let disposed = false
40
+ let stderrBuf = Buffer.alloc(0)
41
+ let finish: (() => void) | undefined
84
42
  const transport: Transport = {
43
+ /** @public @author ddj 2026年09月28号 @param chunk 协议帧 @returns 是否可写 */
85
44
  write(chunk: Buffer): boolean {
86
45
  if (!alive || !child.stdin.writable) return false
87
46
  child.stdin.write(chunk)
88
47
  return true
89
48
  },
90
49
  onMessage,
91
- stderr(): string {
92
- return stderrBuf.toString('utf8')
93
- },
50
+ /** @public @author ddj 2026年09月28号 @returns 有界错误缓冲 */
51
+ stderr(): string { return stderrBuf.toString('utf8') },
94
52
  onExit: null,
53
+ /** @public @author ddj 2026年09月28号 @description 停止整个进程树,根退出不取消回收。 */
95
54
  dispose(): void {
96
- if (!alive) return
97
- // 先 SIGTERM 让服务器有机会清理,1.5s 后强杀进程树
98
- child.kill()
99
- const timer = setTimeout(() => {
100
- if (!alive) return
101
- alive = false
102
- running.delete(child)
103
- try {
104
- if (process.platform === 'win32') {
105
- spawn('taskkill', ['/PID', String(child.pid), '/T', '/F'], { windowsHide: true, stdio: 'ignore' })
106
- } else {
107
- child.kill('SIGKILL')
108
- }
109
- } catch (error) {
110
- logger?.('force kill error: ' + String(error))
111
- }
112
- }, 1500)
113
- timer.unref?.()
114
- },
115
- get alive() {
116
- return alive
117
- },
118
- get pid() {
119
- return child.pid
55
+ if (disposed) return
56
+ disposed = true
57
+ finish = stopTree(child, logger)
120
58
  },
59
+ get alive() { return alive },
60
+ get pid() { return child.pid },
61
+ }
62
+ /** @private @author ddj 2026年09月28号 @param code 退出码 @param signal 退出信号 */
63
+ const emitExit = (code: number | null, signal: string | null): void => {
64
+ if (!alive) return
65
+ alive = false
66
+ disposed = true
67
+ if (finish) finish()
68
+ else killTree(child, logger)
69
+ running.delete(child)
70
+ transport.onExit?.(code, signal)
121
71
  }
122
- // stdout 帧解析 → onMessage(LSP 响应/通知;onMessage 由 client 接管后动态生效)
72
+ child.on('error', (error) => { logger?.('spawn error: ' + String(error)); emitExit(null, null) })
73
+ child.on('exit', emitExit)
74
+ child.on('close', emitExit)
75
+ child.stderr.on('data', (chunk: Buffer) => {
76
+ if (stderrBuf.length < STDERR_CAP) stderrBuf = Buffer.concat([stderrBuf, chunk], Math.min(stderrBuf.length + chunk.length, STDERR_CAP))
77
+ logger?.(chunk.toString('utf8'))
78
+ })
79
+ return transport
80
+ }
81
+
82
+ /**
83
+ * 启动语言服务器并接入协议流,显式 env 在父环境清理后覆盖。
84
+ * @public
85
+ * @author ddj 2026年09月28号
86
+ * @param spec 启动参数,不经 shell
87
+ * @param onMessage stdout 协议消息回调
88
+ * @param logger 诊断日志
89
+ * @returns 传输句柄
90
+ * @throws spawn 的同步参数错误
91
+ */
92
+ export function spawnServer(spec: TransportSpec, onMessage: (message: unknown) => void, logger?: (line: string) => void): Transport {
93
+ const child = spawn(spec.argv[0], spec.argv.slice(1), {
94
+ cwd: spec.cwd,
95
+ env: childEnv(spec.env),
96
+ stdio: ['pipe', 'pipe', 'pipe'],
97
+ windowsHide: true,
98
+ detached: process.platform !== 'win32',
99
+ })
100
+ running.add(child)
101
+ const transport = makeTransport(child, onMessage, logger)
102
+ const parser = createFrameParser()
123
103
  child.stdout.on('data', (chunk: Buffer) => {
124
104
  for (const message of parser.push(chunk)) transport.onMessage?.(message)
125
105
  })
106
+ // 写入管道在退出竞态中可能异步 EPIPE,必须监听以免宿主未处理异常。
107
+ child.stdin.on('error', (error) => logger?.('stdin error: ' + String(error)))
126
108
  return transport
127
109
  }
128
110
 
129
- /** 强杀全部运行中的 LSP 子进程(插件卸载/退出用)。 */
130
- export function disposeAllServers(): void {
131
- for (const child of running) {
132
- try {
133
- if (process.platform === 'win32' && child.pid) {
134
- spawn('taskkill', ['/PID', String(child.pid), '/T', '/F'], { windowsHide: true, stdio: 'ignore' })
135
- } else {
136
- child.kill('SIGKILL')
137
- }
138
- } catch (error) {
139
- /* 忽略单个清理失败 */
140
- }
141
- }
111
+ /**
112
+ * 回收本模块所有进程树。
113
+ * @public
114
+ * @author ddj 2026年09月28号
115
+ * @param sync 宿主 exit 钩子使用同步 taskkill
116
+ */
117
+ export function disposeAllServers(sync = false): void {
118
+ for (const child of running) killTree(child, undefined, sync)
142
119
  running.clear()
143
120
  }
144
121
 
145
- /** 进程退出回收是否已注册(防重复挂监听)。 */
146
122
  let exitHooked = false
147
-
148
123
  /**
149
- * 宿主进程退出时回收 LSP 子进程(幂等)。
150
- *
151
- * 为什么需要:插件的 ctx.effect 清理只在卸载/重载时触发,宿主进程退出不触发,
152
- * 于是 emmylua_ls / dotnet 等服务器会成为跨重启的孤儿常驻(实测孤儿启动时间早于宿主,
153
- * 长期占用内存与工作区文件句柄)。
154
- *
155
- * ⚠️ 只回收、不退出:宿主自身已注册 SIGINT/SIGTERM 处理(DSH profile-boot 走
156
- * shutdown.interrupt → 优雅关闭 + 状态落盘),这里若调 process.exit 会打断该流程。
157
- * 因此仅在信号到达时顺带回收子进程,退出时机与退出码仍由宿主决定。
158
- *
159
- * ⚠️ 信号监听只在宿主已有监听时才挂:Node 的默认信号语义(终止进程)会被首个监听器
160
- * 取代,若本插件是唯一监听者,挂上去反而让 Ctrl+C 不再退出宿主。无既有监听即跳过,
161
- * 交由 'exit' 事件兜底。
162
- *
163
- * exit 事件下只能同步执行,Windows 的 child.kill 不走进程树,故复用
164
- * disposeAllServers 的 taskkill /T /F 强杀路径;best-effort,失败不阻塞退出。
165
- * @author ddj 2026年09月11号
124
+ * 注册宿主退出回收;不夺取宿主原有信号退出语义。
125
+ * @public
126
+ * @author ddj 2026年09月28号
127
+ * @description exit 用同步回收;仅在宿主已有信号监听时追加收尾。
166
128
  */
167
129
  export function hookExitReclaim(): void {
168
130
  if (exitHooked) return
169
131
  exitHooked = true
170
- const reclaim = (): void => {
171
- try {
172
- disposeAllServers()
173
- } catch (error) { /* 退出路径静默 */ }
174
- }
175
- process.on('exit', reclaim)
132
+ process.on('exit', () => disposeAllServers(true))
176
133
  for (const signal of ['SIGINT', 'SIGTERM'] as const) {
177
134
  if (process.listenerCount(signal) === 0) continue
178
- process.on(signal, reclaim)
135
+ process.on(signal, () => disposeAllServers())
179
136
  }
180
137
  }
package/src/mcp.ts CHANGED
@@ -12,6 +12,7 @@ import {
12
12
  } from './compat.js'
13
13
  import type { MpcConfig, MpcServer, MpcTool } from './shared/mcp.js'
14
14
  import type { Ctx } from './store.js'
15
+ import { checkMcpName } from './mcpProject.js'
15
16
 
16
17
  export { LEGACY_PROJECT_PREFIX, MCP_PACKAGE, PROJECT_ENTRY_PREFIX, isProjectEntryId }
17
18
 
@@ -31,10 +32,17 @@ export function publicConfig(config: Record<string, unknown>): MpcConfig {
31
32
  return out as unknown as MpcConfig
32
33
  }
33
34
 
34
- /** 从工具注册表读取服务器命名空间下的工具摘要。 */
35
- export function toolsOf(ctx: Ctx, serverName: string): MpcTool[] {
35
+ /**
36
+ * Read tool summaries from the caller's exact visible scope.
37
+ * @public @author ddj 2026年09月28号
38
+ * @param ctx Host tools service.
39
+ * @param serverName Server namespace.
40
+ * @param agent Optional agent scope; omitted for global entries.
41
+ * @returns Scoped tool summaries, allowing resource-only servers to have zero tools.
42
+ */
43
+ export function toolsOf(ctx: Ctx, serverName: string, agent?: any): MpcTool[] {
36
44
  const tools = ctx.get('tools')
37
- const view = tools?.view?.(void 0)
45
+ const view = tools?.view?.(agent)
38
46
  const visible = view?.visible
39
47
  if (!(visible instanceof Map)) return []
40
48
  const prefix = `mcp__${serverName}__`
@@ -44,7 +52,37 @@ export function toolsOf(ctx: Ctx, serverName: string): MpcTool[] {
44
52
  }))
45
53
  }
46
54
 
47
- /** 将 loader entry 映射为设置页摘要。 */
55
+ /**
56
+ * Restore unchanged masked values without inventing credentials for new keys.
57
+ * @public @author ddj 2026年09月28号
58
+ * @param next Submitted configuration, including editable unknown fields.
59
+ * @param previous Last persisted configuration.
60
+ * @returns Detached configuration with unchanged secrets restored; throws for an orphan mask.
61
+ */
62
+ export function mergeSecrets(next: Record<string, unknown>, previous: Record<string, unknown>): Record<string, unknown> {
63
+ const merged = { ...next }
64
+ for (const field of ['env', 'headers']) {
65
+ const values = next[field]
66
+ if (!values || typeof values !== 'object' || Array.isArray(values)) continue
67
+ const old = previous[field] as Record<string, unknown> | undefined
68
+ const restored: Record<string, unknown> = Object.create(null)
69
+ for (const [key, value] of Object.entries(values)) {
70
+ if (value !== '••••••') { restored[key] = value; continue }
71
+ if (!old || !Object.hasOwn(old, key)) throw new Error('脱敏凭据没有原值,请重新填写:' + key)
72
+ restored[key] = old[key]
73
+ }
74
+ merged[field] = restored
75
+ }
76
+ return merged
77
+ }
78
+
79
+ /**
80
+ * Summarize a global entry without treating plugin activation as a successful handshake.
81
+ * @public @author ddj 2026年09月28号
82
+ * @param ctx Host services.
83
+ * @param entry Global loader entry.
84
+ * @returns Masked status and globally visible tools.
85
+ */
48
86
  export function serverOf(ctx: Ctx, entry: any): MpcServer {
49
87
  const config = (entry.options?.config ?? {}) as Record<string, unknown>
50
88
  const serverName = typeof config.serverName === 'string' ? config.serverName : entry.options?.id ?? 'unknown'
@@ -53,10 +91,12 @@ export function serverOf(ctx: Ctx, entry: any): MpcServer {
53
91
  return {
54
92
  id: String(entry.id ?? entry.options?.id ?? serverName),
55
93
  serverName,
56
- enabled: entry.disabled !== true,
94
+ enabled: entry.disabled !== true && entry.options?.disabled !== true,
57
95
  transport: config.transport === 'streamable-http' ? 'streamable-http' : 'stdio',
58
96
  config: publicConfig(config),
59
- status: state === 2 ? 'connected' : state === 1 ? 'connecting' : 'error',
97
+ status: entry.disabled === true || entry.options?.disabled === true ? 'disabled'
98
+ : state === 2 ? 'unverified' : state === 1 || state === 0 ? 'connecting' : state === 3 ? 'error' : 'configured',
99
+ instanceCount: entry.fiber ? 1 : 0,
60
100
  toolCount: tools.length,
61
101
  tools,
62
102
  error: state === 3 ? 'MCP 插件未正常运行' : undefined,
@@ -75,17 +115,26 @@ export function listMcp(ctx: Ctx): { servers: MpcServer[] } {
75
115
  return { servers: entriesOf(ctx).filter((entry) => !isProjectEntryId(String(entry.id ?? entry.options?.id ?? ''))).map((entry) => serverOf(ctx, entry)) }
76
116
  }
77
117
 
78
- /** 保存 MCP 服务配置并触发 loader 热更新。 */
118
+ /**
119
+ * Save a global loader entry while preserving advanced options and unchanged secrets.
120
+ * @public @author ddj 2026年09月28号
121
+ * @param ctx Host loader context.
122
+ * @param config Submitted global configuration.
123
+ * @returns Masked server status; project name collisions reject before writing.
124
+ */
79
125
  export async function saveMcp(ctx: Ctx, config: MpcConfig): Promise<MpcServer> {
80
126
  validateConfig(config)
127
+ await checkMcpName(ctx, config.serverName)
81
128
  const loader = ctx.get('loader')
82
129
  if (!loader) throw new Error('缺少 loader 服务')
83
130
  const existing = entriesOf(ctx).find((entry) => !isProjectEntryId(String(entry.id ?? entry.options?.id ?? '')) && entry.options?.config?.serverName === config.serverName)
131
+ const previous = existing?.options?.config ?? {}
132
+ const next = mergeSecrets({ ...previous, ...config }, previous)
84
133
  if (existing) {
85
- await loader.update(existing.id, { config: { ...config } })
134
+ await loader.update(existing.id, { config: next })
86
135
  return serverOf(ctx, loader.resolve(existing.id))
87
136
  }
88
- const id = await loader.create({ name: MCP_PACKAGE, config: { ...config } })
137
+ const id = await loader.create({ name: MCP_PACKAGE, config: next })
89
138
  return serverOf(ctx, loader.resolve(id))
90
139
  }
91
140