dsh-vscode-mode 0.1.15 → 0.1.17

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/lib/index.js CHANGED
@@ -2,10 +2,477 @@ import { createRequire } from "node:module";
2
2
  import { readFile } from "node:fs/promises";
3
3
  import { dirname, extname, join, normalize, resolve, sep } from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
- import { existsSync } from "node:fs";
5
+ import { existsSync, readFileSync, readdirSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
6
+ import { homedir } from "node:os";
6
7
  import { createHash } from "node:crypto";
7
- import { installSettingsSection, settingsNamespace } from "@deepseek-ai/dsh-settings";
8
- import z from "schemastery";
8
+ //#region src/mcp.ts
9
+ /**
10
+ * dsh-vscode-mode host — MCP 运行时管理适配层。
11
+ * 只操作已装配的 @deepseek-ai/dsh-mcp-client loader entry,不在浏览器侧连接 MCP。
12
+ * 身份常量(包名/条目前缀/新旧格式判定)统一由 compat 层持有,本模块再导出保持既有 import 面。
13
+ * 作者 ddj 2026年08月22号
14
+ */
15
+ /** 列出当前 loader 中的 MCP entry。 */
16
+ function entriesOf(ctx) {
17
+ const loader = ctx.get("loader");
18
+ if (!loader || typeof loader.entries !== "function") return [];
19
+ return [...loader.entries()].filter((entry) => entry?.options?.name === MCP_PACKAGE);
20
+ }
21
+ /** 脱敏配置并保留可编辑字段的存在性。 */
22
+ function publicConfig(config) {
23
+ const out = { ...config };
24
+ if (out.env && typeof out.env === "object") out.env = Object.fromEntries(Object.keys(out.env).map((key) => [key, "••••••"]));
25
+ if (out.headers && typeof out.headers === "object") out.headers = Object.fromEntries(Object.keys(out.headers).map((key) => [key, "••••••"]));
26
+ return out;
27
+ }
28
+ /** 从工具注册表读取服务器命名空间下的工具摘要。 */
29
+ function toolsOf(ctx, serverName) {
30
+ const visible = (ctx.get("tools")?.view?.(void 0))?.visible;
31
+ if (!(visible instanceof Map)) return [];
32
+ const prefix = `mcp__${serverName}__`;
33
+ return [...visible.entries()].filter(([name]) => String(name).startsWith(prefix)).map(([name, definition]) => ({
34
+ name: String(name).slice(prefix.length),
35
+ description: typeof definition?.description === "string" ? definition.description : void 0
36
+ }));
37
+ }
38
+ /** 将 loader entry 映射为设置页摘要。 */
39
+ function serverOf(ctx, entry) {
40
+ const config = entry.options?.config ?? {};
41
+ const serverName = typeof config.serverName === "string" ? config.serverName : entry.options?.id ?? "unknown";
42
+ const tools = toolsOf(ctx, serverName);
43
+ const state = entry.fiber?.state;
44
+ return {
45
+ id: String(entry.id ?? entry.options?.id ?? serverName),
46
+ serverName,
47
+ enabled: entry.disabled !== true,
48
+ transport: config.transport === "streamable-http" ? "streamable-http" : "stdio",
49
+ config: publicConfig(config),
50
+ status: state === 2 ? "connected" : state === 1 ? "connecting" : "error",
51
+ toolCount: tools.length,
52
+ tools,
53
+ error: state === 3 ? "MCP 插件未正常运行" : void 0
54
+ };
55
+ }
56
+ /** 校验 MCP 配置。 */
57
+ function validateConfig(config) {
58
+ if (!/^[A-Za-z0-9_-]{1,32}$/.test(config.serverName)) throw new Error("serverName 只能包含字母、数字、下划线和连字符(最多 32 位)");
59
+ if (config.transport === "stdio" && !config.command?.trim()) throw new Error("stdio MCP 必须填写 command");
60
+ if (config.transport === "streamable-http" && !/^https?:\/\//.test(config.url ?? "")) throw new Error("HTTP MCP 必须填写 http(s) URL");
61
+ }
62
+ /** 列出全局(profile)MCP 服务,过滤掉新旧格式的项目级条目。 */
63
+ function listMcp(ctx) {
64
+ return { servers: entriesOf(ctx).filter((entry) => !isProjectEntryId(String(entry.id ?? entry.options?.id ?? ""))).map((entry) => serverOf(ctx, entry)) };
65
+ }
66
+ /** 保存 MCP 服务配置并触发 loader 热更新。 */
67
+ async function saveMcp(ctx, config) {
68
+ validateConfig(config);
69
+ const loader = ctx.get("loader");
70
+ if (!loader) throw new Error("缺少 loader 服务");
71
+ const existing = entriesOf(ctx).find((entry) => !isProjectEntryId(String(entry.id ?? entry.options?.id ?? "")) && entry.options?.config?.serverName === config.serverName);
72
+ if (existing) {
73
+ await loader.update(existing.id, { config: { ...config } });
74
+ return serverOf(ctx, loader.resolve(existing.id));
75
+ }
76
+ const id = await loader.create({
77
+ name: MCP_PACKAGE,
78
+ config: { ...config }
79
+ });
80
+ return serverOf(ctx, loader.resolve(id));
81
+ }
82
+ /** 删除 MCP 服务。 */
83
+ async function removeMcp(ctx, id) {
84
+ const loader = ctx.get("loader");
85
+ if (!loader) throw new Error("缺少 loader 服务");
86
+ const entry = entriesOf(ctx).find((candidate) => String(candidate.id) === id);
87
+ if (!entry) throw new Error("MCP 服务不存在");
88
+ await loader.remove(entry.id);
89
+ }
90
+ /** 切换 MCP 服务启用状态。 */
91
+ async function toggleMcp(ctx, id, enabled) {
92
+ const loader = ctx.get("loader");
93
+ if (!loader) throw new Error("缺少 loader 服务");
94
+ const entry = entriesOf(ctx).find((candidate) => String(candidate.id) === id);
95
+ if (!entry) throw new Error("MCP 服务不存在");
96
+ await loader.update(entry.id, { disabled: !enabled });
97
+ return serverOf(ctx, loader.resolve(entry.id));
98
+ }
99
+ /** 重新加载 MCP 服务。 */
100
+ async function refreshMcp(ctx, id) {
101
+ const loader = ctx.get("loader");
102
+ if (!loader) throw new Error("缺少 loader 服务");
103
+ const entry = entriesOf(ctx).find((candidate) => String(candidate.id) === id);
104
+ if (!entry) throw new Error("MCP 服务不存在");
105
+ await loader.update(entry.id, { config: { ...entry.options?.config ?? {} } });
106
+ return serverOf(ctx, loader.resolve(entry.id));
107
+ }
108
+ //#endregion
109
+ //#region src/fileOpenSettings.ts
110
+ const FILE_OPEN_SETTINGS_NS = "dsh-vscode-mode";
111
+ const FILE_OPEN_DEFAULT = "auto";
112
+ let depsPromise;
113
+ /**
114
+ * 动态加载设置依赖(模块级缓存;任一缺失返回 null 而非抛错)。
115
+ * @author ddj 2026年08月24号
116
+ * @returns 设置依赖或 null
117
+ */
118
+ function loadSettingsDeps() {
119
+ if (!depsPromise) depsPromise = Promise.all([import("@deepseek-ai/dsh-settings"), import("schemastery")]).then(([dshSettings, schemastery]) => ({
120
+ installSettingsSection: dshSettings.installSettingsSection,
121
+ z: schemastery.default
122
+ })).catch(() => null);
123
+ return depsPromise;
124
+ }
125
+ function normalizeValue(value) {
126
+ if (typeof value !== "string" || value.trim() === "") return FILE_OPEN_DEFAULT;
127
+ return value.trim();
128
+ }
129
+ function configValue(config) {
130
+ return normalizeValue(config?.fileOpenTool);
131
+ }
132
+ /**
133
+ * 安装设置 section(动态依赖;缺失返回 false 不抛错)。
134
+ * @author ddj 2026年08月24号
135
+ * @param ctx DSH host 上下文
136
+ * @param ns 设置命名空间
137
+ * @param entry 初始设置值
138
+ * @param hooks 设置源绑定与变更回调
139
+ * @param loader 依赖装载器(测试注入)
140
+ * @returns 是否成功安装
141
+ */
142
+ async function installOpenSettingsSection(ctx, ns, entry, hooks, loader = loadSettingsDeps) {
143
+ const deps = await loader();
144
+ if (!deps) return false;
145
+ const schema = deps.z.object({ fileOpenTool: deps.z.string().default(FILE_OPEN_DEFAULT) });
146
+ deps.installSettingsSection(ctx, ns, schema, entry, hooks);
147
+ return true;
148
+ }
149
+ /**
150
+ * 注册设置命名空间,并提供 host 侧的读取与更新状态。
151
+ * @author ddj 2026年08月24号
152
+ * @param ctx DSH host 上下文
153
+ * @param config 插件组合配置
154
+ * @param onChange 设置变化回调
155
+ * @returns 设置状态
156
+ */
157
+ function setupOpenSettings(ctx, config, onChange) {
158
+ let current = configValue(config);
159
+ let revision;
160
+ let provider;
161
+ const notify = (value) => {
162
+ current = normalizeValue(value);
163
+ onChange(current);
164
+ };
165
+ const syncRevision = () => {
166
+ revision = (provider?.describe?.({ redactSecrets: true })?.find((item) => item.ns === FILE_OPEN_SETTINGS_NS))?.revision;
167
+ };
168
+ const setSource = (source) => notify(source().fileOpenTool);
169
+ const settingsChange = () => syncRevision();
170
+ installOpenSettingsSection(ctx, FILE_OPEN_SETTINGS_NS, { fileOpenTool: current }, {
171
+ setSource,
172
+ onChange: settingsChange
173
+ });
174
+ ctx.inject?.(["settings"], (settingsCtx) => {
175
+ provider = settingsCtx.get("settings");
176
+ syncRevision();
177
+ });
178
+ return {
179
+ get value() {
180
+ return current;
181
+ },
182
+ get revision() {
183
+ return revision;
184
+ },
185
+ update: async (value, expectedRevision) => {
186
+ const next = normalizeValue(value);
187
+ if (!provider?.update) {
188
+ notify(next);
189
+ return;
190
+ }
191
+ await provider.update(FILE_OPEN_SETTINGS_NS, { fileOpenTool: next }, expectedRevision);
192
+ const stored = (provider.describe?.({ redactSecrets: true })?.find((item) => item.ns === FILE_OPEN_SETTINGS_NS))?.value;
193
+ notify(stored?.fileOpenTool ?? next);
194
+ syncRevision();
195
+ }
196
+ };
197
+ }
198
+ function normalizeFileOpenTool(value) {
199
+ return normalizeValue(value);
200
+ }
201
+ //#endregion
202
+ //#region src/devForm.ts
203
+ /**
204
+ * dsh-vscode-mode host — 开发形态管理(安装形态切换)。
205
+ * 开发形态 = profile 中以 link: 依赖 + junction 指向工作区的安装(用于插件开发);
206
+ * 关闭 = 改回版本依赖 + 删 junction + pnpm install 装配正式版。
207
+ * 切换后需重启 DSH 生效(运行中的代码仍是旧形态)。
208
+ * 与 compat.ts 存在模块环(devForm 用 pluginVersionOf、compat 用 readDevForm),
209
+ * 双方仅在函数体内交叉使用,模块求值期无依赖,ESM 安全。
210
+ * 作者 ddj 2026-08-24
211
+ */
212
+ /** 扫描 $DSH_HOME/profiles 找到依赖本插件的 profile 目录(无 env 依赖,多 profile 安全)。 */
213
+ function findProfileDir() {
214
+ const home = process.env.DSH_HOME ?? join(homedir(), ".dsh");
215
+ try {
216
+ for (const name of readdirSync(join(home, "profiles"))) {
217
+ const pkgFile = join(home, "profiles", name, "package.json");
218
+ if (!existsSync(pkgFile)) continue;
219
+ try {
220
+ if (JSON.parse(readFileSync(pkgFile, "utf8"))?.dependencies?.["dsh-vscode-mode"]) return dirname(pkgFile);
221
+ } catch {}
222
+ }
223
+ } catch {}
224
+ }
225
+ /**
226
+ * 读取当前开发形态(manifest link: 判定)。
227
+ * @author ddj 2026年08月24号
228
+ * @returns 开发形态状态
229
+ */
230
+ function readDevForm() {
231
+ const profileDir = findProfileDir();
232
+ if (!profileDir) return { enabled: false };
233
+ try {
234
+ const text = readFileSync(join(profileDir, "package.json"), "utf8").replace(/^\uFEFF/, "");
235
+ const spec = JSON.parse(text)?.dependencies?.[PLUGIN_NAME];
236
+ if (typeof spec === "string" && spec.startsWith("link:")) return {
237
+ enabled: true,
238
+ path: spec.slice(5)
239
+ };
240
+ } catch {}
241
+ return { enabled: false };
242
+ }
243
+ /**
244
+ * 规划切换后的 manifest 文本(纯函数,可单测;非法 JSON 抛错)。
245
+ * @author ddj 2026年08月24号
246
+ * @param text 原 manifest 文本
247
+ * @param enabled 目标形态(true=link:,false=版本)
248
+ * @param path 工作区路径(开启时必填)
249
+ * @param version 版本号(关闭时使用,写入 ^<version>)
250
+ * @returns 新 manifest 文本(无 BOM,2 空格缩进)
251
+ */
252
+ function planManifest(text, enabled, path, version) {
253
+ const data = JSON.parse(text.replace(/^\uFEFF/, ""));
254
+ if (!data.dependencies || typeof data.dependencies !== "object") throw new Error("profile package.json 缺少 dependencies 字段");
255
+ data.dependencies[PLUGIN_NAME] = enabled ? "link:" + path.replace(/\\/g, "/") : "^" + version;
256
+ return JSON.stringify(data, null, 2) + "\n";
257
+ }
258
+ /** 建立或移除 node_modules 下的开发 junction(目标不存在时开启失败)。 */
259
+ function applyJunction(profileDir, enabled, target) {
260
+ const linkDir = join(profileDir, "node_modules", PLUGIN_NAME);
261
+ if (existsSync(linkDir)) rmSync(linkDir, {
262
+ recursive: true,
263
+ force: true
264
+ });
265
+ if (!enabled) return;
266
+ if (!existsSync(target)) throw new Error("工作区路径不存在:" + target);
267
+ symlinkSync(target, linkDir, "junction");
268
+ }
269
+ /**
270
+ * 切换开发形态:改 manifest → 建/删 junction → pnpm install(subprocess)。
271
+ * @author ddj 2026年08月24号
272
+ * @param ctx DSH host 上下文(subprocess 服务)
273
+ * @param enabled 目标形态
274
+ * @param path 工作区绝对路径(开启时必填)
275
+ * @returns 切换结果(restart=true 表示需重启 DSH 生效)
276
+ */
277
+ async function setDevForm(ctx, enabled, path) {
278
+ const profileDir = findProfileDir();
279
+ if (!profileDir) return {
280
+ ok: false,
281
+ error: "未找到依赖本插件的 profile(检查 DSH_HOME/profiles)",
282
+ restart: false
283
+ };
284
+ const current = readDevForm();
285
+ const target = (path ?? current.path ?? "").replace(/\\/g, "/");
286
+ if (enabled && !target) return {
287
+ ok: false,
288
+ error: "开启开发形态需要提供工作区路径(path 参数)",
289
+ restart: false
290
+ };
291
+ if (enabled === current.enabled && (!enabled || target === current.path)) return {
292
+ ok: true,
293
+ restart: false
294
+ };
295
+ try {
296
+ const pkgFile = join(profileDir, "package.json");
297
+ const version = pluginVersionOf();
298
+ if (!version) return {
299
+ ok: false,
300
+ error: "读取插件版本失败,无法写入版本依赖",
301
+ restart: false
302
+ };
303
+ const next = planManifest(readFileSync(pkgFile, "utf8").replace(/^\uFEFF/, ""), enabled, target, version);
304
+ writeFileSync(pkgFile, next, "utf8");
305
+ applyJunction(profileDir, enabled, target);
306
+ const installError = await runPnpmInstall(ctx, profileDir);
307
+ return {
308
+ ok: !installError,
309
+ error: installError,
310
+ restart: true
311
+ };
312
+ } catch (error) {
313
+ return {
314
+ ok: false,
315
+ error: String(error),
316
+ restart: false
317
+ };
318
+ }
319
+ }
320
+ /** 在 profile 目录执行 pnpm install(subprocess 契约:spawn({argv,cwd,stdio,graceMs}))。 */
321
+ async function runPnpmInstall(ctx, profileDir) {
322
+ const sub = ctx.get("subprocess");
323
+ if (!sub || typeof sub.spawn !== "function") return "manifest/junction 已更新,但缺少 subprocess:请手动在 profile 目录运行 pnpm install";
324
+ const handle = sub.spawn({
325
+ argv: ["pnpm", "install"],
326
+ cwd: profileDir,
327
+ stdio: {
328
+ stdout: { maxBytes: 65536 },
329
+ stderr: { maxBytes: 65536 },
330
+ stdin: "ignore"
331
+ },
332
+ graceMs: 12e4
333
+ });
334
+ const outcome = await handle.done;
335
+ const code = outcome?.exitCode ?? outcome?.code;
336
+ if (code !== 0) {
337
+ const stderr = handle.collected?.stderr?.readFrom(0).text ?? "";
338
+ return "pnpm install 失败(exit " + code + "):" + stderr.slice(0, 500);
339
+ }
340
+ }
341
+ //#endregion
342
+ //#region src/compat.ts
343
+ /**
344
+ * dsh-vscode-mode host — 兼容层:与其他插件 / DSH 版本的统一探测、适配、护栏与自诊断。
345
+ * 职责:身份常量(包名 / MCP 条目前缀 / 路由前缀)、外部插件探测、路由冲突与重复装配检测、
346
+ * 兼容性报告构建(RPC edrv.compat 与启动日志共用)。
347
+ * 注意:与 mcp.ts 存在模块环(compat 提供身份常量、mcp 提供 entriesOf),
348
+ * 双方仅在函数体内交叉使用导出,模块求值期无依赖,ESM 安全。
349
+ * 作者 ddj 2026-08-24
350
+ */
351
+ const PLUGIN_NAME = "dsh-vscode-mode";
352
+ const MCP_PACKAGE = "@deepseek-ai/dsh-mcp-client";
353
+ /** 项目级 MCP 的 loader entry id 固定前缀(区分 profile 全局 MCP;loader id 不能用 ':',故用 '.')。 */
354
+ const PROJECT_ENTRY_PREFIX = "vsm-mcp.";
355
+ /** webServer 路由前缀(RPC 精确路由 + 静态资源前缀共用)。 */
356
+ const ROUTE_PREFIX = "/edrv";
357
+ /** 判断新旧格式项目 MCP 条目 id(旧残留不被视为全局 MCP)。 */
358
+ function isProjectEntryId(id) {
359
+ return id.startsWith("vsm-mcp.") || id.startsWith("vsm-mcp:");
360
+ }
361
+ /** 从项目条目 id 解析 workspace hash(新旧格式通用)。 */
362
+ function entryHash(id) {
363
+ if (id.startsWith("vsm-mcp.")) return id.slice(8).split(".")[0];
364
+ if (id.startsWith("vsm-mcp:")) return id.slice(8).split(":")[0];
365
+ }
366
+ /** 读取随包 package.json 版本号(缺失/解析失败降级为空串)。 */
367
+ function pluginVersionOf() {
368
+ try {
369
+ const file = join(dirname(fileURLToPath(import.meta.url)), "..", "package.json");
370
+ const pkg = JSON.parse(readFileSync(file, "utf8"));
371
+ return typeof pkg.version === "string" ? pkg.version : "";
372
+ } catch {
373
+ return "";
374
+ }
375
+ }
376
+ /** 探测外部插件/服务适配状态(depsAvailable 由调用方注入,便于测试)。 */
377
+ function detectExternal(ctx, depsAvailable) {
378
+ const mcpCount = entriesOf(ctx).length;
379
+ const settings = ctx.get("settings");
380
+ const hasSettings = Boolean(settings?.describe || settings?.update);
381
+ return [
382
+ {
383
+ name: MCP_PACKAGE,
384
+ active: mcpCount > 0,
385
+ note: mcpCount > 0 ? mcpCount + " 个 MCP 服务条目" : "未检测到 MCP 条目(MCP 管理页显示为空)"
386
+ },
387
+ {
388
+ name: "设置持久化(@deepseek-ai/dsh-settings)",
389
+ active: depsAvailable,
390
+ note: depsAvailable ? "设置 section 已安装" : "未安装:fileOpenTool 持久化降级为配置值"
391
+ },
392
+ {
393
+ name: "settings 服务",
394
+ active: hasSettings,
395
+ note: hasSettings ? "可读写设置" : "不可用(设置读写走配置回退)"
396
+ }
397
+ ];
398
+ }
399
+ /** 本插件已注册的路由(kind → path 集合),冲突检测时排除自身,避免自误报。 */
400
+ const ownRoutes = {
401
+ exact: /* @__PURE__ */ new Set(),
402
+ prefixes: /* @__PURE__ */ new Set()
403
+ };
404
+ /** 清空本插件路由记录(registerRoutes 装配开始时调用,热重载不残留)。 */
405
+ function resetOwnRoutes() {
406
+ ownRoutes.exact.clear();
407
+ ownRoutes.prefixes.clear();
408
+ }
409
+ /** 记录本插件路由(routes.ts 注册成功时调用)。 */
410
+ function noteOwnRoute(kind, path) {
411
+ (kind === "exact" ? ownRoutes.exact : ownRoutes.prefixes).add(path);
412
+ }
413
+ /** 检测路由前缀占用与重叠(排除本插件自有路由;内部表不可读视为无冲突)。 */
414
+ function routeConflict(web, prefix) {
415
+ try {
416
+ const w = web;
417
+ const exact = w?.exact;
418
+ const prefixes = w?.prefixes;
419
+ if (exact instanceof Map) for (const path of exact.keys()) {
420
+ const other = String(path);
421
+ if (other.startsWith(prefix + "/") && !ownRoutes.exact.has(other)) return "精确路由 " + other + " 与 " + prefix + " 前缀重叠(可能来自热重载残留或其他插件)";
422
+ }
423
+ if (prefixes instanceof Map) for (const path of prefixes.keys()) {
424
+ const other = String(path);
425
+ if (other !== prefix && (other.startsWith(prefix + "/") || prefix.startsWith(other + "/")) && !ownRoutes.prefixes.has(other)) return "路由前缀 " + prefix + " 与 " + other + " 重叠";
426
+ }
427
+ } catch {}
428
+ return null;
429
+ }
430
+ /** 检测本插件是否被重复装配(bundle 层 + 用户层重复 insert 的自诊断)。 */
431
+ function duplicateEntries(ctx) {
432
+ const loader = ctx.get("loader");
433
+ if (!loader || typeof loader.entries !== "function") return [];
434
+ const own = [...loader.entries()].filter((entry) => String(entry.options?.name ?? entry.id ?? "") === PLUGIN_NAME);
435
+ if (own.length > 1) return ["检测到 " + own.length + " 个 dsh-vscode-mode loader 条目(重复装配)。请删除 profile 用户层 cordis.patch.yml 的重复 insert(或运行 dev_fix_patch 清理)"];
436
+ return [];
437
+ }
438
+ /** 护栏适配状态:路由前缀唯一性 + 插件装配唯一性。 */
439
+ function detectGuards(ctx) {
440
+ const web = ctx.get("webServer");
441
+ const conflict = web ? routeConflict(web, ROUTE_PREFIX) : null;
442
+ const duplicates = duplicateEntries(ctx);
443
+ return [{
444
+ name: "路由前缀 /edrv",
445
+ active: !conflict,
446
+ note: conflict ?? "无冲突"
447
+ }, {
448
+ name: "插件装配唯一性",
449
+ active: duplicates.length === 0,
450
+ note: duplicates[0] ?? "唯一装配"
451
+ }];
452
+ }
453
+ /**
454
+ * 构建完整兼容性报告(RPC 与启动日志共用)。
455
+ * @author ddj 2026年08月24号
456
+ * @param ctx DSH host 上下文
457
+ * @param options 测试注入:depsAvailable 跳过动态导入、version 固定版本号
458
+ * @returns 兼容性报告
459
+ */
460
+ async function buildReport(ctx, options) {
461
+ const deps = options?.depsAvailable ?? await loadSettingsDeps() !== null;
462
+ const external = detectExternal(ctx, deps);
463
+ const guards = detectGuards(ctx);
464
+ const warnings = [];
465
+ if (!deps) warnings.push("未安装 @deepseek-ai/dsh-settings:文件打开工具设置持久化不可用(降级为配置值)");
466
+ for (const guard of guards) if (!guard.active && guard.note) warnings.push(guard.note);
467
+ return {
468
+ pluginVersion: options?.version ?? pluginVersionOf(),
469
+ external,
470
+ guards,
471
+ warnings,
472
+ devForm: readDevForm()
473
+ };
474
+ }
475
+ //#endregion
9
476
  //#region src/shared/rpc.ts
10
477
  /** webServer 精确路由。 */
11
478
  const RPC_PATH = "/edrv/rpc";
@@ -50,17 +517,32 @@ function imageDirOf(config) {
50
517
  /** Monaco AMD 静态目录(随包发布)。 */
51
518
  const vendorDir = join(dirname(fileURLToPath(import.meta.url)), "..", "assets", "vendor");
52
519
  /**
53
- * 注册全部 webServer 路由。
520
+ * 注册全部 webServer 路由(带兼容护栏:前缀冲突预警 + 注册失败降级跳过)。
54
521
  * @author ddj 2026年08月20号
55
522
  * @param ctx DSH 上下文
56
523
  * @param config 插件配置(可选 imageDir 等)
57
524
  * @param handleRpc RPC 分发入口(method,args → RpcResult)
525
+ * @param onWarning 兼容性警告收集(可选)
58
526
  */
59
- function registerRoutes(ctx, config, handleRpc) {
527
+ function registerRoutes(ctx, config, handleRpc, onWarning) {
60
528
  const imgDir = imageDirOf(config);
61
529
  const web = ctx.get("webServer");
62
530
  if (!web) return;
63
- ctx.effect(() => web.register({
531
+ resetOwnRoutes();
532
+ const conflict = routeConflict(web, ROUTE_PREFIX);
533
+ if (conflict) onWarning?.("兼容性:" + conflict);
534
+ /** 护栏注册:重复路由抛错时降级为警告并跳过,不让单条路由拖垮装配。 */
535
+ const guarded = (route, label) => {
536
+ try {
537
+ const dispose = web.register(route);
538
+ noteOwnRoute(route.kind, route.path);
539
+ return dispose;
540
+ } catch (error) {
541
+ onWarning?.("兼容性:" + label + " 注册失败(" + String(error) + ")");
542
+ return;
543
+ }
544
+ };
545
+ ctx.effect(() => guarded({
64
546
  kind: "exact",
65
547
  path: RPC_PATH,
66
548
  handler: async (req, res) => {
@@ -85,8 +567,8 @@ function registerRoutes(ctx, config, handleRpc) {
85
567
  }));
86
568
  }
87
569
  }
88
- }), "edrv: /edrv/rpc route");
89
- for (const r of IMG_ROUTES) ctx.effect(() => web.register({
570
+ }, "/edrv/rpc 精确路由"), "edrv: routes");
571
+ for (const r of IMG_ROUTES) ctx.effect(() => guarded({
90
572
  kind: "exact",
91
573
  path: r.url,
92
574
  handler: async (req, res) => {
@@ -101,8 +583,8 @@ function registerRoutes(ctx, config, handleRpc) {
101
583
  res.end();
102
584
  }
103
585
  }
104
- }), "edrv: static " + r.file);
105
- ctx.effect(() => web.register({
586
+ }, "静态资源 " + r.file), "edrv: routes");
587
+ ctx.effect(() => guarded({
106
588
  kind: "prefix",
107
589
  path: VENDOR_PREFIX,
108
590
  handler: async (req, res) => {
@@ -135,7 +617,7 @@ function registerRoutes(ctx, config, handleRpc) {
135
617
  res.end();
136
618
  }
137
619
  }
138
- }), "edrv: /edrv/vendor prefix");
620
+ }, "/edrv/vendor 前缀路由"), "edrv: routes");
139
621
  }
140
622
  //#endregion
141
623
  //#region src/shared/diff.ts
@@ -1646,109 +2128,6 @@ async function restoreFile(ctx, session, rec) {
1646
2128
  }
1647
2129
  }
1648
2130
  //#endregion
1649
- //#region src/mcp.ts
1650
- const MCP_PACKAGE = "@deepseek-ai/dsh-mcp-client";
1651
- /** 项目级 MCP 的 loader entry id 固定前缀(区分 profile 全局 MCP;注意 loader id 不能用 ':' 分隔,故用 '.')。 */
1652
- const PROJECT_ENTRY_PREFIX = "vsm-mcp.";
1653
- const LEGACY_PROJECT_PREFIX = "vsm-mcp:";
1654
- /** 判断新旧格式项目 entry id,避免旧残留被显示为全局 MCP。 */
1655
- function isProjectEntryId(id) {
1656
- return id.startsWith("vsm-mcp.") || id.startsWith(LEGACY_PROJECT_PREFIX);
1657
- }
1658
- /** 列出当前 loader 中的 MCP entry。 */
1659
- function entriesOf(ctx) {
1660
- const loader = ctx.get("loader");
1661
- if (!loader || typeof loader.entries !== "function") return [];
1662
- return [...loader.entries()].filter((entry) => entry?.options?.name === MCP_PACKAGE);
1663
- }
1664
- /** 脱敏配置并保留可编辑字段的存在性。 */
1665
- function publicConfig(config) {
1666
- const out = { ...config };
1667
- if (out.env && typeof out.env === "object") out.env = Object.fromEntries(Object.keys(out.env).map((key) => [key, "••••••"]));
1668
- if (out.headers && typeof out.headers === "object") out.headers = Object.fromEntries(Object.keys(out.headers).map((key) => [key, "••••••"]));
1669
- return out;
1670
- }
1671
- /** 从工具注册表读取服务器命名空间下的工具摘要。 */
1672
- function toolsOf(ctx, serverName) {
1673
- const visible = (ctx.get("tools")?.view?.(void 0))?.visible;
1674
- if (!(visible instanceof Map)) return [];
1675
- const prefix = `mcp__${serverName}__`;
1676
- return [...visible.entries()].filter(([name]) => String(name).startsWith(prefix)).map(([name, definition]) => ({
1677
- name: String(name).slice(prefix.length),
1678
- description: typeof definition?.description === "string" ? definition.description : void 0
1679
- }));
1680
- }
1681
- /** 将 loader entry 映射为设置页摘要。 */
1682
- function serverOf(ctx, entry) {
1683
- const config = entry.options?.config ?? {};
1684
- const serverName = typeof config.serverName === "string" ? config.serverName : entry.options?.id ?? "unknown";
1685
- const tools = toolsOf(ctx, serverName);
1686
- const state = entry.fiber?.state;
1687
- return {
1688
- id: String(entry.id ?? entry.options?.id ?? serverName),
1689
- serverName,
1690
- enabled: entry.disabled !== true,
1691
- transport: config.transport === "streamable-http" ? "streamable-http" : "stdio",
1692
- config: publicConfig(config),
1693
- status: state === 2 ? "connected" : state === 1 ? "connecting" : "error",
1694
- toolCount: tools.length,
1695
- tools,
1696
- error: state === 3 ? "MCP 插件未正常运行" : void 0
1697
- };
1698
- }
1699
- /** 校验 MCP 配置。 */
1700
- function validateConfig(config) {
1701
- if (!/^[A-Za-z0-9_-]{1,32}$/.test(config.serverName)) throw new Error("serverName 只能包含字母、数字、下划线和连字符(最多 32 位)");
1702
- if (config.transport === "stdio" && !config.command?.trim()) throw new Error("stdio MCP 必须填写 command");
1703
- if (config.transport === "streamable-http" && !/^https?:\/\//.test(config.url ?? "")) throw new Error("HTTP MCP 必须填写 http(s) URL");
1704
- }
1705
- /** 列出全局(profile)MCP 服务,过滤掉新旧格式的项目级条目。 */
1706
- function listMcp(ctx) {
1707
- return { servers: entriesOf(ctx).filter((entry) => !isProjectEntryId(String(entry.id ?? entry.options?.id ?? ""))).map((entry) => serverOf(ctx, entry)) };
1708
- }
1709
- /** 保存 MCP 服务配置并触发 loader 热更新。 */
1710
- async function saveMcp(ctx, config) {
1711
- validateConfig(config);
1712
- const loader = ctx.get("loader");
1713
- if (!loader) throw new Error("缺少 loader 服务");
1714
- const existing = entriesOf(ctx).find((entry) => !isProjectEntryId(String(entry.id ?? entry.options?.id ?? "")) && entry.options?.config?.serverName === config.serverName);
1715
- if (existing) {
1716
- await loader.update(existing.id, { config: { ...config } });
1717
- return serverOf(ctx, loader.resolve(existing.id));
1718
- }
1719
- const id = await loader.create({
1720
- name: MCP_PACKAGE,
1721
- config: { ...config }
1722
- });
1723
- return serverOf(ctx, loader.resolve(id));
1724
- }
1725
- /** 删除 MCP 服务。 */
1726
- async function removeMcp(ctx, id) {
1727
- const loader = ctx.get("loader");
1728
- if (!loader) throw new Error("缺少 loader 服务");
1729
- const entry = entriesOf(ctx).find((candidate) => String(candidate.id) === id);
1730
- if (!entry) throw new Error("MCP 服务不存在");
1731
- await loader.remove(entry.id);
1732
- }
1733
- /** 切换 MCP 服务启用状态。 */
1734
- async function toggleMcp(ctx, id, enabled) {
1735
- const loader = ctx.get("loader");
1736
- if (!loader) throw new Error("缺少 loader 服务");
1737
- const entry = entriesOf(ctx).find((candidate) => String(candidate.id) === id);
1738
- if (!entry) throw new Error("MCP 服务不存在");
1739
- await loader.update(entry.id, { disabled: !enabled });
1740
- return serverOf(ctx, loader.resolve(entry.id));
1741
- }
1742
- /** 重新加载 MCP 服务。 */
1743
- async function refreshMcp(ctx, id) {
1744
- const loader = ctx.get("loader");
1745
- if (!loader) throw new Error("缺少 loader 服务");
1746
- const entry = entriesOf(ctx).find((candidate) => String(candidate.id) === id);
1747
- if (!entry) throw new Error("MCP 服务不存在");
1748
- await loader.update(entry.id, { config: { ...entry.options?.config ?? {} } });
1749
- return serverOf(ctx, loader.resolve(entry.id));
1750
- }
1751
- //#endregion
1752
2131
  //#region src/mcpProject.ts
1753
2132
  /**
1754
2133
  * dsh-vscode-mode host — 项目级 MCP 管理。
@@ -2023,74 +2402,6 @@ async function projectRefresh(ctx, workspacePath, serverName) {
2023
2402
  return projectOf(ctx, workspacePath, workspace.title ?? "");
2024
2403
  }
2025
2404
  //#endregion
2026
- //#region src/fileOpenSettings.ts
2027
- /**
2028
- * dsh-vscode-mode host — 文件链接打开工具的持久化设置。
2029
- * @author ddj 2026年08月24号
2030
- */
2031
- const FILE_OPEN_SETTINGS_NS = settingsNamespace("dsh-vscode-mode");
2032
- const FILE_OPEN_DEFAULT = "auto";
2033
- const FILE_OPEN_SCHEMA = z.object({ fileOpenTool: z.string().default(FILE_OPEN_DEFAULT) });
2034
- function normalizeValue(value) {
2035
- if (typeof value !== "string" || value.trim() === "") return FILE_OPEN_DEFAULT;
2036
- return value.trim();
2037
- }
2038
- function configValue(config) {
2039
- return normalizeValue(config?.fileOpenTool);
2040
- }
2041
- /**
2042
- * 注册设置命名空间,并提供 host 侧的读取与更新状态。
2043
- * @author ddj 2026年08月24号
2044
- * @param ctx DSH host 上下文
2045
- * @param config 插件组合配置
2046
- * @param onChange 设置变化回调
2047
- * @returns 设置状态
2048
- */
2049
- function setupOpenSettings(ctx, config, onChange) {
2050
- let current = configValue(config);
2051
- let revision;
2052
- let provider;
2053
- const notify = (value) => {
2054
- current = normalizeValue(value);
2055
- onChange(current);
2056
- };
2057
- const syncRevision = () => {
2058
- revision = (provider?.describe?.({ redactSecrets: true })?.find((item) => item.ns === FILE_OPEN_SETTINGS_NS))?.revision;
2059
- };
2060
- const setSource = (source) => notify(source().fileOpenTool);
2061
- const settingsChange = () => syncRevision();
2062
- installSettingsSection(ctx, FILE_OPEN_SETTINGS_NS, FILE_OPEN_SCHEMA, { fileOpenTool: current }, {
2063
- setSource,
2064
- onChange: settingsChange
2065
- });
2066
- ctx.inject?.(["settings"], (settingsCtx) => {
2067
- provider = settingsCtx.get("settings");
2068
- syncRevision();
2069
- });
2070
- return {
2071
- get value() {
2072
- return current;
2073
- },
2074
- get revision() {
2075
- return revision;
2076
- },
2077
- update: async (value, expectedRevision) => {
2078
- const next = normalizeValue(value);
2079
- if (!provider?.update) {
2080
- notify(next);
2081
- return;
2082
- }
2083
- await provider.update(FILE_OPEN_SETTINGS_NS, { fileOpenTool: next }, expectedRevision);
2084
- const stored = (provider.describe?.({ redactSecrets: true })?.find((item) => item.ns === FILE_OPEN_SETTINGS_NS))?.value;
2085
- notify(stored?.fileOpenTool ?? next);
2086
- syncRevision();
2087
- }
2088
- };
2089
- }
2090
- function normalizeFileOpenTool(value) {
2091
- return normalizeValue(value);
2092
- }
2093
- //#endregion
2094
2405
  //#region src/rpc.ts
2095
2406
  /** cwd → Promise 链:串行化 debug 日志追加(fs read+write 非原子,避免并发丢行)。 */
2096
2407
  const debugWriteQueues = /* @__PURE__ */ new Map();
@@ -2617,6 +2928,33 @@ function buildHandlers(ctx, registry, searcher = newSearcher(ctx)) {
2617
2928
  error: String(error)
2618
2929
  };
2619
2930
  }
2931
+ },
2932
+ "compat": async () => ({
2933
+ ok: true,
2934
+ report: await buildReport(ctx)
2935
+ }),
2936
+ "vscode.devFormGet": async () => ({
2937
+ ok: true,
2938
+ devForm: readDevForm()
2939
+ }),
2940
+ "vscode.devFormSet": async (args) => {
2941
+ try {
2942
+ const result = await setDevForm(ctx, args.enabled === true, args.path);
2943
+ if (!result.ok) return {
2944
+ ok: false,
2945
+ error: result.error ?? "切换开发形态失败"
2946
+ };
2947
+ return {
2948
+ ok: true,
2949
+ devForm: readDevForm(),
2950
+ restart: result.restart
2951
+ };
2952
+ } catch (error) {
2953
+ return {
2954
+ ok: false,
2955
+ error: String(error)
2956
+ };
2957
+ }
2620
2958
  }
2621
2959
  };
2622
2960
  }
@@ -2640,7 +2978,6 @@ async function handleRpc(ctx, registry, method, args, searcher = newSearcher(ctx
2640
2978
  * tools.restrict() 负责模型可见性,tools.guard() 负责执行层兜底拒绝。
2641
2979
  * 作者 ddj 2026年08月22号
2642
2980
  */
2643
- const LEGACY_PREFIX = "vsm-mcp:";
2644
2981
  const UNKNOWN_PROJECT = "__unknown_project__";
2645
2982
  /** 统一 workspace 路径,供 cwd 父路径匹配。 */
2646
2983
  function normalizePath(path) {
@@ -2657,15 +2994,8 @@ function matchWorkspace(cwd, paths) {
2657
2994
  matches.sort((a, b) => normalizePath(b).length - normalizePath(a).length);
2658
2995
  return matches[0];
2659
2996
  }
2660
- /** 根据 entry id 判断是否为新旧格式项目 MCP。 */
2661
- function isProjectEntry(id) {
2662
- return id.startsWith("vsm-mcp.") || id.startsWith(LEGACY_PREFIX);
2663
- }
2664
- /** 从项目 entry id 解析 workspace hash。 */
2665
- function entryHash(id) {
2666
- if (id.startsWith("vsm-mcp.")) return id.slice(8).split(".")[0];
2667
- if (id.startsWith(LEGACY_PREFIX)) return id.slice(8).split(":")[0];
2668
- }
2997
+ /** 根据 entry id 判断是否为新旧格式项目 MCP(兼容层统一判定)。 */
2998
+ const isProjectEntry = isProjectEntryId;
2669
2999
  /** 返回一个 agent 不应继承的项目工具名。 */
2670
3000
  function denyTools(toolProjects, currentPath) {
2671
3001
  return [...toolProjects.entries()].filter(([, owner]) => owner !== currentPath).map(([name]) => name).sort();
@@ -2779,9 +3109,10 @@ function dropFileIndex(cwd) {
2779
3109
  //#region src/index.ts
2780
3110
  /**
2781
3111
  * @dsh-external 生态 → dsh-vscode-mode:DSH 上的类 VSCode 编码体验(Host 半入口)。
2782
- * 职责:装配 capture(tools/result 捕获 edit/write 差异)、RPC 分发(11 个 edrv.* 方法)、
2783
- * webServer 路由(/edrv/rpc、/edrv/assets/*、/edrv/vendor/*)、工作区旁车持久化。
2784
- * 结构:shared/(双面契约) + model/store/capture/workspace/revert/registry/rpc/routes(host 模块)。
3112
+ * 职责:装配 capture(tools/result 捕获 edit/write 差异)、RPC 分发(edrv.* / mcp.* / vscode.* / compat)、
3113
+ * webServer 路由(/edrv/rpc、/edrv/assets/*、/edrv/vendor/*)、工作区旁车持久化、
3114
+ * 兼容层装配(依赖守卫 + 重复装配自诊断 + 启动日志兼容性报告)。
3115
+ * 结构:shared/(双面契约) + model/store/capture/workspace/revert/registry/rpc/routes/compat(host 模块)。
2785
3116
  * 作者 ddj 2026-08-20
2786
3117
  */
2787
3118
  const name = "dsh-vscode-mode";
@@ -2795,7 +3126,7 @@ const inject = [
2795
3126
  "agents"
2796
3127
  ];
2797
3128
  /**
2798
- * 装配插件:挂事件监听、注册路由。
3129
+ * 装配插件:挂事件监听、注册路由、安装兼容层。
2799
3130
  * @author ddj 2026年08月20号
2800
3131
  * @param ctx DSH 上下文(sessions/fs/webServer 由 inject 提供;sandboxPolicy/subprocess 惰性获取)
2801
3132
  * @param config 插件配置(可选 imageDir 覆盖图标目录)
@@ -2803,6 +3134,8 @@ const inject = [
2803
3134
  function apply(ctx, config) {
2804
3135
  const registry = /* @__PURE__ */ new Map();
2805
3136
  const searcher = newSearcher(ctx);
3137
+ /** 兼容性警告收集(route 护栏等写入,启动日志一并输出)。 */
3138
+ const warnings = [];
2806
3139
  setupOpenSettings(ctx, config, () => {});
2807
3140
  ctx.on("tools/result", (exec, result) => {
2808
3141
  captureToolResult(ctx, registry, exec, result);
@@ -2815,10 +3148,30 @@ function apply(ctx, config) {
2815
3148
  searcher.dispose(cwd);
2816
3149
  }
2817
3150
  });
2818
- registerRoutes(ctx, config, (method, args) => handleRpc(ctx, registry, method, args, searcher));
3151
+ registerRoutes(ctx, config, (method, args) => handleRpc(ctx, registry, method, args, searcher), (warning) => warnings.push(warning));
2819
3152
  installIsolation(ctx);
3153
+ logCompatSummary(ctx, warnings);
2820
3154
  ctx.logger?.info?.("[dsh-vscode-mode] 编辑差异审查已装配(/edrv/rpc 路由就绪,项目 MCP 隔离已启用)");
2821
3155
  }
3156
+ /**
3157
+ * 异步输出兼容性报告摘要(含重复装配/路由冲突自诊断)。
3158
+ * @author ddj 2026年08月24号
3159
+ * @param ctx DSH host 上下文
3160
+ * @param warnings 装配期收集的兼容性警告
3161
+ */
3162
+ async function logCompatSummary(ctx, warnings) {
3163
+ try {
3164
+ const report = await buildReport(ctx);
3165
+ for (const warning of warnings) report.warnings.push(warning);
3166
+ const logger = ctx.logger;
3167
+ if (!logger?.info) return;
3168
+ const head = "[dsh-vscode-mode] 兼容性:" + report.external.length + " 项外部适配 / " + report.guards.length + " 项护栏";
3169
+ if (report.warnings.length) logger.warn?.(head + ",警告 " + report.warnings.length + " 条:" + report.warnings.join(";"));
3170
+ else logger.info?.(head + ",无警告");
3171
+ } catch (error) {
3172
+ ctx.logger?.warn?.("[dsh-vscode-mode] 兼容性报告生成失败:" + String(error));
3173
+ }
3174
+ }
2822
3175
  //#endregion
2823
3176
  export { apply, inject, name };
2824
3177