dsh-vscode-mode 0.1.16 → 0.1.19
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 +17 -0
- package/lib/client.js +390 -21
- package/lib/client.js.map +1 -1
- package/lib/index.js +250 -26
- package/lib/index.js.map +1 -1
- package/package.json +1 -1
- package/src/client/addToConversation.ts +211 -0
- package/src/client/index.ts +16 -2
- package/src/client/styles/editor.css +6 -0
- package/src/client/styles/mcp.css +9 -0
- package/src/client/ui/EditorView.ts +168 -16
- package/src/client/ui/McpSettings.ts +32 -1
- package/src/compat.ts +3 -2
- package/src/devForm.ts +133 -0
- package/src/rpc.ts +74 -18
- package/src/shared/compat.ts +7 -0
- package/src/shared/rpc.ts +24 -2
- package/src/store.ts +6 -2
package/lib/index.js
CHANGED
|
@@ -2,7 +2,8 @@ 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, readFileSync } 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
8
|
//#region src/mcp.ts
|
|
8
9
|
/**
|
|
@@ -198,6 +199,146 @@ function normalizeFileOpenTool(value) {
|
|
|
198
199
|
return normalizeValue(value);
|
|
199
200
|
}
|
|
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
|
|
201
342
|
//#region src/compat.ts
|
|
202
343
|
/**
|
|
203
344
|
* dsh-vscode-mode host — 兼容层:与其他插件 / DSH 版本的统一探测、适配、护栏与自诊断。
|
|
@@ -327,7 +468,8 @@ async function buildReport(ctx, options) {
|
|
|
327
468
|
pluginVersion: options?.version ?? pluginVersionOf(),
|
|
328
469
|
external,
|
|
329
470
|
guards,
|
|
330
|
-
warnings
|
|
471
|
+
warnings,
|
|
472
|
+
devForm: readDevForm()
|
|
331
473
|
};
|
|
332
474
|
}
|
|
333
475
|
//#endregion
|
|
@@ -1101,15 +1243,18 @@ async function appendArchiveEntries(ctx, cwd, entries, session) {
|
|
|
1101
1243
|
console.error("edrv appendArchiveEntries failed", error);
|
|
1102
1244
|
}
|
|
1103
1245
|
}
|
|
1104
|
-
/**
|
|
1105
|
-
|
|
1246
|
+
/**
|
|
1247
|
+
* 归档记录:标记 archived、按批次写入归档、落盘工作区桶。
|
|
1248
|
+
* @param opts.deferSave 为 true 时只写归档、不落盘桶(由批量调用方统一 saveBucket 一次)
|
|
1249
|
+
*/
|
|
1250
|
+
async function archiveRecords(ctx, session, cwd, bucket, recs, reason, opts) {
|
|
1106
1251
|
if (!recs.length) return;
|
|
1107
1252
|
const fresh = recs.filter((r) => !r.archived);
|
|
1108
1253
|
for (const r of recs) r.archived = true;
|
|
1109
1254
|
const entries = [];
|
|
1110
1255
|
for (const list of groupByBatch(recs).values()) entries.push(archiveEntryFor(list, cwd, reason));
|
|
1111
1256
|
await appendArchiveEntries(ctx, cwd, entries, session);
|
|
1112
|
-
if (fresh.length) await saveBucket(ctx, cwd, bucket, session);
|
|
1257
|
+
if (fresh.length && !opts?.deferSave) await saveBucket(ctx, cwd, bucket, session);
|
|
1113
1258
|
}
|
|
1114
1259
|
//#endregion
|
|
1115
1260
|
//#region src/registry.ts
|
|
@@ -2295,6 +2440,53 @@ async function requireSession(ctx, sessionId) {
|
|
|
2295
2440
|
cwd
|
|
2296
2441
|
};
|
|
2297
2442
|
}
|
|
2443
|
+
/**
|
|
2444
|
+
* 批量决策核心:一次会话/桶解析 + 逐项处理 + 统一落盘(accept/reject 单条与 decideBatch 共用)。
|
|
2445
|
+
* rejected 项先做回滚,失败记入该项 error 且不改决策;本批次新增"已解决"记录一次性归档,
|
|
2446
|
+
* 有任何成功项则整桶只写盘一次,避免逐条读写整个 sidecar。
|
|
2447
|
+
* @author ddj 2026年08月25号
|
|
2448
|
+
* @param items 决策项(按数组顺序处理,rejected 的先后即回滚顺序)
|
|
2449
|
+
* @returns 逐项结果(ok 项含更新后的记录视图)
|
|
2450
|
+
*/
|
|
2451
|
+
async function applyDecisions(ctx, session, cwd, bucket, items) {
|
|
2452
|
+
const results = [];
|
|
2453
|
+
const resolved = [];
|
|
2454
|
+
let changed = false;
|
|
2455
|
+
for (const item of items) {
|
|
2456
|
+
const record = bucket.get(item.callId);
|
|
2457
|
+
if (!record) {
|
|
2458
|
+
results.push({
|
|
2459
|
+
callId: item.callId,
|
|
2460
|
+
ok: false,
|
|
2461
|
+
error: "记录不存在"
|
|
2462
|
+
});
|
|
2463
|
+
continue;
|
|
2464
|
+
}
|
|
2465
|
+
if (item.decision === "rejected") {
|
|
2466
|
+
const outcome = (item.scope ?? "call") === "hunk" ? await revertHunk(ctx, session, record, item.hunkIndex ?? -1) : await revertCall(ctx, session, record);
|
|
2467
|
+
if (!outcome.ok) {
|
|
2468
|
+
results.push({
|
|
2469
|
+
callId: item.callId,
|
|
2470
|
+
ok: false,
|
|
2471
|
+
error: outcome.error
|
|
2472
|
+
});
|
|
2473
|
+
continue;
|
|
2474
|
+
}
|
|
2475
|
+
}
|
|
2476
|
+
const wasResolved = recordResolved(record);
|
|
2477
|
+
markDecision(record, item.scope ?? "call", item.hunkIndex, item.decision);
|
|
2478
|
+
changed = true;
|
|
2479
|
+
if (!wasResolved && recordResolved(record)) resolved.push(record);
|
|
2480
|
+
results.push({
|
|
2481
|
+
callId: item.callId,
|
|
2482
|
+
ok: true,
|
|
2483
|
+
record: recView(record)
|
|
2484
|
+
});
|
|
2485
|
+
}
|
|
2486
|
+
if (resolved.length) await archiveRecords(ctx, session, cwd, bucket, resolved, items.some((item) => item.decision === "rejected") ? "已处理(回滚)" : "已处理", { deferSave: true });
|
|
2487
|
+
if (changed) await saveBucket(ctx, cwd, bucket, session);
|
|
2488
|
+
return results;
|
|
2489
|
+
}
|
|
2298
2490
|
/** 各方法 handler 表(类型由 shared/rpc 的 RpcHandlerMap 约束)。 */
|
|
2299
2491
|
function buildHandlers(ctx, registry, searcher = newSearcher(ctx)) {
|
|
2300
2492
|
return {
|
|
@@ -2306,7 +2498,7 @@ function buildHandlers(ctx, registry, searcher = newSearcher(ctx)) {
|
|
|
2306
2498
|
};
|
|
2307
2499
|
const bucket = await bucketOf(registry, ctx, sc.cwd);
|
|
2308
2500
|
const want = Array.isArray(args.callIds) ? new Set(args.callIds) : null;
|
|
2309
|
-
if (!want) await autoArchiveStale(ctx, sc.session, sc.cwd, bucket);
|
|
2501
|
+
if (!want && args.skipStale !== true) await autoArchiveStale(ctx, sc.session, sc.cwd, bucket);
|
|
2310
2502
|
const out = [];
|
|
2311
2503
|
for (const rec of bucket.values()) {
|
|
2312
2504
|
if (!want && rec.archived) continue;
|
|
@@ -2326,17 +2518,19 @@ function buildHandlers(ctx, registry, searcher = newSearcher(ctx)) {
|
|
|
2326
2518
|
error: sc.err
|
|
2327
2519
|
};
|
|
2328
2520
|
const bucket = await bucketOf(registry, ctx, sc.cwd);
|
|
2329
|
-
const
|
|
2330
|
-
|
|
2521
|
+
const item = (await applyDecisions(ctx, sc.session, sc.cwd, bucket, [{
|
|
2522
|
+
callId: args.callId,
|
|
2523
|
+
scope: args.scope,
|
|
2524
|
+
hunkIndex: args.hunkIndex,
|
|
2525
|
+
decision: "accepted"
|
|
2526
|
+
}]))[0];
|
|
2527
|
+
if (!item?.ok) return {
|
|
2331
2528
|
ok: false,
|
|
2332
|
-
error: "
|
|
2529
|
+
error: item?.error ?? "操作失败"
|
|
2333
2530
|
};
|
|
2334
|
-
markDecision(record, args.scope ?? "call", args.hunkIndex, "accepted");
|
|
2335
|
-
if (recordResolved(record)) await archiveRecords(ctx, sc.session, sc.cwd, bucket, [record], "已处理");
|
|
2336
|
-
else await saveBucket(ctx, sc.cwd, bucket, sc.session);
|
|
2337
2531
|
return {
|
|
2338
2532
|
ok: true,
|
|
2339
|
-
record:
|
|
2533
|
+
record: item.record
|
|
2340
2534
|
};
|
|
2341
2535
|
},
|
|
2342
2536
|
"edrv.reject": async (args) => {
|
|
@@ -2346,24 +2540,31 @@ function buildHandlers(ctx, registry, searcher = newSearcher(ctx)) {
|
|
|
2346
2540
|
error: sc.err
|
|
2347
2541
|
};
|
|
2348
2542
|
const bucket = await bucketOf(registry, ctx, sc.cwd);
|
|
2349
|
-
const
|
|
2350
|
-
|
|
2543
|
+
const item = (await applyDecisions(ctx, sc.session, sc.cwd, bucket, [{
|
|
2544
|
+
callId: args.callId,
|
|
2545
|
+
scope: args.scope,
|
|
2546
|
+
hunkIndex: args.hunkIndex,
|
|
2547
|
+
decision: "rejected"
|
|
2548
|
+
}]))[0];
|
|
2549
|
+
if (!item?.ok) return {
|
|
2351
2550
|
ok: false,
|
|
2352
|
-
error: "
|
|
2551
|
+
error: item?.error ?? "操作失败"
|
|
2353
2552
|
};
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2553
|
+
return {
|
|
2554
|
+
ok: true,
|
|
2555
|
+
record: item.record
|
|
2556
|
+
};
|
|
2557
|
+
},
|
|
2558
|
+
"edrv.decideBatch": async (args) => {
|
|
2559
|
+
const sc = await requireSession(ctx, args.sessionId);
|
|
2560
|
+
if ("err" in sc) return {
|
|
2357
2561
|
ok: false,
|
|
2358
|
-
error:
|
|
2562
|
+
error: sc.err
|
|
2359
2563
|
};
|
|
2360
|
-
|
|
2361
|
-
record.at = (/* @__PURE__ */ new Date()).toISOString();
|
|
2362
|
-
if (recordResolved(record)) await archiveRecords(ctx, sc.session, sc.cwd, bucket, [record], "已处理(回滚)");
|
|
2363
|
-
else await saveBucket(ctx, sc.cwd, bucket, sc.session);
|
|
2564
|
+
const bucket = await bucketOf(registry, ctx, sc.cwd);
|
|
2364
2565
|
return {
|
|
2365
2566
|
ok: true,
|
|
2366
|
-
|
|
2567
|
+
results: await applyDecisions(ctx, sc.session, sc.cwd, bucket, Array.isArray(args.items) ? args.items : [])
|
|
2367
2568
|
};
|
|
2368
2569
|
},
|
|
2369
2570
|
"edrv.read": async (args) => {
|
|
@@ -2790,7 +2991,30 @@ function buildHandlers(ctx, registry, searcher = newSearcher(ctx)) {
|
|
|
2790
2991
|
"compat": async () => ({
|
|
2791
2992
|
ok: true,
|
|
2792
2993
|
report: await buildReport(ctx)
|
|
2793
|
-
})
|
|
2994
|
+
}),
|
|
2995
|
+
"vscode.devFormGet": async () => ({
|
|
2996
|
+
ok: true,
|
|
2997
|
+
devForm: readDevForm()
|
|
2998
|
+
}),
|
|
2999
|
+
"vscode.devFormSet": async (args) => {
|
|
3000
|
+
try {
|
|
3001
|
+
const result = await setDevForm(ctx, args.enabled === true, args.path);
|
|
3002
|
+
if (!result.ok) return {
|
|
3003
|
+
ok: false,
|
|
3004
|
+
error: result.error ?? "切换开发形态失败"
|
|
3005
|
+
};
|
|
3006
|
+
return {
|
|
3007
|
+
ok: true,
|
|
3008
|
+
devForm: readDevForm(),
|
|
3009
|
+
restart: result.restart
|
|
3010
|
+
};
|
|
3011
|
+
} catch (error) {
|
|
3012
|
+
return {
|
|
3013
|
+
ok: false,
|
|
3014
|
+
error: String(error)
|
|
3015
|
+
};
|
|
3016
|
+
}
|
|
3017
|
+
}
|
|
2794
3018
|
};
|
|
2795
3019
|
}
|
|
2796
3020
|
/**
|