dsh-vscode-mode 0.12.0 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +129 -103
- package/assets/shell/dsh-open.cs +65 -149
- package/assets/shell/dsh-open.mjs +241 -0
- package/assets/shell/dsh-open.ps1 +28 -47
- package/assets/shell/dsh-open.sh +25 -66
- package/lib/client.js +12465 -11526
- package/lib/client.js.map +1 -1
- package/lib/index.js +3889 -2336
- package/lib/index.js.map +1 -1
- package/package.json +9 -8
- package/skills/dsh-vscodemode-mcp/SKILL.md +94 -132
- package/src/childEnv.ts +19 -0
- package/src/client/addToConversation.ts +4 -2
- package/src/client/ai/inlineProvider.ts +1 -8
- package/src/client/commandBridge.ts +6 -2
- package/src/client/externalOpen.ts +40 -19
- package/src/client/index.ts +10 -4
- package/src/client/keybindings.ts +18 -1
- package/src/client/monaco/loader.ts +3 -1
- package/src/client/openFlow.ts +36 -28
- package/src/client/openReceipt.ts +64 -0
- package/src/client/panelCommands.ts +101 -0
- package/src/client/pdf/pdfLoader.ts +4 -2
- package/src/client/receiptState.ts +120 -0
- package/src/client/rpc.ts +30 -4
- package/src/client/shortcutRecorder.ts +161 -0
- package/src/client/shortcutsOfficial.ts +13 -4
- package/src/client/sidebarBridge.ts +7 -0
- package/src/client/ui/DiffBadge.ts +2 -1
- package/src/client/ui/EditorView.ts +237 -55
- package/src/client/ui/McpSettings.ts +6 -3
- package/src/client/ui/OfficialSideTab.ts +5 -2
- package/src/client/ui/QuickOpen.ts +10 -9
- package/src/client/ui/ShortcutSettings.ts +34 -54
- package/src/client/ui/commandCatalog.ts +5 -4
- package/src/client/useOpenReceipt.ts +128 -0
- package/src/compat.ts +5 -3
- package/src/dap/manager.ts +46 -13
- package/src/devForm.ts +252 -92
- package/src/externalHandoff.ts +93 -52
- package/src/fileOpenSettings.ts +1 -24
- package/src/hostImport.ts +19 -0
- package/src/httpGuard.ts +101 -0
- package/src/index.ts +106 -97
- package/src/integrate.ts +56 -50
- package/src/lsp/transport.ts +84 -127
- package/src/mcp.ts +58 -9
- package/src/mcpIsolation.ts +211 -102
- package/src/mcpProject.ts +220 -133
- package/src/mcpRuntime.ts +343 -0
- package/src/openBridge.ts +69 -0
- package/src/openInbox.ts +207 -0
- package/src/paths.ts +8 -5
- package/src/processTree.ts +63 -0
- package/src/reveal.ts +5 -3
- package/src/routes.ts +156 -154
- package/src/rpc.ts +14 -13
- package/src/shared/appUrl.ts +19 -0
- package/src/shared/externalOpen.ts +71 -0
- package/src/shared/integration.ts +2 -0
- package/src/shared/keybindings.ts +8 -5
- package/src/shared/mcp.ts +4 -1
- package/src/shared/rpc.ts +7 -4
- package/src/shared/svn.ts +6 -8
- package/src/svnPlanTool.ts +51 -0
- package/src/unityBridge.ts +26 -3
- package/unity/com.dsh.editor/Editor/DshCodeEditor.cs +99 -203
- package/unity/com.dsh.editor/Editor/DshOpenWorker.cs +166 -0
- package/unity/com.dsh.editor/Editor/DshOpenWorker.cs.meta +2 -0
- package/unity/com.dsh.editor/README.md +16 -25
- package/unity/com.dsh.editor/package.json +2 -2
package/src/httpGuard.ts
ADDED
|
@@ -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 =
|
|
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年
|
|
59
|
-
* @param ctx
|
|
60
|
-
* @param config
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
84
|
-
|
|
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
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
-
|
|
126
|
-
|
|
127
|
-
ctx.
|
|
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
|
-
|
|
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年
|
|
139
|
-
* @param ctx
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
8
|
-
*
|
|
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 {
|
|
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
|
|
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="
|
|
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
|
|
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
|
-
*
|
|
458
|
-
*
|
|
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
|
-
|
|
465
|
-
|
|
466
|
-
|
|
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
|
-
/**
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
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
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
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
|
+
}
|