dsh-vscode-mode 0.21.1 → 0.21.2
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 +5 -3
- package/lib/client.js +66 -59
- package/lib/client.js.map +2 -2
- package/lib/index.js +1330 -1293
- package/lib/index.js.map +1 -1
- package/package.json +1 -1
- package/src/client/addToConversation.ts +70 -45
- package/src/mcpDiagnostic.ts +42 -0
- package/src/mcpPool.ts +5 -1
- package/src/mcpRuntime.ts +11 -2
package/lib/index.js
CHANGED
|
@@ -464,1435 +464,1468 @@ var McpBridge = class McpBridge {
|
|
|
464
464
|
}
|
|
465
465
|
};
|
|
466
466
|
//#endregion
|
|
467
|
-
//#region src/
|
|
468
|
-
/**
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
467
|
+
//#region src/shared/rpc.ts
|
|
468
|
+
/** webServer 精确路由。 */
|
|
469
|
+
const RPC_PATH = "/edrv/rpc";
|
|
470
|
+
/**
|
|
471
|
+
* 图片扩展名(小写、无点)→ MIME。edrv.read base64 响应与 client 图片预览共用,
|
|
472
|
+
* 收敛在此避免 host/client 两处漂移。
|
|
473
|
+
* @author ddj 2026年09月08号
|
|
474
|
+
*/
|
|
475
|
+
const IMAGE_MIME = {
|
|
476
|
+
png: "image/png",
|
|
477
|
+
jpg: "image/jpeg",
|
|
478
|
+
jpeg: "image/jpeg",
|
|
479
|
+
gif: "image/gif",
|
|
480
|
+
webp: "image/webp",
|
|
481
|
+
bmp: "image/bmp",
|
|
482
|
+
ico: "image/x-icon",
|
|
483
|
+
avif: "image/avif",
|
|
484
|
+
svg: "image/svg+xml"
|
|
485
|
+
};
|
|
486
|
+
/**
|
|
487
|
+
* 路径 → 图片 MIME:按 basename 扩展名小写查 {@link IMAGE_MIME},未命中回退 application/octet-stream。
|
|
488
|
+
* @author ddj 2026年09月08号
|
|
489
|
+
* @param path 文件路径(`/` 或 `\` 分隔均可)
|
|
490
|
+
* @returns MIME 字符串
|
|
491
|
+
*/
|
|
492
|
+
function imageMimeOf(path) {
|
|
493
|
+
const base = String(path || "").split(/[\\/]/).pop() || "";
|
|
494
|
+
const dot = base.lastIndexOf(".");
|
|
495
|
+
const ext = dot > 0 ? base.slice(dot + 1).toLowerCase() : "";
|
|
496
|
+
return IMAGE_MIME[ext] ?? "application/octet-stream";
|
|
477
497
|
}
|
|
478
|
-
/**
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
498
|
+
/** PDF 统一 MIME(host base64 响应与 client PDF 面板同源,避免两处漂移)。 */
|
|
499
|
+
const PDF_MIME = "application/pdf";
|
|
500
|
+
/**
|
|
501
|
+
* 会话级前置错误的规范文案:host requireSession 系与 client 错误识别共用同一常量,
|
|
502
|
+
* 避免文案漂移导致 client 端分类失效。跨端场景(桌面端/Web 端各自运行独立 DSH 实例、
|
|
503
|
+
* 会话列表互通但会话仅在一端激活)下,本端 host 取不到会话即返回「会话不存在」。
|
|
504
|
+
* @author ddj 2026年10月16号
|
|
505
|
+
*/
|
|
506
|
+
const SESSION_MISSING_ERROR = "会话不存在";
|
|
507
|
+
/** 会话存在但无工作区 cwd 时的规范文案(与 SESSION_MISSING_ERROR 同源共用)。 */
|
|
508
|
+
const SESSION_NO_WORKSPACE_ERROR = "会话无工作区";
|
|
509
|
+
/**
|
|
510
|
+
* 路径 → 二进制预览 MIME:PDF 特判 {@link PDF_MIME},其余走图片表回退 octet-stream。
|
|
511
|
+
* @author ddj 2026年09月22号
|
|
512
|
+
* @param path 文件路径(`/` 或 `\` 分隔均可)
|
|
513
|
+
* @returns MIME 字符串
|
|
514
|
+
*/
|
|
515
|
+
function binaryMimeOf(path) {
|
|
516
|
+
const base = String(path || "").split(/[\\/]/).pop() || "";
|
|
517
|
+
const dot = base.lastIndexOf(".");
|
|
518
|
+
if (dot > 0 && base.slice(dot + 1).toLowerCase() === "pdf") return PDF_MIME;
|
|
519
|
+
return imageMimeOf(path);
|
|
493
520
|
}
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
calls: Promise.resolve()
|
|
548
|
-
};
|
|
549
|
-
this.entries.set(key, entry);
|
|
550
|
-
entry.ready = this.schedule(entry);
|
|
551
|
-
entry.ready.catch(() => void 0);
|
|
552
|
-
}
|
|
553
|
-
if (entry.idle) clearTimeout(entry.idle);
|
|
554
|
-
entry.idle = void 0;
|
|
555
|
-
entry.refs += 1;
|
|
556
|
-
let released = false;
|
|
557
|
-
return {
|
|
558
|
-
entry,
|
|
559
|
-
release: () => {
|
|
560
|
-
if (released) return;
|
|
561
|
-
released = true;
|
|
562
|
-
this.release(entry);
|
|
563
|
-
}
|
|
564
|
-
};
|
|
565
|
-
}
|
|
566
|
-
/** @private @author ddj 2026年10月20号 @param entry New entry. @returns Queued initialization, bounded to two concurrent starts. */
|
|
567
|
-
schedule(entry) {
|
|
568
|
-
return new Promise((resolve, reject) => {
|
|
569
|
-
/** @author ddj 2026年10月20号 Initializes a still-owned queue item. */
|
|
570
|
-
const run = async () => {
|
|
571
|
-
try {
|
|
572
|
-
await this.start(entry);
|
|
573
|
-
resolve();
|
|
574
|
-
} catch (error) {
|
|
575
|
-
reject(error);
|
|
576
|
-
}
|
|
577
|
-
};
|
|
578
|
-
this.queue.push(run);
|
|
579
|
-
this.pump();
|
|
580
|
-
});
|
|
581
|
-
}
|
|
582
|
-
/** @private @author ddj 2026年10月20号 Advances FIFO work; each task holds its slot until startup or cleanup quiesces. */
|
|
583
|
-
pump() {
|
|
584
|
-
while (this.running < 2 && this.queue.length) {
|
|
585
|
-
const run = this.queue.shift();
|
|
586
|
-
this.running += 1;
|
|
587
|
-
const job = run().finally(() => {
|
|
588
|
-
this.running -= 1;
|
|
589
|
-
this.jobs.delete(job);
|
|
590
|
-
this.pump();
|
|
591
|
-
});
|
|
592
|
-
this.jobs.add(job);
|
|
593
|
-
}
|
|
594
|
-
}
|
|
595
|
-
/** @private @author ddj 2026年10月20号 @param entry Connection to initialize. @returns Initialization completion, or failure after owned cleanup. */
|
|
596
|
-
async start(entry) {
|
|
597
|
-
if (!entry.live || this.stopped) throw new Error("MCP 启动已取消");
|
|
598
|
-
entry.phase = "loading";
|
|
599
|
-
entry.phaseAt = Date.now();
|
|
600
|
-
entry.startAt = entry.phaseAt;
|
|
601
|
-
let timer;
|
|
602
|
-
const work = this.mount(entry);
|
|
603
|
-
const deadline = new Promise((_resolve, reject) => {
|
|
604
|
-
timer = setTimeout(() => reject(/* @__PURE__ */ new Error("MCP 初始化超时")), entry.spec.policy.startupTimeoutMs);
|
|
605
|
-
timer.unref?.();
|
|
606
|
-
});
|
|
607
|
-
try {
|
|
608
|
-
await Promise.race([work, deadline]);
|
|
609
|
-
if (!entry.live) throw new Error("MCP 启动已取消");
|
|
610
|
-
entry.phase = "ready";
|
|
611
|
-
entry.finishedAt = Date.now();
|
|
612
|
-
entry.connectedAt = entry.finishedAt;
|
|
613
|
-
} catch (error) {
|
|
614
|
-
entry.error = "MCP 初始化失败或超时,请查看 Host 日志";
|
|
615
|
-
this.ctx.logger?.warn?.("MCP initialization failed: " + String(error));
|
|
616
|
-
entry.live = false;
|
|
617
|
-
entry.phase = "stopping";
|
|
618
|
-
const closing = this.closeBridge(entry);
|
|
619
|
-
await Promise.allSettled([closing, work]);
|
|
620
|
-
entry.phase = "error";
|
|
621
|
-
entry.finishedAt = Date.now();
|
|
622
|
-
entry.retryAt = Date.now() + 5e3;
|
|
623
|
-
throw new Error(entry.error);
|
|
624
|
-
} finally {
|
|
625
|
-
if (timer) clearTimeout(timer);
|
|
626
|
-
}
|
|
627
|
-
}
|
|
628
|
-
/** @private @author ddj 2026年10月20号 @param entry Owned generation. @returns Official initial sync; never publishes after invalidation. */
|
|
629
|
-
async mount(entry) {
|
|
630
|
-
const official = await this.load();
|
|
631
|
-
if (!entry.live || this.stopped) return;
|
|
632
|
-
const bridge = await this.make();
|
|
633
|
-
if (!entry.live || this.stopped) {
|
|
634
|
-
await bridge.dispose();
|
|
635
|
-
return;
|
|
636
|
-
}
|
|
637
|
-
entry.bridge = bridge;
|
|
638
|
-
entry.phase = "starting";
|
|
639
|
-
entry.phaseAt = Date.now();
|
|
640
|
-
entry.fiber = bridge.scope.ctx.plugin(official, entry.spec.config);
|
|
641
|
-
await entry.fiber;
|
|
642
|
-
if (entry.fiber?.state !== void 0 && entry.fiber.state !== 2) throw new Error("MCP 插件未完成激活");
|
|
643
|
-
}
|
|
644
|
-
/** @private @author ddj 2026年10月20号 @param entry Connection to close. @returns Shared closure barrier; rejection retains a fenced entry. */
|
|
645
|
-
async closeBridge(entry) {
|
|
646
|
-
if (entry.close) return entry.close;
|
|
647
|
-
const close = entry.bridge?.dispose() ?? Promise.resolve();
|
|
648
|
-
entry.close = close;
|
|
649
|
-
try {
|
|
650
|
-
await close;
|
|
651
|
-
entry.bridge = void 0;
|
|
652
|
-
entry.fiber = void 0;
|
|
653
|
-
entry.close = void 0;
|
|
654
|
-
} catch (error) {
|
|
655
|
-
entry.error = "MCP 释放未确认,禁止重建,请查看 Host 日志";
|
|
656
|
-
this.ctx.logger?.warn?.("MCP cleanup failed: " + String(error));
|
|
657
|
-
throw error;
|
|
658
|
-
}
|
|
521
|
+
//#endregion
|
|
522
|
+
//#region src/paths.ts
|
|
523
|
+
/**
|
|
524
|
+
* dsh-vscode-mode host — 路径常量与动态路径统一出口(PathConst)。
|
|
525
|
+
* 所有 host 侧文件路径/缓存键/路由前缀从这里取,避免散落内联:
|
|
526
|
+
* - 静态:插件缓存根(~/.dsh/dsh-vscode-mode/cache,下分 workspace/ 工作区级与
|
|
527
|
+
* user/ 用户级)、日志根、工作区侧车名、路由前缀、静态资源目录(import.meta.url 派生)
|
|
528
|
+
* - 动态:每工作区树缓存文件(workspace/<id>/tree.v<schema>.json)、home 解析
|
|
529
|
+
* (DSH_HOME → ~/.dsh)
|
|
530
|
+
* - 清理:缓存 sweep(分级:工作区级按版本/TTL/空目录、用户级按版本/保留期、根级残留)
|
|
531
|
+
* 纯函数可单测;raw fs 写入与引擎自身插件(settings-file 用 node:fs + DSH_HOME)一致。
|
|
532
|
+
* 作者 ddj 2026-09-01
|
|
533
|
+
*/
|
|
534
|
+
/** 插件 id(也是 .dsh 下插件工作区目录名)。 */
|
|
535
|
+
const PLUGIN_ID = "dsh-vscode-mode";
|
|
536
|
+
/** DSH 工作区目录名(默认 home 下)。 */
|
|
537
|
+
const DSH_DIR_NAME = ".dsh";
|
|
538
|
+
/** home 覆盖环境变量(与引擎 resolveDshHome 一致)。 */
|
|
539
|
+
const DSH_HOME_ENV = "DSH_HOME";
|
|
540
|
+
/** 插件缓存目录总预算:超限按 mtime 从旧到新删文件,防任意增长撑爆磁盘(最后兜底)。 */
|
|
541
|
+
const CACHE_TOTAL_CAP = 209715200;
|
|
542
|
+
/** 工作区根侧车:审查记录(用户数据,随项目走,不迁移)。 */
|
|
543
|
+
const SIDECAR = ".dsh-edit-review.json";
|
|
544
|
+
/** 旧 debug 日志文件名(工作区,迁移后由插件清理)。 */
|
|
545
|
+
const DEBUG_LOG = ".dsh-edit-review-debug.log";
|
|
546
|
+
/** 旧树缓存 sidecar 名(工作区,迁移后由插件清理;缓存可重建,删除无损)。 */
|
|
547
|
+
const LEGACY_TREE_SIDECARS = [".dsh-edit-review-tree.json", ".dsh-vscode-mode-tree.json"];
|
|
548
|
+
/** 路由前缀(兼容性检查用)。 */
|
|
549
|
+
const ROUTE_PREFIX = "/edrv";
|
|
550
|
+
/** Monaco 静态资源前缀。 */
|
|
551
|
+
const VENDOR_PREFIX = "/edrv/vendor";
|
|
552
|
+
/** 稳定短哈希(cwd → 缓存文件名段;sha1 截断,跨运行稳定)。 */
|
|
553
|
+
function hashOf(text) {
|
|
554
|
+
return createHash("sha1").update(String(text)).digest("hex").slice(0, 16);
|
|
555
|
+
}
|
|
556
|
+
/**
|
|
557
|
+
* 可读安全路径段编码:非安全码位 → ~XXXX 大写十六进制(`~` 本身也转义)。
|
|
558
|
+
* 与 DSH 引擎 dsh-session-persistence-jsonl 的 encodeSegment 逐字节一致,
|
|
559
|
+
* 用于从 cwd/id 反推 `~/.dsh/sessions` 下的会话目录,纯函数可单测。
|
|
560
|
+
* @author ddj 2026年09月02号
|
|
561
|
+
* @param raw 原始路径段(非空)
|
|
562
|
+
* @returns 路径安全段(`.`/`..` 特判为 ~002E / ~002E~002E)
|
|
563
|
+
*/
|
|
564
|
+
function encodeSegment(raw) {
|
|
565
|
+
if (raw.length === 0) throw new Error("cannot encode an empty path segment");
|
|
566
|
+
if (raw === ".") return "~002E";
|
|
567
|
+
if (raw === "..") return "~002E~002E";
|
|
568
|
+
let out = "";
|
|
569
|
+
for (let i = 0; i < raw.length; i++) {
|
|
570
|
+
const code = raw.charCodeAt(i);
|
|
571
|
+
const ch = String.fromCharCode(code);
|
|
572
|
+
if (ch !== "~" && /^[A-Za-z0-9._-]$/.test(ch)) out += ch;
|
|
573
|
+
else out += "~" + code.toString(16).toUpperCase().padStart(4, "0");
|
|
659
574
|
}
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
575
|
+
return out;
|
|
576
|
+
}
|
|
577
|
+
/**
|
|
578
|
+
* DSH 工作区目录键:cwd → `--<可读键>--`。分隔符(/ \ :)折叠为单个 `-`,
|
|
579
|
+
* 其余非安全码位转 ~XXXX,`--` 包裹、251 截断。与引擎 projectKey 一致。
|
|
580
|
+
* @author ddj 2026年09月02号
|
|
581
|
+
* @param cwd 会话工作区绝对路径(非空)
|
|
582
|
+
* @returns `~/.dsh/sessions` 下的工作区目录名
|
|
583
|
+
*/
|
|
584
|
+
function sessionWorkspaceKey(cwd) {
|
|
585
|
+
if (cwd.length === 0) throw new Error("cannot encode an empty project path");
|
|
586
|
+
let readable = "";
|
|
587
|
+
let separatorRun = false;
|
|
588
|
+
for (let i = 0; i < cwd.length; i++) {
|
|
589
|
+
const code = cwd.charCodeAt(i);
|
|
590
|
+
const ch = String.fromCharCode(code);
|
|
591
|
+
if (ch === "/" || ch === "\\" || ch === ":") {
|
|
592
|
+
if (!separatorRun) readable += "-";
|
|
593
|
+
separatorRun = true;
|
|
594
|
+
} else if (ch !== "~" && /^[A-Za-z0-9._-]$/.test(ch)) {
|
|
595
|
+
readable += ch;
|
|
596
|
+
separatorRun = false;
|
|
597
|
+
} else {
|
|
598
|
+
readable += "~" + code.toString(16).toUpperCase().padStart(4, "0");
|
|
599
|
+
separatorRun = false;
|
|
667
600
|
}
|
|
668
|
-
entry.idle = setTimeout(() => {
|
|
669
|
-
this.retire(entry).catch(() => void 0);
|
|
670
|
-
}, 6e4);
|
|
671
|
-
entry.idle.unref?.();
|
|
672
|
-
}
|
|
673
|
-
/** @public @author ddj 2026年10月20号 @param entry Invalid generation. @returns Quiescent close; callers may await before starting a replacement. */
|
|
674
|
-
async retire(entry) {
|
|
675
|
-
entry.live = false;
|
|
676
|
-
entry.phase = "stopping";
|
|
677
|
-
if (entry.idle) clearTimeout(entry.idle);
|
|
678
|
-
await this.closeBridge(entry);
|
|
679
|
-
await entry.ready.catch(() => void 0);
|
|
680
|
-
await entry.calls.catch(() => void 0);
|
|
681
|
-
if (this.entries.get(entry.key) === entry) this.entries.delete(entry.key);
|
|
682
|
-
}
|
|
683
|
-
/** @public @author ddj 2026年10月20号 @param entry Authorized connection. @param action Per-call work retaining original execution identity. @param signal Caller cancellation. @returns Serialized shared result; no direct registry bypass is introduced here. */
|
|
684
|
-
run(entry, action, signal) {
|
|
685
|
-
/** @author ddj 2026年10月20号 Checks cancellation and generation again at actual dispatch. */
|
|
686
|
-
const invoke = async () => {
|
|
687
|
-
await waitMcp(entry.ready, signal);
|
|
688
|
-
if (signal?.aborted || !entry.live || entry.phase !== "ready") throw new Error("MCP 连接不可用或调用已取消");
|
|
689
|
-
return action();
|
|
690
|
-
};
|
|
691
|
-
const result = entry.calls.then(invoke);
|
|
692
|
-
entry.calls = result.catch(() => void 0);
|
|
693
|
-
return result;
|
|
694
|
-
}
|
|
695
|
-
/** @public @author ddj 2026年10月20号 @returns Full disposal of entries, queue, timers and owned work. */
|
|
696
|
-
async dispose() {
|
|
697
|
-
this.stopped = true;
|
|
698
|
-
await Promise.all([...this.entries.values()].map((entry) => this.retire(entry)));
|
|
699
|
-
await Promise.all([...this.jobs]);
|
|
700
601
|
}
|
|
701
|
-
}
|
|
702
|
-
//#endregion
|
|
703
|
-
//#region src/mcpDiscover.ts
|
|
704
|
-
/** @public @author ddj 2026年10月20号 @param runtime Authorized runtime. @param ctx Host registration context. @returns Global discovery cleanup; normal capability filters remain authoritative. */
|
|
705
|
-
function installDiscover(runtime, ctx) {
|
|
706
|
-
const tools = ctx.get("tools");
|
|
707
|
-
/** @author ddj 2026年10月20号 @param args Validated operation. @param execution Registry-owned caller identity. @returns Safe capability catalogue or real connected schemas. */
|
|
708
|
-
const execute = async (args, execution) => {
|
|
709
|
-
const agent = execution.agent;
|
|
710
|
-
if (!agent || !runtime.agents.has(agent)) throw new Error("会话未激活,不能使用项目 MCP 发现入口");
|
|
711
|
-
if (!args || typeof args !== "object" || Array.isArray(args) || Object.keys(args).some((key) => !["action", "serverName"].includes(key))) throw new Error("发现入口仅接受 action 和 serverName");
|
|
712
|
-
if (!["list", "connect"].includes(args.action)) throw new Error("action 必须为 list 或 connect");
|
|
713
|
-
if (args.action === "list") return runtime.catalogue(agent);
|
|
714
|
-
if (typeof args.serverName !== "string") throw new Error("请指定已配置的 serverName");
|
|
715
|
-
return runtime.connect(agent, args.serverName, execution.signal);
|
|
716
|
-
};
|
|
717
|
-
/** @author ddj 2026年10月20号 @param _args Unused. @param result Safe canonical JSON. @returns Native content without bypassing output validation. */
|
|
718
|
-
const render = (_args, result) => [{
|
|
719
|
-
type: "text",
|
|
720
|
-
text: JSON.stringify(result)
|
|
721
|
-
}];
|
|
722
|
-
return tools.register({
|
|
723
|
-
name: "discover_project_mcp",
|
|
724
|
-
description: "List this workspace’s configured project MCP services without starting them, or connect one to discover its real tools. After connecting, call discovered tools in the NEXT request/program; never guess schemas. Global/preset MCPs are not included.",
|
|
725
|
-
parameters: {
|
|
726
|
-
type: "object",
|
|
727
|
-
properties: {
|
|
728
|
-
action: {
|
|
729
|
-
type: "string",
|
|
730
|
-
enum: ["list", "connect"]
|
|
731
|
-
},
|
|
732
|
-
serverName: { type: "string" }
|
|
733
|
-
},
|
|
734
|
-
required: ["action"],
|
|
735
|
-
additionalProperties: false
|
|
736
|
-
},
|
|
737
|
-
output: {
|
|
738
|
-
schema: {},
|
|
739
|
-
render
|
|
740
|
-
},
|
|
741
|
-
execute
|
|
742
|
-
});
|
|
602
|
+
return `--${(readable.replace(/^-+/, "") || "root").slice(0, 251)}--`;
|
|
743
603
|
}
|
|
744
|
-
/**
|
|
745
|
-
function
|
|
746
|
-
|
|
747
|
-
/** @author ddj 2026年10月20号 @returns Current enabled names only; no credential-bearing configuration. */
|
|
748
|
-
const text = () => {
|
|
749
|
-
const names = runtime.catalogue(agent).map((server) => server.serverName);
|
|
750
|
-
if (!names.length) return "";
|
|
751
|
-
return "Project MCP capabilities (may not be started): " + JSON.stringify(names) + ". Use discover_project_mcp action=list, then action=connect with serverName when needed. Only use returned real tool schemas. Native tools appear on the next model request; PTC must finish the current program and start a new program to refresh its SDK. Do not eval or bypass tool approval.";
|
|
752
|
-
};
|
|
753
|
-
return prompt.section({
|
|
754
|
-
name: "vscode-mode:project-mcp",
|
|
755
|
-
order: 3090,
|
|
756
|
-
text,
|
|
757
|
-
interpolate: false
|
|
758
|
-
});
|
|
604
|
+
/** DSH 会话目录段:id → 路径安全段(与引擎 encodeSegment 一致)。 */
|
|
605
|
+
function sessionIdSegment(id) {
|
|
606
|
+
return encodeSegment(id);
|
|
759
607
|
}
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
function preciseHunk(record, idx) {
|
|
764
|
-
return hunkOf(record, idx);
|
|
608
|
+
/** DSH 会话根目录(~/.dsh/sessions;引擎 dsh-base 默认 root)。 */
|
|
609
|
+
function sessionsRoot(home = dshHome()) {
|
|
610
|
+
return join(home, "sessions");
|
|
765
611
|
}
|
|
766
|
-
/**
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
612
|
+
/**
|
|
613
|
+
* 解析 DSH home:空白环境值视为未设置,展开 ~ 并解析绝对路径。
|
|
614
|
+
* @public
|
|
615
|
+
* @author ddj 2026年09月28号
|
|
616
|
+
* @param env 环境映射(缺省 process.env;测试可注入)
|
|
617
|
+
* @returns 归一化的 DSH home 绝对路径
|
|
618
|
+
*/
|
|
619
|
+
function dshHome(env = process.env) {
|
|
620
|
+
const value = env[DSH_HOME_ENV];
|
|
621
|
+
const path = value && value.trim() ? value : join(homedir(), DSH_DIR_NAME);
|
|
622
|
+
if (path === "~") return resolve(homedir());
|
|
623
|
+
if (path.startsWith("~/") || path.startsWith("~\\")) return resolve(join(homedir(), path.slice(2)));
|
|
624
|
+
return resolve(path);
|
|
775
625
|
}
|
|
776
|
-
/**
|
|
777
|
-
function
|
|
778
|
-
|
|
779
|
-
if (hunk && typeof hunk.newText === "string") return hunk;
|
|
780
|
-
if (idx === 0 && record.callHunk && typeof record.callHunk.newText === "string") return record.callHunk;
|
|
781
|
-
return null;
|
|
626
|
+
/** 插件缓存根(~/.dsh/dsh-vscode-mode/cache,下分 workspace/ 与 user/ 两级)。 */
|
|
627
|
+
function pluginCacheRoot(home = dshHome()) {
|
|
628
|
+
return join(home, PLUGIN_ID, "cache");
|
|
782
629
|
}
|
|
783
|
-
/**
|
|
784
|
-
function
|
|
785
|
-
return
|
|
630
|
+
/** 工作区级缓存目录:按工作区 id(cwd hash)隔离,换工作区即失效。 */
|
|
631
|
+
function workspaceCacheDir(workspaceId, home = dshHome()) {
|
|
632
|
+
return join(pluginCacheRoot(home), "workspace", workspaceId);
|
|
633
|
+
}
|
|
634
|
+
/** 用户级缓存目录(跨工作区共享;当前无实例,预留供全局偏好类缓存使用)。 */
|
|
635
|
+
function userCacheDir(home = dshHome()) {
|
|
636
|
+
return join(pluginCacheRoot(home), "user");
|
|
637
|
+
}
|
|
638
|
+
/** LSP provider 发现结果缓存文件(用户级:user/lsp-specs.v<schema>.json,跨会话/跨重启共享)。 */
|
|
639
|
+
function lspSpecCacheFile(home = dshHome(), schema = 4) {
|
|
640
|
+
return join(userCacheDir(home), "lsp-specs.v" + schema + ".json");
|
|
641
|
+
}
|
|
642
|
+
/** 插件日志根(~/.dsh/dsh-vscode-mode/logs,用户级)。 */
|
|
643
|
+
function pluginLogRoot(home = dshHome()) {
|
|
644
|
+
return join(home, PLUGIN_ID, "logs");
|
|
645
|
+
}
|
|
646
|
+
/** 每工作区树缓存文件(工作区级:workspace/<id>/tree.v<schema>.json,文件名含版本便于 sweep)。 */
|
|
647
|
+
function treeCacheFile(cwd, home = dshHome(), schema = 1) {
|
|
648
|
+
return join(workspaceCacheDir(hashOf(cwd), home), "tree.v" + schema + ".json");
|
|
786
649
|
}
|
|
787
650
|
/**
|
|
788
|
-
*
|
|
789
|
-
*
|
|
790
|
-
*
|
|
791
|
-
* @
|
|
651
|
+
* 每工作区 UI 状态镜像文件(工作区级:workspace/<id>/ui.v<schema>.json)。
|
|
652
|
+
* 存编辑器页签/侧栏/预览态等界面状态的 localStorage 原值副本,供换 origin 后回填。
|
|
653
|
+
* @author ddj 2026年09月28号
|
|
654
|
+
* @param cwd 工作区绝对路径
|
|
655
|
+
* @param home DSH home(测试可注入)
|
|
656
|
+
* @param schema schema 版本(缺省 UI_STATE_SCHEMA)
|
|
657
|
+
* @returns UI 状态镜像文件绝对路径
|
|
792
658
|
*/
|
|
793
|
-
function
|
|
794
|
-
return
|
|
659
|
+
function uiStateFile(cwd, home = dshHome(), schema = 1) {
|
|
660
|
+
return join(workspaceCacheDir(hashOf(cwd), home), "ui.v" + schema + ".json");
|
|
661
|
+
}
|
|
662
|
+
/**
|
|
663
|
+
* 用户级 UI 状态镜像文件(跨工作区共享:user/ui-globals.v<schema>.json)。
|
|
664
|
+
* 存与工作区无关的全局键(侧栏提示已忽略、工作区折叠态等)。
|
|
665
|
+
* @author ddj 2026年09月28号
|
|
666
|
+
* @param home DSH home(测试可注入)
|
|
667
|
+
* @param schema schema 版本(缺省 UI_STATE_SCHEMA)
|
|
668
|
+
* @returns 全局 UI 状态镜像文件绝对路径
|
|
669
|
+
*/
|
|
670
|
+
function uiGlobalsFile(home = dshHome(), schema = 1) {
|
|
671
|
+
return join(userCacheDir(home), "ui-globals.v" + schema + ".json");
|
|
672
|
+
}
|
|
673
|
+
/** 每工作区 debug 日志文件(用户级日志根下,按 cwd hash 隔离)。 */
|
|
674
|
+
function debugLogFile(cwd, home = dshHome()) {
|
|
675
|
+
return join(pluginLogRoot(home), "debug." + hashOf(cwd) + ".log");
|
|
676
|
+
}
|
|
677
|
+
/**
|
|
678
|
+
* 每工作区归档数据库(用户数据,不受缓存清理预算约束)。
|
|
679
|
+
* @author ddj 2026年09月30号
|
|
680
|
+
* @param cwd 工作区绝对路径
|
|
681
|
+
* @param home DSH home
|
|
682
|
+
* @returns 归档数据库绝对路径
|
|
683
|
+
*/
|
|
684
|
+
function archiveDbFile(cwd, home = dshHome()) {
|
|
685
|
+
return join(home, PLUGIN_ID, "data", "archive", hashOf(cwd) + ".sqlite");
|
|
686
|
+
}
|
|
687
|
+
/** 插件包 assets 目录(import.meta.url 派生;config.imageDir 可覆盖,见 imageDirOf)。 */
|
|
688
|
+
function assetsDirOf(moduleUrl) {
|
|
689
|
+
return join(dirname(fileURLToPath(moduleUrl)), "..", "assets");
|
|
690
|
+
}
|
|
691
|
+
/** Monaco AMD 静态目录(随包发布)。 */
|
|
692
|
+
function vendorDirOf(moduleUrl) {
|
|
693
|
+
return join(assetsDirOf(moduleUrl), "vendor");
|
|
694
|
+
}
|
|
695
|
+
/**
|
|
696
|
+
* 插件自带技能组根目录(包根 skills/,与 assets/ 同级;随包发布)。
|
|
697
|
+
* 布局:<根>/<技能名>/SKILL.md(目录式)或 <根>/<技能名>.md(扁平式)。
|
|
698
|
+
* @author ddj 2026年09月11号
|
|
699
|
+
* @param moduleUrl 模块 URL(缺省 import.meta.url;测试注入)
|
|
700
|
+
* @returns 技能组根目录绝对路径
|
|
701
|
+
*/
|
|
702
|
+
function skillsDirOf(moduleUrl) {
|
|
703
|
+
return join(dirname(fileURLToPath(moduleUrl)), "..", "skills");
|
|
704
|
+
}
|
|
705
|
+
/** 图标目录:config.imageDir 覆盖优先,否则插件包 assets/。 */
|
|
706
|
+
function imageDirOf(config, moduleUrl) {
|
|
707
|
+
const cfg = config;
|
|
708
|
+
if (cfg && typeof cfg.imageDir === "string" && cfg.imageDir) return cfg.imageDir.replace(/\\/g, "/");
|
|
709
|
+
return assetsDirOf(moduleUrl);
|
|
795
710
|
}
|
|
796
|
-
/**
|
|
797
|
-
|
|
711
|
+
/**
|
|
712
|
+
* 已知缓存文件种类 → 当前 schema 版本(sweep 判旧依据)。
|
|
713
|
+
* 新增缓存文件必须登记在此,否则被当作未知残留清理。
|
|
714
|
+
*/
|
|
715
|
+
const CACHE_SCHEMAS = {
|
|
716
|
+
tree: 1,
|
|
717
|
+
"lsp-specs": 4,
|
|
718
|
+
ui: 1,
|
|
719
|
+
"ui-globals": 1
|
|
720
|
+
};
|
|
721
|
+
/** 缓存文件名 → 种类与 schema 版本(`<kind>.v<schema>.json`;未登记种类/命名不合规 → null)。 */
|
|
722
|
+
function parseCacheName(filename) {
|
|
723
|
+
const m = /^([a-z-]+)\.v(\d+)\.json$/.exec(filename);
|
|
724
|
+
if (!m) return null;
|
|
725
|
+
const kind = m[1];
|
|
726
|
+
if (CACHE_SCHEMAS[kind] === void 0) return null;
|
|
798
727
|
return {
|
|
799
|
-
|
|
800
|
-
|
|
728
|
+
kind,
|
|
729
|
+
schema: Number(m[2])
|
|
801
730
|
};
|
|
802
731
|
}
|
|
803
|
-
/**
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
732
|
+
/**
|
|
733
|
+
* 该缓存文件名是否为已登记种类的当前 schema 版本(sweep 判旧/判残留依据)。
|
|
734
|
+
* @author ddj 2026年09月28号
|
|
735
|
+
* @param filename 缓存文件名(不含目录)
|
|
736
|
+
* @returns 已登记种类且版本为当前版本
|
|
737
|
+
*/
|
|
738
|
+
function isCurrentCacheFile(filename) {
|
|
739
|
+
const parsed = parseCacheName(filename);
|
|
740
|
+
return parsed !== null && parsed.schema === CACHE_SCHEMAS[parsed.kind];
|
|
741
|
+
}
|
|
742
|
+
/** 按「schema 版本 + 保留期」清扫某目录内文件(目录级,返回相对路径清单)。 */
|
|
743
|
+
async function sweepDirFiles(dir, removed, now, dropAll) {
|
|
744
|
+
const names = await readdir(dir).catch(() => []);
|
|
745
|
+
for (const name of names) {
|
|
746
|
+
const full = join(dir, name);
|
|
747
|
+
let drop = dropAll;
|
|
748
|
+
if (!drop) {
|
|
749
|
+
if (!isCurrentCacheFile(name)) drop = true;
|
|
750
|
+
else try {
|
|
751
|
+
if (now - (await stat(full)).mtimeMs > 2592e6) drop = true;
|
|
752
|
+
} catch (error) {
|
|
753
|
+
drop = true;
|
|
817
754
|
}
|
|
818
|
-
return hunk;
|
|
819
755
|
}
|
|
820
|
-
if (
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
...hunk,
|
|
827
|
-
afterStart: removedStart,
|
|
828
|
-
afterEnd: removedStart,
|
|
829
|
-
beforeStart: removedStart,
|
|
830
|
-
beforeEnd: removedEnd
|
|
831
|
-
};
|
|
832
|
-
});
|
|
833
|
-
}
|
|
834
|
-
/** 计算两个文本的最长公共前缀长度。 */
|
|
835
|
-
function commonPrefix(left, right) {
|
|
836
|
-
const limit = Math.min(left.length, right.length);
|
|
837
|
-
let i = 0;
|
|
838
|
-
while (i < limit && left.charCodeAt(i) === right.charCodeAt(i)) i++;
|
|
839
|
-
return i;
|
|
840
|
-
}
|
|
841
|
-
/** 计算不与 prefix 重叠的最长公共后缀长度。 */
|
|
842
|
-
function commonSuffix(left, right, prefix) {
|
|
843
|
-
const limit = Math.min(left.length, right.length) - prefix;
|
|
844
|
-
let i = 0;
|
|
845
|
-
while (i < limit && left.charCodeAt(left.length - 1 - i) === right.charCodeAt(right.length - 1 - i)) i++;
|
|
846
|
-
return i;
|
|
756
|
+
if (!drop) continue;
|
|
757
|
+
try {
|
|
758
|
+
await rm(full, { force: true });
|
|
759
|
+
removed.push(name);
|
|
760
|
+
} catch (error) {}
|
|
761
|
+
}
|
|
847
762
|
}
|
|
848
|
-
/**
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
function findCandidate(content, needle, cursor, occupied, state) {
|
|
859
|
-
if (!needle.length) return void 0;
|
|
860
|
-
let fallback;
|
|
861
|
-
for (let i = 0; i < state.ranges.length; i++) {
|
|
862
|
-
const range = state.ranges[i];
|
|
863
|
-
if (occupied.some((used) => overlaps(range, used))) continue;
|
|
864
|
-
if (range.start >= cursor) return range;
|
|
865
|
-
if (!fallback) fallback = range;
|
|
763
|
+
/** 清扫工作区级缓存:逐工作区目录按版本/TTL 清文件,空目录删除。 */
|
|
764
|
+
async function sweepWorkspaceTier(dir, removed, now) {
|
|
765
|
+
const ids = await readdir(dir).catch(() => []);
|
|
766
|
+
for (const id of ids) {
|
|
767
|
+
const wsDir = join(dir, id);
|
|
768
|
+
await sweepDirFiles(wsDir, removed, now, false);
|
|
769
|
+
if (!(await readdir(wsDir).catch(() => [])).length) await rm(wsDir, {
|
|
770
|
+
recursive: true,
|
|
771
|
+
force: true
|
|
772
|
+
}).catch(() => {});
|
|
866
773
|
}
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
774
|
+
}
|
|
775
|
+
/** 总预算兜底:递归收集全部缓存文件,按 mtime 从旧到新删,直到总大小 ≤ 预算。 */
|
|
776
|
+
async function sweepByTotalCap(root, removed, cap) {
|
|
777
|
+
const files = [];
|
|
778
|
+
const walk = async (dir) => {
|
|
779
|
+
const names = await readdir(dir).catch(() => []);
|
|
780
|
+
for (const name of names) {
|
|
781
|
+
const full = join(dir, name);
|
|
782
|
+
try {
|
|
783
|
+
const info = await stat(full);
|
|
784
|
+
if (info.isDirectory()) await walk(full);
|
|
785
|
+
else files.push({
|
|
786
|
+
path: full,
|
|
787
|
+
mtime: info.mtimeMs,
|
|
788
|
+
size: info.size
|
|
789
|
+
});
|
|
790
|
+
} catch (error) {}
|
|
872
791
|
}
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
792
|
+
};
|
|
793
|
+
await walk(root);
|
|
794
|
+
let total = files.reduce((sum, f) => sum + f.size, 0);
|
|
795
|
+
files.sort((a, b) => a.mtime - b.mtime);
|
|
796
|
+
for (const f of files) {
|
|
797
|
+
if (total <= cap) break;
|
|
798
|
+
try {
|
|
799
|
+
await rm(f.path, { force: true });
|
|
800
|
+
total -= f.size;
|
|
801
|
+
removed.push(f.path.slice(root.length + 1));
|
|
802
|
+
} catch (error) {}
|
|
882
803
|
}
|
|
883
|
-
return fallback;
|
|
884
804
|
}
|
|
885
805
|
/**
|
|
886
|
-
*
|
|
887
|
-
*
|
|
888
|
-
*
|
|
889
|
-
*
|
|
890
|
-
*
|
|
891
|
-
*
|
|
806
|
+
* 清理插件缓存根(分级 + 总预算兜底):
|
|
807
|
+
* - 根级残留文件(旧扁平格式/未知)→ 删
|
|
808
|
+
* - workspace/ 工作区级 → 按「schema 版本 + 保留期」清文件,空目录删除
|
|
809
|
+
* - user/ 用户级 → 按「schema 版本 + 保留期」清文件
|
|
810
|
+
* - 总大小超预算 → 按最旧先删(最后防线,防任意增长撑爆磁盘)
|
|
811
|
+
* best-effort 静默。
|
|
812
|
+
* @author ddj 2026年09月01号
|
|
813
|
+
* @param home DSH home(测试可注入)
|
|
814
|
+
* @param cap 总预算字节数(缺省 CACHE_TOTAL_CAP;测试可注入小预算)
|
|
815
|
+
* @returns 删除的文件相对路径清单
|
|
892
816
|
*/
|
|
893
|
-
function
|
|
894
|
-
const
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
const
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
const
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
done: false
|
|
916
|
-
};
|
|
917
|
-
matches.set(newText, state);
|
|
918
|
-
}
|
|
919
|
-
candidate = findCandidate(content, newText, cursor, occupied, state);
|
|
920
|
-
}
|
|
921
|
-
if (candidate) {
|
|
922
|
-
locations.push({
|
|
923
|
-
idx,
|
|
924
|
-
hunk,
|
|
925
|
-
...candidate,
|
|
926
|
-
matched: true
|
|
927
|
-
});
|
|
928
|
-
occupied.push(candidate);
|
|
929
|
-
cursor = Math.max(cursor, candidate.end);
|
|
930
|
-
continue;
|
|
931
|
-
}
|
|
932
|
-
if (newText.length === 0 && hunk.afterStart === void 0 && hunk.afterEnd === void 0 && content.length === 0) {
|
|
933
|
-
locations.push({
|
|
934
|
-
idx,
|
|
935
|
-
hunk,
|
|
936
|
-
start: 0,
|
|
937
|
-
end: 0,
|
|
938
|
-
matched: true
|
|
939
|
-
});
|
|
940
|
-
continue;
|
|
941
|
-
}
|
|
942
|
-
if (newText.length === 0 && hunk.afterStart !== void 0 && hunk.afterEnd !== void 0 && hunk.afterStart === hunk.afterEnd && hunk.afterStart >= 0 && hunk.afterStart <= content.length) {
|
|
943
|
-
const point = {
|
|
944
|
-
start: hunk.afterStart,
|
|
945
|
-
end: hunk.afterEnd
|
|
946
|
-
};
|
|
947
|
-
if (!occupied.some((used) => overlaps(point, used))) {
|
|
948
|
-
locations.push({
|
|
949
|
-
idx,
|
|
950
|
-
hunk,
|
|
951
|
-
...point,
|
|
952
|
-
matched: true
|
|
953
|
-
});
|
|
954
|
-
occupied.push(point);
|
|
955
|
-
cursor = Math.max(cursor, point.end);
|
|
956
|
-
continue;
|
|
817
|
+
async function sweepTreeCache(home = dshHome(), cap = CACHE_TOTAL_CAP) {
|
|
818
|
+
const dir = pluginCacheRoot(home);
|
|
819
|
+
const names = await readdir(dir).catch(() => []);
|
|
820
|
+
if (!names.length) return [];
|
|
821
|
+
const removed = [];
|
|
822
|
+
const now = Date.now();
|
|
823
|
+
await Promise.all(names.map(async (name) => {
|
|
824
|
+
const full = join(dir, name);
|
|
825
|
+
try {
|
|
826
|
+
if ((await stat(full)).isDirectory()) {
|
|
827
|
+
if (name === "workspace") await sweepWorkspaceTier(full, removed, now);
|
|
828
|
+
else if (name === "user") await sweepDirFiles(full, removed, now, false);
|
|
829
|
+
else {
|
|
830
|
+
await rm(full, {
|
|
831
|
+
recursive: true,
|
|
832
|
+
force: true
|
|
833
|
+
});
|
|
834
|
+
removed.push(name);
|
|
835
|
+
}
|
|
836
|
+
} else {
|
|
837
|
+
await rm(full, { force: true });
|
|
838
|
+
removed.push(name);
|
|
957
839
|
}
|
|
958
|
-
}
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
start: -1,
|
|
963
|
-
end: -1,
|
|
964
|
-
matched: false
|
|
965
|
-
});
|
|
966
|
-
}
|
|
967
|
-
return locations;
|
|
968
|
-
}
|
|
969
|
-
/** 判断两个半开区间是否重叠。零长度区间只与同一位置的零长度区间重叠。 */
|
|
970
|
-
function overlaps(left, right) {
|
|
971
|
-
if (left.start === left.end || right.start === right.end) return left.start === right.start && left.end === right.end;
|
|
972
|
-
return left.start < right.end && right.start < left.end;
|
|
973
|
-
}
|
|
974
|
-
/** 在可定位区间上执行一次 hunk 反向替换。 */
|
|
975
|
-
function replaceRange(content, location, reverse = false) {
|
|
976
|
-
if (!location.matched || location.start < 0) return null;
|
|
977
|
-
const replacement = reverse ? location.hunk.oldText ?? "" : location.hunk.newText;
|
|
978
|
-
return content.slice(0, location.start) + replacement + content.slice(location.end);
|
|
840
|
+
} catch (error) {}
|
|
841
|
+
}));
|
|
842
|
+
await sweepByTotalCap(dir, removed, cap);
|
|
843
|
+
return removed;
|
|
979
844
|
}
|
|
845
|
+
//#endregion
|
|
846
|
+
//#region src/shared/logger.ts
|
|
980
847
|
/**
|
|
981
|
-
*
|
|
982
|
-
*
|
|
983
|
-
*
|
|
984
|
-
*
|
|
985
|
-
*
|
|
986
|
-
*
|
|
848
|
+
* dsh-vscode-mode — 统一日志核心(双面共享,平台中立)。
|
|
849
|
+
* 输出规范:
|
|
850
|
+
* - 格式:`[dsh-vscode-mode][:scope] 消息`;前缀只在模块内部拼接,业务代码禁止手写前缀
|
|
851
|
+
* - 级别语义:debug=诊断明细 / info=装配与路由里程碑 / warn=降级与兼容回退 / error=失败
|
|
852
|
+
* - 出口:host 面经 bindHostLog(ctx) 绑定 ctx.logger(缺失回退 console);client 面固定 console
|
|
853
|
+
* - 诊断文件通道(edrv.debug → ~/.dsh/dsh-vscode-mode/logs/)见 debugLog.ts,与本模块前缀共用
|
|
854
|
+
* 新增日志一律 `import { log } from '../log.js'`(client 为 './log.js')后调 log.debug/info/warn/error;
|
|
855
|
+
* 子域日志用 log.child('scope'),禁止另起 console。
|
|
856
|
+
* 作者 ddj 2026-09-08
|
|
987
857
|
*/
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
parts.push(content.slice(location.end, end), reverse ? location.hunk.oldText ?? "" : location.hunk.newText);
|
|
1007
|
-
end = location.start;
|
|
1008
|
-
}
|
|
1009
|
-
parts.push(content.slice(0, end));
|
|
1010
|
-
parts.reverse();
|
|
1011
|
-
return {
|
|
1012
|
-
content: parts.join(""),
|
|
1013
|
-
stale: stale.sort((a, b) => a - b)
|
|
858
|
+
/** 插件统一日志前缀(与包安装名一致)。 */
|
|
859
|
+
const LOG_PREFIX = "[dsh-vscode-mode]";
|
|
860
|
+
/** 默认级别到 console 方法名的映射。 */
|
|
861
|
+
const CONSOLE_METHODS = {
|
|
862
|
+
debug: "debug",
|
|
863
|
+
info: "info",
|
|
864
|
+
warn: "warn",
|
|
865
|
+
error: "error"
|
|
866
|
+
};
|
|
867
|
+
/**
|
|
868
|
+
* 默认出口:console(级别直映方法;方法缺失时静默丢弃,不抛错)。
|
|
869
|
+
* @author ddj 2026年09月08号
|
|
870
|
+
* @returns console 出口
|
|
871
|
+
*/
|
|
872
|
+
function consoleSink() {
|
|
873
|
+
return (level, line) => {
|
|
874
|
+
const emit = console[CONSOLE_METHODS[level]];
|
|
875
|
+
if (typeof emit === "function") emit.call(console, line);
|
|
1014
876
|
};
|
|
1015
877
|
}
|
|
1016
878
|
/**
|
|
1017
|
-
*
|
|
1018
|
-
* @author ddj 2026年08
|
|
1019
|
-
* @param
|
|
1020
|
-
* @
|
|
1021
|
-
* @param idx hunk 下标(仅 scope=hunk 时生效)
|
|
1022
|
-
* @param value 决策值
|
|
1023
|
-
* @returns 更新后的记录(原对象就地修改)
|
|
879
|
+
* DSH host 出口工厂:转接 ctx.logger(方法级可选调用,缺服务/缺方法回退 console)。
|
|
880
|
+
* @author ddj 2026年09月08号
|
|
881
|
+
* @param ctx DSH host 上下文
|
|
882
|
+
* @returns 日志出口
|
|
1024
883
|
*/
|
|
1025
|
-
function
|
|
1026
|
-
const
|
|
1027
|
-
|
|
1028
|
-
const
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
}
|
|
1034
|
-
record.at = (/* @__PURE__ */ new Date()).toISOString();
|
|
1035
|
-
return record;
|
|
1036
|
-
}
|
|
1037
|
-
/** 记录是否已无任何待决策差异(superseded 或全部已决策)。 */
|
|
1038
|
-
function recordResolved(record) {
|
|
1039
|
-
if (record.superseded === true) return true;
|
|
1040
|
-
const hunks = Array.isArray(record.hunks) ? record.hunks : [];
|
|
1041
|
-
const perHunk = Array.isArray(record.decisions.perHunk) ? record.decisions.perHunk : [];
|
|
1042
|
-
const count = Math.max(hunks.length, perHunk.length);
|
|
1043
|
-
if (!count) return record.decisions.call === "accepted" || record.decisions.call === "rejected";
|
|
1044
|
-
for (let idx = 0; idx < count; idx++) {
|
|
1045
|
-
const precise = preciseHunk(record, idx);
|
|
1046
|
-
const value = perHunk[idx] ?? record.decisions.call;
|
|
1047
|
-
if (isNoopHunk(precise) && value === "pending") return false;
|
|
1048
|
-
if (value !== "accepted" && value !== "rejected") return false;
|
|
1049
|
-
}
|
|
1050
|
-
return true;
|
|
884
|
+
function ctxLogSink(ctx) {
|
|
885
|
+
const base = consoleSink();
|
|
886
|
+
return (level, line) => {
|
|
887
|
+
const logger = ctx?.logger;
|
|
888
|
+
const emit = logger?.[level];
|
|
889
|
+
if (typeof emit === "function") emit.call(logger, line);
|
|
890
|
+
else base(level, line);
|
|
891
|
+
};
|
|
1051
892
|
}
|
|
1052
893
|
/**
|
|
1053
|
-
*
|
|
1054
|
-
* @author ddj 2026年08
|
|
1055
|
-
* @param
|
|
1056
|
-
* @
|
|
894
|
+
* 拼一行日志:`[dsh-vscode-mode][:scope] 消息`(scope 为空省略冒号段)。
|
|
895
|
+
* @author ddj 2026年09月08号
|
|
896
|
+
* @param scope 子域(可空)
|
|
897
|
+
* @param message 消息文本
|
|
898
|
+
* @returns 整行文本
|
|
1057
899
|
*/
|
|
1058
|
-
function
|
|
1059
|
-
const
|
|
1060
|
-
|
|
1061
|
-
const n = Math.max(1, hunks.length, perHunk.length);
|
|
1062
|
-
let accepted = 0;
|
|
1063
|
-
let rejected = 0;
|
|
1064
|
-
let pending = 0;
|
|
1065
|
-
for (let i = 0; i < n; i++) {
|
|
1066
|
-
const hunk = preciseHunk(record, i);
|
|
1067
|
-
const value = perHunk[i] ?? record.decisions.call;
|
|
1068
|
-
if (i < hunks.length && isNoopHunk(hunk) && value === "pending") continue;
|
|
1069
|
-
if (value === "accepted") accepted++;
|
|
1070
|
-
else if (value === "rejected") rejected++;
|
|
1071
|
-
else pending++;
|
|
1072
|
-
}
|
|
1073
|
-
return {
|
|
1074
|
-
accepted,
|
|
1075
|
-
rejected,
|
|
1076
|
-
pending,
|
|
1077
|
-
superseded: record.superseded === true
|
|
1078
|
-
};
|
|
900
|
+
function formatLine(scope, message) {
|
|
901
|
+
const text = String(message);
|
|
902
|
+
return scope ? LOG_PREFIX + ":" + scope + " " + text : LOG_PREFIX + " " + text;
|
|
1079
903
|
}
|
|
1080
904
|
/**
|
|
1081
|
-
*
|
|
1082
|
-
*
|
|
1083
|
-
* @
|
|
1084
|
-
* @param
|
|
1085
|
-
* @
|
|
1086
|
-
* @returns 重建内容与 stale 定位
|
|
905
|
+
* 日志器工厂(内部):emit 时读共享出口状态,保证父 bind 后子实例跟随。
|
|
906
|
+
* @author ddj 2026年09月08号
|
|
907
|
+
* @param state 共享出口状态
|
|
908
|
+
* @param scope 子域文本(可空)
|
|
909
|
+
* @returns 日志器
|
|
1087
910
|
*/
|
|
1088
|
-
function
|
|
1089
|
-
const
|
|
1090
|
-
for (const rec of records) for (let i = 0; i < rec.hunks.length; i++) {
|
|
1091
|
-
const st = rec.decisions.perHunk[i] ?? rec.decisions.call;
|
|
1092
|
-
const hunk = preciseHunk(rec, i);
|
|
1093
|
-
if (st === "pending" && hunk && !isNoopHunk(hunk)) pending.push({
|
|
1094
|
-
rec,
|
|
1095
|
-
idx: i,
|
|
1096
|
-
hunk
|
|
1097
|
-
});
|
|
1098
|
-
}
|
|
1099
|
-
pending.sort((a, b) => a.rec.at < b.rec.at ? 1 : a.rec.at > b.rec.at ? -1 : 0);
|
|
1100
|
-
const rebuilt = applyLocations(content, locateHunks(content, pending.map((item) => item.hunk)), true);
|
|
911
|
+
function makeLogger(state, scope) {
|
|
912
|
+
const emit = (level, message) => state.sink(level, formatLine(scope, message));
|
|
1101
913
|
return {
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
914
|
+
debug: (message) => emit("debug", message),
|
|
915
|
+
info: (message) => emit("info", message),
|
|
916
|
+
warn: (message) => emit("warn", message),
|
|
917
|
+
error: (message) => emit("error", message),
|
|
918
|
+
bind(next) {
|
|
919
|
+
state.sink = next;
|
|
920
|
+
},
|
|
921
|
+
child(next) {
|
|
922
|
+
return makeLogger(state, scope ? scope + "." + next : next);
|
|
923
|
+
}
|
|
1107
924
|
};
|
|
1108
925
|
}
|
|
1109
926
|
/**
|
|
1110
|
-
*
|
|
1111
|
-
* @author ddj 2026年
|
|
1112
|
-
*
|
|
1113
|
-
* @param
|
|
1114
|
-
* @returns
|
|
927
|
+
* 创建插件日志器(默认 console 出口;host 面单例由 bindHostLog 换到 ctx.logger)。
|
|
928
|
+
* @author ddj 2026年09月08号
|
|
929
|
+
* @param sink 初始出口(缺省 console)
|
|
930
|
+
* @param scope 初始子域(业务方一般经 child 派生,不直接传)
|
|
931
|
+
* @returns 日志器
|
|
1115
932
|
*/
|
|
1116
|
-
function
|
|
1117
|
-
return
|
|
933
|
+
function createLogger(sink = consoleSink(), scope = "") {
|
|
934
|
+
return makeLogger({ sink }, scope);
|
|
1118
935
|
}
|
|
936
|
+
//#endregion
|
|
937
|
+
//#region src/log.ts
|
|
1119
938
|
/**
|
|
1120
|
-
*
|
|
1121
|
-
*
|
|
1122
|
-
*
|
|
1123
|
-
*
|
|
1124
|
-
* @param later 后续修改的差异块
|
|
1125
|
-
* @returns 文本归一化后整组逆反且包含真实修改时为 true
|
|
939
|
+
* dsh-vscode-mode host — 日志单例(全 host 面唯一日志入口)。
|
|
940
|
+
* 默认 console 出口;apply() 首行调 bindHostLog(ctx) 切到 ctx.logger(缺失回退 console)。
|
|
941
|
+
* 用法:`import { log } from './log.js'` → `log.debug/info/warn/error(msg)`;子域用 `log.child('scope')`。
|
|
942
|
+
* 作者 ddj 2026-09-08
|
|
1126
943
|
*/
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
let changed = false;
|
|
1130
|
-
for (let idx = 0; idx < prior.length; idx++) {
|
|
1131
|
-
const a = normalizeHunk(prior[idx]);
|
|
1132
|
-
const b = normalizeHunk(later[idx]);
|
|
1133
|
-
if (a.oldText !== b.newText || a.newText !== b.oldText) return false;
|
|
1134
|
-
if (!isNoopHunk(a)) changed = true;
|
|
1135
|
-
}
|
|
1136
|
-
return changed;
|
|
1137
|
-
}
|
|
944
|
+
/** host 面插件日志单例(格式/级别语义见 shared/logger.ts 头注释)。 */
|
|
945
|
+
const log = createLogger();
|
|
1138
946
|
/**
|
|
1139
|
-
*
|
|
1140
|
-
* @author ddj 2026年
|
|
1141
|
-
*
|
|
1142
|
-
* @param a 第一条记录
|
|
1143
|
-
* @param b 第二条记录
|
|
1144
|
-
* @returns 负数表示 a 更早,零表示先后无法区分
|
|
947
|
+
* 装配期绑定 host 日志出口:ctx.logger 可用时全部日志转经 DSH 日志系统。
|
|
948
|
+
* @author ddj 2026年09月08号
|
|
949
|
+
* @param ctx DSH host 上下文(logger 服务缺失时保持 console 出口)
|
|
1145
950
|
*/
|
|
1146
|
-
function
|
|
1147
|
-
|
|
1148
|
-
if (Number.isInteger(a.batch) && Number.isInteger(b.batch) && a.batch > 0 && b.batch > 0 && a.batch !== b.batch) return a.batch - b.batch;
|
|
1149
|
-
const delta = Date.parse(a.at) - Date.parse(b.at);
|
|
1150
|
-
return Number.isFinite(delta) ? delta : 0;
|
|
951
|
+
function bindHostLog(ctx) {
|
|
952
|
+
log.bind(ctxLogSink(ctx));
|
|
1151
953
|
}
|
|
954
|
+
//#endregion
|
|
955
|
+
//#region src/debugLog.ts
|
|
1152
956
|
/**
|
|
1153
|
-
*
|
|
1154
|
-
*
|
|
1155
|
-
*
|
|
1156
|
-
*
|
|
1157
|
-
*
|
|
1158
|
-
*
|
|
957
|
+
* dsh-vscode-mode host — 诊断文件通道(client 上报 → 缓冲批量落盘)+ 日志文件管理。
|
|
958
|
+
* 通道:edrv.debug RPC → 内存缓冲(满/空闲触发)→ 串行读改写
|
|
959
|
+
* `~/.dsh/dsh-vscode-mode/logs/debug.<cwdHash>.log`(512KB 上限截断保留尾部)。
|
|
960
|
+
* 设计:console 不一定落盘,文件可靠;写失败静默忽略(调试日志不阻塞业务)。
|
|
961
|
+
* 管理:edrv.dlog.* RPC 用的清单/尾部读取/清空三能力;file 参数一律先过
|
|
962
|
+
* isDebugLogName 白名单(纯文件名、无路径分隔),杜绝穿越出日志根。
|
|
963
|
+
* 作者 ddj 2026-09-08 / 2026-09-17
|
|
1159
964
|
*/
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
965
|
+
/** cwd → 内存缓冲:行数组 + 累计字节数 + 待触发 flush 定时器(攒批落盘,避免每条日志全文件读改写)。 */
|
|
966
|
+
const debugBuffers = /* @__PURE__ */ new Map();
|
|
967
|
+
/** cwd → Promise 链:串行化 debug 日志落盘(fs read+write 非原子,避免并发丢行)。 */
|
|
968
|
+
const debugWriteQueues = /* @__PURE__ */ new Map();
|
|
969
|
+
/** debug 日志单文件上限:超限截断保留尾部,防文件无限增长拖慢每次追加。 */
|
|
970
|
+
const DEBUG_LOG_CAP = 524288;
|
|
971
|
+
/** debug 日志批量缓冲上限:攒满即落盘(一次读改写),上限内不逐条写文件。 */
|
|
972
|
+
const DEBUG_BUF_CAP = 32768;
|
|
973
|
+
/** debug 日志空闲 flush 延迟:缓冲未满时,静默一段时间后落盘一次。 */
|
|
974
|
+
const DEBUG_FLUSH_IDLE_MS = 1e3;
|
|
975
|
+
/** cwd → 已清理旧工作区 debug 日志标记(一次性)。 */
|
|
976
|
+
const debugLegacyCleaned = /* @__PURE__ */ new Set();
|
|
977
|
+
/** 诊断回显日志器(终端回显统一带 `[dsh-vscode-mode:debug]` 前缀,级别跟随上报级别)。 */
|
|
978
|
+
const debugLog = log.child("debug");
|
|
979
|
+
/** 诊断日志文件名白名单(cwd hash 命名;请求回传的 file 参数必须整名匹配,防路径穿越)。 */
|
|
980
|
+
const DEBUG_LOG_NAME = /^debug\.[0-9a-f]{16}\.log$/;
|
|
1165
981
|
/**
|
|
1166
|
-
*
|
|
1167
|
-
* @author ddj 2026年
|
|
1168
|
-
*
|
|
1169
|
-
* @
|
|
1170
|
-
* @param record 新记录
|
|
1171
|
-
* @returns 可安全配对的先前记录,证据不足或部分撤销时为 null
|
|
982
|
+
* 文件名是否为合法诊断日志名(白名单整名匹配,杜绝路径穿越)。
|
|
983
|
+
* @author ddj 2026年09月17号
|
|
984
|
+
* @param name 请求回传的文件名(可为空)
|
|
985
|
+
* @returns 是否合法
|
|
1172
986
|
*/
|
|
1173
|
-
function
|
|
1174
|
-
|
|
1175
|
-
let found = null;
|
|
1176
|
-
for (const prior of records) {
|
|
1177
|
-
if (prior.callId === record.callId || prior.path !== record.path || !fullyPending(prior)) continue;
|
|
1178
|
-
if (recordOrder(prior, record) >= 0 || !hunksInverse(prior.hunks, record.hunks)) continue;
|
|
1179
|
-
if (!snapshotsRestore(prior, record)) continue;
|
|
1180
|
-
if (!found || recordOrder(found, prior) < 0) found = prior;
|
|
1181
|
-
}
|
|
1182
|
-
return found;
|
|
987
|
+
function isDebugLogName(name) {
|
|
988
|
+
return typeof name === "string" && DEBUG_LOG_NAME.test(name);
|
|
1183
989
|
}
|
|
1184
990
|
/**
|
|
1185
|
-
*
|
|
1186
|
-
*
|
|
1187
|
-
*
|
|
1188
|
-
* @param
|
|
1189
|
-
* @
|
|
991
|
+
* 批量落盘一条 debug 日志缓冲:读旧文件 → 追加 → 超上限截断保留尾部 → 一次写回。
|
|
992
|
+
* 串行链保证同一 cwd 的读改写不交错丢行;写失败静默忽略(调试日志不阻塞业务)。
|
|
993
|
+
* @author ddj 2026年09月08号
|
|
994
|
+
* @param cwd 工作区(日志落 ~/.dsh/dsh-vscode-mode/logs/debug.<cwdHash>.log)
|
|
995
|
+
* @param batch 本批日志行
|
|
1190
996
|
*/
|
|
1191
|
-
function
|
|
1192
|
-
const
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
function fileMaxBatch(records, path) {
|
|
1208
|
-
let m = 0;
|
|
1209
|
-
for (const r of records.values()) if (r.path === path && Number.isInteger(r.batch) && r.batch > m) m = r.batch;
|
|
1210
|
-
return m;
|
|
1211
|
-
}
|
|
1212
|
-
/** @public @author ddj 2026年10月26号 @param map Legacy records. @param max Cache target, never a pending retention limit. */
|
|
1213
|
-
function prune(map, max = 200) {
|
|
1214
|
-
if (map.size <= max) return;
|
|
1215
|
-
const sorted = [...map.values()].filter((record) => record.archived && recordResolved(record)).sort((a, b) => a.at < b.at ? -1 : 1);
|
|
1216
|
-
for (const record of sorted) {
|
|
1217
|
-
if (map.size <= max) break;
|
|
1218
|
-
map.delete(record.callId);
|
|
1219
|
-
}
|
|
997
|
+
function flushDebug(cwd, batch) {
|
|
998
|
+
const task = (debugWriteQueues.get(cwd) ?? Promise.resolve()).then(async () => {
|
|
999
|
+
try {
|
|
1000
|
+
await mkdir(pluginLogRoot(), { recursive: true });
|
|
1001
|
+
const target = debugLogFile(cwd);
|
|
1002
|
+
const appended = await readFile(target, "utf8").catch(() => "") + batch.map((l) => (/* @__PURE__ */ new Date()).toISOString() + " " + l + "\n").join("");
|
|
1003
|
+
const next = appended.length > DEBUG_LOG_CAP ? appended.slice(-Math.floor(DEBUG_LOG_CAP / 2)) : appended;
|
|
1004
|
+
await writeFile(target, next, "utf8");
|
|
1005
|
+
if (!debugLegacyCleaned.has(cwd)) {
|
|
1006
|
+
debugLegacyCleaned.add(cwd);
|
|
1007
|
+
await rm(join(cwd, DEBUG_LOG), { force: true }).catch(() => {});
|
|
1008
|
+
}
|
|
1009
|
+
} catch (e) {}
|
|
1010
|
+
});
|
|
1011
|
+
debugWriteQueues.set(cwd, task);
|
|
1012
|
+
return task;
|
|
1220
1013
|
}
|
|
1221
1014
|
/**
|
|
1222
|
-
*
|
|
1223
|
-
*
|
|
1224
|
-
* @
|
|
1225
|
-
* @param cwd
|
|
1226
|
-
* @param
|
|
1015
|
+
* 记录一条 client 上报的诊断日志:入队缓冲批量落盘 + 终端回显(级别跟随,缺省 debug)。
|
|
1016
|
+
* 落盘行格式 `<iso> [level] text`;级别标记在入队时补齐,flush 侧无感。
|
|
1017
|
+
* @author ddj 2026年09月08号 / 2026年09月17号
|
|
1018
|
+
* @param _ctx DSH 上下文(入参保留:诊断通道按 cwd 隔离,当前不需要 ctx,未来过滤用)
|
|
1019
|
+
* @param cwd 工作区(日志文件按 cwd hash 隔离)
|
|
1020
|
+
* @param text 单条日志文本(不含换行)
|
|
1021
|
+
* @param level 日志级别(缺省 debug;落盘行与终端回显均带此级别)
|
|
1227
1022
|
*/
|
|
1228
|
-
function
|
|
1229
|
-
const
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
decisions: r.decisions,
|
|
1244
|
-
note: r.note ?? null,
|
|
1245
|
-
before: typeof r.before === "string" ? r.before : null,
|
|
1246
|
-
after: typeof r.after === "string" ? r.after : null,
|
|
1247
|
-
baseFingerprint: r.baseFingerprint ?? null,
|
|
1248
|
-
afterFingerprint: r.afterFingerprint ?? null,
|
|
1249
|
-
conflict: r.conflict === true,
|
|
1250
|
-
legacy: r.legacy === true,
|
|
1251
|
-
superseded: r.superseded === true,
|
|
1252
|
-
batch: Number.isInteger(r.batch) ? r.batch : null,
|
|
1253
|
-
at: r.at,
|
|
1254
|
-
summary: recSummary(r)
|
|
1255
|
-
}))
|
|
1023
|
+
function debugRecord(_ctx, cwd, text, level = "debug") {
|
|
1024
|
+
const tagged = "[" + level + "] " + text;
|
|
1025
|
+
const st = debugBuffers.get(cwd) ?? {
|
|
1026
|
+
lines: [],
|
|
1027
|
+
len: 0,
|
|
1028
|
+
timer: null
|
|
1029
|
+
};
|
|
1030
|
+
st.lines.push(tagged);
|
|
1031
|
+
st.len += tagged.length;
|
|
1032
|
+
const flush = () => {
|
|
1033
|
+
st.timer = null;
|
|
1034
|
+
const batch = st.lines;
|
|
1035
|
+
st.lines = [];
|
|
1036
|
+
st.len = 0;
|
|
1037
|
+
flushDebug(cwd, batch);
|
|
1256
1038
|
};
|
|
1039
|
+
if (st.len >= DEBUG_BUF_CAP) {
|
|
1040
|
+
if (st.timer) clearTimeout(st.timer);
|
|
1041
|
+
flush();
|
|
1042
|
+
} else if (!st.timer) st.timer = setTimeout(flush, DEBUG_FLUSH_IDLE_MS);
|
|
1043
|
+
debugBuffers.set(cwd, st);
|
|
1044
|
+
debugLog[level](text);
|
|
1257
1045
|
}
|
|
1258
|
-
//#endregion
|
|
1259
|
-
//#region src/shared/rpc.ts
|
|
1260
|
-
/** webServer 精确路由。 */
|
|
1261
|
-
const RPC_PATH = "/edrv/rpc";
|
|
1262
1046
|
/**
|
|
1263
|
-
*
|
|
1264
|
-
*
|
|
1265
|
-
* @
|
|
1047
|
+
* 清单:日志根下全部诊断日志文件(名字/字节/mtime;非法名跳过),mtime 新的在前。
|
|
1048
|
+
* @author ddj 2026年09月17号
|
|
1049
|
+
* @param home DSH home(测试可注入;缺省按环境解析)
|
|
1050
|
+
* @returns 清单项数组
|
|
1266
1051
|
*/
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1052
|
+
async function listDebugLogs(home = dshHome()) {
|
|
1053
|
+
const names = await readdir(pluginLogRoot(home)).catch(() => []);
|
|
1054
|
+
const items = [];
|
|
1055
|
+
for (const name of names) {
|
|
1056
|
+
if (!isDebugLogName(name)) continue;
|
|
1057
|
+
const info = await stat(join(pluginLogRoot(home), name)).catch(() => null);
|
|
1058
|
+
if (!info?.isFile()) continue;
|
|
1059
|
+
items.push({
|
|
1060
|
+
name,
|
|
1061
|
+
bytes: info.size,
|
|
1062
|
+
mtimeMs: info.mtimeMs
|
|
1063
|
+
});
|
|
1064
|
+
}
|
|
1065
|
+
items.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
1066
|
+
return items;
|
|
1067
|
+
}
|
|
1278
1068
|
/**
|
|
1279
|
-
*
|
|
1280
|
-
*
|
|
1281
|
-
* @
|
|
1282
|
-
* @
|
|
1069
|
+
* 尾部读取一份诊断日志:保留文件尾 maxBytes 字节(缺省 256KB),truncated 标记截头。
|
|
1070
|
+
* 单文件本体受 DEBUG_LOG_CAP=512KB 约束,整读为字符串的成本可接受。
|
|
1071
|
+
* @author ddj 2026年09月17号
|
|
1072
|
+
* @param name 日志文件名(isDebugLogName 白名单校验,非法直接 null)
|
|
1073
|
+
* @param maxBytes 尾部保留字节数(缺省 256KB)
|
|
1074
|
+
* @param home DSH home(测试可注入)
|
|
1075
|
+
* @returns 读取结果;文件不存在或名字非法返回 null
|
|
1283
1076
|
*/
|
|
1284
|
-
function
|
|
1285
|
-
|
|
1286
|
-
const
|
|
1287
|
-
const
|
|
1288
|
-
|
|
1077
|
+
async function readDebugLog(name, maxBytes = 262144, home = dshHome()) {
|
|
1078
|
+
if (!isDebugLogName(name)) return null;
|
|
1079
|
+
const target = join(pluginLogRoot(home), name);
|
|
1080
|
+
const info = await stat(target).catch(() => null);
|
|
1081
|
+
if (!info?.isFile()) return null;
|
|
1082
|
+
const text = await readFile(target, "utf8").catch(() => null);
|
|
1083
|
+
if (text === null) return null;
|
|
1084
|
+
const cut = Math.max(0, text.length - maxBytes);
|
|
1085
|
+
return {
|
|
1086
|
+
name,
|
|
1087
|
+
path: target,
|
|
1088
|
+
content: cut > 0 ? text.slice(cut) : text,
|
|
1089
|
+
size: info.size,
|
|
1090
|
+
truncated: cut > 0
|
|
1091
|
+
};
|
|
1289
1092
|
}
|
|
1290
|
-
/** PDF 统一 MIME(host base64 响应与 client PDF 面板同源,避免两处漂移)。 */
|
|
1291
|
-
const PDF_MIME = "application/pdf";
|
|
1292
|
-
/**
|
|
1293
|
-
* 会话级前置错误的规范文案:host requireSession 系与 client 错误识别共用同一常量,
|
|
1294
|
-
* 避免文案漂移导致 client 端分类失效。跨端场景(桌面端/Web 端各自运行独立 DSH 实例、
|
|
1295
|
-
* 会话列表互通但会话仅在一端激活)下,本端 host 取不到会话即返回「会话不存在」。
|
|
1296
|
-
* @author ddj 2026年10月16号
|
|
1297
|
-
*/
|
|
1298
|
-
const SESSION_MISSING_ERROR = "会话不存在";
|
|
1299
|
-
/** 会话存在但无工作区 cwd 时的规范文案(与 SESSION_MISSING_ERROR 同源共用)。 */
|
|
1300
|
-
const SESSION_NO_WORKSPACE_ERROR = "会话无工作区";
|
|
1301
1093
|
/**
|
|
1302
|
-
*
|
|
1303
|
-
* @author ddj 2026年09月
|
|
1304
|
-
* @param
|
|
1305
|
-
* @
|
|
1094
|
+
* 清空一份诊断日志(truncate 到 0,保留文件本身)。
|
|
1095
|
+
* @author ddj 2026年09月17号
|
|
1096
|
+
* @param name 日志文件名(isDebugLogName 白名单校验)
|
|
1097
|
+
* @param home DSH home(测试可注入)
|
|
1098
|
+
* @returns 是否清空成功(文件不存在视为未成功,文案由调用方给)
|
|
1306
1099
|
*/
|
|
1307
|
-
function
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
if (dot > 0 && base.slice(dot + 1).toLowerCase() === "pdf") return PDF_MIME;
|
|
1311
|
-
return imageMimeOf(path);
|
|
1100
|
+
async function clearDebugLog(name, home = dshHome()) {
|
|
1101
|
+
if (!isDebugLogName(name)) return false;
|
|
1102
|
+
return await truncate(join(pluginLogRoot(home), name)).then(() => true).catch(() => false);
|
|
1312
1103
|
}
|
|
1313
1104
|
//#endregion
|
|
1314
|
-
//#region src/
|
|
1105
|
+
//#region src/mcpDiagnostic.ts
|
|
1315
1106
|
/**
|
|
1316
|
-
*
|
|
1317
|
-
*
|
|
1318
|
-
*
|
|
1319
|
-
*
|
|
1320
|
-
* -
|
|
1321
|
-
* (DSH_HOME → ~/.dsh)
|
|
1322
|
-
* - 清理:缓存 sweep(分级:工作区级按版本/TTL/空目录、用户级按版本/保留期、根级残留)
|
|
1323
|
-
* 纯函数可单测;raw fs 写入与引擎自身插件(settings-file 用 node:fs + DSH_HOME)一致。
|
|
1324
|
-
* 作者 ddj 2026-09-01
|
|
1107
|
+
* Format an initialization failure without serializing the MCP configuration.
|
|
1108
|
+
* @public @author ddj 2026年10月28号
|
|
1109
|
+
* @param error Original failure and optional cause chain.
|
|
1110
|
+
* @param config Effective configuration, used only to remove known secret values.
|
|
1111
|
+
* @returns Bounded single-line diagnostic; URLs and explicit credentials are removed.
|
|
1325
1112
|
*/
|
|
1326
|
-
|
|
1327
|
-
const
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
const
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
const
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1113
|
+
function mcpErrorText(error, config) {
|
|
1114
|
+
const messages = [];
|
|
1115
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1116
|
+
let cause = error;
|
|
1117
|
+
while (cause !== void 0 && !seen.has(cause) && messages.length < 4) {
|
|
1118
|
+
seen.add(cause);
|
|
1119
|
+
if (cause instanceof Error) {
|
|
1120
|
+
const code = cause.code;
|
|
1121
|
+
messages.push(cause.name + ": " + cause.message + (typeof code === "string" ? " [" + code + "]" : ""));
|
|
1122
|
+
cause = cause.cause;
|
|
1123
|
+
} else {
|
|
1124
|
+
messages.push(typeof cause === "string" ? cause : "Unknown MCP failure");
|
|
1125
|
+
break;
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
let text = messages.join(" <- ");
|
|
1129
|
+
const secrets = [...Object.values(config.env ?? {}), ...Object.values(config.headers ?? {})];
|
|
1130
|
+
for (const argument of config.args ?? []) if (!argument.startsWith("-")) secrets.push(argument);
|
|
1131
|
+
secrets.sort((left, right) => right.length - left.length);
|
|
1132
|
+
for (const secret of secrets) if (secret) text = text.split(secret).join("[REDACTED]");
|
|
1133
|
+
text = text.replace(/https?:\/\/[^\s"'<>]+/gi, "[URL]").replace(/\b(authorization|api[-_]?key|token|password|secret)\s*[:=]\s*[^\s,;]+/gi, "$1=[REDACTED]").replace(/[\r\n\x00-\x1f\x7f]+/g, " ");
|
|
1134
|
+
return text.slice(0, 2048);
|
|
1347
1135
|
}
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1136
|
+
//#endregion
|
|
1137
|
+
//#region src/mcpPool.ts
|
|
1138
|
+
/** Connection ownership, bounded cold starts and workspace leases. @author ddj 2026年10月20号 */
|
|
1139
|
+
/** @private @author ddj 2026年10月20号 @param input Plain effective configuration. @returns Stable secret-bearing identity used only as digest input. */
|
|
1140
|
+
function stableConfig(input) {
|
|
1141
|
+
if (Array.isArray(input)) return "[" + input.map(stableConfig).join(",") + "]";
|
|
1142
|
+
if (input && typeof input === "object") {
|
|
1143
|
+
const config = input;
|
|
1144
|
+
return "{" + Object.keys(config).sort().map((key) => JSON.stringify(key) + ":" + stableConfig(config[key])).join(",") + "}";
|
|
1145
|
+
}
|
|
1146
|
+
return JSON.stringify(input) ?? "null";
|
|
1147
|
+
}
|
|
1148
|
+
/** @public @author ddj 2026年10月20号 @param task Shared work that must remain alive for other waiters. @param signal One waiter's cancellation. @returns Caller-local wait without abandoning owned pool work. */
|
|
1149
|
+
function waitMcp(task, signal) {
|
|
1150
|
+
if (!signal) return task;
|
|
1151
|
+
return new Promise((resolve, reject) => {
|
|
1152
|
+
/** @author ddj 2026年10月20号 Rejects only this waiter. */
|
|
1153
|
+
const abort = () => {
|
|
1154
|
+
reject(/* @__PURE__ */ new Error("MCP 等待已取消"));
|
|
1155
|
+
};
|
|
1156
|
+
if (signal.aborted) {
|
|
1157
|
+
abort();
|
|
1158
|
+
return;
|
|
1159
|
+
}
|
|
1160
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
1161
|
+
task.then(resolve, reject).finally(() => signal.removeEventListener("abort", abort));
|
|
1162
|
+
});
|
|
1163
|
+
}
|
|
1164
|
+
/** The pool owns private scopes independently from any first subscriber. */
|
|
1165
|
+
var McpPool = class {
|
|
1166
|
+
ctx;
|
|
1167
|
+
load;
|
|
1168
|
+
make;
|
|
1169
|
+
entries = /* @__PURE__ */ new Map();
|
|
1170
|
+
owners = /* @__PURE__ */ new WeakMap();
|
|
1171
|
+
ownerSeq = 0;
|
|
1172
|
+
running = 0;
|
|
1173
|
+
queue = [];
|
|
1174
|
+
jobs = /* @__PURE__ */ new Set();
|
|
1175
|
+
stopped = false;
|
|
1176
|
+
/** @public @author ddj 2026年10月20号 @param ctx Host lifecycle owner. @param load Official plugin loader. @param make Private scope factory. */
|
|
1177
|
+
constructor(ctx, load, make = () => McpBridge.create(ctx)) {
|
|
1178
|
+
this.ctx = ctx;
|
|
1179
|
+
this.load = load;
|
|
1180
|
+
this.make = make;
|
|
1181
|
+
}
|
|
1182
|
+
/** @private @author ddj 2026年10月20号 @param spec Effective request. @returns Private digest; never emit configuration or secrets. */
|
|
1183
|
+
keyOf(spec) {
|
|
1184
|
+
let owner = 0;
|
|
1185
|
+
if (spec.policy.sharing === "agent") {
|
|
1186
|
+
if (!this.owners.has(spec.owner)) this.owners.set(spec.owner, ++this.ownerSeq);
|
|
1187
|
+
owner = this.owners.get(spec.owner);
|
|
1188
|
+
}
|
|
1189
|
+
const workspace = process.platform === "win32" ? spec.workspace.replace(/\\/g, "/").toLowerCase() : spec.workspace;
|
|
1190
|
+
return createHash("sha256").update(stableConfig([
|
|
1191
|
+
workspace,
|
|
1192
|
+
owner,
|
|
1193
|
+
spec.config,
|
|
1194
|
+
spec.policy
|
|
1195
|
+
])).digest("hex");
|
|
1196
|
+
}
|
|
1197
|
+
/** @public @author ddj 2026年10月20号 @param spec Authorized connection request. @returns Reference-counted single-flight lease. */
|
|
1198
|
+
acquire(spec) {
|
|
1199
|
+
if (this.stopped) throw new Error("MCP runtime is disposed");
|
|
1200
|
+
const key = this.keyOf(spec);
|
|
1201
|
+
let entry = this.entries.get(key);
|
|
1202
|
+
if (entry?.phase === "error" && entry.retryAt !== void 0 && Date.now() >= entry.retryAt && !entry.close) {
|
|
1203
|
+
this.entries.delete(key);
|
|
1204
|
+
entry = void 0;
|
|
1205
|
+
}
|
|
1206
|
+
if (entry && !entry.live) throw new Error(entry.error ?? "MCP 旧连接仍在释放,暂不能创建替代实例");
|
|
1207
|
+
if (!entry) {
|
|
1208
|
+
entry = {
|
|
1209
|
+
key,
|
|
1210
|
+
spec,
|
|
1211
|
+
refs: 0,
|
|
1212
|
+
phase: "queued",
|
|
1213
|
+
queuedAt: Date.now(),
|
|
1214
|
+
phaseAt: Date.now(),
|
|
1215
|
+
ready: Promise.resolve(),
|
|
1216
|
+
live: true,
|
|
1217
|
+
calls: Promise.resolve()
|
|
1218
|
+
};
|
|
1219
|
+
this.entries.set(key, entry);
|
|
1220
|
+
entry.ready = this.schedule(entry);
|
|
1221
|
+
entry.ready.catch(() => void 0);
|
|
1222
|
+
}
|
|
1223
|
+
if (entry.idle) clearTimeout(entry.idle);
|
|
1224
|
+
entry.idle = void 0;
|
|
1225
|
+
entry.refs += 1;
|
|
1226
|
+
let released = false;
|
|
1227
|
+
return {
|
|
1228
|
+
entry,
|
|
1229
|
+
release: () => {
|
|
1230
|
+
if (released) return;
|
|
1231
|
+
released = true;
|
|
1232
|
+
this.release(entry);
|
|
1233
|
+
}
|
|
1234
|
+
};
|
|
1235
|
+
}
|
|
1236
|
+
/** @private @author ddj 2026年10月20号 @param entry New entry. @returns Queued initialization, bounded to two concurrent starts. */
|
|
1237
|
+
schedule(entry) {
|
|
1238
|
+
return new Promise((resolve, reject) => {
|
|
1239
|
+
/** @author ddj 2026年10月20号 Initializes a still-owned queue item. */
|
|
1240
|
+
const run = async () => {
|
|
1241
|
+
try {
|
|
1242
|
+
await this.start(entry);
|
|
1243
|
+
resolve();
|
|
1244
|
+
} catch (error) {
|
|
1245
|
+
reject(error);
|
|
1246
|
+
}
|
|
1247
|
+
};
|
|
1248
|
+
this.queue.push(run);
|
|
1249
|
+
this.pump();
|
|
1250
|
+
});
|
|
1251
|
+
}
|
|
1252
|
+
/** @private @author ddj 2026年10月20号 Advances FIFO work; each task holds its slot until startup or cleanup quiesces. */
|
|
1253
|
+
pump() {
|
|
1254
|
+
while (this.running < 2 && this.queue.length) {
|
|
1255
|
+
const run = this.queue.shift();
|
|
1256
|
+
this.running += 1;
|
|
1257
|
+
const job = run().finally(() => {
|
|
1258
|
+
this.running -= 1;
|
|
1259
|
+
this.jobs.delete(job);
|
|
1260
|
+
this.pump();
|
|
1261
|
+
});
|
|
1262
|
+
this.jobs.add(job);
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
/** @private @author ddj 2026年10月20号 @param entry Connection to initialize. @returns Initialization completion, or failure after owned cleanup. */
|
|
1266
|
+
async start(entry) {
|
|
1267
|
+
if (!entry.live || this.stopped) throw new Error("MCP 启动已取消");
|
|
1268
|
+
entry.phase = "loading";
|
|
1269
|
+
entry.phaseAt = Date.now();
|
|
1270
|
+
entry.startAt = entry.phaseAt;
|
|
1271
|
+
let timer;
|
|
1272
|
+
const work = this.mount(entry);
|
|
1273
|
+
const deadline = new Promise((_resolve, reject) => {
|
|
1274
|
+
timer = setTimeout(() => reject(/* @__PURE__ */ new Error("MCP 初始化超时")), entry.spec.policy.startupTimeoutMs);
|
|
1275
|
+
timer.unref?.();
|
|
1276
|
+
});
|
|
1277
|
+
try {
|
|
1278
|
+
await Promise.race([work, deadline]);
|
|
1279
|
+
if (!entry.live) throw new Error("MCP 启动已取消");
|
|
1280
|
+
entry.phase = "ready";
|
|
1281
|
+
entry.finishedAt = Date.now();
|
|
1282
|
+
entry.connectedAt = entry.finishedAt;
|
|
1283
|
+
} catch (error) {
|
|
1284
|
+
entry.error = "MCP 初始化失败或超时,请查看 Host 日志";
|
|
1285
|
+
const diagnostic = "MCP initialization failed; phase=" + entry.phase + "; transport=" + entry.spec.config.transport + "; elapsedMs=" + (Date.now() - entry.startAt) + "; " + mcpErrorText(error, entry.spec.config);
|
|
1286
|
+
debugRecord(this.ctx, entry.spec.workspace, diagnostic, "warn");
|
|
1287
|
+
entry.live = false;
|
|
1288
|
+
entry.phase = "stopping";
|
|
1289
|
+
const closing = this.closeBridge(entry);
|
|
1290
|
+
await Promise.allSettled([closing, work]);
|
|
1291
|
+
entry.phase = "error";
|
|
1292
|
+
entry.finishedAt = Date.now();
|
|
1293
|
+
entry.retryAt = Date.now() + 5e3;
|
|
1294
|
+
throw new Error(entry.error);
|
|
1295
|
+
} finally {
|
|
1296
|
+
if (timer) clearTimeout(timer);
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
/** @private @author ddj 2026年10月20号 @param entry Owned generation. @returns Official initial sync; never publishes after invalidation. */
|
|
1300
|
+
async mount(entry) {
|
|
1301
|
+
const official = await this.load();
|
|
1302
|
+
if (!entry.live || this.stopped) return;
|
|
1303
|
+
const bridge = await this.make();
|
|
1304
|
+
if (!entry.live || this.stopped) {
|
|
1305
|
+
await bridge.dispose();
|
|
1306
|
+
return;
|
|
1307
|
+
}
|
|
1308
|
+
entry.bridge = bridge;
|
|
1309
|
+
entry.phase = "starting";
|
|
1310
|
+
entry.phaseAt = Date.now();
|
|
1311
|
+
entry.fiber = bridge.scope.ctx.plugin(official, entry.spec.config);
|
|
1312
|
+
await entry.fiber;
|
|
1313
|
+
if (entry.fiber?.state !== void 0 && entry.fiber.state !== 2) throw new Error("MCP 插件未完成激活");
|
|
1366
1314
|
}
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
const code = cwd.charCodeAt(i);
|
|
1382
|
-
const ch = String.fromCharCode(code);
|
|
1383
|
-
if (ch === "/" || ch === "\\" || ch === ":") {
|
|
1384
|
-
if (!separatorRun) readable += "-";
|
|
1385
|
-
separatorRun = true;
|
|
1386
|
-
} else if (ch !== "~" && /^[A-Za-z0-9._-]$/.test(ch)) {
|
|
1387
|
-
readable += ch;
|
|
1388
|
-
separatorRun = false;
|
|
1389
|
-
} else {
|
|
1390
|
-
readable += "~" + code.toString(16).toUpperCase().padStart(4, "0");
|
|
1391
|
-
separatorRun = false;
|
|
1315
|
+
/** @private @author ddj 2026年10月20号 @param entry Connection to close. @returns Shared closure barrier; rejection retains a fenced entry. */
|
|
1316
|
+
async closeBridge(entry) {
|
|
1317
|
+
if (entry.close) return entry.close;
|
|
1318
|
+
const close = entry.bridge?.dispose() ?? Promise.resolve();
|
|
1319
|
+
entry.close = close;
|
|
1320
|
+
try {
|
|
1321
|
+
await close;
|
|
1322
|
+
entry.bridge = void 0;
|
|
1323
|
+
entry.fiber = void 0;
|
|
1324
|
+
entry.close = void 0;
|
|
1325
|
+
} catch (error) {
|
|
1326
|
+
entry.error = "MCP 释放未确认,禁止重建,请查看 Host 日志";
|
|
1327
|
+
this.ctx.logger?.warn?.("MCP cleanup failed: " + String(error));
|
|
1328
|
+
throw error;
|
|
1392
1329
|
}
|
|
1393
1330
|
}
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
/**
|
|
1439
|
-
function
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1331
|
+
/** @private @author ddj 2026年10月20号 @param entry One lease's owner. Arms workspace-only warm retention. */
|
|
1332
|
+
release(entry) {
|
|
1333
|
+
entry.refs = Math.max(0, entry.refs - 1);
|
|
1334
|
+
if (entry.refs || !entry.live) return;
|
|
1335
|
+
if (entry.spec.policy.sharing === "agent") {
|
|
1336
|
+
this.retire(entry).catch(() => void 0);
|
|
1337
|
+
return;
|
|
1338
|
+
}
|
|
1339
|
+
entry.idle = setTimeout(() => {
|
|
1340
|
+
this.retire(entry).catch(() => void 0);
|
|
1341
|
+
}, 6e4);
|
|
1342
|
+
entry.idle.unref?.();
|
|
1343
|
+
}
|
|
1344
|
+
/** @public @author ddj 2026年10月20号 @param entry Invalid generation. @returns Quiescent close; callers may await before starting a replacement. */
|
|
1345
|
+
async retire(entry) {
|
|
1346
|
+
entry.live = false;
|
|
1347
|
+
entry.phase = "stopping";
|
|
1348
|
+
if (entry.idle) clearTimeout(entry.idle);
|
|
1349
|
+
await this.closeBridge(entry);
|
|
1350
|
+
await entry.ready.catch(() => void 0);
|
|
1351
|
+
await entry.calls.catch(() => void 0);
|
|
1352
|
+
if (this.entries.get(entry.key) === entry) this.entries.delete(entry.key);
|
|
1353
|
+
}
|
|
1354
|
+
/** @public @author ddj 2026年10月20号 @param entry Authorized connection. @param action Per-call work retaining original execution identity. @param signal Caller cancellation. @returns Serialized shared result; no direct registry bypass is introduced here. */
|
|
1355
|
+
run(entry, action, signal) {
|
|
1356
|
+
/** @author ddj 2026年10月20号 Checks cancellation and generation again at actual dispatch. */
|
|
1357
|
+
const invoke = async () => {
|
|
1358
|
+
await waitMcp(entry.ready, signal);
|
|
1359
|
+
if (signal?.aborted || !entry.live || entry.phase !== "ready") throw new Error("MCP 连接不可用或调用已取消");
|
|
1360
|
+
return action();
|
|
1361
|
+
};
|
|
1362
|
+
const result = entry.calls.then(invoke);
|
|
1363
|
+
entry.calls = result.catch(() => void 0);
|
|
1364
|
+
return result;
|
|
1365
|
+
}
|
|
1366
|
+
/** @public @author ddj 2026年10月20号 @returns Full disposal of entries, queue, timers and owned work. */
|
|
1367
|
+
async dispose() {
|
|
1368
|
+
this.stopped = true;
|
|
1369
|
+
await Promise.all([...this.entries.values()].map((entry) => this.retire(entry)));
|
|
1370
|
+
await Promise.all([...this.jobs]);
|
|
1371
|
+
}
|
|
1372
|
+
};
|
|
1373
|
+
//#endregion
|
|
1374
|
+
//#region src/mcpDiscover.ts
|
|
1375
|
+
/** @public @author ddj 2026年10月20号 @param runtime Authorized runtime. @param ctx Host registration context. @returns Global discovery cleanup; normal capability filters remain authoritative. */
|
|
1376
|
+
function installDiscover(runtime, ctx) {
|
|
1377
|
+
const tools = ctx.get("tools");
|
|
1378
|
+
/** @author ddj 2026年10月20号 @param args Validated operation. @param execution Registry-owned caller identity. @returns Safe capability catalogue or real connected schemas. */
|
|
1379
|
+
const execute = async (args, execution) => {
|
|
1380
|
+
const agent = execution.agent;
|
|
1381
|
+
if (!agent || !runtime.agents.has(agent)) throw new Error("会话未激活,不能使用项目 MCP 发现入口");
|
|
1382
|
+
if (!args || typeof args !== "object" || Array.isArray(args) || Object.keys(args).some((key) => !["action", "serverName"].includes(key))) throw new Error("发现入口仅接受 action 和 serverName");
|
|
1383
|
+
if (!["list", "connect"].includes(args.action)) throw new Error("action 必须为 list 或 connect");
|
|
1384
|
+
if (args.action === "list") return runtime.catalogue(agent);
|
|
1385
|
+
if (typeof args.serverName !== "string") throw new Error("请指定已配置的 serverName");
|
|
1386
|
+
return runtime.connect(agent, args.serverName, execution.signal);
|
|
1387
|
+
};
|
|
1388
|
+
/** @author ddj 2026年10月20号 @param _args Unused. @param result Safe canonical JSON. @returns Native content without bypassing output validation. */
|
|
1389
|
+
const render = (_args, result) => [{
|
|
1390
|
+
type: "text",
|
|
1391
|
+
text: JSON.stringify(result)
|
|
1392
|
+
}];
|
|
1393
|
+
return tools.register({
|
|
1394
|
+
name: "discover_project_mcp",
|
|
1395
|
+
description: "List this workspace’s configured project MCP services without starting them, or connect one to discover its real tools. After connecting, call discovered tools in the NEXT request/program; never guess schemas. Global/preset MCPs are not included.",
|
|
1396
|
+
parameters: {
|
|
1397
|
+
type: "object",
|
|
1398
|
+
properties: {
|
|
1399
|
+
action: {
|
|
1400
|
+
type: "string",
|
|
1401
|
+
enum: ["list", "connect"]
|
|
1402
|
+
},
|
|
1403
|
+
serverName: { type: "string" }
|
|
1404
|
+
},
|
|
1405
|
+
required: ["action"],
|
|
1406
|
+
additionalProperties: false
|
|
1407
|
+
},
|
|
1408
|
+
output: {
|
|
1409
|
+
schema: {},
|
|
1410
|
+
render
|
|
1411
|
+
},
|
|
1412
|
+
execute
|
|
1413
|
+
});
|
|
1468
1414
|
}
|
|
1469
|
-
/**
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1415
|
+
/** @public @author ddj 2026年10月20号 @param runtime Active owners. @param agent Scoped catalogue owner. @returns Scoped instruction cleanup; does not register a tool override. */
|
|
1416
|
+
function discoverPrompt(runtime, agent) {
|
|
1417
|
+
const prompt = agent.ctx.get("systemPrompt");
|
|
1418
|
+
/** @author ddj 2026年10月20号 @returns Current enabled names only; no credential-bearing configuration. */
|
|
1419
|
+
const text = () => {
|
|
1420
|
+
const names = runtime.catalogue(agent).map((server) => server.serverName);
|
|
1421
|
+
if (!names.length) return "";
|
|
1422
|
+
return "Project MCP capabilities (may not be started): " + JSON.stringify(names) + ". Use discover_project_mcp action=list, then action=connect with serverName when needed. Only use returned real tool schemas. Native tools appear on the next model request; PTC must finish the current program and start a new program to refresh its SDK. Do not eval or bypass tool approval.";
|
|
1423
|
+
};
|
|
1424
|
+
return prompt.section({
|
|
1425
|
+
name: "vscode-mode:project-mcp",
|
|
1426
|
+
order: 3090,
|
|
1427
|
+
text,
|
|
1428
|
+
interpolate: false
|
|
1429
|
+
});
|
|
1478
1430
|
}
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1431
|
+
//#endregion
|
|
1432
|
+
//#region src/shared/diff.ts
|
|
1433
|
+
/** 统一取对应 hunk;callHunk 只用于旧记录缺失 hunks 时兼容。 */
|
|
1434
|
+
function preciseHunk(record, idx) {
|
|
1435
|
+
return hunkOf(record, idx);
|
|
1482
1436
|
}
|
|
1483
|
-
/**
|
|
1484
|
-
function
|
|
1485
|
-
|
|
1437
|
+
/** 轻量、确定性的字符串指纹;不引入 Node crypto。 */
|
|
1438
|
+
function fingerprint(text) {
|
|
1439
|
+
if (typeof text !== "string") return null;
|
|
1440
|
+
let hash = 2166136261;
|
|
1441
|
+
for (let i = 0; i < text.length; i++) {
|
|
1442
|
+
hash ^= text.charCodeAt(i);
|
|
1443
|
+
hash = Math.imul(hash, 16777619);
|
|
1444
|
+
}
|
|
1445
|
+
return "fnv1a:" + (hash >>> 0).toString(16) + ":" + text.length;
|
|
1486
1446
|
}
|
|
1487
|
-
/**
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
*/
|
|
1494
|
-
function skillsDirOf(moduleUrl) {
|
|
1495
|
-
return join(dirname(fileURLToPath(moduleUrl)), "..", "skills");
|
|
1447
|
+
/** 取记录第 idx 个有效 hunk;只有记录缺 hunk 时才回退 callHunk。 */
|
|
1448
|
+
function hunkOf(record, idx) {
|
|
1449
|
+
const hunk = Array.isArray(record.hunks) ? record.hunks[idx] : void 0;
|
|
1450
|
+
if (hunk && typeof hunk.newText === "string") return hunk;
|
|
1451
|
+
if (idx === 0 && record.callHunk && typeof record.callHunk.newText === "string") return record.callHunk;
|
|
1452
|
+
return null;
|
|
1496
1453
|
}
|
|
1497
|
-
/**
|
|
1498
|
-
function
|
|
1499
|
-
|
|
1500
|
-
if (cfg && typeof cfg.imageDir === "string" && cfg.imageDir) return cfg.imageDir.replace(/\\/g, "/");
|
|
1501
|
-
return assetsDirOf(moduleUrl);
|
|
1454
|
+
/** hunk 是否表示没有实际内容变化。 */
|
|
1455
|
+
function isNoopHunk(hunk) {
|
|
1456
|
+
return !!hunk && hunk.oldText !== null && hunk.oldText === hunk.newText;
|
|
1502
1457
|
}
|
|
1503
1458
|
/**
|
|
1504
|
-
*
|
|
1505
|
-
*
|
|
1459
|
+
* 差异比较口径归一化:剥离 UTF-8 BOM、统一 CRLF 为 LF。
|
|
1460
|
+
* 背景:外部工具(pwsh/编辑器)可能改变 BOM/行尾,导致 after 指纹与 hunk
|
|
1461
|
+
* 定位在内容语义未变的情况下误报 conflict,把待确认修改从 UI 排除。
|
|
1462
|
+
* @author ddj 2026年09月09号
|
|
1506
1463
|
*/
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
};
|
|
1513
|
-
/** 缓存文件名 → 种类与 schema 版本(`<kind>.v<schema>.json`;未登记种类/命名不合规 → null)。 */
|
|
1514
|
-
function parseCacheName(filename) {
|
|
1515
|
-
const m = /^([a-z-]+)\.v(\d+)\.json$/.exec(filename);
|
|
1516
|
-
if (!m) return null;
|
|
1517
|
-
const kind = m[1];
|
|
1518
|
-
if (CACHE_SCHEMAS[kind] === void 0) return null;
|
|
1464
|
+
function normalizeForCompare(text) {
|
|
1465
|
+
return text.replace(/^\uFEFF/, "").replace(/\r\n/g, "\n");
|
|
1466
|
+
}
|
|
1467
|
+
/** 归一化一个 hunk 的两侧文本(与 normalizeForCompare 同口径)。 */
|
|
1468
|
+
function normalizeHunk(hunk) {
|
|
1519
1469
|
return {
|
|
1520
|
-
|
|
1521
|
-
|
|
1470
|
+
oldText: hunk.oldText === null ? null : normalizeForCompare(hunk.oldText),
|
|
1471
|
+
newText: normalizeForCompare(hunk.newText)
|
|
1522
1472
|
};
|
|
1523
1473
|
}
|
|
1524
|
-
/**
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
const full = join(dir, name);
|
|
1539
|
-
let drop = dropAll;
|
|
1540
|
-
if (!drop) {
|
|
1541
|
-
if (!isCurrentCacheFile(name)) drop = true;
|
|
1542
|
-
else try {
|
|
1543
|
-
if (now - (await stat(full)).mtimeMs > 2592e6) drop = true;
|
|
1544
|
-
} catch (error) {
|
|
1545
|
-
drop = true;
|
|
1474
|
+
/** 按文件顺序为多 hunk 写入不重复的 after 坐标。 */
|
|
1475
|
+
function annotateHunks(hunks, before, after) {
|
|
1476
|
+
if (typeof after !== "string") return hunks;
|
|
1477
|
+
let cursor = 0;
|
|
1478
|
+
return hunks.map((hunk) => {
|
|
1479
|
+
if (hunk.newText.length > 0) {
|
|
1480
|
+
const start = after.indexOf(hunk.newText, cursor);
|
|
1481
|
+
if (start >= 0) {
|
|
1482
|
+
cursor = start + hunk.newText.length;
|
|
1483
|
+
return {
|
|
1484
|
+
...hunk,
|
|
1485
|
+
afterStart: start,
|
|
1486
|
+
afterEnd: cursor
|
|
1487
|
+
};
|
|
1546
1488
|
}
|
|
1489
|
+
return hunk;
|
|
1547
1490
|
}
|
|
1548
|
-
if (
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1491
|
+
if (hunks.length !== 1 || typeof before !== "string" || before === after || hunk.oldText === null) return hunk;
|
|
1492
|
+
const prefix = commonPrefix(before, after);
|
|
1493
|
+
const suffix = commonSuffix(before, after, prefix);
|
|
1494
|
+
const removedStart = prefix;
|
|
1495
|
+
const removedEnd = Math.max(removedStart, before.length - suffix);
|
|
1496
|
+
return {
|
|
1497
|
+
...hunk,
|
|
1498
|
+
afterStart: removedStart,
|
|
1499
|
+
afterEnd: removedStart,
|
|
1500
|
+
beforeStart: removedStart,
|
|
1501
|
+
beforeEnd: removedEnd
|
|
1502
|
+
};
|
|
1503
|
+
});
|
|
1554
1504
|
}
|
|
1555
|
-
/**
|
|
1556
|
-
|
|
1557
|
-
const
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1505
|
+
/** 计算两个文本的最长公共前缀长度。 */
|
|
1506
|
+
function commonPrefix(left, right) {
|
|
1507
|
+
const limit = Math.min(left.length, right.length);
|
|
1508
|
+
let i = 0;
|
|
1509
|
+
while (i < limit && left.charCodeAt(i) === right.charCodeAt(i)) i++;
|
|
1510
|
+
return i;
|
|
1511
|
+
}
|
|
1512
|
+
/** 计算不与 prefix 重叠的最长公共后缀长度。 */
|
|
1513
|
+
function commonSuffix(left, right, prefix) {
|
|
1514
|
+
const limit = Math.min(left.length, right.length) - prefix;
|
|
1515
|
+
let i = 0;
|
|
1516
|
+
while (i < limit && left.charCodeAt(left.length - 1 - i) === right.charCodeAt(right.length - 1 - i)) i++;
|
|
1517
|
+
return i;
|
|
1518
|
+
}
|
|
1519
|
+
/**
|
|
1520
|
+
* 懒扫描候选:先找 cursor 后未占用项,找不到再回退至首个未占用项。
|
|
1521
|
+
* @author ddj 2026年09月29号
|
|
1522
|
+
* @param content 当前内容
|
|
1523
|
+
* @param needle 搜索串
|
|
1524
|
+
* @param cursor 前一块的结束位置
|
|
1525
|
+
* @param occupied 已选区间
|
|
1526
|
+
* @param state 此搜索串已扫描的候选
|
|
1527
|
+
* @returns 与完整候选列表双轮查找相同的匹配
|
|
1528
|
+
*/
|
|
1529
|
+
function findCandidate(content, needle, cursor, occupied, state) {
|
|
1530
|
+
if (!needle.length) return void 0;
|
|
1531
|
+
let fallback;
|
|
1532
|
+
for (let i = 0; i < state.ranges.length; i++) {
|
|
1533
|
+
const range = state.ranges[i];
|
|
1534
|
+
if (occupied.some((used) => overlaps(range, used))) continue;
|
|
1535
|
+
if (range.start >= cursor) return range;
|
|
1536
|
+
if (!fallback) fallback = range;
|
|
1537
|
+
}
|
|
1538
|
+
while (!state.done) {
|
|
1539
|
+
const at = content.indexOf(needle, state.next);
|
|
1540
|
+
if (at < 0) {
|
|
1541
|
+
state.done = true;
|
|
1542
|
+
break;
|
|
1543
|
+
}
|
|
1544
|
+
const range = {
|
|
1545
|
+
start: at,
|
|
1546
|
+
end: at + needle.length
|
|
1547
|
+
};
|
|
1548
|
+
state.ranges.push(range);
|
|
1549
|
+
state.next = range.end;
|
|
1550
|
+
if (occupied.some((used) => overlaps(range, used))) continue;
|
|
1551
|
+
if (range.start >= cursor) return range;
|
|
1552
|
+
if (!fallback) fallback = range;
|
|
1565
1553
|
}
|
|
1554
|
+
return fallback;
|
|
1566
1555
|
}
|
|
1567
|
-
/**
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1556
|
+
/**
|
|
1557
|
+
* 从 after 快照定位所有 hunk,按 hunk 顺序优先选未占用且靠近前一块的候选。
|
|
1558
|
+
* 若 hunk 带 afterStart/afterEnd,则优先验证快照坐标,避免重复文本错配。
|
|
1559
|
+
* @author ddj 2026年08月24号
|
|
1560
|
+
* @param content 当前文件内容
|
|
1561
|
+
* @param hunks 待定位 hunk
|
|
1562
|
+
* @returns 每个 hunk 一个结果,matched=false 表示冲突/stale
|
|
1563
|
+
*/
|
|
1564
|
+
function locateHunks(content, hunks) {
|
|
1565
|
+
const locations = [];
|
|
1566
|
+
let cursor = 0;
|
|
1567
|
+
const occupied = [];
|
|
1568
|
+
const matches = /* @__PURE__ */ new Map();
|
|
1569
|
+
for (let idx = 0; idx < hunks.length; idx++) {
|
|
1570
|
+
const hunk = hunks[idx];
|
|
1571
|
+
const newText = hunk.newText;
|
|
1572
|
+
const snapshot = Number.isInteger(hunk.afterStart) && Number.isInteger(hunk.afterEnd) ? {
|
|
1573
|
+
start: hunk.afterStart,
|
|
1574
|
+
end: hunk.afterEnd
|
|
1575
|
+
} : null;
|
|
1576
|
+
const snapshotMatches = snapshot && snapshot.start >= 0 && snapshot.end >= snapshot.start && (newText.length === 0 || content.slice(snapshot.start, snapshot.end) === newText) ? snapshot : null;
|
|
1577
|
+
let candidate;
|
|
1578
|
+
if (snapshotMatches) {
|
|
1579
|
+
if (!occupied.some((used) => overlaps(snapshotMatches, used))) candidate = snapshotMatches;
|
|
1580
|
+
} else if (newText.length) {
|
|
1581
|
+
let state = matches.get(newText);
|
|
1582
|
+
if (!state) {
|
|
1583
|
+
state = {
|
|
1584
|
+
ranges: [],
|
|
1585
|
+
next: 0,
|
|
1586
|
+
done: false
|
|
1587
|
+
};
|
|
1588
|
+
matches.set(newText, state);
|
|
1589
|
+
}
|
|
1590
|
+
candidate = findCandidate(content, newText, cursor, occupied, state);
|
|
1591
|
+
}
|
|
1592
|
+
if (candidate) {
|
|
1593
|
+
locations.push({
|
|
1594
|
+
idx,
|
|
1595
|
+
hunk,
|
|
1596
|
+
...candidate,
|
|
1597
|
+
matched: true
|
|
1598
|
+
});
|
|
1599
|
+
occupied.push(candidate);
|
|
1600
|
+
cursor = Math.max(cursor, candidate.end);
|
|
1601
|
+
continue;
|
|
1602
|
+
}
|
|
1603
|
+
if (newText.length === 0 && hunk.afterStart === void 0 && hunk.afterEnd === void 0 && content.length === 0) {
|
|
1604
|
+
locations.push({
|
|
1605
|
+
idx,
|
|
1606
|
+
hunk,
|
|
1607
|
+
start: 0,
|
|
1608
|
+
end: 0,
|
|
1609
|
+
matched: true
|
|
1610
|
+
});
|
|
1611
|
+
continue;
|
|
1612
|
+
}
|
|
1613
|
+
if (newText.length === 0 && hunk.afterStart !== void 0 && hunk.afterEnd !== void 0 && hunk.afterStart === hunk.afterEnd && hunk.afterStart >= 0 && hunk.afterStart <= content.length) {
|
|
1614
|
+
const point = {
|
|
1615
|
+
start: hunk.afterStart,
|
|
1616
|
+
end: hunk.afterEnd
|
|
1617
|
+
};
|
|
1618
|
+
if (!occupied.some((used) => overlaps(point, used))) {
|
|
1619
|
+
locations.push({
|
|
1620
|
+
idx,
|
|
1621
|
+
hunk,
|
|
1622
|
+
...point,
|
|
1623
|
+
matched: true
|
|
1581
1624
|
});
|
|
1582
|
-
|
|
1625
|
+
occupied.push(point);
|
|
1626
|
+
cursor = Math.max(cursor, point.end);
|
|
1627
|
+
continue;
|
|
1628
|
+
}
|
|
1583
1629
|
}
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
await rm(f.path, { force: true });
|
|
1592
|
-
total -= f.size;
|
|
1593
|
-
removed.push(f.path.slice(root.length + 1));
|
|
1594
|
-
} catch (error) {}
|
|
1630
|
+
locations.push({
|
|
1631
|
+
idx,
|
|
1632
|
+
hunk,
|
|
1633
|
+
start: -1,
|
|
1634
|
+
end: -1,
|
|
1635
|
+
matched: false
|
|
1636
|
+
});
|
|
1595
1637
|
}
|
|
1638
|
+
return locations;
|
|
1596
1639
|
}
|
|
1597
|
-
/**
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
* - user/ 用户级 → 按「schema 版本 + 保留期」清文件
|
|
1602
|
-
* - 总大小超预算 → 按最旧先删(最后防线,防任意增长撑爆磁盘)
|
|
1603
|
-
* best-effort 静默。
|
|
1604
|
-
* @author ddj 2026年09月01号
|
|
1605
|
-
* @param home DSH home(测试可注入)
|
|
1606
|
-
* @param cap 总预算字节数(缺省 CACHE_TOTAL_CAP;测试可注入小预算)
|
|
1607
|
-
* @returns 删除的文件相对路径清单
|
|
1608
|
-
*/
|
|
1609
|
-
async function sweepTreeCache(home = dshHome(), cap = CACHE_TOTAL_CAP) {
|
|
1610
|
-
const dir = pluginCacheRoot(home);
|
|
1611
|
-
const names = await readdir(dir).catch(() => []);
|
|
1612
|
-
if (!names.length) return [];
|
|
1613
|
-
const removed = [];
|
|
1614
|
-
const now = Date.now();
|
|
1615
|
-
await Promise.all(names.map(async (name) => {
|
|
1616
|
-
const full = join(dir, name);
|
|
1617
|
-
try {
|
|
1618
|
-
if ((await stat(full)).isDirectory()) {
|
|
1619
|
-
if (name === "workspace") await sweepWorkspaceTier(full, removed, now);
|
|
1620
|
-
else if (name === "user") await sweepDirFiles(full, removed, now, false);
|
|
1621
|
-
else {
|
|
1622
|
-
await rm(full, {
|
|
1623
|
-
recursive: true,
|
|
1624
|
-
force: true
|
|
1625
|
-
});
|
|
1626
|
-
removed.push(name);
|
|
1627
|
-
}
|
|
1628
|
-
} else {
|
|
1629
|
-
await rm(full, { force: true });
|
|
1630
|
-
removed.push(name);
|
|
1631
|
-
}
|
|
1632
|
-
} catch (error) {}
|
|
1633
|
-
}));
|
|
1634
|
-
await sweepByTotalCap(dir, removed, cap);
|
|
1635
|
-
return removed;
|
|
1640
|
+
/** 判断两个半开区间是否重叠。零长度区间只与同一位置的零长度区间重叠。 */
|
|
1641
|
+
function overlaps(left, right) {
|
|
1642
|
+
if (left.start === left.end || right.start === right.end) return left.start === right.start && left.end === right.end;
|
|
1643
|
+
return left.start < right.end && right.start < left.end;
|
|
1636
1644
|
}
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
* - 格式:`[dsh-vscode-mode][:scope] 消息`;前缀只在模块内部拼接,业务代码禁止手写前缀
|
|
1643
|
-
* - 级别语义:debug=诊断明细 / info=装配与路由里程碑 / warn=降级与兼容回退 / error=失败
|
|
1644
|
-
* - 出口:host 面经 bindHostLog(ctx) 绑定 ctx.logger(缺失回退 console);client 面固定 console
|
|
1645
|
-
* - 诊断文件通道(edrv.debug → ~/.dsh/dsh-vscode-mode/logs/)见 debugLog.ts,与本模块前缀共用
|
|
1646
|
-
* 新增日志一律 `import { log } from '../log.js'`(client 为 './log.js')后调 log.debug/info/warn/error;
|
|
1647
|
-
* 子域日志用 log.child('scope'),禁止另起 console。
|
|
1648
|
-
* 作者 ddj 2026-09-08
|
|
1649
|
-
*/
|
|
1650
|
-
/** 插件统一日志前缀(与包安装名一致)。 */
|
|
1651
|
-
const LOG_PREFIX = "[dsh-vscode-mode]";
|
|
1652
|
-
/** 默认级别到 console 方法名的映射。 */
|
|
1653
|
-
const CONSOLE_METHODS = {
|
|
1654
|
-
debug: "debug",
|
|
1655
|
-
info: "info",
|
|
1656
|
-
warn: "warn",
|
|
1657
|
-
error: "error"
|
|
1658
|
-
};
|
|
1659
|
-
/**
|
|
1660
|
-
* 默认出口:console(级别直映方法;方法缺失时静默丢弃,不抛错)。
|
|
1661
|
-
* @author ddj 2026年09月08号
|
|
1662
|
-
* @returns console 出口
|
|
1663
|
-
*/
|
|
1664
|
-
function consoleSink() {
|
|
1665
|
-
return (level, line) => {
|
|
1666
|
-
const emit = console[CONSOLE_METHODS[level]];
|
|
1667
|
-
if (typeof emit === "function") emit.call(console, line);
|
|
1668
|
-
};
|
|
1645
|
+
/** 在可定位区间上执行一次 hunk 反向替换。 */
|
|
1646
|
+
function replaceRange(content, location, reverse = false) {
|
|
1647
|
+
if (!location.matched || location.start < 0) return null;
|
|
1648
|
+
const replacement = reverse ? location.hunk.oldText ?? "" : location.hunk.newText;
|
|
1649
|
+
return content.slice(0, location.start) + replacement + content.slice(location.end);
|
|
1669
1650
|
}
|
|
1670
1651
|
/**
|
|
1671
|
-
*
|
|
1672
|
-
* @author ddj 2026年09月
|
|
1673
|
-
* @param
|
|
1674
|
-
* @
|
|
1652
|
+
* 按位置倒序收集片段,最后一次拼接;非互斥区间退回原替换语义。
|
|
1653
|
+
* @author ddj 2026年09月29号
|
|
1654
|
+
* @param content 当前文件内容
|
|
1655
|
+
* @param locations 已定位区间
|
|
1656
|
+
* @param reverse 是否反向应用
|
|
1657
|
+
* @returns 合成内容及失效块索引
|
|
1675
1658
|
*/
|
|
1676
|
-
function
|
|
1677
|
-
const
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1659
|
+
function applyLocations(content, locations, reverse = false) {
|
|
1660
|
+
const stale = locations.filter((location) => !location.matched).map((location) => location.idx);
|
|
1661
|
+
const matched = locations.filter((location) => location.matched).sort((a, b) => b.start - a.start);
|
|
1662
|
+
const parts = [];
|
|
1663
|
+
let end = content.length;
|
|
1664
|
+
for (const location of matched) {
|
|
1665
|
+
if (location.start < 0 || location.end > end) {
|
|
1666
|
+
let out = content;
|
|
1667
|
+
for (const item of matched) {
|
|
1668
|
+
const next = replaceRange(out, item, reverse);
|
|
1669
|
+
if (next === null) stale.push(item.idx);
|
|
1670
|
+
else out = next;
|
|
1671
|
+
}
|
|
1672
|
+
return {
|
|
1673
|
+
content: out,
|
|
1674
|
+
stale: stale.sort((a, b) => a - b)
|
|
1675
|
+
};
|
|
1676
|
+
}
|
|
1677
|
+
parts.push(content.slice(location.end, end), reverse ? location.hunk.oldText ?? "" : location.hunk.newText);
|
|
1678
|
+
end = location.start;
|
|
1679
|
+
}
|
|
1680
|
+
parts.push(content.slice(0, end));
|
|
1681
|
+
parts.reverse();
|
|
1682
|
+
return {
|
|
1683
|
+
content: parts.join(""),
|
|
1684
|
+
stale: stale.sort((a, b) => a - b)
|
|
1683
1685
|
};
|
|
1684
1686
|
}
|
|
1685
1687
|
/**
|
|
1686
|
-
*
|
|
1687
|
-
* @author ddj 2026年
|
|
1688
|
-
* @param
|
|
1689
|
-
* @param
|
|
1690
|
-
* @
|
|
1688
|
+
* 写决策到记录(scope=call 整体 / hunk 单个块)。
|
|
1689
|
+
* @author ddj 2026年08月20号
|
|
1690
|
+
* @param record 记录
|
|
1691
|
+
* @param scope 作用域
|
|
1692
|
+
* @param idx hunk 下标(仅 scope=hunk 时生效)
|
|
1693
|
+
* @param value 决策值
|
|
1694
|
+
* @returns 更新后的记录(原对象就地修改)
|
|
1691
1695
|
*/
|
|
1692
|
-
function
|
|
1693
|
-
const
|
|
1694
|
-
|
|
1696
|
+
function markDecision(record, scope, idx, value) {
|
|
1697
|
+
const dec = record.decisions;
|
|
1698
|
+
if (scope === "hunk" && Array.isArray(dec.perHunk) && Number.isInteger(idx)) {
|
|
1699
|
+
const i = idx;
|
|
1700
|
+
if (i >= 0 && i < dec.perHunk.length) dec.perHunk[i] = value;
|
|
1701
|
+
} else {
|
|
1702
|
+
dec.call = value;
|
|
1703
|
+
if (Array.isArray(dec.perHunk)) dec.perHunk = dec.perHunk.map(() => value);
|
|
1704
|
+
}
|
|
1705
|
+
record.at = (/* @__PURE__ */ new Date()).toISOString();
|
|
1706
|
+
return record;
|
|
1707
|
+
}
|
|
1708
|
+
/** 记录是否已无任何待决策差异(superseded 或全部已决策)。 */
|
|
1709
|
+
function recordResolved(record) {
|
|
1710
|
+
if (record.superseded === true) return true;
|
|
1711
|
+
const hunks = Array.isArray(record.hunks) ? record.hunks : [];
|
|
1712
|
+
const perHunk = Array.isArray(record.decisions.perHunk) ? record.decisions.perHunk : [];
|
|
1713
|
+
const count = Math.max(hunks.length, perHunk.length);
|
|
1714
|
+
if (!count) return record.decisions.call === "accepted" || record.decisions.call === "rejected";
|
|
1715
|
+
for (let idx = 0; idx < count; idx++) {
|
|
1716
|
+
const precise = preciseHunk(record, idx);
|
|
1717
|
+
const value = perHunk[idx] ?? record.decisions.call;
|
|
1718
|
+
if (isNoopHunk(precise) && value === "pending") return false;
|
|
1719
|
+
if (value !== "accepted" && value !== "rejected") return false;
|
|
1720
|
+
}
|
|
1721
|
+
return true;
|
|
1695
1722
|
}
|
|
1696
1723
|
/**
|
|
1697
|
-
*
|
|
1698
|
-
* @author ddj 2026年
|
|
1699
|
-
* @param
|
|
1700
|
-
* @
|
|
1701
|
-
* @returns 日志器
|
|
1724
|
+
* 记录决策摘要(采纳/拒绝/待处理计数,superseded 标记)。
|
|
1725
|
+
* @author ddj 2026年08月20号
|
|
1726
|
+
* @param record 记录
|
|
1727
|
+
* @returns 摘要
|
|
1702
1728
|
*/
|
|
1703
|
-
function
|
|
1704
|
-
const
|
|
1729
|
+
function recSummary(record) {
|
|
1730
|
+
const hunks = Array.isArray(record.hunks) ? record.hunks : [];
|
|
1731
|
+
const perHunk = Array.isArray(record.decisions.perHunk) ? record.decisions.perHunk : [];
|
|
1732
|
+
const n = Math.max(1, hunks.length, perHunk.length);
|
|
1733
|
+
let accepted = 0;
|
|
1734
|
+
let rejected = 0;
|
|
1735
|
+
let pending = 0;
|
|
1736
|
+
for (let i = 0; i < n; i++) {
|
|
1737
|
+
const hunk = preciseHunk(record, i);
|
|
1738
|
+
const value = perHunk[i] ?? record.decisions.call;
|
|
1739
|
+
if (i < hunks.length && isNoopHunk(hunk) && value === "pending") continue;
|
|
1740
|
+
if (value === "accepted") accepted++;
|
|
1741
|
+
else if (value === "rejected") rejected++;
|
|
1742
|
+
else pending++;
|
|
1743
|
+
}
|
|
1705
1744
|
return {
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
bind(next) {
|
|
1711
|
-
state.sink = next;
|
|
1712
|
-
},
|
|
1713
|
-
child(next) {
|
|
1714
|
-
return makeLogger(state, scope ? scope + "." + next : next);
|
|
1715
|
-
}
|
|
1745
|
+
accepted,
|
|
1746
|
+
rejected,
|
|
1747
|
+
pending,
|
|
1748
|
+
superseded: record.superseded === true
|
|
1716
1749
|
};
|
|
1717
1750
|
}
|
|
1718
1751
|
/**
|
|
1719
|
-
*
|
|
1720
|
-
*
|
|
1721
|
-
* @
|
|
1722
|
-
* @param
|
|
1723
|
-
* @
|
|
1752
|
+
* 重建"本批次修改前"内容:把仍待处理(pending)的差异块按 新→旧 顺序从当前内容反解
|
|
1753
|
+
* (newText → oldText)。已采纳/已拒绝的块跳过。反解失败的块标记为 stale。
|
|
1754
|
+
* @author ddj 2026年08月20号
|
|
1755
|
+
* @param records 该文件的活动记录
|
|
1756
|
+
* @param content 当前磁盘内容
|
|
1757
|
+
* @returns 重建内容与 stale 定位
|
|
1724
1758
|
*/
|
|
1725
|
-
function
|
|
1726
|
-
|
|
1759
|
+
function reconstructOriginal(records, content) {
|
|
1760
|
+
const pending = [];
|
|
1761
|
+
for (const rec of records) for (let i = 0; i < rec.hunks.length; i++) {
|
|
1762
|
+
const st = rec.decisions.perHunk[i] ?? rec.decisions.call;
|
|
1763
|
+
const hunk = preciseHunk(rec, i);
|
|
1764
|
+
if (st === "pending" && hunk && !isNoopHunk(hunk)) pending.push({
|
|
1765
|
+
rec,
|
|
1766
|
+
idx: i,
|
|
1767
|
+
hunk
|
|
1768
|
+
});
|
|
1769
|
+
}
|
|
1770
|
+
pending.sort((a, b) => a.rec.at < b.rec.at ? 1 : a.rec.at > b.rec.at ? -1 : 0);
|
|
1771
|
+
const rebuilt = applyLocations(content, locateHunks(content, pending.map((item) => item.hunk)), true);
|
|
1772
|
+
return {
|
|
1773
|
+
content: rebuilt.content,
|
|
1774
|
+
stale: rebuilt.stale.map((idx) => ({
|
|
1775
|
+
callId: pending[idx].rec.callId,
|
|
1776
|
+
idx: pending[idx].idx
|
|
1777
|
+
}))
|
|
1778
|
+
};
|
|
1727
1779
|
}
|
|
1728
|
-
//#endregion
|
|
1729
|
-
//#region src/log.ts
|
|
1730
|
-
/**
|
|
1731
|
-
* dsh-vscode-mode host — 日志单例(全 host 面唯一日志入口)。
|
|
1732
|
-
* 默认 console 出口;apply() 首行调 bindHostLog(ctx) 切到 ctx.logger(缺失回退 console)。
|
|
1733
|
-
* 用法:`import { log } from './log.js'` → `log.debug/info/warn/error(msg)`;子域用 `log.child('scope')`。
|
|
1734
|
-
* 作者 ddj 2026-09-08
|
|
1735
|
-
*/
|
|
1736
|
-
/** host 面插件日志单例(格式/级别语义见 shared/logger.ts 头注释)。 */
|
|
1737
|
-
const log = createLogger();
|
|
1738
1780
|
/**
|
|
1739
|
-
*
|
|
1740
|
-
* @author ddj 2026年
|
|
1741
|
-
*
|
|
1781
|
+
* @public
|
|
1782
|
+
* @author ddj 2026年10月10号
|
|
1783
|
+
* 判断记录是否完全未决策;缺少逐块状态时沿用 pending 的 call 状态。
|
|
1784
|
+
* @param record 审查记录
|
|
1785
|
+
* @returns 未归档、未覆盖且没有用户决策时为 true
|
|
1742
1786
|
*/
|
|
1743
|
-
function
|
|
1744
|
-
|
|
1787
|
+
function fullyPending(record) {
|
|
1788
|
+
return !record.archived && !record.superseded && record.decisions.call === "pending" && record.decisions.perHunk.every((value) => value === "pending");
|
|
1745
1789
|
}
|
|
1746
|
-
//#endregion
|
|
1747
|
-
//#region src/debugLog.ts
|
|
1748
1790
|
/**
|
|
1749
|
-
*
|
|
1750
|
-
*
|
|
1751
|
-
*
|
|
1752
|
-
*
|
|
1753
|
-
*
|
|
1754
|
-
*
|
|
1755
|
-
* 作者 ddj 2026-09-08 / 2026-09-17
|
|
1791
|
+
* @public
|
|
1792
|
+
* @author ddj 2026年10月10号
|
|
1793
|
+
* 比较整组 hunk 是否逐块精确逆反;不把新建或纯 no-op 当作抵消。
|
|
1794
|
+
* @param prior 先前修改的差异块
|
|
1795
|
+
* @param later 后续修改的差异块
|
|
1796
|
+
* @returns 文本归一化后整组逆反且包含真实修改时为 true
|
|
1756
1797
|
*/
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
const
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
const debugLegacyCleaned = /* @__PURE__ */ new Set();
|
|
1769
|
-
/** 诊断回显日志器(终端回显统一带 `[dsh-vscode-mode:debug]` 前缀,级别跟随上报级别)。 */
|
|
1770
|
-
const debugLog = log.child("debug");
|
|
1771
|
-
/** 诊断日志文件名白名单(cwd hash 命名;请求回传的 file 参数必须整名匹配,防路径穿越)。 */
|
|
1772
|
-
const DEBUG_LOG_NAME = /^debug\.[0-9a-f]{16}\.log$/;
|
|
1798
|
+
function hunksInverse(prior, later) {
|
|
1799
|
+
if (!prior.length || prior.length !== later.length) return false;
|
|
1800
|
+
let changed = false;
|
|
1801
|
+
for (let idx = 0; idx < prior.length; idx++) {
|
|
1802
|
+
const a = normalizeHunk(prior[idx]);
|
|
1803
|
+
const b = normalizeHunk(later[idx]);
|
|
1804
|
+
if (a.oldText !== b.newText || a.newText !== b.oldText) return false;
|
|
1805
|
+
if (!isNoopHunk(a)) changed = true;
|
|
1806
|
+
}
|
|
1807
|
+
return changed;
|
|
1808
|
+
}
|
|
1773
1809
|
/**
|
|
1774
|
-
*
|
|
1775
|
-
* @author ddj 2026年
|
|
1776
|
-
*
|
|
1777
|
-
* @
|
|
1810
|
+
* @private
|
|
1811
|
+
* @author ddj 2026年10月10号
|
|
1812
|
+
* 同路径优先按捕获批次排序;旧记录仅使用有效时间,不拿 callId 伪造先后证据。
|
|
1813
|
+
* @param a 第一条记录
|
|
1814
|
+
* @param b 第二条记录
|
|
1815
|
+
* @returns 负数表示 a 更早,零表示先后无法区分
|
|
1778
1816
|
*/
|
|
1779
|
-
function
|
|
1780
|
-
|
|
1817
|
+
function recordOrder(a, b) {
|
|
1818
|
+
if (a.path !== b.path) return a.path.localeCompare(b.path);
|
|
1819
|
+
if (Number.isInteger(a.batch) && Number.isInteger(b.batch) && a.batch > 0 && b.batch > 0 && a.batch !== b.batch) return a.batch - b.batch;
|
|
1820
|
+
const delta = Date.parse(a.at) - Date.parse(b.at);
|
|
1821
|
+
return Number.isFinite(delta) ? delta : 0;
|
|
1781
1822
|
}
|
|
1782
1823
|
/**
|
|
1783
|
-
*
|
|
1784
|
-
*
|
|
1785
|
-
*
|
|
1786
|
-
* @param
|
|
1787
|
-
* @param
|
|
1824
|
+
* @private
|
|
1825
|
+
* @author ddj 2026年10月10号
|
|
1826
|
+
* 确认完整文件快照往返,防止相同文本在不同位置的反向修改被误隐藏。
|
|
1827
|
+
* @param prior 先前记录
|
|
1828
|
+
* @param later 后续记录
|
|
1829
|
+
* @returns 两端快照互换且文件存在性未变化时为 true;缺少证据时保留审查
|
|
1788
1830
|
*/
|
|
1789
|
-
function
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
const target = debugLogFile(cwd);
|
|
1794
|
-
const appended = await readFile(target, "utf8").catch(() => "") + batch.map((l) => (/* @__PURE__ */ new Date()).toISOString() + " " + l + "\n").join("");
|
|
1795
|
-
const next = appended.length > DEBUG_LOG_CAP ? appended.slice(-Math.floor(DEBUG_LOG_CAP / 2)) : appended;
|
|
1796
|
-
await writeFile(target, next, "utf8");
|
|
1797
|
-
if (!debugLegacyCleaned.has(cwd)) {
|
|
1798
|
-
debugLegacyCleaned.add(cwd);
|
|
1799
|
-
await rm(join(cwd, DEBUG_LOG), { force: true }).catch(() => {});
|
|
1800
|
-
}
|
|
1801
|
-
} catch (e) {}
|
|
1802
|
-
});
|
|
1803
|
-
debugWriteQueues.set(cwd, task);
|
|
1804
|
-
return task;
|
|
1831
|
+
function snapshotsRestore(prior, later) {
|
|
1832
|
+
if (prior.create || later.create) return false;
|
|
1833
|
+
if (typeof prior.before !== "string" || typeof prior.after !== "string" || typeof later.before !== "string" || typeof later.after !== "string") return false;
|
|
1834
|
+
return normalizeForCompare(prior.before) === normalizeForCompare(later.after) && normalizeForCompare(prior.after) === normalizeForCompare(later.before);
|
|
1805
1835
|
}
|
|
1806
1836
|
/**
|
|
1807
|
-
*
|
|
1808
|
-
*
|
|
1809
|
-
*
|
|
1810
|
-
* @param
|
|
1811
|
-
* @param
|
|
1812
|
-
* @
|
|
1813
|
-
* @param level 日志级别(缺省 debug;落盘行与终端回显均带此级别)
|
|
1837
|
+
* @public
|
|
1838
|
+
* @author ddj 2026年10月10号
|
|
1839
|
+
* 查找最近一条未决的整记录逆反;必须有文件快照证明原内容确实恢复。
|
|
1840
|
+
* @param records 工作区记录(不修改输入)
|
|
1841
|
+
* @param record 新记录
|
|
1842
|
+
* @returns 可安全配对的先前记录,证据不足或部分撤销时为 null
|
|
1814
1843
|
*/
|
|
1815
|
-
function
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
st.timer = null;
|
|
1826
|
-
const batch = st.lines;
|
|
1827
|
-
st.lines = [];
|
|
1828
|
-
st.len = 0;
|
|
1829
|
-
flushDebug(cwd, batch);
|
|
1830
|
-
};
|
|
1831
|
-
if (st.len >= DEBUG_BUF_CAP) {
|
|
1832
|
-
if (st.timer) clearTimeout(st.timer);
|
|
1833
|
-
flush();
|
|
1834
|
-
} else if (!st.timer) st.timer = setTimeout(flush, DEBUG_FLUSH_IDLE_MS);
|
|
1835
|
-
debugBuffers.set(cwd, st);
|
|
1836
|
-
debugLog[level](text);
|
|
1844
|
+
function findInversePrior(records, record) {
|
|
1845
|
+
if (!fullyPending(record)) return null;
|
|
1846
|
+
let found = null;
|
|
1847
|
+
for (const prior of records) {
|
|
1848
|
+
if (prior.callId === record.callId || prior.path !== record.path || !fullyPending(prior)) continue;
|
|
1849
|
+
if (recordOrder(prior, record) >= 0 || !hunksInverse(prior.hunks, record.hunks)) continue;
|
|
1850
|
+
if (!snapshotsRestore(prior, record)) continue;
|
|
1851
|
+
if (!found || recordOrder(found, prior) < 0) found = prior;
|
|
1852
|
+
}
|
|
1853
|
+
return found;
|
|
1837
1854
|
}
|
|
1838
1855
|
/**
|
|
1839
|
-
*
|
|
1840
|
-
* @author ddj 2026年
|
|
1841
|
-
*
|
|
1842
|
-
* @
|
|
1856
|
+
* @public
|
|
1857
|
+
* @author ddj 2026年10月10号
|
|
1858
|
+
* 按捕获顺序贪心配对历史抵消记录,每条最多使用一次;桶上限为 200。
|
|
1859
|
+
* @param records 工作区记录(不修改输入或记录状态)
|
|
1860
|
+
* @returns 可以整对归档的记录对
|
|
1843
1861
|
*/
|
|
1844
|
-
|
|
1845
|
-
const
|
|
1846
|
-
const
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
const
|
|
1850
|
-
if (!
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1862
|
+
function findCanceledPairs(records) {
|
|
1863
|
+
const available = [];
|
|
1864
|
+
const pairs = [];
|
|
1865
|
+
const ordered = [...records].sort(recordOrder);
|
|
1866
|
+
for (const record of ordered) {
|
|
1867
|
+
const prior = findInversePrior(available, record);
|
|
1868
|
+
if (!prior) {
|
|
1869
|
+
if (fullyPending(record)) available.push(record);
|
|
1870
|
+
continue;
|
|
1871
|
+
}
|
|
1872
|
+
pairs.push([prior, record]);
|
|
1873
|
+
available.splice(available.indexOf(prior), 1);
|
|
1874
|
+
}
|
|
1875
|
+
return pairs;
|
|
1876
|
+
}
|
|
1877
|
+
/** 文件当前最大批次号(无记录为 0)。 */
|
|
1878
|
+
function fileMaxBatch(records, path) {
|
|
1879
|
+
let m = 0;
|
|
1880
|
+
for (const r of records.values()) if (r.path === path && Number.isInteger(r.batch) && r.batch > m) m = r.batch;
|
|
1881
|
+
return m;
|
|
1882
|
+
}
|
|
1883
|
+
/** @public @author ddj 2026年10月26号 @param map Legacy records. @param max Cache target, never a pending retention limit. */
|
|
1884
|
+
function prune(map, max = 200) {
|
|
1885
|
+
if (map.size <= max) return;
|
|
1886
|
+
const sorted = [...map.values()].filter((record) => record.archived && recordResolved(record)).sort((a, b) => a.at < b.at ? -1 : 1);
|
|
1887
|
+
for (const record of sorted) {
|
|
1888
|
+
if (map.size <= max) break;
|
|
1889
|
+
map.delete(record.callId);
|
|
1856
1890
|
}
|
|
1857
|
-
items.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
1858
|
-
return items;
|
|
1859
1891
|
}
|
|
1860
1892
|
/**
|
|
1861
|
-
*
|
|
1862
|
-
*
|
|
1863
|
-
* @
|
|
1864
|
-
* @param
|
|
1865
|
-
* @param
|
|
1866
|
-
* @param home DSH home(测试可注入)
|
|
1867
|
-
* @returns 读取结果;文件不存在或名字非法返回 null
|
|
1893
|
+
* 归档条目构造(一条批次一条)。
|
|
1894
|
+
* @author ddj 2026年08月20号
|
|
1895
|
+
* @param recs 同批次记录(非空)
|
|
1896
|
+
* @param cwd 工作区
|
|
1897
|
+
* @param reason 归档原因
|
|
1868
1898
|
*/
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
const target = join(pluginLogRoot(home), name);
|
|
1872
|
-
const info = await stat(target).catch(() => null);
|
|
1873
|
-
if (!info?.isFile()) return null;
|
|
1874
|
-
const text = await readFile(target, "utf8").catch(() => null);
|
|
1875
|
-
if (text === null) return null;
|
|
1876
|
-
const cut = Math.max(0, text.length - maxBytes);
|
|
1899
|
+
function archiveEntryFor(recs, cwd, reason) {
|
|
1900
|
+
const first = recs[0];
|
|
1877
1901
|
return {
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1902
|
+
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1903
|
+
cwd,
|
|
1904
|
+
path: first.path,
|
|
1905
|
+
batch: Number.isInteger(first.batch) ? first.batch : null,
|
|
1906
|
+
reason,
|
|
1907
|
+
records: recs.map((r) => ({
|
|
1908
|
+
callId: r.callId,
|
|
1909
|
+
toolName: r.toolName,
|
|
1910
|
+
path: r.path,
|
|
1911
|
+
create: r.create === true,
|
|
1912
|
+
callHunk: r.callHunk,
|
|
1913
|
+
hunks: r.hunks,
|
|
1914
|
+
decisions: r.decisions,
|
|
1915
|
+
note: r.note ?? null,
|
|
1916
|
+
before: typeof r.before === "string" ? r.before : null,
|
|
1917
|
+
after: typeof r.after === "string" ? r.after : null,
|
|
1918
|
+
baseFingerprint: r.baseFingerprint ?? null,
|
|
1919
|
+
afterFingerprint: r.afterFingerprint ?? null,
|
|
1920
|
+
conflict: r.conflict === true,
|
|
1921
|
+
legacy: r.legacy === true,
|
|
1922
|
+
superseded: r.superseded === true,
|
|
1923
|
+
batch: Number.isInteger(r.batch) ? r.batch : null,
|
|
1924
|
+
at: r.at,
|
|
1925
|
+
summary: recSummary(r)
|
|
1926
|
+
}))
|
|
1883
1927
|
};
|
|
1884
1928
|
}
|
|
1885
|
-
/**
|
|
1886
|
-
* 清空一份诊断日志(truncate 到 0,保留文件本身)。
|
|
1887
|
-
* @author ddj 2026年09月17号
|
|
1888
|
-
* @param name 日志文件名(isDebugLogName 白名单校验)
|
|
1889
|
-
* @param home DSH home(测试可注入)
|
|
1890
|
-
* @returns 是否清空成功(文件不存在视为未成功,文案由调用方给)
|
|
1891
|
-
*/
|
|
1892
|
-
async function clearDebugLog(name, home = dshHome()) {
|
|
1893
|
-
if (!isDebugLogName(name)) return false;
|
|
1894
|
-
return await truncate(join(pluginLogRoot(home), name)).then(() => true).catch(() => false);
|
|
1895
|
-
}
|
|
1896
1929
|
//#endregion
|
|
1897
1930
|
//#region src/archiveDb.ts
|
|
1898
1931
|
/**
|
|
@@ -2986,10 +3019,14 @@ var McpRuntime = class {
|
|
|
2986
3019
|
instance ??= this.reserve(agent, record, path, name, def);
|
|
2987
3020
|
await waitMcp(instance.ready, signal);
|
|
2988
3021
|
if (!this.valid(agent, record, name, instance)) throw new Error("项目 MCP 配置已失效");
|
|
2989
|
-
const
|
|
3022
|
+
const registry = agent.ctx.get("tools");
|
|
3023
|
+
let visible;
|
|
3024
|
+
if (typeof registry?.definitions === "function") visible = new Set(registry.definitions(agent).map((tool) => tool.name));
|
|
3025
|
+
else if (typeof registry?.view === "function") visible = new Set(registry.view(agent).visible.keys());
|
|
3026
|
+
else throw new Error("项目 MCP 工具目录不可用:缺少公开 definitions 或旧版 view 接口");
|
|
2990
3027
|
return {
|
|
2991
3028
|
serverName: name,
|
|
2992
|
-
tools: [...instance.lease.entry.bridge.tools.values()].filter((tool) => visible
|
|
3029
|
+
tools: [...instance.lease.entry.bridge.tools.values()].filter((tool) => visible.has(tool.name)).map((tool) => ({
|
|
2993
3030
|
name: tool.name,
|
|
2994
3031
|
description: tool.description,
|
|
2995
3032
|
parameters: tool.parameters
|