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
@@ -0,0 +1,101 @@
1
+ /** HTTP admission and bounded RPC decoding. @author ddj 2026年09月28号 */
2
+ import type { IncomingMessage, ServerResponse } from 'node:http'
3
+ import { BINARY_READ_CAP, READ_CAP } from './store.js'
4
+ import type { Ctx } from './store.js'
5
+
6
+ /** Text JSON can expand each byte into a six-character escape; binary uses base64. */
7
+ export const RPC_BODY_CAP = Math.max(READ_CAP * 6, Math.ceil(BINARY_READ_CAP / 3) * 4) + 64 * 1024
8
+ export interface RpcInput { method: string; args: Record<string, unknown> }
9
+ type InputResult = { input: RpcInput } | { status: number; error: string }
10
+
11
+ /**
12
+ * Send a JSON response without allowing credential-dependent results to be cached.
13
+ * @author ddj 2026年09月28号
14
+ * @param res HTTP response.
15
+ * @param status HTTP status code.
16
+ * @param value JSON payload.
17
+ */
18
+ export function replyJson(res: ServerResponse, status: number, value: unknown): void {
19
+ res.statusCode = status
20
+ res.setHeader('content-type', 'application/json; charset=utf-8')
21
+ res.setHeader('cache-control', 'no-store')
22
+ res.end(JSON.stringify(value))
23
+ }
24
+
25
+ /**
26
+ * Admit a request through the Host connection fence before inspecting its body.
27
+ * @author ddj 2026年09月28号
28
+ * @param ctx Host context; connection is an optional capability on older hosts.
29
+ * @param req Original Node request, including authentication headers.
30
+ * @param res Response receiving a refusal when unavailable or unauthorized.
31
+ * @param methods Allowed HTTP methods for this route.
32
+ * @returns Whether the request may proceed.
33
+ */
34
+ export function admitRoute(ctx: Ctx, req: IncomingMessage, res: ServerResponse, methods: string[]): boolean {
35
+ let rejection: number | undefined = 503
36
+ try {
37
+ const connection = ctx.get('connection')
38
+ if (typeof connection?.requestRejection === 'function') rejection = connection.requestRejection(req)
39
+ } catch { /* A missing or failed authentication service must not admit the request. */ }
40
+ if (rejection !== undefined) {
41
+ const status = rejection === 401 || rejection === 403 ? rejection : 503
42
+ replyJson(res, status, { ok: false, error: status === 503 ? 'Host authentication service unavailable' : 'Host authentication rejected this request' })
43
+ return false
44
+ }
45
+ if (!methods.includes(req.method ?? '')) {
46
+ res.setHeader('allow', methods.join(', '))
47
+ replyJson(res, 405, { ok: false, error: 'Method not allowed' })
48
+ return false
49
+ }
50
+ return true
51
+ }
52
+
53
+ /**
54
+ * Decode the RPC envelope without coercing scalar or array arguments into objects.
55
+ * @author ddj 2026年09月28号
56
+ * @param text Bounded UTF-8 JSON document.
57
+ * @returns Valid envelope or an HTTP 400 diagnostic.
58
+ */
59
+ function parseRpc(text: string): InputResult {
60
+ try {
61
+ const value: unknown = JSON.parse(text)
62
+ if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error('Invalid envelope')
63
+ const { method, args = {} } = value as { method?: unknown; args?: unknown }
64
+ if (typeof method !== 'string' || !/^[\w]+(?:\.[\w]+)*$/.test(method)) throw new Error('Invalid method')
65
+ if (!args || typeof args !== 'object' || Array.isArray(args)) throw new Error('Invalid arguments')
66
+ return { input: { method, args: args as Record<string, unknown> } }
67
+ } catch {
68
+ return { status: 400, error: 'Expected a JSON RPC object with method and object arguments' }
69
+ }
70
+ }
71
+
72
+ /**
73
+ * Read a bounded JSON body; reject headers and streamed overruns before RPC dispatch.
74
+ * @author ddj 2026年09月28号
75
+ * @param req Authenticated HTTP request.
76
+ * @param cap Maximum encoded bytes, including JSON escaping and base64 overhead.
77
+ * @returns Parsed input or an HTTP validation error.
78
+ */
79
+ export async function readRpc(req: IncomingMessage, cap = RPC_BODY_CAP): Promise<InputResult> {
80
+ if (!/^application\/json(?:\s*;|\s*$)/i.test(req.headers['content-type'] ?? '')) {
81
+ req.resume()
82
+ return { status: 415, error: 'Content-Type must be application/json' }
83
+ }
84
+ const length = req.headers['content-length']
85
+ if (length !== undefined && (!/^\d+$/.test(length) || Number(length) > cap)) {
86
+ req.resume()
87
+ return { status: 413, error: 'RPC request exceeds the supported file size' }
88
+ }
89
+ const chunks: Uint8Array[] = []
90
+ let bytes = 0
91
+ for await (const chunk of req.iterator({ destroyOnReturn: false })) {
92
+ const data = typeof chunk === 'string' ? Buffer.from(chunk) : chunk as Uint8Array
93
+ bytes += data.byteLength
94
+ if (bytes > cap) {
95
+ req.resume()
96
+ return { status: 413, error: 'RPC request exceeds the supported file size' }
97
+ }
98
+ chunks.push(data)
99
+ }
100
+ return parseRpc(Buffer.concat(chunks, bytes).toString('utf8'))
101
+ }
package/src/index.ts CHANGED
@@ -1,23 +1,18 @@
1
- /**
2
- * @dsh-external 生态 → dsh-vscode-mode:DSH 上的类 VSCode 编码体验(Host 半入口)。
3
- * 职责:装配 capture(tools/result 捕获 edit/write 差异)、RPC 分发(edrv.* / mcp.* / vscode.* / compat)、
4
- * webServer 路由(/edrv/rpc、/edrv/assets/*、/edrv/vendor/*)、工作区旁车持久化、
5
- * 兼容层装配(依赖守卫 + 重复装配自诊断 + 启动日志兼容性报告)。
6
- * 结构:shared/(双面契约) + model/store/capture/workspace/revert/registry/rpc/routes/compat(host 模块)
7
- * + log/debugLog(统一日志:bindHostLog 绑定 ctx.logger,edrv.debug 诊断文件通道)。
8
- * 作者 ddj 2026-08-20
9
- */
1
+ /** dsh-vscode-mode Host composition. @author ddj 2026年09月28号 */
10
2
  import { registerRoutes } from './routes.js'
11
3
  import { captureToolResult } from './capture.js'
12
4
  import { handleRpc } from './rpc.js'
13
5
  import { newSearcher } from './search/orchestrator.js'
14
6
  import { newContentSearcher } from './search/content.js'
15
7
  import { installIsolation } from './mcpIsolation.js'
8
+ import { installMcpRuntime } from './mcpRuntime.js'
16
9
  import { cwdOf } from './registry.js'
17
10
  import { setupOpenSettings, buildSettingsSchema } from './fileOpenSettings.js'
18
11
  import type { SettingsDeps } from './fileOpenSettings.js'
19
12
  import z from '@deepseek-ai/schemastery'
20
13
  import { shellMenuLifecycle } from './integrate.js'
14
+ import { startOpenInbox } from './openInbox.js'
15
+ import { installPlanTool } from './svnPlanTool.js'
21
16
  import { disposeIndex } from './treeIndex.js'
22
17
  import { sweepTreeCache } from './paths.js'
23
18
  import { buildReport } from './compat.js'
@@ -31,125 +26,139 @@ import { createSvnRpc } from './svn.js'
31
26
  import { createDapRpc } from './dap/rpc.js'
32
27
  import { installRulesSection } from './rules.js'
33
28
  import { installSkillGroup } from './skills.js'
34
- import type { RpcHandlerMap } from './shared/rpc.js'
29
+ import type { RpcHandlerMap, RpcMethod, RpcRequestMap } from './shared/rpc.js'
35
30
  import type { Registry } from './registry.js'
36
31
  import type { Ctx } from './store.js'
37
32
 
38
- export const name = "dsh-vscode-mode"
33
+ export const name = 'dsh-vscode-mode'
39
34
  export const inject = ['sessions', 'fs', 'webServer', 'loader', 'tools', 'workspaceRegistry', 'agents']
40
-
41
- /**
42
- * 插件 Config schema(cordis 从 module 导出读 `plugin.Config` 做启动校验;
43
- * DSH 0.1.7 起设置页按此 schema 自动生成表单,设置值 = profile 插件配置)。
44
- * 字段集与 installOpenSettingsSection 的 section schema 同源(buildSettingsSchema),
45
- * 全字段带默认值——undefined/空配置经 schemastery 校验自动填充(rc/alpha 两代
46
- * cordis 均读该导出,实测 4.0.0-rc.8 与 0.1.7 行为一致,校验必过)。
47
- * volatile: true —— 0.1.7 的 SettingsForms.describe 只下发含 volatile 字段的 entry
48
- * (volatileForm 门槛),漏标则 ns 不出现 → client configForms 恒 unavailable →
49
- * 设置页「设置服务暂不可用」;同时 volatile 字段由 Loader 就地提交免重载。
50
- * 运行时解析:@deepseek-ai/schemastery 经插件自身 dependencies(link/dev/npm 安装
51
- * 三形态一致,钉 ~3.18.4 —— 3.18.1 起才有 .volatile(),低版本经 markVolatile 守卫降级)。
52
- * @author ddj 2026年09月22号(2026年09月23号 增补 volatile 标记)
53
- */
35
+ /** Shared settings schema; volatile fields remain live across supported Host generations. */
54
36
  export const Config = buildSettingsSchema(z as unknown as SettingsDeps['z'], { volatile: true })
37
+ type Runtime = ReturnType<typeof createRuntime>
55
38
 
39
+ // #region Owned services
56
40
  /**
57
- * 装配插件:挂事件监听、注册路由、安装兼容层。
58
- * @author ddj 2026年08月20号
59
- * @param ctx DSH 上下文(sessions/fs/webServer 由 inject 提供;sandboxPolicy/subprocess 惰性获取)
60
- * @param config 插件配置(可选 imageDir 覆盖图标目录)
41
+ * Create the managers shared by RPC routes, tools and lifecycle callbacks.
42
+ * @author ddj 2026年09月28号
43
+ * @param ctx Host context.
44
+ * @param config Plugin configuration.
45
+ * @returns One owned set of managers for this plugin instance.
61
46
  */
62
- export function apply(ctx: Ctx, config?: unknown): void {
63
- // 统一日志出口:全部 host 日志经 ctx.logger(缺失时回退 console),此后任何模块 log.* 即生效
64
- bindHostLog(ctx)
47
+ function createRuntime(ctx: Ctx, config: unknown) {
65
48
  const registry: Registry = new Map()
66
49
  const searcher = newSearcher(ctx)
67
50
  const contentSearcher = newContentSearcher(ctx)
68
- /** LSP 服务器管理器(语言智能:跳转/引用/大纲;诊断行带模块内前缀如 [lsp-manager])。 */
51
+ const settings = setupOpenSettings(ctx, config, () => {})
69
52
  const lspManager = createLspManager((line) => log.debug(line))
70
- /** 兼容性警告收集(route 护栏等写入,启动日志一并输出)。 */
71
- const warnings: string[] = []
72
- /** 设置状态(fileOpenTool + AI 补全配置读写;settings 不可用时内存态降级)。 */
73
- const openSettings = setupOpenSettings(ctx, config, () => {})
74
- /** LSP 配置为配置值模式:插件组合配置 + 会话内运行时覆盖(原 settings section 命名空间不合法,见 lsp/config.ts)。 */
75
- /** 规则注入 section(~/.dsh/rules 与 <工作区>/.dsh/rules;旧版 DSH 无 systemPrompt 时静默降级)。 */
76
- const rulesInstalled = installRulesSection(ctx)
77
- if (!rulesInstalled) log.warn('未检测到 systemPrompt 服务,规则仅可管理不注入')
78
- /** 插件自带技能组(<包根>/skills,惰性获取 skills 服务;返回值仅表示"已调度",实际结果见 skillGroupState)。 */
79
- const skillsDispatched = installSkillGroup(ctx)
80
- if (!skillsDispatched) log.warn('技能组未调度,插件技能组不可用')
81
- /** LSP RPC 与会话清理(一次性创建,tracker 状态跨请求保留)。 */
82
53
  const lspRpc = createLspRpc({ ctx, pluginConfig: config, manager: lspManager })
83
- const lspHandlers: Partial<RpcHandlerMap> = lspRpc.handlers
84
- /** AI 内联补全 RPC(settings 状态桥接 + llm 惰性获取)。 */
85
- const aiRpc = createAiRpc({ ctx, settings: openSettings })
86
- const aiHandlers: Partial<RpcHandlerMap> = aiRpc.handlers
87
- /** SVN RPC(检测/更新/Tortoise 发射;settings 提供 svnPath/tortoisePath)。 */
88
- const svnRpc = createSvnRpc({ ctx, settings: openSettings })
89
- const svnHandlers: Partial<RpcHandlerMap> = svnRpc.handlers as Partial<RpcHandlerMap>
90
- /** 调试 RPC(DAP 桥单例:spawn 扩展适配器 + 事件缓冲 + findFile 反向匹配)。 */
54
+ const aiRpc = createAiRpc({ ctx, settings })
55
+ const svnRpc = createSvnRpc({ ctx, settings })
91
56
  const dapRpc = createDapRpc(ctx)
92
- const dapHandlers: Partial<RpcHandlerMap> = dapRpc.handlers
93
- /** 文件磁盘新鲜度观察器(客户端轮询 edrv.versions;变化时顺带失效目录树缓存)。 */
94
57
  const fileVersions = createFileVersions(ctx)
58
+ return { registry, searcher, contentSearcher, lspManager, lspRpc, aiRpc, svnRpc, dapRpc, fileVersions }
59
+ }
95
60
 
96
- ctx.on('tools/result', (exec: unknown, result: unknown) => {
97
- void captureToolResult(ctx, registry, exec, result)
98
- })
99
-
100
- ctx.on('session/disposed', (session: unknown) => {
101
- const cwd = cwdOf(session as never)
61
+ /**
62
+ * Own tool capture, per-session caches, and process cleanup under one Host fiber.
63
+ * @author ddj 2026年09月28号
64
+ * @param ctx Host context.
65
+ * @param runtime Managers created by this instance.
66
+ */
67
+ function attachRuntime(ctx: Ctx, runtime: Runtime): void {
68
+ /** @author ddj 2026年09月28号 @param exec Tool execution. @param result Tool result. */
69
+ function onResult(exec: unknown, result: unknown): void {
70
+ void captureToolResult(ctx, runtime.registry, exec, result)
71
+ }
72
+ /** @author ddj 2026年09月28号 @param session Disposed session. */
73
+ function onDisposed(session: { id?: unknown }): void {
74
+ const cwd = cwdOf(session)
102
75
  if (cwd) {
103
- registry.delete(cwd)
104
- searcher.dispose(cwd)
105
- contentSearcher.dispose(cwd)
76
+ runtime.registry.delete(cwd)
77
+ runtime.searcher.dispose(cwd)
78
+ runtime.contentSearcher.dispose(cwd)
106
79
  disposeIndex(cwd)
107
80
  }
108
- const sid = (session as { id?: unknown })?.id
109
- if (typeof sid === 'string') lspRpc.disposeSession(sid)
110
- })
111
-
112
- registerRoutes(ctx, config, (method, args) => handleRpc(ctx, registry, method, args, searcher, contentSearcher, lspHandlers, aiHandlers, fileVersions, svnHandlers, dapHandlers), (warning) => warnings.push(warning))
113
- installIsolation(ctx)
114
- // 系统集成生命周期:启动自动恢复右键菜单注册(marker 存在时);插件卸载/reload 清理注册痕迹
115
- ctx.effect(() => shellMenuLifecycle(ctx))
116
- // 启动清理缓存目录:非当前 schema / 超保留期 / 未知残留(best-effort,不阻塞装配)
117
- void sweepTreeCache()
118
- void logCompatSummary(ctx, warnings)
119
- // 卸载/重启时强杀 LSP 子进程(防残留)
120
- ctx.effect(() => () => {
121
- void lspManager.disposeAll().catch(() => {})
81
+ if (typeof session.id === 'string') runtime.lspRpc.disposeSession(session.id)
82
+ }
83
+ /** @author ddj 2026年09月28号 Release every manager even if another cleanup rejects. */
84
+ async function cleanup(): Promise<void> {
85
+ runtime.fileVersions.dispose()
122
86
  disposeAllServers()
123
- })
124
- // 卸载时清空文件版本基准表(观察器为模块内单例,不清会跨装配残留陈旧版本)
125
- ctx.effect(() => () => fileVersions.dispose())
126
- // 卸载时结束仍在运行的 SVN 更新,避免无主工作副本更新继续写入
127
- ctx.effect(() => () => svnRpc.dispose())
128
- // 卸载/重启时结束调试会话并强杀适配器子进程(防残留注入器/适配器孤儿)
129
- ctx.effect(() => () => dapRpc.dispose())
130
- // 宿主进程退出回收:ctx.effect 清理不覆盖进程退出,缺此注册会留下跨重启的孤儿服务器
87
+ const tasks = [() => runtime.lspManager.disposeAll(), () => runtime.svnRpc.dispose(), () => runtime.dapRpc.dispose()]
88
+ await Promise.allSettled(tasks.map((run) => Promise.resolve().then(run)))
89
+ }
90
+ ctx.on('tools/result', onResult)
91
+ ctx.on('session/disposed', onDisposed)
92
+ ctx.effect(() => cleanup, 'vscode-mode:runtime')
131
93
  hookExitReclaim()
94
+ }
95
+
96
+ /**
97
+ * Add user rules and bundled skills without requiring them on older Hosts.
98
+ * @author ddj 2026年09月28号
99
+ * @param ctx Host context.
100
+ */
101
+ function installContent(ctx: Ctx): void {
102
+ if (!installRulesSection(ctx)) log.warn('未检测到 systemPrompt 服务,规则仅可管理不注入')
103
+ if (!installSkillGroup(ctx)) log.warn('技能组未调度,插件技能组不可用')
104
+ }
105
+ // #endregion
132
106
 
133
- log.info('编辑差异审查已装配(/edrv/rpc 路由就绪,项目 MCP 隔离已启用,语言服务器 LSP 已接入,规则注入' + (rulesInstalled ? '已接入' : '未接入') + ',技能组' + (skillsDispatched ? '装配中' : '未装配') + ')')
107
+ /**
108
+ * Activate authenticated routes, scoped MCP, analysis submission and private file opens.
109
+ * @author ddj 2026年09月28号
110
+ * @param ctx Injected Host context.
111
+ * @param config Validated live plugin configuration.
112
+ * @returns Startup readiness; owned effects are released on activation failure or unload.
113
+ */
114
+ export async function apply(ctx: Ctx, config?: unknown): Promise<void> {
115
+ bindHostLog(ctx)
116
+ const runtime = createRuntime(ctx, config)
117
+ const warnings: string[] = []
118
+ attachRuntime(ctx, runtime)
119
+ installContent(ctx)
120
+ /** @author ddj 2026年09月28号 @param method RPC name. @param args Typed request. @returns RPC result. */
121
+ function dispatch<M extends RpcMethod>(method: M, args: RpcRequestMap[M]) {
122
+ const { registry, searcher, contentSearcher, lspRpc, aiRpc, fileVersions, svnRpc, dapRpc } = runtime
123
+ return handleRpc(ctx, registry, method, args, searcher, contentSearcher, lspRpc.handlers,
124
+ aiRpc.handlers, fileVersions, svnRpc.handlers as Partial<RpcHandlerMap>, dapRpc.handlers)
125
+ }
126
+ /** @author ddj 2026年09月28号 @param warning Startup compatibility diagnostic. */
127
+ function noteWarning(warning: string): void { warnings.push(warning) }
128
+ /** @author ddj 2026年09月28号 @returns Private inbox disposer, or a reported disabled capability. */
129
+ async function mountInbox(): Promise<() => void> {
130
+ try { return await startOpenInbox(ctx) } catch (error) {
131
+ noteWarning('外部打开通道未启用:' + String(error))
132
+ return () => {}
133
+ }
134
+ }
135
+ registerRoutes(ctx, config, dispatch, noteWarning)
136
+ installIsolation(ctx)
137
+ await installMcpRuntime(ctx)
138
+ /** @author ddj 2026年09月28号 @param args Session-bound plan. @returns Validated inbox result. */
139
+ function submitPlan(args: RpcRequestMap['svn.aiPlanSubmit']) { return dispatch('svn.aiPlanSubmit', args) }
140
+ await installPlanTool(ctx, submitPlan)
141
+ ctx.effect(mountInbox, 'vscode-mode:open-inbox')
142
+ ctx.effect(() => shellMenuLifecycle(ctx), 'vscode-mode:integration')
143
+ void sweepTreeCache()
144
+ await logCompatSummary(ctx, warnings)
145
+ log.info('编辑差异审查已装配(认证路由、项目 MCP 作用域、SVN 方案工具与外部打开通道)')
134
146
  }
135
147
 
136
148
  /**
137
- * 异步输出兼容性报告摘要(含重复装配/路由冲突自诊断)。
138
- * @author ddj 2026年08月24号
139
- * @param ctx DSH host 上下文
140
- * @param warnings 装配期收集的兼容性警告
149
+ * Log the actual compatibility report without turning a reporting failure into boot failure.
150
+ * @author ddj 2026年09月28号
151
+ * @param ctx Host context.
152
+ * @param warnings Activation diagnostics.
141
153
  */
142
154
  async function logCompatSummary(ctx: Ctx, warnings: string[]): Promise<void> {
143
155
  try {
144
156
  const report = await buildReport(ctx)
145
- for (const warning of warnings) report.warnings.push(warning)
157
+ report.warnings.push(...warnings)
146
158
  const dsh = report.dshVersion ? 'DSH ' + report.dshVersion : 'DSH 未探测'
147
159
  const head = '兼容性:' + dsh + ' · ' + report.external.length + ' 项外部适配 / ' + report.guards.length + ' 项护栏 / ' + (report.adapters?.length ?? 0) + ' 项版本适配'
148
- if (report.warnings.length) {
149
- log.warn(head + ',警告 ' + report.warnings.length + ' 条:' + report.warnings.join(';'))
150
- } else {
151
- log.info(head + ',无警告')
152
- }
160
+ if (report.warnings.length) log.warn(head + ',警告 ' + report.warnings.length + ' 条:' + report.warnings.join(';'))
161
+ else log.info(head + ',无警告')
153
162
  } catch (error) {
154
163
  log.warn('兼容性报告生成失败:' + String(error))
155
164
  }
package/src/integrate.ts CHANGED
@@ -4,17 +4,19 @@
4
4
  * ~/.dsh/dsh-vscode-mode/shell/,HKCU\Software\Classes 三类菜单键(`*`/Directory/Directory\Background)。
5
5
  * - Linux:dsh-open.sh + GNOME Files(Nautilus)右键脚本 + KDE Dolphin 服务菜单(纯文件写入)。
6
6
  * - macOS:dsh-open.sh(Finder 快速操作由用户按配方手动创建,自动检测/移除)。
7
- * 生命周期:注册成功写 marker(shell/registered.json)→ 插件卸载/reload 清理注册痕迹,
8
- * 启动时 marker 存在则自动恢复(更新插件不丢注册);用户显式「移除注册」删 marker 永不自动恢复。
7
+ * 生命周期:注册成功写 marker;启动按 marker 升级 launcher 文件,不反复重写系统注册。
8
+ * 用户显式「移除注册」删 marker,后续启动不会恢复。
9
9
  * 所有 reg/csc 调用经 subprocess 服务 argv 数组(卸载清理用 child_process 直调,teardown 可靠)。
10
10
  * 纯函数(键路径/命令行/ini/脚本内容)可单测。
11
11
  * 作者 ddj 2026-09-07
12
12
  */
13
- import { spawn } from 'node:child_process'
13
+ import { childEnv } from './childEnv.js'
14
14
  import { chmod, copyFile, mkdir, readFile, rm, stat, writeFile } from 'node:fs/promises'
15
15
  import { homedir } from 'node:os'
16
16
  import { join } from 'node:path'
17
17
  import { assetsDirOf, dshHome, PLUGIN_ID } from './paths.js'
18
+ import { findProfileDir } from './devForm.js'
19
+ import { openIni, writeOpenConfig } from './openBridge.js'
18
20
  import { INTEGRATION_BASE_DEFAULT, SHELL_MENU_LEAF } from './shared/integration.js'
19
21
  import { unitySourceDir } from './unityBridge.js'
20
22
  import type { ShellIntegrationStatus, ShellLauncherState, ShellMenuEntry } from './shared/integration.js'
@@ -53,7 +55,7 @@ export const WHALE_ICO = 'dsh-whale.ico'
53
55
  interface RunOutcome { code: number | null; output: string }
54
56
 
55
57
  /** 注册 marker 内容。 */
56
- interface RegistrationMarker { baseUrl: string; at: string }
58
+ interface RegistrationMarker { baseUrl: string; at: string; transport?: number; profile?: string }
57
59
 
58
60
  /**
59
61
  * reg.exe 路径(SystemRoot 派生;env 可注入便于测试)。
@@ -172,8 +174,8 @@ export function ps1CommandValue(ps1Path: string, token: string = FILE_TOKEN, pow
172
174
  * @param baseUrl 深链基址
173
175
  * @returns ini 文件全文
174
176
  */
175
- export function iniContent(baseUrl: string): string {
176
- return '[dsh]\nbase=' + (baseUrl.trim() || INTEGRATION_BASE_DEFAULT) + '\n'
177
+ export function iniContent(baseUrl: string, metadata: Record<string, string> = {}): string {
178
+ return openIni(baseUrl, metadata)
177
179
  }
178
180
 
179
181
  /**
@@ -186,7 +188,7 @@ export function nautilusScript(launcherPath: string): string {
186
188
  return [
187
189
  '#!/usr/bin/env bash',
188
190
  '# 由 dsh-vscode-mode 生成:GNOME Files(Nautilus)右键脚本 → DSH 文件编辑',
189
- 'launcher="$HOME/.dsh/dsh-vscode-mode/shell/' + LAUNCHER_SH + '"',
191
+ 'launcher=' + "'" + launcherPath.replace(/'/g, "'\"'\"'") + "'",
190
192
  'args=()',
191
193
  'while IFS= read -r line; do',
192
194
  ' [ -n "$line" ] && args+=("$line")',
@@ -251,6 +253,7 @@ async function runArgv(ctx: Ctx, argv: string[], cwd = dshHome()): Promise<RunOu
251
253
  handle = sub.spawn({
252
254
  argv,
253
255
  cwd,
256
+ env: childEnv(),
254
257
  stdio: { stdout: 'inherit', stderr: 'inherit', stdin: 'ignore' },
255
258
  graceMs: 10000,
256
259
  })
@@ -292,17 +295,17 @@ async function keyExists(ctx: Ctx, key: string): Promise<boolean> {
292
295
  /** 读注册 marker(缺失/非法 → null)。 */
293
296
  async function readMarker(home = dshHome()): Promise<RegistrationMarker | null> {
294
297
  try {
295
- const data = JSON.parse(await readFile(markerFile(home), 'utf8')) as { baseUrl?: unknown; at?: unknown }
298
+ const data = JSON.parse(await readFile(markerFile(home), 'utf8')) as { baseUrl?: unknown; at?: unknown; transport?: unknown; profile?: unknown }
296
299
  if (typeof data?.baseUrl !== 'string' || !data.baseUrl) return null
297
- return { baseUrl: data.baseUrl, at: typeof data.at === 'string' ? data.at : '' }
300
+ return { baseUrl: data.baseUrl, at: typeof data.at === 'string' ? data.at : '', transport: typeof data.transport === 'number' ? data.transport : undefined, profile: typeof data.profile === 'string' ? data.profile : undefined }
298
301
  } catch {
299
302
  return null
300
303
  }
301
304
  }
302
305
 
303
306
  /** 写注册 marker(父目录已存在)。 */
304
- async function saveMarker(baseUrl: string, home = dshHome()): Promise<void> {
305
- const data: RegistrationMarker = { baseUrl, at: new Date().toISOString() }
307
+ async function saveMarker(baseUrl: string, home = dshHome(), profile?: string): Promise<void> {
308
+ const data: RegistrationMarker = { baseUrl, at: new Date().toISOString(), transport: 1, profile }
306
309
  await writeFile(markerFile(home), JSON.stringify(data, null, 2) + '\n', 'utf8')
307
310
  }
308
311
 
@@ -310,9 +313,9 @@ async function saveMarker(baseUrl: string, home = dshHome()): Promise<void> {
310
313
  async function installPosix(dir: string, baseUrl: string): Promise<void> {
311
314
  await copyFile(join(assetsDirOf(import.meta.url), 'shell', LAUNCHER_SH), join(dir, LAUNCHER_SH))
312
315
  await chmod(join(dir, LAUNCHER_SH), 0o755)
313
- await writeFile(join(dir, INI_NAME), iniContent(baseUrl), 'utf8')
314
316
  }
315
317
 
318
+
316
319
  /** 写 Nautilus 右键脚本(父目录按需创建 + 执行位)。 */
317
320
  async function writeNautilus(home: string, launcherPath: string): Promise<void> {
318
321
  const script = nautilusPath(home)
@@ -348,19 +351,6 @@ async function dropFinderMenu(home: string): Promise<void> {
348
351
  await rm(dir, { recursive: true, force: true })
349
352
  }
350
353
 
351
- /** 卸载清理专用:直调 reg delete(subprocess 服务在 teardown 可能已卸载)。⚠️ stdio 'ignore':受管环境禁管道。 */
352
- function regDeleteDirect(key: string): Promise<void> {
353
- return new Promise((resolve) => {
354
- try {
355
- const child = spawn(regExe(), ['delete', key, '/f'], { windowsHide: true, timeout: 5000, stdio: 'ignore' })
356
- child.once('close', () => resolve())
357
- child.once('error', () => resolve())
358
- } catch {
359
- resolve()
360
- }
361
- })
362
- }
363
-
364
354
  /**
365
355
  * 查询系统集成状态(按平台探测条目注册状态 + launcher 安装态)。
366
356
  * @author ddj 2026年09月07号
@@ -412,7 +402,6 @@ export async function shellMenuRegister(ctx: Ctx, baseUrl: string): Promise<Shel
412
402
  const dir = shellInstallDir()
413
403
  await mkdir(dir, { recursive: true })
414
404
  if (process.platform === 'win32') {
415
- await writeFile(join(dir, INI_NAME), iniContent(baseUrl), 'utf8')
416
405
  await installIcon(dir)
417
406
  const compiled = await installLauncher(ctx, dir, true)
418
407
  if (!(await readMarker())) await backupKeys(ctx, dir)
@@ -426,7 +415,8 @@ export async function shellMenuRegister(ctx: Ctx, baseUrl: string): Promise<Shel
426
415
  // macOS 菜单由用户按 Automator 配方手动创建(UI 提供一键复制);这里仅安装 launcher
427
416
  await installPosix(dir, baseUrl)
428
417
  }
429
- await saveMarker(baseUrl)
418
+ await writeOpenConfig(ctx, dir, baseUrl)
419
+ await saveMarker(baseUrl, dshHome(), findProfileDir(ctx))
430
420
  return shellMenuStatus(ctx, baseUrl)
431
421
  }
432
422
 
@@ -454,38 +444,40 @@ export async function shellMenuRemove(ctx: Ctx, baseUrl: string): Promise<ShellI
454
444
 
455
445
  /**
456
446
  * 系统集成生命周期(index.ts 经 ctx.effect 挂载):
457
- * 启动时 marker 存在 → 幂等自动恢复注册(插件更新/reload 不丢注册);
458
- * 返回的 disposer 在插件卸载/reload 时清理注册痕迹(marker 保留供下次恢复)。
447
+ * 启动时仅升级用户已注册的 launcher 文件与配置;保留系统注册和明确选中的 profile。
448
+ * 卸载时取消后续迁移步骤;移除系统注册只由用户显式操作触发。
459
449
  * @author ddj 2026年09月07号
460
450
  * @param ctx DSH 上下文
461
451
  * @returns 卸载清理 disposer
462
452
  */
463
453
  export function shellMenuLifecycle(ctx: Ctx): () => void {
464
- void restoreOnLoad(ctx).catch(() => {})
465
- return () => {
466
- void cleanupOnUnload().catch(() => {})
467
- }
454
+ let disposed = false
455
+ void restoreOnLoad(ctx, () => disposed).catch((error) => console.warn('[dsh-vscode-mode] launcher migration failed:', error))
456
+ return () => { disposed = true }
468
457
  }
469
458
 
470
- /** 启动自动恢复:marker 存在才执行(幂等;exe 已存在跳过编译)。 */
471
- async function restoreOnLoad(ctx: Ctx): Promise<void> {
472
- const marker = await readMarker()
473
- if (!marker) return
474
- await shellMenuRegister(ctx, marker.baseUrl)
475
- }
476
-
477
- /** 卸载清理:marker 存在 → 按平台移除注册痕迹(launcher 文件与 marker 保留)。 */
478
- async function cleanupOnUnload(): Promise<void> {
459
+ /** @private @author ddj 2026年09月28号
460
+ * Upgrade installed assets only; OS registration remains user-owned across reloads.
461
+ * @param ctx Host profile context.
462
+ * @param disposed Whether lifecycle disposal has started.
463
+ */
464
+ async function restoreOnLoad(ctx: Ctx, disposed: () => boolean): Promise<void> {
479
465
  const marker = await readMarker()
480
- if (!marker) return
481
- if (process.platform === 'win32') {
482
- const paths = menuKeyPaths()
483
- for (const key of [paths.files, paths.dir, paths.background]) await regDeleteDirect(key)
484
- } else if (process.platform === 'linux') {
485
- await removePosixFiles(homedir())
486
- } else if (process.platform === 'darwin') {
487
- await dropFinderMenu(homedir())
466
+ if (!marker || disposed()) return
467
+ const profile = findProfileDir(ctx)
468
+ if (!profile || (marker.profile && marker.profile !== profile)) return
469
+ const dir = shellInstallDir()
470
+ const baseUrl = await storedOpenBase(dir, marker.baseUrl)
471
+ if (marker.transport !== 1) {
472
+ if (process.platform === 'win32') {
473
+ const hadExe = await fileExists(join(dir, LAUNCHER_EXE))
474
+ const compiled = await installLauncher(ctx, dir, true)
475
+ if (hadExe && !compiled) throw new Error('旧 launcher 编译升级失败,请在设置中重新注册')
476
+ } else await installPosix(dir, baseUrl)
488
477
  }
478
+ if (disposed()) return
479
+ await writeOpenConfig(ctx, dir, baseUrl)
480
+ if (!disposed()) await saveMarker(baseUrl, dshHome(), profile)
489
481
  }
490
482
 
491
483
  /**
@@ -500,6 +492,7 @@ async function installLauncher(ctx: Ctx, dir: string, force: boolean): Promise<b
500
492
  const exe = join(dir, LAUNCHER_EXE)
501
493
  if (!force && await fileExists(exe)) return true
502
494
  const shellAssets = join(assetsDirOf(import.meta.url), 'shell')
495
+ await copyFile(join(shellAssets, LAUNCHER_PS1), join(dir, LAUNCHER_PS1))
503
496
  for (const candidate of cscCandidates()) {
504
497
  if (!(await fileExists(candidate))) continue
505
498
  try {
@@ -574,3 +567,16 @@ async function addMenuKey(ctx: Ctx, key: string, iconPath: string, command: stri
574
567
  await runArgv(ctx, [reg, 'add', key, '/v', REG_VALUE_NAME_ICON, '/t', 'REG_SZ', '/d', iconPath, '/f'])
575
568
  await runArgv(ctx, [reg, 'add', key + '\\command', '/ve', '/t', 'REG_SZ', '/d', command, '/f'])
576
569
  }
570
+
571
+ /** @private @author ddj 2026年09月28号
572
+ * Preserve a manually configured public URL during launcher migration.
573
+ * @param dir Launcher directory.
574
+ * @param fallback Registered legacy address.
575
+ * @returns Existing base value, or the marker's address.
576
+ */
577
+ async function storedOpenBase(dir: string, fallback: string): Promise<string> {
578
+ try {
579
+ const text = await readFile(join(dir, INI_NAME), 'utf8')
580
+ return /^base\s*=\s*(.+)$/im.exec(text)?.[1]?.trim() || fallback
581
+ } catch { return fallback }
582
+ }