dsh-vscode-mode 0.25.1 → 0.27.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -11
- package/lib/client.js +1969 -1344
- package/lib/client.js.map +2 -2
- package/lib/index.js +1443 -716
- package/lib/index.js.map +1 -1
- package/package.json +1 -1
- package/src/client/compatSummary.ts +4 -0
- package/src/client/contextMenusOfficial.ts +80 -0
- package/src/client/forgeBridge.ts +28 -0
- package/src/client/index.ts +18 -3
- package/src/client/monaco/lsp/lspClient.ts +25 -7
- package/src/client/officialSidebar.ts +15 -3
- package/src/client/reviewBar.ts +3 -0
- package/src/client/reviewBatch.ts +9 -1
- package/src/client/reviewKeepAll.ts +72 -0
- package/src/client/sidebar/menuItems.ts +6 -0
- package/src/client/sidebar/panels/FileExplorer.ts +26 -1
- package/src/client/sidebar/types.ts +3 -0
- package/src/client/state/diffSummary.ts +34 -7
- package/src/client/state/recordsFeed.ts +40 -3
- package/src/client/state/reviewFeed.ts +9 -2
- package/src/client/tabMenu.ts +5 -0
- package/src/client/ui/ClaimRouter.ts +11 -5
- package/src/client/ui/EditorPane.ts +9 -1
- package/src/client/ui/EditorView.ts +164 -39
- package/src/client/ui/OfficialSideTab.ts +13 -5
- package/src/client/ui/ReviewAllConfirm.ts +1 -1
- package/src/index.ts +1 -1
- package/src/pending/capture.ts +96 -47
- package/src/pending/persist.ts +94 -5
- package/src/pending/store.ts +171 -58
- package/src/pendingCompat.ts +72 -9
- package/src/pendingRpc.ts +10 -10
- package/src/reviewCapture.ts +64 -23
- package/src/reviewDb.ts +119 -8
- package/src/reviewEvidence.ts +1 -1
- package/src/reviewRpc.ts +115 -9
- package/src/rpc.ts +14 -10
- package/src/shared/review.ts +21 -0
- package/src/shared/rpc.ts +3 -1
- package/src/store.ts +44 -0
- package/src/toolTargets.ts +63 -0
package/lib/index.js
CHANGED
|
@@ -2445,6 +2445,568 @@ function closeArchiveDbs() {
|
|
|
2445
2445
|
connections.clear();
|
|
2446
2446
|
}
|
|
2447
2447
|
//#endregion
|
|
2448
|
+
//#region src/shared/pathKey.ts
|
|
2449
|
+
/**
|
|
2450
|
+
* 路径比较口径:Host 与客户端共用同一套「远端 / 盘符 / 相对路径」判定,只在平台折叠上分工。
|
|
2451
|
+
* @author ddj 2026年10月30号
|
|
2452
|
+
*/
|
|
2453
|
+
/** 远端仓库身份:`scheme://...` 区分大小写,绝不能折叠。 */
|
|
2454
|
+
const REMOTE = /^[a-z]+:\/\//i;
|
|
2455
|
+
/** 盘符绝对路径:大小写不敏感与宿主平台无关,两边都必须折叠。 */
|
|
2456
|
+
const DRIVE = /^[a-z]:\//i;
|
|
2457
|
+
/**
|
|
2458
|
+
* @author ddj 2026年10月30号
|
|
2459
|
+
* @description: 统一分隔符为 `/`,其余字符原样保留。
|
|
2460
|
+
* @param path 原始路径
|
|
2461
|
+
* @returns 反斜杠已归一化的路径
|
|
2462
|
+
*/
|
|
2463
|
+
function unifiedPath(path) {
|
|
2464
|
+
return path.replace(/\\/g, "/");
|
|
2465
|
+
}
|
|
2466
|
+
/**
|
|
2467
|
+
* @author ddj 2026年10月30号
|
|
2468
|
+
* @description: 生成路径比较键。盘符路径恒折叠大小写;其余路径是否折叠由调用方按运行平台决定
|
|
2469
|
+
* (Host 在 Windows 上折叠,客户端不在宿主进程里,取 false)。
|
|
2470
|
+
* @param path 原始路径
|
|
2471
|
+
* @param foldCase 非盘符路径是否折叠大小写
|
|
2472
|
+
* @returns 比较键
|
|
2473
|
+
*/
|
|
2474
|
+
function pathKeyFor(path, foldCase) {
|
|
2475
|
+
const unified = unifiedPath(path);
|
|
2476
|
+
if (REMOTE.test(unified)) return unified;
|
|
2477
|
+
if (DRIVE.test(unified)) return unified.toLowerCase();
|
|
2478
|
+
return foldCase ? unified.toLowerCase() : unified;
|
|
2479
|
+
}
|
|
2480
|
+
//#endregion
|
|
2481
|
+
//#region src/shared/pendingReview.ts
|
|
2482
|
+
/** 待审条目在面板列表里的 id 前缀;客户端与 Host 共用这一份口径。 */
|
|
2483
|
+
const PENDING_PREFIX = "pending:";
|
|
2484
|
+
//#endregion
|
|
2485
|
+
//#region src/pendingCompat.ts
|
|
2486
|
+
/**
|
|
2487
|
+
* 进程内"已被待审接管"的路径集合;旧写入口守卫只读它,绝不碰数据库。
|
|
2488
|
+
* 按工作区登记,来源会话仅作追溯;任一对话重建或注销都会更新同一份集合。
|
|
2489
|
+
*/
|
|
2490
|
+
const coveredWorkspace = /* @__PURE__ */ new Map();
|
|
2491
|
+
/**
|
|
2492
|
+
* @author ddj 2026年10月29号
|
|
2493
|
+
* @description: 由条目 id 派生稳定序号,保证轮询期间面板锚点不跳。
|
|
2494
|
+
* @param id 待审条目身份
|
|
2495
|
+
* @returns 正整数序号
|
|
2496
|
+
*/
|
|
2497
|
+
function stableSeq(id) {
|
|
2498
|
+
let hash = 2166136261;
|
|
2499
|
+
for (let index = 0; index < id.length; index++) {
|
|
2500
|
+
hash ^= id.charCodeAt(index);
|
|
2501
|
+
hash = Math.imul(hash, 16777619);
|
|
2502
|
+
}
|
|
2503
|
+
return Math.abs(hash) % 2147483647 + 1;
|
|
2504
|
+
}
|
|
2505
|
+
/**
|
|
2506
|
+
* @author ddj 2026年10月29号
|
|
2507
|
+
* @description: Host 侧的路径比较口径:远端身份保留大小写,其余按运行平台折叠。
|
|
2508
|
+
* @param path 路径
|
|
2509
|
+
* @returns 比较键
|
|
2510
|
+
*/
|
|
2511
|
+
function pathKey$1(path) {
|
|
2512
|
+
return pathKeyFor(path, process.platform === "win32");
|
|
2513
|
+
}
|
|
2514
|
+
/**
|
|
2515
|
+
* @author ddj 2026年10月30号
|
|
2516
|
+
* @description: 判断两个路径是否指同一文件:比较键相同,或一方是另一方的工作区相对写法。
|
|
2517
|
+
* 面板传下来的路径可能是相对路径,而条目里存的是绝对路径。
|
|
2518
|
+
* @param left 路径
|
|
2519
|
+
* @param right 路径
|
|
2520
|
+
* @returns 是否同一文件
|
|
2521
|
+
*/
|
|
2522
|
+
function pendingSamePath(left, right) {
|
|
2523
|
+
const a = pathKey$1(left);
|
|
2524
|
+
const b = pathKey$1(right);
|
|
2525
|
+
if (!a || !b) return false;
|
|
2526
|
+
return a === b || a.endsWith("/" + b) || b.endsWith("/" + a);
|
|
2527
|
+
}
|
|
2528
|
+
/**
|
|
2529
|
+
* @author ddj 2026年10月30号
|
|
2530
|
+
* @description: 列表里的条目 id 带待审前缀用于区分通道,内核按原始身份存取,这里统一还原。
|
|
2531
|
+
* @param id 请求里的条目 id
|
|
2532
|
+
* @returns 待审内核使用的原始 id
|
|
2533
|
+
*/
|
|
2534
|
+
function pendingRawId(id) {
|
|
2535
|
+
return id.startsWith("pending:") ? id.slice(8) : id;
|
|
2536
|
+
}
|
|
2537
|
+
/**
|
|
2538
|
+
* @author ddj 2026年10月30号
|
|
2539
|
+
* @description: 列表显示用的短路径:工作区内显示相对路径,区外保留原文。
|
|
2540
|
+
* @param cwd 工作区
|
|
2541
|
+
* @param path 条目路径
|
|
2542
|
+
* @returns 显示用路径
|
|
2543
|
+
*/
|
|
2544
|
+
function pendingDisplay(cwd, path) {
|
|
2545
|
+
const root = cwd.replace(/[\\/]+$/, "");
|
|
2546
|
+
const unified = path.replace(/\\/g, "/");
|
|
2547
|
+
const base = root.replace(/\\/g, "/");
|
|
2548
|
+
if (base && (unified === base || unified.toLowerCase().startsWith(base.toLowerCase() + "/"))) return unified.slice(base.length + 1);
|
|
2549
|
+
return path;
|
|
2550
|
+
}
|
|
2551
|
+
/**
|
|
2552
|
+
* @author ddj 2026年10月29号
|
|
2553
|
+
* @description: 待审条目转面板列表项。
|
|
2554
|
+
* @param item 待审条目
|
|
2555
|
+
* @param cwd 工作区,用于显示相对路径;缺省显示原始路径
|
|
2556
|
+
* @returns 面板列表项
|
|
2557
|
+
*/
|
|
2558
|
+
function pendingItemOf(item, cwd) {
|
|
2559
|
+
return {
|
|
2560
|
+
id: PENDING_PREFIX + item.id,
|
|
2561
|
+
sessionId: item.sessionId,
|
|
2562
|
+
turn: 0,
|
|
2563
|
+
seq: stableSeq(item.id),
|
|
2564
|
+
path: item.path,
|
|
2565
|
+
currentPath: item.path,
|
|
2566
|
+
display: cwd ? pendingDisplay(cwd, item.path) : item.path,
|
|
2567
|
+
added: item.added,
|
|
2568
|
+
deleted: item.deleted,
|
|
2569
|
+
kind: item.oversized ? "oversized" : "text",
|
|
2570
|
+
decision: item.settled ? "accepted" : "pending",
|
|
2571
|
+
revision: item.revision,
|
|
2572
|
+
current: true
|
|
2573
|
+
};
|
|
2574
|
+
}
|
|
2575
|
+
/**
|
|
2576
|
+
* @author ddj 2026年10月29号
|
|
2577
|
+
* @description: 待审详情转面板详情;Revert 映射到面板的 Undo 能力位。
|
|
2578
|
+
* @param detail 待审详情
|
|
2579
|
+
* @param capturedAt 采集时间文案
|
|
2580
|
+
* @returns 面板详情
|
|
2581
|
+
*/
|
|
2582
|
+
function pendingDetailOf(detail, capturedAt, cwd) {
|
|
2583
|
+
return {
|
|
2584
|
+
item: pendingItemOf(detail.item, cwd),
|
|
2585
|
+
diff: detail.diff,
|
|
2586
|
+
capturedAt,
|
|
2587
|
+
blocks: detail.blocks,
|
|
2588
|
+
capability: {
|
|
2589
|
+
keep: detail.capability.keep,
|
|
2590
|
+
undo: detail.capability.revert,
|
|
2591
|
+
reason: detail.capability.reason
|
|
2592
|
+
}
|
|
2593
|
+
};
|
|
2594
|
+
}
|
|
2595
|
+
/**
|
|
2596
|
+
* @author ddj 2026年11月3号
|
|
2597
|
+
* @description: 覆盖登记键:工作区与路径各自归一化后再拼接,避免同一文件多种写法登记成两条。
|
|
2598
|
+
* @param cwd 工作区
|
|
2599
|
+
* @param path 路径
|
|
2600
|
+
* @returns 覆盖登记键
|
|
2601
|
+
*/
|
|
2602
|
+
function coverKey(cwd, path) {
|
|
2603
|
+
return `${pathKey$1(cwd)}|${pathKey$1(path)}`;
|
|
2604
|
+
}
|
|
2605
|
+
/**
|
|
2606
|
+
* @private
|
|
2607
|
+
* @author ddj 2026年11月04号
|
|
2608
|
+
* @description: 取工作区唯一接管集合;来源对话仅保留为诊断信息。
|
|
2609
|
+
* @param cwd 工作区路径
|
|
2610
|
+
* @returns 路径到登记来源的映射
|
|
2611
|
+
*/
|
|
2612
|
+
function workspaceCovers(cwd) {
|
|
2613
|
+
const workspaceKey = pathKey$1(cwd);
|
|
2614
|
+
const existing = coveredWorkspace.get(workspaceKey);
|
|
2615
|
+
if (existing) return existing;
|
|
2616
|
+
const created = /* @__PURE__ */ new Map();
|
|
2617
|
+
coveredWorkspace.set(workspaceKey, created);
|
|
2618
|
+
return created;
|
|
2619
|
+
}
|
|
2620
|
+
/**
|
|
2621
|
+
* @author ddj 2026年11月3号
|
|
2622
|
+
* @description: 记住/忘记"该路径由待审投影接管"。进程内集合,绝不在旧写入口上查库(避免撞上写事务锁)。
|
|
2623
|
+
* @param cwd 工作区
|
|
2624
|
+
* @param path 路径
|
|
2625
|
+
* @param covered 是否接管
|
|
2626
|
+
* @param sessionId 会话 id;缺省落到无归属的工作区集合
|
|
2627
|
+
* @returns 无
|
|
2628
|
+
*/
|
|
2629
|
+
function trackPendingCover(cwd, path, covered, sessionId) {
|
|
2630
|
+
const key = coverKey(cwd, path);
|
|
2631
|
+
const target = workspaceCovers(cwd);
|
|
2632
|
+
if (covered) target.set(key, sessionId ?? null);
|
|
2633
|
+
else target.delete(key);
|
|
2634
|
+
}
|
|
2635
|
+
/**
|
|
2636
|
+
* @author ddj 2026年11月3号
|
|
2637
|
+
* @description: 用完整列表重建工作区接管集合,清除任何对话留下的过期登记。
|
|
2638
|
+
* @param cwd 工作区
|
|
2639
|
+
* @param sessionId 会话 id
|
|
2640
|
+
* @param paths 本会话当前可见的待审条目路径
|
|
2641
|
+
* @returns 无
|
|
2642
|
+
*/
|
|
2643
|
+
function setPendingCovers(cwd, sessionId, paths) {
|
|
2644
|
+
const next = /* @__PURE__ */ new Map();
|
|
2645
|
+
for (const path of paths) if (path) next.set(coverKey(cwd, path), sessionId);
|
|
2646
|
+
coveredWorkspace.set(pathKey$1(cwd), next);
|
|
2647
|
+
}
|
|
2648
|
+
/**
|
|
2649
|
+
* @author ddj 2026年10月29号
|
|
2650
|
+
* @description: 判断某路径是否已由待审投影接管;仅读进程内集合,不产生任何 IO。
|
|
2651
|
+
* 所有对话按同一工作区集合判定,兼容参数不影响可见范围。
|
|
2652
|
+
* @param cwd 工作区
|
|
2653
|
+
* @param path 请求路径
|
|
2654
|
+
* @param sessionId 会话 id;缺省按并集判定
|
|
2655
|
+
* @returns 是否已被接管
|
|
2656
|
+
*/
|
|
2657
|
+
function pendingCovers(cwd, path, sessionId) {
|
|
2658
|
+
const key = coverKey(cwd, path);
|
|
2659
|
+
return coveredWorkspace.get(pathKey$1(cwd))?.has(key) === true;
|
|
2660
|
+
}
|
|
2661
|
+
/**
|
|
2662
|
+
* @author ddj 2026年10月29号
|
|
2663
|
+
* @description: 合并官方页与待审页:待审覆盖的路径从官方结果中剔除,其余原样保留。
|
|
2664
|
+
* @param official 官方页
|
|
2665
|
+
* @param pending 待审页
|
|
2666
|
+
* @param cwd 工作区,用于把待审行显示成相对路径
|
|
2667
|
+
* @returns 合并后的页
|
|
2668
|
+
*/
|
|
2669
|
+
function mergedPage(official, pending, cwd) {
|
|
2670
|
+
const covered = /* @__PURE__ */ new Set();
|
|
2671
|
+
for (const item of pending.items) {
|
|
2672
|
+
covered.add(pathKey$1(item.path));
|
|
2673
|
+
covered.add(pathKey$1(item.id));
|
|
2674
|
+
}
|
|
2675
|
+
const kept = official.items.filter((item) => !covered.has(pathKey$1(item.currentPath)) && !covered.has(pathKey$1(item.path)));
|
|
2676
|
+
return {
|
|
2677
|
+
items: [...pending.items.map((item) => pendingItemOf(item, cwd)), ...kept],
|
|
2678
|
+
pending: pending.pending + kept.filter((item) => item.decision === "pending").length,
|
|
2679
|
+
nextCursor: official.nextCursor,
|
|
2680
|
+
gaps: official.gaps,
|
|
2681
|
+
...official.overview ? { overview: official.overview } : {}
|
|
2682
|
+
};
|
|
2683
|
+
}
|
|
2684
|
+
//#endregion
|
|
2685
|
+
//#region src/pending/persist.ts
|
|
2686
|
+
/** 待审库版本号,写进 metadata 便于日后迁移。v3 给评论表加了引文与上下文列。 */
|
|
2687
|
+
const SCHEMA_VERSION = "3";
|
|
2688
|
+
/**
|
|
2689
|
+
* @author ddj 2026年10月29号
|
|
2690
|
+
* @description: 打开工作区库并确保待审表存在;DDL 与版本标记在同一事务里提交。
|
|
2691
|
+
* @param cwd 已迁移工作区
|
|
2692
|
+
* @returns 可读写的连接
|
|
2693
|
+
* @throws 未完成活跃库迁移时拒绝
|
|
2694
|
+
*/
|
|
2695
|
+
function pendingDb(cwd) {
|
|
2696
|
+
if (!activeReady(cwd)) throw new Error("待审投影要求先完成活跃库迁移");
|
|
2697
|
+
const db = dbFor(cwd);
|
|
2698
|
+
if (db.prepare("SELECT value FROM metadata WHERE name='pending_schema'").get()?.value === SCHEMA_VERSION) return db;
|
|
2699
|
+
db.exec("BEGIN IMMEDIATE");
|
|
2700
|
+
try {
|
|
2701
|
+
db.exec(DDL);
|
|
2702
|
+
migrateHistory(db);
|
|
2703
|
+
db.prepare("INSERT INTO metadata(name,value) VALUES('pending_schema',?) ON CONFLICT(name) DO UPDATE SET value=excluded.value").run(SCHEMA_VERSION);
|
|
2704
|
+
db.exec("COMMIT");
|
|
2705
|
+
} catch (error) {
|
|
2706
|
+
if (db.isTransaction) db.exec("ROLLBACK");
|
|
2707
|
+
throw error;
|
|
2708
|
+
}
|
|
2709
|
+
return db;
|
|
2710
|
+
}
|
|
2711
|
+
/** 待审表结构:条目、块决定、幂等操作、撤销栈。 */
|
|
2712
|
+
const DDL = `
|
|
2713
|
+
CREATE TABLE IF NOT EXISTS pending_entries (
|
|
2714
|
+
id TEXT PRIMARY KEY, path TEXT NOT NULL, earlier TEXT NOT NULL,
|
|
2715
|
+
old_text TEXT NOT NULL, new_text TEXT NOT NULL, updated_at INTEGER NOT NULL,
|
|
2716
|
+
unseen INTEGER NOT NULL DEFAULT 0, missing INTEGER NOT NULL DEFAULT 0,
|
|
2717
|
+
diverged INTEGER NOT NULL DEFAULT 0, oversized INTEGER NOT NULL DEFAULT 0,
|
|
2718
|
+
revision INTEGER NOT NULL DEFAULT 0, session_id TEXT NOT NULL,
|
|
2719
|
+
session_ids TEXT NOT NULL, lineage TEXT);
|
|
2720
|
+
CREATE TABLE IF NOT EXISTS pending_blocks (
|
|
2721
|
+
entry_id TEXT NOT NULL, block_id TEXT NOT NULL, decision TEXT NOT NULL,
|
|
2722
|
+
PRIMARY KEY(entry_id,block_id));
|
|
2723
|
+
CREATE TABLE IF NOT EXISTS pending_ops (
|
|
2724
|
+
operation_id TEXT PRIMARY KEY, request TEXT NOT NULL, state TEXT NOT NULL,
|
|
2725
|
+
result TEXT, undo TEXT);
|
|
2726
|
+
CREATE TABLE IF NOT EXISTS pending_history (
|
|
2727
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT, kind TEXT NOT NULL, entry_id TEXT,
|
|
2728
|
+
snapshot TEXT NOT NULL, created_at INTEGER NOT NULL,
|
|
2729
|
+
state TEXT NOT NULL DEFAULT 'applied');
|
|
2730
|
+
`;
|
|
2731
|
+
/**
|
|
2732
|
+
* @author ddj 2026年10月29号
|
|
2733
|
+
* @description: v1 历史表补 state 列(rebuild 成本高于一次 ALTER)。
|
|
2734
|
+
* @param db 已打开的工作区库
|
|
2735
|
+
* @returns 无
|
|
2736
|
+
*/
|
|
2737
|
+
function migrateHistory(db) {
|
|
2738
|
+
if (db.prepare("PRAGMA table_info(pending_history)").all().some((column) => column.name === "state")) return;
|
|
2739
|
+
db.exec("ALTER TABLE pending_history ADD COLUMN state TEXT NOT NULL DEFAULT 'applied'");
|
|
2740
|
+
}
|
|
2741
|
+
/**
|
|
2742
|
+
* @author ddj 2026年10月29号
|
|
2743
|
+
* @description: 读出全部待审条目,按更新时间升序。
|
|
2744
|
+
* @param cwd 工作区
|
|
2745
|
+
* @returns 条目与标记
|
|
2746
|
+
*/
|
|
2747
|
+
function loadEntries(cwd) {
|
|
2748
|
+
return pendingDb(cwd).prepare("SELECT * FROM pending_entries ORDER BY updated_at ASC").all().map((row) => ({
|
|
2749
|
+
entry: entryOfRow(row),
|
|
2750
|
+
flags: flagsOfRow(row)
|
|
2751
|
+
}));
|
|
2752
|
+
}
|
|
2753
|
+
/**
|
|
2754
|
+
* @author ddj 2026年10月29号
|
|
2755
|
+
* @description: 写入或更新一条条目(含标记),单条原子完成。
|
|
2756
|
+
* @param cwd 工作区
|
|
2757
|
+
* @param stored 条目与标记
|
|
2758
|
+
* @returns 无
|
|
2759
|
+
*/
|
|
2760
|
+
function saveEntry(cwd, stored) {
|
|
2761
|
+
upsertEntry(pendingDb(cwd), stored);
|
|
2762
|
+
}
|
|
2763
|
+
/**
|
|
2764
|
+
* @private
|
|
2765
|
+
* @author ddj 2026年11月03号
|
|
2766
|
+
* @description: 在给定连接上写入或更新一条条目(含标记),供单条与批量两个入口复用。
|
|
2767
|
+
* @param db 已迁移连接(可能已在事务中)
|
|
2768
|
+
* @param stored 条目与标记
|
|
2769
|
+
* @returns 无
|
|
2770
|
+
*/
|
|
2771
|
+
function upsertEntry(db, stored) {
|
|
2772
|
+
const { entry, flags } = stored;
|
|
2773
|
+
db.prepare(`INSERT INTO pending_entries
|
|
2774
|
+
(id,path,earlier,old_text,new_text,updated_at,unseen,missing,diverged,oversized,revision,session_id,session_ids,lineage)
|
|
2775
|
+
VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|
|
2776
|
+
ON CONFLICT(id) DO UPDATE SET path=excluded.path,earlier=excluded.earlier,old_text=excluded.old_text,
|
|
2777
|
+
new_text=excluded.new_text,updated_at=excluded.updated_at,unseen=excluded.unseen,missing=excluded.missing,
|
|
2778
|
+
diverged=excluded.diverged,oversized=excluded.oversized,revision=excluded.revision,
|
|
2779
|
+
session_id=excluded.session_id,session_ids=excluded.session_ids,lineage=excluded.lineage`).run(entry.id, entry.path, entry.earlierVersion, entry.oldText, entry.newText, entry.updatedAt, entry.unseen === true ? 1 : 0, flags.missing ? 1 : 0, flags.diverged ? 1 : 0, flags.oversized ? 1 : 0, flags.revision, entry.sessionId, JSON.stringify(entry.sessionIds), entry.lineage ? JSON.stringify(entry.lineage) : null);
|
|
2780
|
+
}
|
|
2781
|
+
/**
|
|
2782
|
+
* @author ddj 2026年10月29号
|
|
2783
|
+
* @description: 删除条目及其块决定,用于"已解决条目移除"。
|
|
2784
|
+
* @param cwd 工作区
|
|
2785
|
+
* @param id 条目身份
|
|
2786
|
+
* @returns 无
|
|
2787
|
+
*/
|
|
2788
|
+
function dropEntry(cwd, id) {
|
|
2789
|
+
const db = pendingDb(cwd);
|
|
2790
|
+
db.exec("BEGIN IMMEDIATE");
|
|
2791
|
+
try {
|
|
2792
|
+
db.prepare("DELETE FROM pending_entries WHERE id=?").run(id);
|
|
2793
|
+
db.prepare("DELETE FROM pending_blocks WHERE entry_id=?").run(id);
|
|
2794
|
+
db.exec("COMMIT");
|
|
2795
|
+
} catch (error) {
|
|
2796
|
+
if (db.isTransaction) db.exec("ROLLBACK");
|
|
2797
|
+
throw error;
|
|
2798
|
+
}
|
|
2799
|
+
}
|
|
2800
|
+
/**
|
|
2801
|
+
* @author ddj 2026年10月29号
|
|
2802
|
+
* @description: 覆盖某条目的块决定集合。
|
|
2803
|
+
* @param cwd 工作区
|
|
2804
|
+
* @param entryId 条目身份
|
|
2805
|
+
* @param blocks 块决定
|
|
2806
|
+
* @returns 无
|
|
2807
|
+
*/
|
|
2808
|
+
function saveBlocks(cwd, entryId, blocks) {
|
|
2809
|
+
const db = pendingDb(cwd);
|
|
2810
|
+
db.exec("BEGIN IMMEDIATE");
|
|
2811
|
+
try {
|
|
2812
|
+
writeBlocks(db, entryId, blocks);
|
|
2813
|
+
db.exec("COMMIT");
|
|
2814
|
+
} catch (error) {
|
|
2815
|
+
if (db.isTransaction) db.exec("ROLLBACK");
|
|
2816
|
+
throw error;
|
|
2817
|
+
}
|
|
2818
|
+
}
|
|
2819
|
+
/**
|
|
2820
|
+
* @private
|
|
2821
|
+
* @author ddj 2026年11月03号
|
|
2822
|
+
* @description: 在给定连接上覆盖某条目的块决定集合,供单条与批量两个入口复用。
|
|
2823
|
+
* @param db 已迁移连接(可能已在事务中)
|
|
2824
|
+
* @param entryId 条目身份
|
|
2825
|
+
* @param blocks 块决定
|
|
2826
|
+
* @returns 无
|
|
2827
|
+
*/
|
|
2828
|
+
function writeBlocks(db, entryId, blocks) {
|
|
2829
|
+
db.prepare("DELETE FROM pending_blocks WHERE entry_id=?").run(entryId);
|
|
2830
|
+
const insert = db.prepare("INSERT INTO pending_blocks VALUES(?,?,?)");
|
|
2831
|
+
for (const block of blocks) insert.run(entryId, block.id, block.decision);
|
|
2832
|
+
}
|
|
2833
|
+
/**
|
|
2834
|
+
* @author ddj 2026年10月29号
|
|
2835
|
+
* @description: 读取幂等操作记录;同 operationId 的重复请求必须复用结果。
|
|
2836
|
+
* @param cwd 工作区
|
|
2837
|
+
* @param operationId 操作身份
|
|
2838
|
+
* @returns 请求原文、状态与结果
|
|
2839
|
+
*/
|
|
2840
|
+
function readOp(cwd, operationId) {
|
|
2841
|
+
const row = pendingDb(cwd).prepare("SELECT request,state,result FROM pending_ops WHERE operation_id=?").get(operationId);
|
|
2842
|
+
return row ? {
|
|
2843
|
+
request: String(row.request),
|
|
2844
|
+
state: String(row.state),
|
|
2845
|
+
result: row.result === null ? null : String(row.result)
|
|
2846
|
+
} : void 0;
|
|
2847
|
+
}
|
|
2848
|
+
/**
|
|
2849
|
+
* @author ddj 2026年10月29号
|
|
2850
|
+
* @description: 记录一次操作的阶段与撤销快照。
|
|
2851
|
+
* @param cwd 工作区
|
|
2852
|
+
* @param operationId 操作身份
|
|
2853
|
+
* @param request 请求原文
|
|
2854
|
+
* @param state prepared / committed
|
|
2855
|
+
* @param payload 结果与撤销快照
|
|
2856
|
+
* @returns 无
|
|
2857
|
+
*/
|
|
2858
|
+
function writeOp(cwd, operationId, request, state, payload = {}) {
|
|
2859
|
+
pendingDb(cwd).prepare(`INSERT INTO pending_ops VALUES(?,?,?,?,?)
|
|
2860
|
+
ON CONFLICT(operation_id) DO UPDATE SET state=excluded.state,
|
|
2861
|
+
result=COALESCE(excluded.result,pending_ops.result), undo=COALESCE(excluded.undo,pending_ops.undo)`).run(operationId, request, state, payload.result ?? null, payload.undo ?? null);
|
|
2862
|
+
}
|
|
2863
|
+
/**
|
|
2864
|
+
* @author ddj 2026年10月29号
|
|
2865
|
+
* @description: 追加一条撤销栈记录,并截断重做分支(新决定之后旧的"已撤销"记录不能再重做)。
|
|
2866
|
+
* @param cwd 工作区
|
|
2867
|
+
* @param kind 操作种类
|
|
2868
|
+
* @param entryId 涉及的条目
|
|
2869
|
+
* @param snapshot 可重放快照
|
|
2870
|
+
* @param now 时间
|
|
2871
|
+
* @returns 无
|
|
2872
|
+
*/
|
|
2873
|
+
function pushHistory(cwd, kind, entryId, snapshot, now) {
|
|
2874
|
+
insertHistory(pendingDb(cwd), kind, entryId, snapshot, now);
|
|
2875
|
+
}
|
|
2876
|
+
/**
|
|
2877
|
+
* @private
|
|
2878
|
+
* @author ddj 2026年11月03号
|
|
2879
|
+
* @description: 在给定连接上追加撤销栈记录并截断重做分支,供单条与批量两个入口复用。
|
|
2880
|
+
* @param db 已迁移连接(可能已在事务中)
|
|
2881
|
+
* @param kind 操作种类
|
|
2882
|
+
* @param entryId 涉及的条目
|
|
2883
|
+
* @param snapshot 可重放快照
|
|
2884
|
+
* @param now 时间
|
|
2885
|
+
* @returns 无
|
|
2886
|
+
*/
|
|
2887
|
+
function insertHistory(db, kind, entryId, snapshot, now) {
|
|
2888
|
+
db.prepare("DELETE FROM pending_history WHERE state='undone'").run();
|
|
2889
|
+
db.prepare("INSERT INTO pending_history(kind,entry_id,snapshot,created_at,state) VALUES(?,?,?,?,'applied')").run(kind, entryId, snapshot, now);
|
|
2890
|
+
}
|
|
2891
|
+
/**
|
|
2892
|
+
* @author ddj 2026年10月30号
|
|
2893
|
+
* @description: 取最近一条仍生效的历史记录,撤销就作用在它上面。
|
|
2894
|
+
* @param cwd 工作区
|
|
2895
|
+
* @returns 记录或 undefined
|
|
2896
|
+
*/
|
|
2897
|
+
function latestApplied(cwd) {
|
|
2898
|
+
const row = pendingDb(cwd).prepare("SELECT id,kind,entry_id,snapshot FROM pending_history WHERE state='applied' ORDER BY id DESC LIMIT 1").get();
|
|
2899
|
+
return row ? rowOfHistory(row) : void 0;
|
|
2900
|
+
}
|
|
2901
|
+
/**
|
|
2902
|
+
* @author ddj 2026年10月30号
|
|
2903
|
+
* @description: 取最早一条被撤销的记录,重做按"撤销的逆序"重新生效。
|
|
2904
|
+
* @param cwd 工作区
|
|
2905
|
+
* @returns 记录或 undefined
|
|
2906
|
+
*/
|
|
2907
|
+
function earliestUndone(cwd) {
|
|
2908
|
+
const row = pendingDb(cwd).prepare("SELECT id,kind,entry_id,snapshot FROM pending_history WHERE state='undone' ORDER BY id ASC LIMIT 1").get();
|
|
2909
|
+
return row ? rowOfHistory(row) : void 0;
|
|
2910
|
+
}
|
|
2911
|
+
/**
|
|
2912
|
+
* @author ddj 2026年10月30号
|
|
2913
|
+
* @description: 翻转一条历史记录的生效状态。
|
|
2914
|
+
* @param cwd 工作区
|
|
2915
|
+
* @param id 记录主键
|
|
2916
|
+
* @param state applied / undone
|
|
2917
|
+
* @returns 无
|
|
2918
|
+
*/
|
|
2919
|
+
function setHistoryState(cwd, id, state) {
|
|
2920
|
+
pendingDb(cwd).prepare("UPDATE pending_history SET state=? WHERE id=?").run(state, id);
|
|
2921
|
+
}
|
|
2922
|
+
/**
|
|
2923
|
+
* @author ddj 2026年10月30号
|
|
2924
|
+
* @description: 把数据库行还原成历史记录。
|
|
2925
|
+
* @param row 数据库行
|
|
2926
|
+
* @returns 记录
|
|
2927
|
+
*/
|
|
2928
|
+
function rowOfHistory(row) {
|
|
2929
|
+
return {
|
|
2930
|
+
id: Number(row.id),
|
|
2931
|
+
kind: String(row.kind),
|
|
2932
|
+
entryId: String(row.entry_id),
|
|
2933
|
+
snapshot: String(row.snapshot)
|
|
2934
|
+
};
|
|
2935
|
+
}
|
|
2936
|
+
/**
|
|
2937
|
+
* @author ddj 2026年10月29号
|
|
2938
|
+
* @description: 由数据库行还原条目。
|
|
2939
|
+
* @param row 数据库行
|
|
2940
|
+
* @returns 条目
|
|
2941
|
+
*/
|
|
2942
|
+
function entryOfRow(row) {
|
|
2943
|
+
const lineage = row.lineage === null ? void 0 : JSON.parse(String(row.lineage));
|
|
2944
|
+
const entry = {
|
|
2945
|
+
id: String(row.id),
|
|
2946
|
+
path: String(row.path),
|
|
2947
|
+
earlierVersion: String(row.earlier) === "none" ? "none" : "file",
|
|
2948
|
+
oldText: String(row.old_text),
|
|
2949
|
+
newText: String(row.new_text),
|
|
2950
|
+
updatedAt: Number(row.updated_at),
|
|
2951
|
+
sessionId: String(row.session_id),
|
|
2952
|
+
sessionIds: JSON.parse(String(row.session_ids))
|
|
2953
|
+
};
|
|
2954
|
+
if (Number(row.unseen) === 1) entry.unseen = true;
|
|
2955
|
+
if (lineage !== void 0) entry.lineage = lineage;
|
|
2956
|
+
return entry;
|
|
2957
|
+
}
|
|
2958
|
+
/**
|
|
2959
|
+
* @author ddj 2026年10月29号
|
|
2960
|
+
* @description: 由数据库行还原标记。
|
|
2961
|
+
* @param row 数据库行
|
|
2962
|
+
* @returns 标记
|
|
2963
|
+
*/
|
|
2964
|
+
function flagsOfRow(row) {
|
|
2965
|
+
return {
|
|
2966
|
+
missing: Number(row.missing) === 1,
|
|
2967
|
+
diverged: Number(row.diverged) === 1,
|
|
2968
|
+
oversized: Number(row.oversized) === 1,
|
|
2969
|
+
revision: Number(row.revision)
|
|
2970
|
+
};
|
|
2971
|
+
}
|
|
2972
|
+
/**
|
|
2973
|
+
* @public
|
|
2974
|
+
* @author ddj 2026年11月03号
|
|
2975
|
+
* @description: 在一次事务内批量推进多条待审条目(批量 Keep 用)。每条按 revision 做 CAS,
|
|
2976
|
+
* 只跳过冲突的那一条,不牵连整批;块决定与撤销快照与单条路径同口径。
|
|
2977
|
+
* @param cwd 工作区
|
|
2978
|
+
* @param records 待落库记录
|
|
2979
|
+
* @returns 因版本已变被跳过的条目
|
|
2980
|
+
* @throws 事务级异常(SQL/程序错误)整体回滚后抛出
|
|
2981
|
+
*/
|
|
2982
|
+
function saveDecisions(cwd, records) {
|
|
2983
|
+
const db = pendingDb(cwd);
|
|
2984
|
+
const skipped = [];
|
|
2985
|
+
const advance = db.prepare(`UPDATE pending_entries SET path=?,earlier=?,old_text=?,new_text=?,updated_at=?,unseen=?,
|
|
2986
|
+
missing=?,diverged=?,oversized=?,revision=?,session_id=?,session_ids=?,lineage=? WHERE id=? AND revision=?`);
|
|
2987
|
+
db.exec("BEGIN IMMEDIATE");
|
|
2988
|
+
try {
|
|
2989
|
+
for (const record of records) {
|
|
2990
|
+
const { entry, flags } = record.next;
|
|
2991
|
+
const expected = record.stored.flags.revision;
|
|
2992
|
+
if (advance.run(entry.path, entry.earlierVersion, entry.oldText, entry.newText, entry.updatedAt, entry.unseen === true ? 1 : 0, flags.missing ? 1 : 0, flags.diverged ? 1 : 0, flags.oversized ? 1 : 0, flags.revision, entry.sessionId, JSON.stringify(entry.sessionIds), entry.lineage ? JSON.stringify(entry.lineage) : null, record.entryId, expected).changes !== 1) {
|
|
2993
|
+
skipped.push({
|
|
2994
|
+
id: record.entryId,
|
|
2995
|
+
reason: "待审条目已变化,请重试"
|
|
2996
|
+
});
|
|
2997
|
+
continue;
|
|
2998
|
+
}
|
|
2999
|
+
writeBlocks(db, record.entryId, record.blocks);
|
|
3000
|
+
insertHistory(db, "decide", record.entryId, record.snapshot, Date.now());
|
|
3001
|
+
}
|
|
3002
|
+
db.exec("COMMIT");
|
|
3003
|
+
return skipped;
|
|
3004
|
+
} catch (error) {
|
|
3005
|
+
if (db.isTransaction) db.exec("ROLLBACK");
|
|
3006
|
+
throw error;
|
|
3007
|
+
}
|
|
3008
|
+
}
|
|
3009
|
+
//#endregion
|
|
2448
3010
|
//#region src/store.ts
|
|
2449
3011
|
const READ_CAP$1 = 8388608;
|
|
2450
3012
|
/** 二进制预览/写回上限(PDF 等整文件 base64 传输;文本侧仍用 READ_CAP)。 */
|
|
@@ -2718,6 +3280,50 @@ async function autoArchiveStale(ctx, session, cwd, bucket) {
|
|
|
2718
3280
|
}
|
|
2719
3281
|
/**
|
|
2720
3282
|
* @public
|
|
3283
|
+
* @author ddj 2026年11月3号
|
|
3284
|
+
* 待审累计基线接管某路径后,旧 sidecar 记录不再有独立决策价值:标记 superseded 并归档,
|
|
3285
|
+
* 连编辑器的行内 Keep/Undo 一起消失,彻底去掉"旧通道还能点、点了又被守卫拒绝"的双权威残留。
|
|
3286
|
+
* 接管是文件级事实,按工作区全部待审条目判定,不跟随"本会话能否看见"漂移。
|
|
3287
|
+
* @param ctx DSH 上下文
|
|
3288
|
+
* @param session 当前会话
|
|
3289
|
+
* @param cwd 工作区
|
|
3290
|
+
* @param bucket 活跃记录桶
|
|
3291
|
+
* @returns 本次结案记录数
|
|
3292
|
+
* @throws 归档失败时恢复内存标志并透传错误
|
|
3293
|
+
*/
|
|
3294
|
+
async function retireCovered(ctx, session, cwd, bucket) {
|
|
3295
|
+
const covers = loadEntries(cwd).flatMap((stored) => [stored.entry.path, stored.entry.id]).filter((path) => !!path);
|
|
3296
|
+
if (!covers.length) return 0;
|
|
3297
|
+
const taken = [];
|
|
3298
|
+
for (const rec of bucket.values()) {
|
|
3299
|
+
if (rec.archived || rec.superseded === true) continue;
|
|
3300
|
+
if (covers.some((cover) => pendingSamePath(cover, rec.path))) taken.push(rec);
|
|
3301
|
+
}
|
|
3302
|
+
if (!taken.length) return 0;
|
|
3303
|
+
const original = taken.map((rec) => ({
|
|
3304
|
+
rec,
|
|
3305
|
+
at: rec.at,
|
|
3306
|
+
superseded: rec.superseded
|
|
3307
|
+
}));
|
|
3308
|
+
const at = (/* @__PURE__ */ new Date()).toISOString();
|
|
3309
|
+
for (const rec of taken) {
|
|
3310
|
+
rec.superseded = true;
|
|
3311
|
+
rec.at = at;
|
|
3312
|
+
}
|
|
3313
|
+
try {
|
|
3314
|
+
await archiveRecords(ctx, session, cwd, bucket, taken, "已由待审累计基线接管,旧工具记录不再单独决策");
|
|
3315
|
+
} catch (error) {
|
|
3316
|
+
for (const item of original) {
|
|
3317
|
+
if (item.rec.archived) continue;
|
|
3318
|
+
item.rec.superseded = item.superseded;
|
|
3319
|
+
item.rec.at = item.at;
|
|
3320
|
+
}
|
|
3321
|
+
throw error;
|
|
3322
|
+
}
|
|
3323
|
+
return taken.length;
|
|
3324
|
+
}
|
|
3325
|
+
/**
|
|
3326
|
+
* @public
|
|
2721
3327
|
* @author ddj 2026年10月10号
|
|
2722
3328
|
* 归档整记录精确抵消对;捕获时仅检查新记录,列表维护时检查整个桶。
|
|
2723
3329
|
* @param ctx DSH 上下文
|
|
@@ -7720,17 +8326,41 @@ function reviewDetail(cwd, id) {
|
|
|
7720
8326
|
}
|
|
7721
8327
|
};
|
|
7722
8328
|
}
|
|
8329
|
+
/**
|
|
8330
|
+
* @private
|
|
8331
|
+
* @author ddj 2026年11月03号
|
|
8332
|
+
* @description: 把一个文件的待处理差异块整体置为目标决策,块级判定仍走 changeBlocks 的 hunk 分支。
|
|
8333
|
+
* @param db 判定事务连接
|
|
8334
|
+
* @param id 文件身份
|
|
8335
|
+
* @param decision 目标决策
|
|
8336
|
+
* @returns 无
|
|
8337
|
+
*/
|
|
8338
|
+
function markBlocks(db, id, decision) {
|
|
8339
|
+
db.prepare("UPDATE review_blocks SET decision=? WHERE file_id=? AND decision='pending'").run(decision, id);
|
|
8340
|
+
}
|
|
8341
|
+
/**
|
|
8342
|
+
* @private
|
|
8343
|
+
* @author ddj 2026年11月03号
|
|
8344
|
+
* @description: 按块决定聚合回写文件决定:仍含 pending 即 pending,多值即 mixed,单一值取该值。
|
|
8345
|
+
* @param db 判定事务连接
|
|
8346
|
+
* @param id 文件身份
|
|
8347
|
+
* @param fallback 没有任何块时的兜底决策
|
|
8348
|
+
* @returns 无
|
|
8349
|
+
*/
|
|
8350
|
+
function aggregateDecision(db, id, fallback) {
|
|
8351
|
+
const decisions = db.prepare("SELECT DISTINCT decision FROM review_blocks WHERE file_id=?").all(id).map((row) => String(row.decision));
|
|
8352
|
+
let aggregate = fallback;
|
|
8353
|
+
if (decisions.includes("pending")) aggregate = "pending";
|
|
8354
|
+
else if (decisions.length > 1) aggregate = "mixed";
|
|
8355
|
+
else if (decisions.length === 1) aggregate = decisions[0];
|
|
8356
|
+
db.prepare("UPDATE review_files SET decision=? WHERE id=?").run(aggregate, id);
|
|
8357
|
+
}
|
|
7723
8358
|
/** @public @author ddj 2026年10月27号 @param db Active decision transaction. @param request File or block action. Updates aggregate without losing mixed decisions. @throws Invalid or already-decided block. */
|
|
7724
8359
|
function changeBlocks(db, request) {
|
|
7725
8360
|
if (request.scope === "hunk") {
|
|
7726
8361
|
if (db.prepare("UPDATE review_blocks SET decision=? WHERE file_id=? AND block_id=? AND decision='pending'").run(request.decision, request.id, request.blockId ?? "").changes !== 1) throw new Error("差异块不存在或已处理");
|
|
7727
|
-
} else db
|
|
7728
|
-
|
|
7729
|
-
let aggregate = request.decision;
|
|
7730
|
-
if (decisions.includes("pending")) aggregate = "pending";
|
|
7731
|
-
else if (decisions.length > 1) aggregate = "mixed";
|
|
7732
|
-
else if (decisions.length === 1) aggregate = decisions[0];
|
|
7733
|
-
db.prepare("UPDATE review_files SET decision=? WHERE id=?").run(aggregate, request.id);
|
|
8362
|
+
} else markBlocks(db, request.id, request.decision);
|
|
8363
|
+
aggregateDecision(db, request.id, request.decision);
|
|
7734
8364
|
}
|
|
7735
8365
|
/** @public @author ddj 2026年10月26号 @param cwd Workspace. @param request Frozen identity/revision. @returns Idempotent decision; rejection never guesses a target. */
|
|
7736
8366
|
function decideReview(cwd, request) {
|
|
@@ -7760,6 +8390,87 @@ function decideReview(cwd, request) {
|
|
|
7760
8390
|
throw error;
|
|
7761
8391
|
}
|
|
7762
8392
|
}
|
|
8393
|
+
/**
|
|
8394
|
+
* @private
|
|
8395
|
+
* @author ddj 2026年11月03号
|
|
8396
|
+
* @description: 取当前仍待审的官方记录摘要,作为批量写入的权威依据(不用客户端快照值)。
|
|
8397
|
+
* @param db 判定事务连接
|
|
8398
|
+
* @returns id → 库内 seq/revision/identity
|
|
8399
|
+
*/
|
|
8400
|
+
function pendingRows(db) {
|
|
8401
|
+
const rows = db.prepare("SELECT id,seq,revision,identity FROM review_files WHERE current=1 AND decision='pending'").all();
|
|
8402
|
+
const map = /* @__PURE__ */ new Map();
|
|
8403
|
+
for (const row of rows) map.set(String(row.id), {
|
|
8404
|
+
seq: Number(row.seq),
|
|
8405
|
+
revision: Number(row.revision),
|
|
8406
|
+
identity: String(row.identity)
|
|
8407
|
+
});
|
|
8408
|
+
return map;
|
|
8409
|
+
}
|
|
8410
|
+
/**
|
|
8411
|
+
* @public
|
|
8412
|
+
* @author ddj 2026年11月03号
|
|
8413
|
+
* @description: 一次事务内批量保留官方待审记录。写入条件取库内当前 revision,客户端的 seq/revision 只用于派生操作号,
|
|
8414
|
+
* 因此快照漂移不会让整批中止;已结案、有尚待核对 Undo、操作号冲突的条目只跳过自己。
|
|
8415
|
+
* @param cwd 工作区
|
|
8416
|
+
* @param items 待保留条目(含派生好的 operationId)
|
|
8417
|
+
* @returns 成功保留的 identity 列表与跳过明细
|
|
8418
|
+
* @throws 事务级异常(SQL/程序错误)整体回滚后抛出
|
|
8419
|
+
*/
|
|
8420
|
+
function keepOfficialAll(cwd, items) {
|
|
8421
|
+
const db = reviewDb(cwd);
|
|
8422
|
+
const rows = pendingRows(db);
|
|
8423
|
+
const outcome = {
|
|
8424
|
+
paths: [],
|
|
8425
|
+
skipped: []
|
|
8426
|
+
};
|
|
8427
|
+
const keep = db.prepare(`UPDATE review_files SET decision='accepted',revision=revision+1
|
|
8428
|
+
WHERE id=? AND seq=? AND revision=? AND current=1 AND decision='pending'`);
|
|
8429
|
+
const prepared = db.prepare("SELECT 1 FROM review_ops WHERE state='prepared' AND json_extract(request,'$.id')=?");
|
|
8430
|
+
db.exec("BEGIN IMMEDIATE");
|
|
8431
|
+
try {
|
|
8432
|
+
for (const item of items) {
|
|
8433
|
+
const encoded = JSON.stringify({
|
|
8434
|
+
id: item.id,
|
|
8435
|
+
seq: item.seq
|
|
8436
|
+
});
|
|
8437
|
+
const previous = db.prepare("SELECT request,result FROM review_ops WHERE operation_id=?").get(item.operationId);
|
|
8438
|
+
if (previous) {
|
|
8439
|
+
if (previous.request !== encoded || !previous.result) outcome.skipped.push({
|
|
8440
|
+
id: item.id,
|
|
8441
|
+
reason: "该条目已由其他操作处理"
|
|
8442
|
+
});
|
|
8443
|
+
else outcome.paths.push(String(JSON.parse(String(previous.result)).identity));
|
|
8444
|
+
continue;
|
|
8445
|
+
}
|
|
8446
|
+
const row = rows.get(item.id);
|
|
8447
|
+
if (!row || prepared.get(item.id)) {
|
|
8448
|
+
outcome.skipped.push({
|
|
8449
|
+
id: item.id,
|
|
8450
|
+
reason: row ? "该文件有尚待核对的 Undo,暂不能 Keep" : "已结案或不再是待审记录"
|
|
8451
|
+
});
|
|
8452
|
+
continue;
|
|
8453
|
+
}
|
|
8454
|
+
if (keep.run(item.id, row.seq, row.revision).changes !== 1) {
|
|
8455
|
+
outcome.skipped.push({
|
|
8456
|
+
id: item.id,
|
|
8457
|
+
reason: "审查版本冲突,请刷新后重试"
|
|
8458
|
+
});
|
|
8459
|
+
continue;
|
|
8460
|
+
}
|
|
8461
|
+
markBlocks(db, item.id, "accepted");
|
|
8462
|
+
aggregateDecision(db, item.id, "accepted");
|
|
8463
|
+
db.prepare("INSERT INTO review_ops VALUES(?,?,'committed',?,NULL)").run(item.operationId, encoded, JSON.stringify({ identity: row.identity }));
|
|
8464
|
+
outcome.paths.push(row.identity);
|
|
8465
|
+
rows.delete(item.id);
|
|
8466
|
+
}
|
|
8467
|
+
db.exec("COMMIT");
|
|
8468
|
+
return outcome;
|
|
8469
|
+
} catch (error) {
|
|
8470
|
+
if (db.isTransaction) db.exec("ROLLBACK");
|
|
8471
|
+
throw error;
|
|
8472
|
+
}
|
|
8473
|
+
}
|
|
7763
8474
|
//#endregion
|
|
7764
8475
|
//#region src/officialCapture.ts
|
|
7765
8476
|
/** Deferred official-event collection; no private snapshot access. @author ddj 2026年10月26号 */
|
|
@@ -8066,6 +8777,67 @@ function evidenceText(evidence) {
|
|
|
8066
8777
|
};
|
|
8067
8778
|
}
|
|
8068
8779
|
//#endregion
|
|
8780
|
+
//#region src/toolTargets.ts
|
|
8781
|
+
/** Optional Host patch target extraction; results never substitute raw preimages. @author ddj 2026年11月04号 */
|
|
8782
|
+
/** @public @author ddj 2026年11月04号 @param result Untrusted tool envelope. @returns Diagnostic-only status; never writable preimages. */
|
|
8783
|
+
function patchDiagnostics(result) {
|
|
8784
|
+
const payload = result?.value ?? result;
|
|
8785
|
+
if (payload?.phase !== "commit" || !Array.isArray(payload.operations)) return ["apply_patch result unavailable or malformed"];
|
|
8786
|
+
const diagnostics = [];
|
|
8787
|
+
for (const operation of payload.operations) {
|
|
8788
|
+
if (!operation || typeof operation.path !== "string" || ![
|
|
8789
|
+
"add",
|
|
8790
|
+
"update",
|
|
8791
|
+
"delete",
|
|
8792
|
+
"move"
|
|
8793
|
+
].includes(operation.kind) || ![
|
|
8794
|
+
"completed",
|
|
8795
|
+
"failed",
|
|
8796
|
+
"unknown",
|
|
8797
|
+
"skipped"
|
|
8798
|
+
].includes(operation.status)) {
|
|
8799
|
+
diagnostics.push("apply_patch operation malformed");
|
|
8800
|
+
continue;
|
|
8801
|
+
}
|
|
8802
|
+
if (operation.status !== "completed") diagnostics.push(operation.path + ": " + operation.status);
|
|
8803
|
+
if (operation.changes !== void 0 && (!Array.isArray(operation.changes) || operation.changes.some((change) => !change || typeof change.path !== "string" || !(change.before === null || typeof change.before === "string") || !(change.after === null || typeof change.after === "string")))) diagnostics.push(operation.path + ": changes malformed");
|
|
8804
|
+
}
|
|
8805
|
+
return diagnostics;
|
|
8806
|
+
}
|
|
8807
|
+
let parserLoad;
|
|
8808
|
+
/** @public @author ddj 2026年11月04号 @returns Optional official parser; missing capabilities remain retryable. */
|
|
8809
|
+
async function patchParser() {
|
|
8810
|
+
if (!parserLoad) parserLoad = loadHostModule("@deepseek-ai/dsh-fs-patch").then((module) => {
|
|
8811
|
+
const parser = module.parsePatch;
|
|
8812
|
+
return typeof parser === "function" ? parser : null;
|
|
8813
|
+
}).catch(() => null);
|
|
8814
|
+
const parser = await parserLoad;
|
|
8815
|
+
if (!parser) parserLoad = void 0;
|
|
8816
|
+
return parser;
|
|
8817
|
+
}
|
|
8818
|
+
/** @public @author ddj 2026年11月04号 @param call Tool identity and arguments. @param parser Optional injectable official parser. @returns Unique unresolved source/destination paths. @throws Unsupported parser or malformed patch. */
|
|
8819
|
+
async function toolTargets(call, parser) {
|
|
8820
|
+
if ([
|
|
8821
|
+
"edit",
|
|
8822
|
+
"write",
|
|
8823
|
+
"str_replace_editor"
|
|
8824
|
+
].includes(String(call.name))) {
|
|
8825
|
+
const path = call.arguments?.file_path ?? call.arguments?.path;
|
|
8826
|
+
return typeof path === "string" && path ? [path] : [];
|
|
8827
|
+
}
|
|
8828
|
+
if (call.name !== "apply_patch") return [];
|
|
8829
|
+
if (typeof call.arguments?.input !== "string") throw new Error("apply_patch input unavailable");
|
|
8830
|
+
const parse = parser === void 0 ? await patchParser() : parser;
|
|
8831
|
+
if (!parse) throw new Error("Host patch parser unavailable");
|
|
8832
|
+
const paths = /* @__PURE__ */ new Set();
|
|
8833
|
+
for (const operation of parse(call.arguments.input)) {
|
|
8834
|
+
if (typeof operation.path !== "string" || !operation.path) throw new Error("Invalid patch target");
|
|
8835
|
+
paths.add(operation.path);
|
|
8836
|
+
if (operation.moveTo) paths.add(operation.moveTo);
|
|
8837
|
+
}
|
|
8838
|
+
return [...paths];
|
|
8839
|
+
}
|
|
8840
|
+
//#endregion
|
|
8069
8841
|
//#region src/reviewCapture.ts
|
|
8070
8842
|
/** Independent byte evidence; official diff remains the only display authority. @author ddj 2026年10月27号 */
|
|
8071
8843
|
/** @public @author ddj 2026年10月27号 @param ctx Host lifecycle owner. @returns Scoped byte recorder; failures disable undo evidence only, never tool execution. */
|
|
@@ -8073,12 +8845,15 @@ function installEvidence(ctx) {
|
|
|
8073
8845
|
const chains = /* @__PURE__ */ new Map();
|
|
8074
8846
|
const staged = /* @__PURE__ */ new Map();
|
|
8075
8847
|
let stopped = false;
|
|
8848
|
+
const disposed = /* @__PURE__ */ new Set();
|
|
8076
8849
|
ctx.on("tools/execute", aroundTool);
|
|
8077
8850
|
ctx.on("session/event", onEvent);
|
|
8078
8851
|
ctx.on("session/disposed", onDisposed);
|
|
8079
8852
|
/** @private @author ddj 2026年10月27号 @param session Disposed owner. Drops optional in-memory evidence, never persisted official facts. */
|
|
8080
8853
|
function onDisposed(session) {
|
|
8854
|
+
disposed.add(session.id);
|
|
8081
8855
|
chains.delete(session.id);
|
|
8856
|
+
for (const key of staged.keys()) if (JSON.parse(key)[0] === session.id) staged.delete(key);
|
|
8082
8857
|
}
|
|
8083
8858
|
/** @private @author ddj 2026年10月27号 @returns Current bounded capture footprint, excluding transient filesystem reads. */
|
|
8084
8859
|
function footprint() {
|
|
@@ -8089,48 +8864,94 @@ function installEvidence(ctx) {
|
|
|
8089
8864
|
/** @private @author ddj 2026年10月27号 @param exec Allowed tool dispatch. @param next Original dispatch. @returns Original result unchanged, regardless of capture failure. */
|
|
8090
8865
|
async function aroundTool(exec, next) {
|
|
8091
8866
|
const session = exec.agent?.session;
|
|
8092
|
-
|
|
8093
|
-
|
|
8094
|
-
let fs;
|
|
8095
|
-
let chain;
|
|
8096
|
-
let target;
|
|
8867
|
+
if (stopped || !session?.id) return next();
|
|
8868
|
+
const captures = [];
|
|
8097
8869
|
try {
|
|
8098
|
-
|
|
8099
|
-
fs = ctx.get("fs");
|
|
8100
|
-
|
|
8101
|
-
const
|
|
8102
|
-
|
|
8103
|
-
|
|
8104
|
-
|
|
8105
|
-
files = /* @__PURE__ */ new Map();
|
|
8106
|
-
chains.set(session.id, files);
|
|
8107
|
-
}
|
|
8108
|
-
if (files.size >= 64 && !files.has(identity)) return next();
|
|
8109
|
-
chain = files.get(identity);
|
|
8110
|
-
if (!chain) {
|
|
8111
|
-
chain = {
|
|
8112
|
-
before,
|
|
8113
|
-
after: before,
|
|
8114
|
-
busy: false,
|
|
8115
|
-
invalid: false
|
|
8116
|
-
};
|
|
8117
|
-
files.set(identity, chain);
|
|
8118
|
-
}
|
|
8119
|
-
if (chain.busy || chain.after.hash !== before.hash) chain.invalid = true;
|
|
8120
|
-
chain.busy = true;
|
|
8870
|
+
const cwd = cwdOfSession(ctx, session);
|
|
8871
|
+
const fs = ctx.get("fs");
|
|
8872
|
+
const paths = await toolTargets(exec);
|
|
8873
|
+
for (const path of paths) try {
|
|
8874
|
+
const capture = await prepareRaw(fs, session.id, cwd ?? void 0, path, exec.signal);
|
|
8875
|
+
if (capture) captures.push(capture);
|
|
8876
|
+
} catch {}
|
|
8121
8877
|
} catch {}
|
|
8122
8878
|
try {
|
|
8123
8879
|
return await next();
|
|
8124
8880
|
} finally {
|
|
8125
|
-
|
|
8881
|
+
for (const capture of captures) {
|
|
8126
8882
|
try {
|
|
8127
|
-
|
|
8883
|
+
const after = await readRaw(capture.fs, capture.target, exec.signal);
|
|
8884
|
+
if (!stopped && !disposed.has(session.id)) {
|
|
8885
|
+
capture.chain.after = after;
|
|
8886
|
+
if (footprint() > 33554432) {
|
|
8887
|
+
capture.chain.invalid = true;
|
|
8888
|
+
capture.chain.before = rawReviewFile(null);
|
|
8889
|
+
capture.chain.after = rawReviewFile(null);
|
|
8890
|
+
}
|
|
8891
|
+
}
|
|
8128
8892
|
} catch {
|
|
8129
|
-
chain.invalid = true;
|
|
8893
|
+
capture.chain.invalid = true;
|
|
8130
8894
|
}
|
|
8895
|
+
capture.chain.busy = false;
|
|
8896
|
+
}
|
|
8897
|
+
}
|
|
8898
|
+
}
|
|
8899
|
+
/** @private @author ddj 2026年11月04号 @param sessionId Frozen owner. @param cwd Frozen workspace. @param path Patch target. @param signal Tool cancellation. @returns Reserved raw capture or no evidence. */
|
|
8900
|
+
async function prepareRaw(fs, sessionId, cwd, path, signal) {
|
|
8901
|
+
if (stopped || disposed.has(sessionId) || !cwd || chains.size >= 16 && !chains.has(sessionId) || footprint() >= 33554432) return null;
|
|
8902
|
+
const target = await fs.resolve(path, { cwd });
|
|
8903
|
+
if (stopped || disposed.has(sessionId)) return null;
|
|
8904
|
+
const identity = fs.processPath(target);
|
|
8905
|
+
let files = chains.get(sessionId);
|
|
8906
|
+
if (!files) {
|
|
8907
|
+
files = /* @__PURE__ */ new Map();
|
|
8908
|
+
chains.set(sessionId, files);
|
|
8909
|
+
}
|
|
8910
|
+
if (files.size >= 64 && !files.has(identity)) return null;
|
|
8911
|
+
let chain = files.get(identity);
|
|
8912
|
+
if (chain?.busy) {
|
|
8913
|
+
chain.invalid = true;
|
|
8914
|
+
return null;
|
|
8915
|
+
}
|
|
8916
|
+
const empty = rawReviewFile(null);
|
|
8917
|
+
if (!chain) {
|
|
8918
|
+
chain = {
|
|
8919
|
+
before: empty,
|
|
8920
|
+
after: empty,
|
|
8921
|
+
busy: true,
|
|
8922
|
+
invalid: false
|
|
8923
|
+
};
|
|
8924
|
+
files.set(identity, chain);
|
|
8925
|
+
try {
|
|
8926
|
+
chain.before = await readRaw(fs, target, signal);
|
|
8927
|
+
chain.after = chain.before;
|
|
8928
|
+
} catch (error) {
|
|
8929
|
+
chain.invalid = true;
|
|
8930
|
+
chain.busy = false;
|
|
8931
|
+
throw error;
|
|
8932
|
+
}
|
|
8933
|
+
} else {
|
|
8934
|
+
chain.busy = true;
|
|
8935
|
+
try {
|
|
8936
|
+
const before = await readRaw(fs, target, signal);
|
|
8937
|
+
if (chain.after.hash !== before.hash) chain.invalid = true;
|
|
8938
|
+
} catch (error) {
|
|
8939
|
+
chain.invalid = true;
|
|
8131
8940
|
chain.busy = false;
|
|
8941
|
+
throw error;
|
|
8132
8942
|
}
|
|
8133
8943
|
}
|
|
8944
|
+
if (stopped || disposed.has(sessionId) || footprint() > 33554432) {
|
|
8945
|
+
chain.invalid = true;
|
|
8946
|
+
chain.busy = false;
|
|
8947
|
+
files.delete(identity);
|
|
8948
|
+
return null;
|
|
8949
|
+
}
|
|
8950
|
+
return {
|
|
8951
|
+
fs,
|
|
8952
|
+
target,
|
|
8953
|
+
chain
|
|
8954
|
+
};
|
|
8134
8955
|
}
|
|
8135
8956
|
/** @private @author ddj 2026年10月27号 @param session Source session. @param event Published official turn. Freezes the chain before the next turn can mutate it. */
|
|
8136
8957
|
function onEvent(session, event) {
|
|
@@ -8194,10 +9015,34 @@ async function readRaw(fs, target, signal) {
|
|
|
8194
9015
|
const WRITE_TOOLS = [
|
|
8195
9016
|
"edit",
|
|
8196
9017
|
"write",
|
|
8197
|
-
"str_replace_editor"
|
|
9018
|
+
"str_replace_editor",
|
|
9019
|
+
"apply_patch"
|
|
8198
9020
|
];
|
|
8199
9021
|
/** 单文件文本上限:超过即标注 oversized,不静默截断。 */
|
|
8200
9022
|
const MAX_TEXT = 1048576;
|
|
9023
|
+
const captureGaps = /* @__PURE__ */ new Map();
|
|
9024
|
+
/** @public @author ddj 2026年11月04号 @param cwd Workspace identity. @returns Bounded runtime-only capture diagnostics. */
|
|
9025
|
+
function pendingGaps(cwd) {
|
|
9026
|
+
return [...captureGaps.values()].filter((gap) => gap.cwd === cwd).map((gap) => gap.message);
|
|
9027
|
+
}
|
|
9028
|
+
/** @private @author ddj 2026年11月04号 @param ctx Host scope. @param exec Original call. @param path Failed target. @param error Evidence failure. Records no writable state. */
|
|
9029
|
+
function recordGap(scope, exec, path, error) {
|
|
9030
|
+
const { cwd, sessionId } = scope;
|
|
9031
|
+
const key = JSON.stringify([
|
|
9032
|
+
cwd,
|
|
9033
|
+
sessionId,
|
|
9034
|
+
exec.id,
|
|
9035
|
+
path
|
|
9036
|
+
]);
|
|
9037
|
+
captureGaps.set(key, {
|
|
9038
|
+
cwd,
|
|
9039
|
+
sessionId,
|
|
9040
|
+
message: path + ": " + String(error)
|
|
9041
|
+
});
|
|
9042
|
+
const local = [...captureGaps].filter(([, gap]) => gap.cwd === cwd);
|
|
9043
|
+
while (local.length > 50) captureGaps.delete(local.shift()[0]);
|
|
9044
|
+
while (captureGaps.size > 500) captureGaps.delete(captureGaps.keys().next().value);
|
|
9045
|
+
}
|
|
8201
9046
|
/**
|
|
8202
9047
|
* @author ddj 2026年10月30号
|
|
8203
9048
|
* @description: 安装写文件采集;读写失败只影响待审来源,绝不影响用户工具本身。
|
|
@@ -8208,12 +9053,18 @@ const MAX_TEXT = 1048576;
|
|
|
8208
9053
|
function installPending(ctx, sink) {
|
|
8209
9054
|
const state = {
|
|
8210
9055
|
busy: /* @__PURE__ */ new Set(),
|
|
8211
|
-
stopped: false
|
|
9056
|
+
stopped: false,
|
|
9057
|
+
disposed: /* @__PURE__ */ new Set()
|
|
8212
9058
|
};
|
|
8213
9059
|
ctx.on("tools/execute", (exec, next) => aroundTool(ctx, state, sink, exec, next));
|
|
9060
|
+
ctx.on("session/disposed", (session) => {
|
|
9061
|
+
state.disposed.add(session.id);
|
|
9062
|
+
for (const [key, gap] of captureGaps) if (gap.sessionId === session.id) captureGaps.delete(key);
|
|
9063
|
+
});
|
|
8214
9064
|
return { dispose() {
|
|
8215
9065
|
state.stopped = true;
|
|
8216
9066
|
state.busy.clear();
|
|
9067
|
+
captureGaps.clear();
|
|
8217
9068
|
} };
|
|
8218
9069
|
}
|
|
8219
9070
|
/**
|
|
@@ -8224,27 +9075,32 @@ function installPending(ctx, sink) {
|
|
|
8224
9075
|
* @param exec 工具分发上下文
|
|
8225
9076
|
* @returns 采集计划或 null
|
|
8226
9077
|
*/
|
|
8227
|
-
async function planOf(
|
|
8228
|
-
const
|
|
8229
|
-
|
|
8230
|
-
|
|
8231
|
-
if (!WRITE_TOOLS.includes(String(exec.name))) return null;
|
|
8232
|
-
const cwd = cwdOf(session);
|
|
8233
|
-
if (!cwd) return null;
|
|
8234
|
-
const fs = ctx.get("fs");
|
|
8235
|
-
const target = await resolveTarget(ctx, session, requested);
|
|
9078
|
+
async function planOf(scope, state, requested) {
|
|
9079
|
+
const { cwd, sessionId, fs } = scope;
|
|
9080
|
+
if (state.stopped || state.disposed.has(sessionId)) return null;
|
|
9081
|
+
const target = await fs.resolve(requested, { cwd });
|
|
8236
9082
|
const identity = fs.processPath(target);
|
|
8237
|
-
if (typeof identity !== "string" || !identity || state.busy.has(identity)) return null;
|
|
8238
|
-
|
|
8239
|
-
|
|
8240
|
-
|
|
8241
|
-
|
|
8242
|
-
|
|
8243
|
-
|
|
8244
|
-
|
|
8245
|
-
|
|
8246
|
-
|
|
8247
|
-
|
|
9083
|
+
if (state.stopped || state.disposed.has(sessionId) || typeof identity !== "string" || !identity || state.busy.has(identity)) return null;
|
|
9084
|
+
state.busy.add(identity);
|
|
9085
|
+
try {
|
|
9086
|
+
const before = await readText(fs, target);
|
|
9087
|
+
if (state.stopped || state.disposed.has(sessionId) || before !== null && before.length > MAX_TEXT) {
|
|
9088
|
+
state.busy.delete(identity);
|
|
9089
|
+
return null;
|
|
9090
|
+
}
|
|
9091
|
+
return {
|
|
9092
|
+
cwd: String(cwd),
|
|
9093
|
+
sessionId,
|
|
9094
|
+
path: requested,
|
|
9095
|
+
identity,
|
|
9096
|
+
before,
|
|
9097
|
+
target,
|
|
9098
|
+
fs
|
|
9099
|
+
};
|
|
9100
|
+
} catch (error) {
|
|
9101
|
+
state.busy.delete(identity);
|
|
9102
|
+
throw error;
|
|
9103
|
+
}
|
|
8248
9104
|
}
|
|
8249
9105
|
/**
|
|
8250
9106
|
* @author ddj 2026年10月30号
|
|
@@ -8257,63 +9113,87 @@ async function planOf(ctx, state, exec) {
|
|
|
8257
9113
|
* @returns 原始工具结果
|
|
8258
9114
|
*/
|
|
8259
9115
|
async function aroundTool(ctx, state, sink, exec, next) {
|
|
8260
|
-
|
|
9116
|
+
if (state.stopped || !WRITE_TOOLS.includes(String(exec.name))) return next();
|
|
9117
|
+
const session = exec.agent?.session;
|
|
9118
|
+
const cwd = session ? cwdOfSession(ctx, session) : null;
|
|
9119
|
+
if (!session?.id || !cwd) return next();
|
|
9120
|
+
const scope = {
|
|
9121
|
+
cwd,
|
|
9122
|
+
sessionId: session.id,
|
|
9123
|
+
fs: ctx.get("fs")
|
|
9124
|
+
};
|
|
9125
|
+
const plans = [];
|
|
8261
9126
|
try {
|
|
8262
|
-
|
|
9127
|
+
const paths = await toolTargets(exec);
|
|
9128
|
+
for (const path of paths) try {
|
|
9129
|
+
if (plans.length >= 16) throw new Error("capture target budget exceeded");
|
|
9130
|
+
const plan = await planOf(scope, state, path);
|
|
9131
|
+
if (plan) plans.push(plan);
|
|
9132
|
+
} catch (error) {
|
|
9133
|
+
if (!state.stopped && !state.disposed.has(scope.sessionId)) recordGap(scope, exec, path, error);
|
|
9134
|
+
log.debug("待审采集跳过:" + path + ": " + String(error));
|
|
9135
|
+
}
|
|
8263
9136
|
} catch (error) {
|
|
8264
|
-
|
|
9137
|
+
if (!state.stopped && !state.disposed.has(scope.sessionId)) recordGap(scope, exec, String(exec.name), error);
|
|
9138
|
+
log.debug("待审目标不可用:" + String(error));
|
|
8265
9139
|
}
|
|
8266
|
-
if (!plan) return next();
|
|
8267
|
-
state.busy.add(plan.identity);
|
|
8268
9140
|
try {
|
|
8269
|
-
|
|
9141
|
+
const result = await next();
|
|
9142
|
+
if (exec.name === "apply_patch" && !state.stopped && !state.disposed.has(scope.sessionId)) for (const diagnostic of patchDiagnostics(result)) recordGap(scope, exec, "apply_patch", diagnostic);
|
|
9143
|
+
return result;
|
|
8270
9144
|
} finally {
|
|
8271
|
-
|
|
8272
|
-
|
|
8273
|
-
reportWrite(ctx, sink, plan);
|
|
9145
|
+
for (const plan of plans) try {
|
|
9146
|
+
await reportWrite(state, sink, plan);
|
|
8274
9147
|
} catch (error) {
|
|
9148
|
+
if (!state.stopped && !state.disposed.has(scope.sessionId)) recordGap(scope, exec, plan.path, error);
|
|
8275
9149
|
log.debug("待审上报失败:" + String(error));
|
|
9150
|
+
} finally {
|
|
9151
|
+
state.busy.delete(plan.identity);
|
|
8276
9152
|
}
|
|
8277
9153
|
}
|
|
8278
9154
|
}
|
|
8279
9155
|
/**
|
|
8280
|
-
* @author ddj 2026年
|
|
8281
|
-
* @description:
|
|
8282
|
-
* @param
|
|
9156
|
+
* @author ddj 2026年11月04号
|
|
9157
|
+
* @description: 固定目标写后读回;内容一致不产生条目,卸载后不再上报。
|
|
9158
|
+
* @param state 采集生命周期状态
|
|
8283
9159
|
* @param sink 已确认写入的接收方
|
|
8284
9160
|
* @param plan 采集计划
|
|
8285
9161
|
* @returns 无;读回失败只记日志
|
|
8286
9162
|
*/
|
|
8287
|
-
function reportWrite(
|
|
8288
|
-
|
|
8289
|
-
|
|
8290
|
-
|
|
8291
|
-
|
|
8292
|
-
|
|
8293
|
-
|
|
8294
|
-
|
|
8295
|
-
|
|
8296
|
-
|
|
8297
|
-
|
|
8298
|
-
|
|
8299
|
-
|
|
8300
|
-
|
|
8301
|
-
|
|
8302
|
-
|
|
9163
|
+
async function reportWrite(state, sink, plan) {
|
|
9164
|
+
if (state.stopped || state.disposed.has(plan.sessionId)) return;
|
|
9165
|
+
const after = await readText(plan.fs, plan.target);
|
|
9166
|
+
if (state.stopped || state.disposed.has(plan.sessionId)) return;
|
|
9167
|
+
if (after !== null && after.length > MAX_TEXT) throw new Error("capture text budget exceeded");
|
|
9168
|
+
for (const [key, gap] of captureGaps) {
|
|
9169
|
+
const identity = JSON.parse(key);
|
|
9170
|
+
if (gap.cwd === plan.cwd && gap.sessionId === plan.sessionId && identity[3] === plan.path) captureGaps.delete(key);
|
|
9171
|
+
}
|
|
9172
|
+
if (plan.before === after) return;
|
|
9173
|
+
sink({
|
|
9174
|
+
cwd: plan.cwd,
|
|
9175
|
+
sessionId: plan.sessionId,
|
|
9176
|
+
path: plan.path,
|
|
9177
|
+
identity: plan.identity,
|
|
9178
|
+
before: plan.before,
|
|
9179
|
+
after,
|
|
9180
|
+
now: Date.now()
|
|
9181
|
+
});
|
|
8303
9182
|
}
|
|
8304
9183
|
/**
|
|
8305
9184
|
* @author ddj 2026年10月29号
|
|
8306
|
-
* @description:
|
|
9185
|
+
* @description: 读取全文;只有明确缺失返回 null,不可读或超限错误保留失败语义。
|
|
8307
9186
|
* @param fs 文件系统
|
|
8308
9187
|
* @param target 目标
|
|
8309
9188
|
* @returns 文本或 null
|
|
8310
9189
|
*/
|
|
8311
|
-
async function readText(fs, target) {
|
|
9190
|
+
async function readText(fs, target, signal) {
|
|
8312
9191
|
try {
|
|
8313
|
-
return
|
|
9192
|
+
if (typeof fs.readBytes === "function") return rawText(rawReviewFile(await fs.readBytes(target, signal, MAX_TEXT)));
|
|
9193
|
+
throw new Error("Raw byte capture unavailable");
|
|
8314
9194
|
} catch (error) {
|
|
8315
9195
|
const code = error.code;
|
|
8316
|
-
if (code === "ENOENT" || code === "FS_NOT_FOUND"
|
|
9196
|
+
if (code === "ENOENT" || code === "FS_NOT_FOUND") return null;
|
|
8317
9197
|
throw error;
|
|
8318
9198
|
}
|
|
8319
9199
|
}
|
|
@@ -9216,375 +10096,123 @@ async function revert(input, targets, whole) {
|
|
|
9216
10096
|
revision: input.flags.revision + 1,
|
|
9217
10097
|
missing: written.content === null
|
|
9218
10098
|
},
|
|
9219
|
-
blocks: markAll(input.blocks, targets, "rejected")
|
|
9220
|
-
};
|
|
9221
|
-
}
|
|
9222
|
-
/**
|
|
9223
|
-
* @author ddj 2026年10月29号
|
|
9224
|
-
* @description: 整文件回退目标:基线为"文件不存在"时删除,否则写回基线全文。
|
|
9225
|
-
* @param entry 待审条目
|
|
9226
|
-
* @returns 目标内容,null 表示删除
|
|
9227
|
-
*/
|
|
9228
|
-
function wholeRevert(entry) {
|
|
9229
|
-
return entry.earlierVersion === "none" ? null : entry.oldText;
|
|
9230
|
-
}
|
|
9231
|
-
/**
|
|
9232
|
-
* @author ddj 2026年10月29号
|
|
9233
|
-
* @description: 块级回退:在最新内容里依次换回目标块,任一不可定位即整体放弃。
|
|
9234
|
-
* @param entry 待审条目
|
|
9235
|
-
* @param targets 目标块
|
|
9236
|
-
* @returns 新内容或 undefined
|
|
9237
|
-
*/
|
|
9238
|
-
function blockRevert(entry, targets) {
|
|
9239
|
-
let newText = entry.newText;
|
|
9240
|
-
for (const block of targets) {
|
|
9241
|
-
const next = revertBlock(newText, block);
|
|
9242
|
-
if (next === null) return void 0;
|
|
9243
|
-
newText = next;
|
|
9244
|
-
}
|
|
9245
|
-
return newText;
|
|
9246
|
-
}
|
|
9247
|
-
/**
|
|
9248
|
-
* @author ddj 2026年10月29号
|
|
9249
|
-
* @description: 按请求粒度挑出目标块;块不存在时返回 null 以区分"没有块"。
|
|
9250
|
-
* @param blocks 当前差异块
|
|
9251
|
-
* @param input 判定输入
|
|
9252
|
-
* @returns 目标块,未知块 ID 时为 null
|
|
9253
|
-
*/
|
|
9254
|
-
function pickBlocks(blocks, input) {
|
|
9255
|
-
const scope = input.request.scope;
|
|
9256
|
-
if (scope === "file" || scope === "all") return [];
|
|
9257
|
-
if (scope === "block") {
|
|
9258
|
-
const block = blocks.find((item) => item.id === input.request.blockId);
|
|
9259
|
-
return block ? [block] : null;
|
|
9260
|
-
}
|
|
9261
|
-
const start = input.request.startLine ?? 0;
|
|
9262
|
-
const end = input.request.endLine ?? start;
|
|
9263
|
-
return blocks.filter((block) => block.newStart <= end && block.newStart + Math.max(block.newLines.length, 1) >= start);
|
|
9264
|
-
}
|
|
9265
|
-
/**
|
|
9266
|
-
* @author ddj 2026年10月29号
|
|
9267
|
-
* @description: 把目标块标记为已决定;整文件时把全部块一起标记。
|
|
9268
|
-
* @param states 已存块决定
|
|
9269
|
-
* @param targets 目标块
|
|
9270
|
-
* @param decision 决定
|
|
9271
|
-
* @returns 新的块决定列表
|
|
9272
|
-
*/
|
|
9273
|
-
function markAll(states, targets, decision) {
|
|
9274
|
-
const next = states.slice();
|
|
9275
|
-
for (const block of targets) {
|
|
9276
|
-
const at = next.findIndex((state) => state.id === block.id);
|
|
9277
|
-
if (at >= 0) next[at] = {
|
|
9278
|
-
id: block.id,
|
|
9279
|
-
decision
|
|
9280
|
-
};
|
|
9281
|
-
else next.push({
|
|
9282
|
-
id: block.id,
|
|
9283
|
-
decision
|
|
9284
|
-
});
|
|
9285
|
-
}
|
|
9286
|
-
return next;
|
|
9287
|
-
}
|
|
9288
|
-
/**
|
|
9289
|
-
* @author ddj 2026年10月29号
|
|
9290
|
-
* @description: 能力口径:Keep 恒可;Revert 受超大/缺失/已解决限制,拒绝原因要能展示。
|
|
9291
|
-
* @param entry 待审条目
|
|
9292
|
-
* @param flags 持久标记
|
|
9293
|
-
* @returns 能力与原因
|
|
9294
|
-
*/
|
|
9295
|
-
function capabilityOf(entry, flags) {
|
|
9296
|
-
if (isSettled(entry)) return {
|
|
9297
|
-
keep: false,
|
|
9298
|
-
revert: false,
|
|
9299
|
-
reason: "settled"
|
|
9300
|
-
};
|
|
9301
|
-
if (flags.oversized) return {
|
|
9302
|
-
keep: false,
|
|
9303
|
-
revert: false,
|
|
9304
|
-
reason: "oversized"
|
|
9305
|
-
};
|
|
9306
|
-
if (entry.earlierVersion === "none" && entry.newText.length === 0) return {
|
|
9307
|
-
keep: true,
|
|
9308
|
-
revert: false,
|
|
9309
|
-
reason: "empty-create"
|
|
9310
|
-
};
|
|
9311
|
-
return {
|
|
9312
|
-
keep: true,
|
|
9313
|
-
revert: true,
|
|
9314
|
-
reason: ""
|
|
9315
|
-
};
|
|
9316
|
-
}
|
|
9317
|
-
/**
|
|
9318
|
-
* @author ddj 2026年10月29号
|
|
9319
|
-
* @description: 供详情接口复用的差异构造。
|
|
9320
|
-
* @param entry 待审条目
|
|
9321
|
-
* @param baselineEmpty 基线置空:文件由本次会话新建,旧基线只是会话自己的中间产物
|
|
9322
|
-
* @returns 官方差异
|
|
9323
|
-
*/
|
|
9324
|
-
function diffOf(entry, baselineEmpty = false) {
|
|
9325
|
-
if (!baselineEmpty) return textDiff(entry.path, entry.path, entry.oldText, entry.newText);
|
|
9326
|
-
if (entry.newText === "") return textDiff(entry.path, entry.path, "", "");
|
|
9327
|
-
const diff = textDiff(entry.path, entry.path, "", entry.newText);
|
|
9328
|
-
if (diff.kind !== "text") return diff;
|
|
9329
|
-
return {
|
|
9330
|
-
...diff,
|
|
9331
|
-
hunks: diff.hunks.map((hunk) => ({
|
|
9332
|
-
...hunk,
|
|
9333
|
-
oldLines: 0,
|
|
9334
|
-
lines: hunk.lines.filter((line) => line !== "-")
|
|
9335
|
-
}))
|
|
9336
|
-
};
|
|
9337
|
-
}
|
|
9338
|
-
//#endregion
|
|
9339
|
-
//#region src/pending/persist.ts
|
|
9340
|
-
/** 待审库版本号,写进 metadata 便于日后迁移。v3 给评论表加了引文与上下文列。 */
|
|
9341
|
-
const SCHEMA_VERSION = "3";
|
|
9342
|
-
/**
|
|
9343
|
-
* @author ddj 2026年10月29号
|
|
9344
|
-
* @description: 打开工作区库并确保待审表存在;DDL 与版本标记在同一事务里提交。
|
|
9345
|
-
* @param cwd 已迁移工作区
|
|
9346
|
-
* @returns 可读写的连接
|
|
9347
|
-
* @throws 未完成活跃库迁移时拒绝
|
|
9348
|
-
*/
|
|
9349
|
-
function pendingDb(cwd) {
|
|
9350
|
-
if (!activeReady(cwd)) throw new Error("待审投影要求先完成活跃库迁移");
|
|
9351
|
-
const db = dbFor(cwd);
|
|
9352
|
-
if (db.prepare("SELECT value FROM metadata WHERE name='pending_schema'").get()?.value === SCHEMA_VERSION) return db;
|
|
9353
|
-
db.exec("BEGIN IMMEDIATE");
|
|
9354
|
-
try {
|
|
9355
|
-
db.exec(DDL);
|
|
9356
|
-
migrateHistory(db);
|
|
9357
|
-
db.prepare("INSERT INTO metadata(name,value) VALUES('pending_schema',?) ON CONFLICT(name) DO UPDATE SET value=excluded.value").run(SCHEMA_VERSION);
|
|
9358
|
-
db.exec("COMMIT");
|
|
9359
|
-
} catch (error) {
|
|
9360
|
-
if (db.isTransaction) db.exec("ROLLBACK");
|
|
9361
|
-
throw error;
|
|
9362
|
-
}
|
|
9363
|
-
return db;
|
|
9364
|
-
}
|
|
9365
|
-
/** 待审表结构:条目、块决定、幂等操作、撤销栈。 */
|
|
9366
|
-
const DDL = `
|
|
9367
|
-
CREATE TABLE IF NOT EXISTS pending_entries (
|
|
9368
|
-
id TEXT PRIMARY KEY, path TEXT NOT NULL, earlier TEXT NOT NULL,
|
|
9369
|
-
old_text TEXT NOT NULL, new_text TEXT NOT NULL, updated_at INTEGER NOT NULL,
|
|
9370
|
-
unseen INTEGER NOT NULL DEFAULT 0, missing INTEGER NOT NULL DEFAULT 0,
|
|
9371
|
-
diverged INTEGER NOT NULL DEFAULT 0, oversized INTEGER NOT NULL DEFAULT 0,
|
|
9372
|
-
revision INTEGER NOT NULL DEFAULT 0, session_id TEXT NOT NULL,
|
|
9373
|
-
session_ids TEXT NOT NULL, lineage TEXT);
|
|
9374
|
-
CREATE TABLE IF NOT EXISTS pending_blocks (
|
|
9375
|
-
entry_id TEXT NOT NULL, block_id TEXT NOT NULL, decision TEXT NOT NULL,
|
|
9376
|
-
PRIMARY KEY(entry_id,block_id));
|
|
9377
|
-
CREATE TABLE IF NOT EXISTS pending_ops (
|
|
9378
|
-
operation_id TEXT PRIMARY KEY, request TEXT NOT NULL, state TEXT NOT NULL,
|
|
9379
|
-
result TEXT, undo TEXT);
|
|
9380
|
-
CREATE TABLE IF NOT EXISTS pending_history (
|
|
9381
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT, kind TEXT NOT NULL, entry_id TEXT,
|
|
9382
|
-
snapshot TEXT NOT NULL, created_at INTEGER NOT NULL,
|
|
9383
|
-
state TEXT NOT NULL DEFAULT 'applied');
|
|
9384
|
-
`;
|
|
9385
|
-
/**
|
|
9386
|
-
* @author ddj 2026年10月29号
|
|
9387
|
-
* @description: v1 历史表补 state 列(rebuild 成本高于一次 ALTER)。
|
|
9388
|
-
* @param db 已打开的工作区库
|
|
9389
|
-
* @returns 无
|
|
9390
|
-
*/
|
|
9391
|
-
function migrateHistory(db) {
|
|
9392
|
-
if (db.prepare("PRAGMA table_info(pending_history)").all().some((column) => column.name === "state")) return;
|
|
9393
|
-
db.exec("ALTER TABLE pending_history ADD COLUMN state TEXT NOT NULL DEFAULT 'applied'");
|
|
9394
|
-
}
|
|
9395
|
-
/**
|
|
9396
|
-
* @author ddj 2026年10月29号
|
|
9397
|
-
* @description: 读出全部待审条目,按更新时间升序。
|
|
9398
|
-
* @param cwd 工作区
|
|
9399
|
-
* @returns 条目与标记
|
|
9400
|
-
*/
|
|
9401
|
-
function loadEntries(cwd) {
|
|
9402
|
-
return pendingDb(cwd).prepare("SELECT * FROM pending_entries ORDER BY updated_at ASC").all().map((row) => ({
|
|
9403
|
-
entry: entryOfRow(row),
|
|
9404
|
-
flags: flagsOfRow(row)
|
|
9405
|
-
}));
|
|
9406
|
-
}
|
|
9407
|
-
/**
|
|
9408
|
-
* @author ddj 2026年10月29号
|
|
9409
|
-
* @description: 写入或更新一条条目(含标记),单条原子完成。
|
|
9410
|
-
* @param cwd 工作区
|
|
9411
|
-
* @param stored 条目与标记
|
|
9412
|
-
* @returns 无
|
|
9413
|
-
*/
|
|
9414
|
-
function saveEntry(cwd, stored) {
|
|
9415
|
-
const { entry, flags } = stored;
|
|
9416
|
-
pendingDb(cwd).prepare(`INSERT INTO pending_entries
|
|
9417
|
-
(id,path,earlier,old_text,new_text,updated_at,unseen,missing,diverged,oversized,revision,session_id,session_ids,lineage)
|
|
9418
|
-
VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|
|
9419
|
-
ON CONFLICT(id) DO UPDATE SET path=excluded.path,earlier=excluded.earlier,old_text=excluded.old_text,
|
|
9420
|
-
new_text=excluded.new_text,updated_at=excluded.updated_at,unseen=excluded.unseen,missing=excluded.missing,
|
|
9421
|
-
diverged=excluded.diverged,oversized=excluded.oversized,revision=excluded.revision,
|
|
9422
|
-
session_id=excluded.session_id,session_ids=excluded.session_ids,lineage=excluded.lineage`).run(entry.id, entry.path, entry.earlierVersion, entry.oldText, entry.newText, entry.updatedAt, entry.unseen === true ? 1 : 0, flags.missing ? 1 : 0, flags.diverged ? 1 : 0, flags.oversized ? 1 : 0, flags.revision, entry.sessionId, JSON.stringify(entry.sessionIds), entry.lineage ? JSON.stringify(entry.lineage) : null);
|
|
9423
|
-
}
|
|
9424
|
-
/**
|
|
9425
|
-
* @author ddj 2026年10月29号
|
|
9426
|
-
* @description: 删除条目及其块决定,用于"已解决条目移除"。
|
|
9427
|
-
* @param cwd 工作区
|
|
9428
|
-
* @param id 条目身份
|
|
9429
|
-
* @returns 无
|
|
9430
|
-
*/
|
|
9431
|
-
function dropEntry(cwd, id) {
|
|
9432
|
-
const db = pendingDb(cwd);
|
|
9433
|
-
db.exec("BEGIN IMMEDIATE");
|
|
9434
|
-
try {
|
|
9435
|
-
db.prepare("DELETE FROM pending_entries WHERE id=?").run(id);
|
|
9436
|
-
db.prepare("DELETE FROM pending_blocks WHERE entry_id=?").run(id);
|
|
9437
|
-
db.exec("COMMIT");
|
|
9438
|
-
} catch (error) {
|
|
9439
|
-
if (db.isTransaction) db.exec("ROLLBACK");
|
|
9440
|
-
throw error;
|
|
9441
|
-
}
|
|
9442
|
-
}
|
|
9443
|
-
/**
|
|
9444
|
-
* @author ddj 2026年10月29号
|
|
9445
|
-
* @description: 覆盖某条目的块决定集合。
|
|
9446
|
-
* @param cwd 工作区
|
|
9447
|
-
* @param entryId 条目身份
|
|
9448
|
-
* @param blocks 块决定
|
|
9449
|
-
* @returns 无
|
|
9450
|
-
*/
|
|
9451
|
-
function saveBlocks(cwd, entryId, blocks) {
|
|
9452
|
-
const db = pendingDb(cwd);
|
|
9453
|
-
db.exec("BEGIN IMMEDIATE");
|
|
9454
|
-
try {
|
|
9455
|
-
db.prepare("DELETE FROM pending_blocks WHERE entry_id=?").run(entryId);
|
|
9456
|
-
const insert = db.prepare("INSERT INTO pending_blocks VALUES(?,?,?)");
|
|
9457
|
-
for (const block of blocks) insert.run(entryId, block.id, block.decision);
|
|
9458
|
-
db.exec("COMMIT");
|
|
9459
|
-
} catch (error) {
|
|
9460
|
-
if (db.isTransaction) db.exec("ROLLBACK");
|
|
9461
|
-
throw error;
|
|
9462
|
-
}
|
|
9463
|
-
}
|
|
9464
|
-
/**
|
|
9465
|
-
* @author ddj 2026年10月29号
|
|
9466
|
-
* @description: 读取幂等操作记录;同 operationId 的重复请求必须复用结果。
|
|
9467
|
-
* @param cwd 工作区
|
|
9468
|
-
* @param operationId 操作身份
|
|
9469
|
-
* @returns 请求原文、状态与结果
|
|
9470
|
-
*/
|
|
9471
|
-
function readOp(cwd, operationId) {
|
|
9472
|
-
const row = pendingDb(cwd).prepare("SELECT request,state,result FROM pending_ops WHERE operation_id=?").get(operationId);
|
|
9473
|
-
return row ? {
|
|
9474
|
-
request: String(row.request),
|
|
9475
|
-
state: String(row.state),
|
|
9476
|
-
result: row.result === null ? null : String(row.result)
|
|
9477
|
-
} : void 0;
|
|
10099
|
+
blocks: markAll(input.blocks, targets, "rejected")
|
|
10100
|
+
};
|
|
9478
10101
|
}
|
|
9479
10102
|
/**
|
|
9480
10103
|
* @author ddj 2026年10月29号
|
|
9481
|
-
* @description:
|
|
9482
|
-
* @param
|
|
9483
|
-
* @
|
|
9484
|
-
* @param request 请求原文
|
|
9485
|
-
* @param state prepared / committed
|
|
9486
|
-
* @param payload 结果与撤销快照
|
|
9487
|
-
* @returns 无
|
|
10104
|
+
* @description: 整文件回退目标:基线为"文件不存在"时删除,否则写回基线全文。
|
|
10105
|
+
* @param entry 待审条目
|
|
10106
|
+
* @returns 目标内容,null 表示删除
|
|
9488
10107
|
*/
|
|
9489
|
-
function
|
|
9490
|
-
|
|
9491
|
-
ON CONFLICT(operation_id) DO UPDATE SET state=excluded.state,
|
|
9492
|
-
result=COALESCE(excluded.result,pending_ops.result), undo=COALESCE(excluded.undo,pending_ops.undo)`).run(operationId, request, state, payload.result ?? null, payload.undo ?? null);
|
|
10108
|
+
function wholeRevert(entry) {
|
|
10109
|
+
return entry.earlierVersion === "none" ? null : entry.oldText;
|
|
9493
10110
|
}
|
|
9494
10111
|
/**
|
|
9495
10112
|
* @author ddj 2026年10月29号
|
|
9496
|
-
* @description:
|
|
9497
|
-
* @param
|
|
9498
|
-
* @param
|
|
9499
|
-
* @
|
|
9500
|
-
* @param snapshot 可重放快照
|
|
9501
|
-
* @param now 时间
|
|
9502
|
-
* @returns 无
|
|
9503
|
-
*/
|
|
9504
|
-
function pushHistory(cwd, kind, entryId, snapshot, now) {
|
|
9505
|
-
const db = pendingDb(cwd);
|
|
9506
|
-
db.prepare("DELETE FROM pending_history WHERE state='undone'").run();
|
|
9507
|
-
db.prepare("INSERT INTO pending_history(kind,entry_id,snapshot,created_at,state) VALUES(?,?,?,?,'applied')").run(kind, entryId, snapshot, now);
|
|
9508
|
-
}
|
|
9509
|
-
/**
|
|
9510
|
-
* @author ddj 2026年10月30号
|
|
9511
|
-
* @description: 取最近一条仍生效的历史记录,撤销就作用在它上面。
|
|
9512
|
-
* @param cwd 工作区
|
|
9513
|
-
* @returns 记录或 undefined
|
|
9514
|
-
*/
|
|
9515
|
-
function latestApplied(cwd) {
|
|
9516
|
-
const row = pendingDb(cwd).prepare("SELECT id,kind,entry_id,snapshot FROM pending_history WHERE state='applied' ORDER BY id DESC LIMIT 1").get();
|
|
9517
|
-
return row ? rowOfHistory(row) : void 0;
|
|
9518
|
-
}
|
|
9519
|
-
/**
|
|
9520
|
-
* @author ddj 2026年10月30号
|
|
9521
|
-
* @description: 取最早一条被撤销的记录,重做按"撤销的逆序"重新生效。
|
|
9522
|
-
* @param cwd 工作区
|
|
9523
|
-
* @returns 记录或 undefined
|
|
10113
|
+
* @description: 块级回退:在最新内容里依次换回目标块,任一不可定位即整体放弃。
|
|
10114
|
+
* @param entry 待审条目
|
|
10115
|
+
* @param targets 目标块
|
|
10116
|
+
* @returns 新内容或 undefined
|
|
9524
10117
|
*/
|
|
9525
|
-
function
|
|
9526
|
-
|
|
9527
|
-
|
|
10118
|
+
function blockRevert(entry, targets) {
|
|
10119
|
+
let newText = entry.newText;
|
|
10120
|
+
for (const block of targets) {
|
|
10121
|
+
const next = revertBlock(newText, block);
|
|
10122
|
+
if (next === null) return void 0;
|
|
10123
|
+
newText = next;
|
|
10124
|
+
}
|
|
10125
|
+
return newText;
|
|
9528
10126
|
}
|
|
9529
10127
|
/**
|
|
9530
|
-
* @author ddj 2026年10月
|
|
9531
|
-
* @description:
|
|
9532
|
-
* @param
|
|
9533
|
-
* @param
|
|
9534
|
-
* @
|
|
9535
|
-
* @returns 无
|
|
10128
|
+
* @author ddj 2026年10月29号
|
|
10129
|
+
* @description: 按请求粒度挑出目标块;块不存在时返回 null 以区分"没有块"。
|
|
10130
|
+
* @param blocks 当前差异块
|
|
10131
|
+
* @param input 判定输入
|
|
10132
|
+
* @returns 目标块,未知块 ID 时为 null
|
|
9536
10133
|
*/
|
|
9537
|
-
function
|
|
9538
|
-
|
|
10134
|
+
function pickBlocks(blocks, input) {
|
|
10135
|
+
const scope = input.request.scope;
|
|
10136
|
+
if (scope === "file" || scope === "all") return [];
|
|
10137
|
+
if (scope === "block") {
|
|
10138
|
+
const block = blocks.find((item) => item.id === input.request.blockId);
|
|
10139
|
+
return block ? [block] : null;
|
|
10140
|
+
}
|
|
10141
|
+
const start = input.request.startLine ?? 0;
|
|
10142
|
+
const end = input.request.endLine ?? start;
|
|
10143
|
+
return blocks.filter((block) => block.newStart <= end && block.newStart + Math.max(block.newLines.length, 1) >= start);
|
|
9539
10144
|
}
|
|
9540
10145
|
/**
|
|
9541
|
-
* @author ddj 2026年10月
|
|
9542
|
-
* @description:
|
|
9543
|
-
* @param
|
|
9544
|
-
* @
|
|
10146
|
+
* @author ddj 2026年10月29号
|
|
10147
|
+
* @description: 把目标块标记为已决定;整文件时把全部块一起标记。
|
|
10148
|
+
* @param states 已存块决定
|
|
10149
|
+
* @param targets 目标块
|
|
10150
|
+
* @param decision 决定
|
|
10151
|
+
* @returns 新的块决定列表
|
|
9545
10152
|
*/
|
|
9546
|
-
function
|
|
9547
|
-
|
|
9548
|
-
|
|
9549
|
-
|
|
9550
|
-
|
|
9551
|
-
|
|
9552
|
-
|
|
10153
|
+
function markAll(states, targets, decision) {
|
|
10154
|
+
const next = states.slice();
|
|
10155
|
+
for (const block of targets) {
|
|
10156
|
+
const at = next.findIndex((state) => state.id === block.id);
|
|
10157
|
+
if (at >= 0) next[at] = {
|
|
10158
|
+
id: block.id,
|
|
10159
|
+
decision
|
|
10160
|
+
};
|
|
10161
|
+
else next.push({
|
|
10162
|
+
id: block.id,
|
|
10163
|
+
decision
|
|
10164
|
+
});
|
|
10165
|
+
}
|
|
10166
|
+
return next;
|
|
9553
10167
|
}
|
|
9554
10168
|
/**
|
|
9555
10169
|
* @author ddj 2026年10月29号
|
|
9556
|
-
* @description:
|
|
9557
|
-
* @param
|
|
9558
|
-
* @
|
|
10170
|
+
* @description: 能力口径:Keep 恒可;Revert 受超大/缺失/已解决限制,拒绝原因要能展示。
|
|
10171
|
+
* @param entry 待审条目
|
|
10172
|
+
* @param flags 持久标记
|
|
10173
|
+
* @returns 能力与原因
|
|
9559
10174
|
*/
|
|
9560
|
-
function
|
|
9561
|
-
|
|
9562
|
-
|
|
9563
|
-
|
|
9564
|
-
|
|
9565
|
-
|
|
9566
|
-
|
|
9567
|
-
|
|
9568
|
-
|
|
9569
|
-
|
|
9570
|
-
|
|
10175
|
+
function capabilityOf(entry, flags) {
|
|
10176
|
+
if (isSettled(entry)) return {
|
|
10177
|
+
keep: false,
|
|
10178
|
+
revert: false,
|
|
10179
|
+
reason: "settled"
|
|
10180
|
+
};
|
|
10181
|
+
if (flags.oversized) return {
|
|
10182
|
+
keep: false,
|
|
10183
|
+
revert: false,
|
|
10184
|
+
reason: "oversized"
|
|
10185
|
+
};
|
|
10186
|
+
if (entry.earlierVersion === "none" && entry.newText.length === 0) return {
|
|
10187
|
+
keep: true,
|
|
10188
|
+
revert: false,
|
|
10189
|
+
reason: "empty-create"
|
|
10190
|
+
};
|
|
10191
|
+
return {
|
|
10192
|
+
keep: true,
|
|
10193
|
+
revert: true,
|
|
10194
|
+
reason: ""
|
|
9571
10195
|
};
|
|
9572
|
-
if (Number(row.unseen) === 1) entry.unseen = true;
|
|
9573
|
-
if (lineage !== void 0) entry.lineage = lineage;
|
|
9574
|
-
return entry;
|
|
9575
10196
|
}
|
|
9576
10197
|
/**
|
|
9577
10198
|
* @author ddj 2026年10月29号
|
|
9578
|
-
* @description:
|
|
9579
|
-
* @param
|
|
9580
|
-
* @
|
|
10199
|
+
* @description: 供详情接口复用的差异构造。
|
|
10200
|
+
* @param entry 待审条目
|
|
10201
|
+
* @param baselineEmpty 基线置空:文件由本次会话新建,旧基线只是会话自己的中间产物
|
|
10202
|
+
* @returns 官方差异
|
|
9581
10203
|
*/
|
|
9582
|
-
function
|
|
10204
|
+
function diffOf(entry, baselineEmpty = false) {
|
|
10205
|
+
if (!baselineEmpty) return textDiff(entry.path, entry.path, entry.oldText, entry.newText);
|
|
10206
|
+
if (entry.newText === "") return textDiff(entry.path, entry.path, "", "");
|
|
10207
|
+
const diff = textDiff(entry.path, entry.path, "", entry.newText);
|
|
10208
|
+
if (diff.kind !== "text") return diff;
|
|
9583
10209
|
return {
|
|
9584
|
-
|
|
9585
|
-
|
|
9586
|
-
|
|
9587
|
-
|
|
10210
|
+
...diff,
|
|
10211
|
+
hunks: diff.hunks.map((hunk) => ({
|
|
10212
|
+
...hunk,
|
|
10213
|
+
oldLines: 0,
|
|
10214
|
+
lines: hunk.lines.filter((line) => line !== "-")
|
|
10215
|
+
}))
|
|
9588
10216
|
};
|
|
9589
10217
|
}
|
|
9590
10218
|
//#endregion
|
|
@@ -9693,18 +10321,39 @@ function itemOf(stored, baselineEmpty = false) {
|
|
|
9693
10321
|
return item;
|
|
9694
10322
|
}
|
|
9695
10323
|
/**
|
|
10324
|
+
* @private
|
|
10325
|
+
* @author ddj 2026年11月04号
|
|
10326
|
+
* @description 从条目来源判断创建基线;任何决定历史都阻止展示层复活已推进的旧基线。
|
|
10327
|
+
* @param cwd 工作区
|
|
10328
|
+
* @param stored 累计条目
|
|
10329
|
+
* @param sourceAdds 单次读取内的来源创建集合缓存
|
|
10330
|
+
* @returns 是否将展示基线置空
|
|
10331
|
+
*/
|
|
10332
|
+
function emptyBaseOf(cwd, stored, sourceAdds = /* @__PURE__ */ new Map()) {
|
|
10333
|
+
if (isSettled(stored.entry)) return false;
|
|
10334
|
+
if (pendingDb(cwd).prepare("SELECT 1 FROM pending_history WHERE entry_id=? LIMIT 1").get(stored.entry.id)) return false;
|
|
10335
|
+
for (const sourceId of stored.entry.sessionIds) {
|
|
10336
|
+
let createdPaths = sourceAdds.get(sourceId);
|
|
10337
|
+
if (!createdPaths) {
|
|
10338
|
+
createdPaths = addedFiles(cwd, sourceId);
|
|
10339
|
+
sourceAdds.set(sourceId, createdPaths);
|
|
10340
|
+
}
|
|
10341
|
+
if (createdPaths.has(stored.entry.id)) return true;
|
|
10342
|
+
}
|
|
10343
|
+
return false;
|
|
10344
|
+
}
|
|
10345
|
+
/**
|
|
9696
10346
|
* @author ddj 2026年10月29号
|
|
9697
|
-
* @description:
|
|
10347
|
+
* @description: 读工作区全部待审条目,按更新时间升序;来源会话仅用于追溯。
|
|
9698
10348
|
* @param cwd 工作区
|
|
9699
|
-
* @param
|
|
9700
|
-
* @param wholeAdd 本次会话新建的文件身份集合,命中时基线置空
|
|
10349
|
+
* @param _sessionId 兼容旧调用签名,不再用于可见性过滤
|
|
9701
10350
|
* @returns 列表
|
|
9702
10351
|
*/
|
|
9703
|
-
function loadPage(cwd,
|
|
9704
|
-
const
|
|
9705
|
-
const
|
|
9706
|
-
const items = visible.map((stored) => itemOf(stored,
|
|
9707
|
-
const gaps =
|
|
10352
|
+
function loadPage(cwd, _sessionId) {
|
|
10353
|
+
const visible = loadEntries(cwd);
|
|
10354
|
+
const sourceAdds = /* @__PURE__ */ new Map();
|
|
10355
|
+
const items = visible.map((stored) => itemOf(stored, emptyBaseOf(cwd, stored, sourceAdds)));
|
|
10356
|
+
const gaps = pendingGaps(cwd);
|
|
9708
10357
|
for (const stored of visible) if (stored.flags.oversized) gaps.push(`${stored.entry.path}: 文本超出上限,仅保留标记`);
|
|
9709
10358
|
return {
|
|
9710
10359
|
items,
|
|
@@ -9724,9 +10373,10 @@ function loadPage(cwd, sessionId, wholeAdd) {
|
|
|
9724
10373
|
function loadDetail(cwd, id, baselineEmpty = false) {
|
|
9725
10374
|
const stored = loadEntries(cwd).find((item) => item.entry.id === id);
|
|
9726
10375
|
if (!stored) return null;
|
|
9727
|
-
const
|
|
10376
|
+
const emptyBase = baselineEmpty || emptyBaseOf(cwd, stored);
|
|
10377
|
+
const diff = diffOf(stored.entry, emptyBase);
|
|
9728
10378
|
return {
|
|
9729
|
-
item: itemOf(stored,
|
|
10379
|
+
item: itemOf(stored, emptyBase),
|
|
9730
10380
|
diff,
|
|
9731
10381
|
blocks: blockStatesOf(stored.entry),
|
|
9732
10382
|
capability: capabilityOf(stored.entry, stored.flags)
|
|
@@ -9826,23 +10476,25 @@ function writerFor(fs, cwd, identity, expected, signal, policy, allowCreate = fa
|
|
|
9826
10476
|
reason: "resolve-failed"
|
|
9827
10477
|
};
|
|
9828
10478
|
}
|
|
9829
|
-
const
|
|
9830
|
-
if (
|
|
10479
|
+
const snapshot = await stableRead(fs, target, signal);
|
|
10480
|
+
if (!snapshot.ok) return snapshot;
|
|
10481
|
+
const recreate = allowCreate && snapshot.content === null;
|
|
10482
|
+
if (!recreate && snapshot.content !== expected) return {
|
|
9831
10483
|
ok: false,
|
|
9832
|
-
reason: "
|
|
10484
|
+
reason: "diverged"
|
|
9833
10485
|
};
|
|
9834
|
-
if (
|
|
10486
|
+
if (snapshot.content === null && !recreate) return {
|
|
9835
10487
|
ok: false,
|
|
9836
|
-
reason: "
|
|
10488
|
+
reason: "missing"
|
|
10489
|
+
};
|
|
10490
|
+
const intent = recreate ? { kind: "createIfAbsent" } : {
|
|
10491
|
+
kind: "replaceIfVersion",
|
|
10492
|
+
version: snapshot.version
|
|
9837
10493
|
};
|
|
9838
|
-
const currentStat = await fs.stat(target, signal).catch(() => void 0);
|
|
9839
10494
|
try {
|
|
9840
|
-
await fs.writeText(target, content ?? "",
|
|
9841
|
-
kind: "replaceIfVersion",
|
|
9842
|
-
version: currentStat.version
|
|
9843
|
-
} : void 0, signal, policy);
|
|
10495
|
+
await fs.writeText(target, content ?? "", intent, signal, policy);
|
|
9844
10496
|
const readBack = await readText(fs, target);
|
|
9845
|
-
if (
|
|
10497
|
+
if (content !== readBack) return {
|
|
9846
10498
|
ok: false,
|
|
9847
10499
|
reason: "read-back-mismatch"
|
|
9848
10500
|
};
|
|
@@ -9885,7 +10537,7 @@ async function applyDecide(fs, cwd, request, signal, policy) {
|
|
|
9885
10537
|
blocks: beforeBlocks,
|
|
9886
10538
|
now: Date.now(),
|
|
9887
10539
|
write: (content) => {
|
|
9888
|
-
if (content === null) return deleteTarget(fs, cwd, stored.entry.id, signal);
|
|
10540
|
+
if (content === null) return deleteTarget(fs, cwd, stored.entry.id, stored.entry.newText, signal, policy);
|
|
9889
10541
|
return writerFor(fs, cwd, stored.entry.id, stored.entry.newText, signal, policy)(content);
|
|
9890
10542
|
}
|
|
9891
10543
|
});
|
|
@@ -9914,6 +10566,113 @@ async function applyDecide(fs, cwd, request, signal, policy) {
|
|
|
9914
10566
|
return outcome;
|
|
9915
10567
|
}
|
|
9916
10568
|
/**
|
|
10569
|
+
* @public
|
|
10570
|
+
* @author ddj 2026年11月03号
|
|
10571
|
+
* @description: 一次读取全表后批量保留待审条目(Keep 不写盘)。写入用刚读到的最新 revision,
|
|
10572
|
+
* 客户端快照漂移不会整批中止;单条 oversized、不存在或落库时被抢先推进只跳过自己。
|
|
10573
|
+
* @param cwd 工作区
|
|
10574
|
+
* @param items 待保留条目
|
|
10575
|
+
* @returns 保留成功与跳过的明细
|
|
10576
|
+
* @throws 待审库迁移未就绪等存储级异常
|
|
10577
|
+
*/
|
|
10578
|
+
async function keepAll(cwd, items) {
|
|
10579
|
+
const stored = new Map(loadEntries(cwd).map((item) => [item.entry.id, item]));
|
|
10580
|
+
const now = Date.now();
|
|
10581
|
+
const outcome = {
|
|
10582
|
+
kept: [],
|
|
10583
|
+
skipped: []
|
|
10584
|
+
};
|
|
10585
|
+
const records = [];
|
|
10586
|
+
for (const item of items) {
|
|
10587
|
+
const before = stored.get(item.id);
|
|
10588
|
+
if (!before) {
|
|
10589
|
+
outcome.skipped.push({
|
|
10590
|
+
id: item.id,
|
|
10591
|
+
display: item.display,
|
|
10592
|
+
reason: "待审条目不存在"
|
|
10593
|
+
});
|
|
10594
|
+
continue;
|
|
10595
|
+
}
|
|
10596
|
+
if (before.flags.oversized) {
|
|
10597
|
+
outcome.skipped.push({
|
|
10598
|
+
id: item.id,
|
|
10599
|
+
display: item.display,
|
|
10600
|
+
reason: "文本超出上限,不能整文件保留"
|
|
10601
|
+
});
|
|
10602
|
+
continue;
|
|
10603
|
+
}
|
|
10604
|
+
if (isSettled(before.entry)) {
|
|
10605
|
+
outcome.kept.push({
|
|
10606
|
+
id: item.id,
|
|
10607
|
+
path: before.entry.path
|
|
10608
|
+
});
|
|
10609
|
+
continue;
|
|
10610
|
+
}
|
|
10611
|
+
const beforeBlocks = blockStatesOf(before.entry);
|
|
10612
|
+
const outcomeOf = await decide({
|
|
10613
|
+
request: {
|
|
10614
|
+
sessionId: before.entry.sessionId,
|
|
10615
|
+
id: item.id,
|
|
10616
|
+
revision: before.flags.revision,
|
|
10617
|
+
operationId: item.operationId,
|
|
10618
|
+
decision: "accepted",
|
|
10619
|
+
scope: "file"
|
|
10620
|
+
},
|
|
10621
|
+
entry: before.entry,
|
|
10622
|
+
flags: before.flags,
|
|
10623
|
+
blocks: beforeBlocks,
|
|
10624
|
+
now,
|
|
10625
|
+
write: () => {
|
|
10626
|
+
throw new Error("批量 Keep 不写盘");
|
|
10627
|
+
}
|
|
10628
|
+
});
|
|
10629
|
+
if (!outcomeOf.ok) {
|
|
10630
|
+
outcome.skipped.push({
|
|
10631
|
+
id: item.id,
|
|
10632
|
+
display: item.display,
|
|
10633
|
+
reason: outcomeOf.reason
|
|
10634
|
+
});
|
|
10635
|
+
continue;
|
|
10636
|
+
}
|
|
10637
|
+
outcome.kept.push({
|
|
10638
|
+
id: item.id,
|
|
10639
|
+
path: before.entry.path
|
|
10640
|
+
});
|
|
10641
|
+
records.push({
|
|
10642
|
+
entryId: item.id,
|
|
10643
|
+
stored: before,
|
|
10644
|
+
next: {
|
|
10645
|
+
entry: outcomeOf.entry,
|
|
10646
|
+
flags: outcomeOf.flags
|
|
10647
|
+
},
|
|
10648
|
+
blocks: outcomeOf.blocks,
|
|
10649
|
+
snapshot: JSON.stringify({
|
|
10650
|
+
before: {
|
|
10651
|
+
entry: before.entry,
|
|
10652
|
+
flags: before.flags,
|
|
10653
|
+
blocks: beforeBlocks
|
|
10654
|
+
},
|
|
10655
|
+
after: {
|
|
10656
|
+
entry: outcomeOf.entry,
|
|
10657
|
+
flags: outcomeOf.flags,
|
|
10658
|
+
blocks: outcomeOf.blocks
|
|
10659
|
+
},
|
|
10660
|
+
wrote: outcomeOf.wrote
|
|
10661
|
+
})
|
|
10662
|
+
});
|
|
10663
|
+
}
|
|
10664
|
+
const displayOf = new Map(items.map((item) => [item.id, item.display]));
|
|
10665
|
+
for (const conflict of saveDecisions(cwd, records)) {
|
|
10666
|
+
outcome.kept = outcome.kept.filter((entry) => entry.id !== conflict.id);
|
|
10667
|
+
outcome.skipped.push({
|
|
10668
|
+
id: conflict.id,
|
|
10669
|
+
display: displayOf.get(conflict.id) ?? conflict.id,
|
|
10670
|
+
reason: conflict.reason
|
|
10671
|
+
});
|
|
10672
|
+
}
|
|
10673
|
+
return outcome;
|
|
10674
|
+
}
|
|
10675
|
+
/**
|
|
9917
10676
|
* @author ddj 2026年10月30号
|
|
9918
10677
|
* @description: 版本库导入:把工作副本相对基线的改动折进待审条目,已存在条目按折叠规则并入。
|
|
9919
10678
|
* @param shell shell 能力
|
|
@@ -10006,7 +10765,7 @@ async function undoDecide(fs, cwd, move, signal, policy) {
|
|
|
10006
10765
|
* @returns 写盘结果
|
|
10007
10766
|
*/
|
|
10008
10767
|
async function writeBack(fs, cwd, stored, target, signal, policy) {
|
|
10009
|
-
if (target.flags.missing) return deleteTarget(fs, cwd, stored.entry.id, signal);
|
|
10768
|
+
if (target.flags.missing) return deleteTarget(fs, cwd, stored.entry.id, stored.entry.newText, signal, policy);
|
|
10010
10769
|
return writerFor(fs, cwd, stored.entry.id, stored.entry.newText, signal, policy, true)(target.entry.newText);
|
|
10011
10770
|
}
|
|
10012
10771
|
/**
|
|
@@ -10025,65 +10784,93 @@ function parseSnapshot(text) {
|
|
|
10025
10784
|
}
|
|
10026
10785
|
}
|
|
10027
10786
|
/**
|
|
10028
|
-
* @
|
|
10029
|
-
* @
|
|
10030
|
-
* @
|
|
10031
|
-
* @param
|
|
10032
|
-
* @param
|
|
10787
|
+
* @private
|
|
10788
|
+
* @author ddj 2026年11月04号
|
|
10789
|
+
* @description 版本—内容—版本读取,防止将新版本令牌与旧内容拼成写入依据。
|
|
10790
|
+
* @param fs 文件提供方
|
|
10791
|
+
* @param target 已解析目标
|
|
10033
10792
|
* @param signal 取消信号
|
|
10034
|
-
* @returns
|
|
10793
|
+
* @returns 稳定快照或拒绝原因
|
|
10035
10794
|
*/
|
|
10036
|
-
async function
|
|
10037
|
-
let target;
|
|
10795
|
+
async function stableRead(fs, target, signal) {
|
|
10038
10796
|
try {
|
|
10039
|
-
|
|
10040
|
-
|
|
10041
|
-
|
|
10042
|
-
|
|
10043
|
-
|
|
10797
|
+
const before = await fs.stat(target, signal);
|
|
10798
|
+
if (!before) return {
|
|
10799
|
+
ok: true,
|
|
10800
|
+
content: null
|
|
10801
|
+
};
|
|
10802
|
+
if (before.type !== "file" || !before.version) return {
|
|
10803
|
+
ok: false,
|
|
10804
|
+
reason: "unguarded-file"
|
|
10805
|
+
};
|
|
10806
|
+
const content = await readText(fs, target, signal);
|
|
10807
|
+
if (content === null) return {
|
|
10808
|
+
ok: false,
|
|
10809
|
+
reason: "revision-mismatch"
|
|
10810
|
+
};
|
|
10811
|
+
const after = await fs.stat(target, signal);
|
|
10812
|
+
if (after?.type !== "file" || after.version !== before.version) return {
|
|
10813
|
+
ok: false,
|
|
10814
|
+
reason: "revision-mismatch"
|
|
10815
|
+
};
|
|
10816
|
+
return {
|
|
10817
|
+
ok: true,
|
|
10818
|
+
content,
|
|
10819
|
+
version: before.version
|
|
10820
|
+
};
|
|
10821
|
+
} catch (error) {
|
|
10044
10822
|
return {
|
|
10045
10823
|
ok: false,
|
|
10046
|
-
reason:
|
|
10824
|
+
reason: reasonOf(error)
|
|
10047
10825
|
};
|
|
10048
10826
|
}
|
|
10049
|
-
|
|
10050
|
-
|
|
10051
|
-
|
|
10052
|
-
|
|
10053
|
-
|
|
10054
|
-
|
|
10827
|
+
}
|
|
10828
|
+
/**
|
|
10829
|
+
* @private
|
|
10830
|
+
* @author ddj 2026年11月04号
|
|
10831
|
+
* @description 通过 provider 受控删除,保留会话策略与原子版本校验;旧 Host 不回落直删。
|
|
10832
|
+
* @param fs 文件提供方
|
|
10833
|
+
* @param cwd 工作目录
|
|
10834
|
+
* @param identity 目标身份
|
|
10835
|
+
* @param expected 应有内容
|
|
10836
|
+
* @param signal 取消信号
|
|
10837
|
+
* @param policy 会话沙箱策略
|
|
10838
|
+
* @returns 删除结果;结果不明不提交决定
|
|
10839
|
+
*/
|
|
10840
|
+
async function deleteTarget(fs, cwd, identity, expected, signal, policy) {
|
|
10841
|
+
if (typeof fs.deleteFile !== "function") return {
|
|
10055
10842
|
ok: false,
|
|
10056
|
-
reason: "
|
|
10843
|
+
reason: "delete-unavailable"
|
|
10057
10844
|
};
|
|
10058
10845
|
try {
|
|
10059
|
-
await
|
|
10846
|
+
const target = await fs.resolve(identity, {
|
|
10847
|
+
cwd,
|
|
10848
|
+
signal
|
|
10849
|
+
});
|
|
10850
|
+
const snapshot = await stableRead(fs, target, signal);
|
|
10851
|
+
if (!snapshot.ok) return snapshot;
|
|
10852
|
+
if (snapshot.content === null) return {
|
|
10853
|
+
ok: true,
|
|
10854
|
+
content: null
|
|
10855
|
+
};
|
|
10856
|
+
if (snapshot.content !== expected) return {
|
|
10857
|
+
ok: false,
|
|
10858
|
+
reason: "diverged"
|
|
10859
|
+
};
|
|
10860
|
+
await fs.deleteFile(target, { version: snapshot.version }, signal, policy);
|
|
10861
|
+
return await fs.stat(target, signal) ? {
|
|
10862
|
+
ok: false,
|
|
10863
|
+
reason: "delete-failed"
|
|
10864
|
+
} : {
|
|
10865
|
+
ok: true,
|
|
10866
|
+
content: null
|
|
10867
|
+
};
|
|
10060
10868
|
} catch (error) {
|
|
10061
10869
|
return {
|
|
10062
10870
|
ok: false,
|
|
10063
10871
|
reason: reasonOf(error)
|
|
10064
10872
|
};
|
|
10065
10873
|
}
|
|
10066
|
-
return await fs.stat(target, signal).catch(() => void 0) ? {
|
|
10067
|
-
ok: false,
|
|
10068
|
-
reason: "delete-failed"
|
|
10069
|
-
} : {
|
|
10070
|
-
ok: true,
|
|
10071
|
-
content: null
|
|
10072
|
-
};
|
|
10073
|
-
}
|
|
10074
|
-
/**
|
|
10075
|
-
* @author ddj 2026年10月29号
|
|
10076
|
-
* @description: 判断宿主路径是否位于工作区内,Windows 下按大小写不敏感比较。
|
|
10077
|
-
* @param cwd 工作区
|
|
10078
|
-
* @param absolute 目标绝对路径
|
|
10079
|
-
* @returns 是否在工作区内
|
|
10080
|
-
*/
|
|
10081
|
-
function insideWorkspace(cwd, absolute) {
|
|
10082
|
-
const root = resolve(cwd);
|
|
10083
|
-
const candidate = resolve(absolute);
|
|
10084
|
-
const normalize = (value) => process.platform === "win32" ? value.toLowerCase() : value;
|
|
10085
|
-
const rootKey = normalize(root).endsWith(sep) ? normalize(root) : normalize(root) + sep;
|
|
10086
|
-
return normalize(candidate).startsWith(rootKey);
|
|
10087
10874
|
}
|
|
10088
10875
|
/**
|
|
10089
10876
|
* @author ddj 2026年10月29号
|
|
@@ -10110,197 +10897,6 @@ function reasonOf(error) {
|
|
|
10110
10897
|
return typeof code === "string" ? code : String(error);
|
|
10111
10898
|
}
|
|
10112
10899
|
//#endregion
|
|
10113
|
-
//#region src/shared/pathKey.ts
|
|
10114
|
-
/**
|
|
10115
|
-
* 路径比较口径:Host 与客户端共用同一套「远端 / 盘符 / 相对路径」判定,只在平台折叠上分工。
|
|
10116
|
-
* @author ddj 2026年10月30号
|
|
10117
|
-
*/
|
|
10118
|
-
/** 远端仓库身份:`scheme://...` 区分大小写,绝不能折叠。 */
|
|
10119
|
-
const REMOTE = /^[a-z]+:\/\//i;
|
|
10120
|
-
/** 盘符绝对路径:大小写不敏感与宿主平台无关,两边都必须折叠。 */
|
|
10121
|
-
const DRIVE = /^[a-z]:\//i;
|
|
10122
|
-
/**
|
|
10123
|
-
* @author ddj 2026年10月30号
|
|
10124
|
-
* @description: 统一分隔符为 `/`,其余字符原样保留。
|
|
10125
|
-
* @param path 原始路径
|
|
10126
|
-
* @returns 反斜杠已归一化的路径
|
|
10127
|
-
*/
|
|
10128
|
-
function unifiedPath(path) {
|
|
10129
|
-
return path.replace(/\\/g, "/");
|
|
10130
|
-
}
|
|
10131
|
-
/**
|
|
10132
|
-
* @author ddj 2026年10月30号
|
|
10133
|
-
* @description: 生成路径比较键。盘符路径恒折叠大小写;其余路径是否折叠由调用方按运行平台决定
|
|
10134
|
-
* (Host 在 Windows 上折叠,客户端不在宿主进程里,取 false)。
|
|
10135
|
-
* @param path 原始路径
|
|
10136
|
-
* @param foldCase 非盘符路径是否折叠大小写
|
|
10137
|
-
* @returns 比较键
|
|
10138
|
-
*/
|
|
10139
|
-
function pathKeyFor(path, foldCase) {
|
|
10140
|
-
const unified = unifiedPath(path);
|
|
10141
|
-
if (REMOTE.test(unified)) return unified;
|
|
10142
|
-
if (DRIVE.test(unified)) return unified.toLowerCase();
|
|
10143
|
-
return foldCase ? unified.toLowerCase() : unified;
|
|
10144
|
-
}
|
|
10145
|
-
//#endregion
|
|
10146
|
-
//#region src/shared/pendingReview.ts
|
|
10147
|
-
/** 待审条目在面板列表里的 id 前缀;客户端与 Host 共用这一份口径。 */
|
|
10148
|
-
const PENDING_PREFIX = "pending:";
|
|
10149
|
-
//#endregion
|
|
10150
|
-
//#region src/pendingCompat.ts
|
|
10151
|
-
/** 进程内"已被待审接管"的路径集合;旧写入口守卫只读它,绝不碰数据库。 */
|
|
10152
|
-
const coveredPaths = /* @__PURE__ */ new Set();
|
|
10153
|
-
/**
|
|
10154
|
-
* @author ddj 2026年10月29号
|
|
10155
|
-
* @description: 由条目 id 派生稳定序号,保证轮询期间面板锚点不跳。
|
|
10156
|
-
* @param id 待审条目身份
|
|
10157
|
-
* @returns 正整数序号
|
|
10158
|
-
*/
|
|
10159
|
-
function stableSeq(id) {
|
|
10160
|
-
let hash = 2166136261;
|
|
10161
|
-
for (let index = 0; index < id.length; index++) {
|
|
10162
|
-
hash ^= id.charCodeAt(index);
|
|
10163
|
-
hash = Math.imul(hash, 16777619);
|
|
10164
|
-
}
|
|
10165
|
-
return Math.abs(hash) % 2147483647 + 1;
|
|
10166
|
-
}
|
|
10167
|
-
/**
|
|
10168
|
-
* @author ddj 2026年10月29号
|
|
10169
|
-
* @description: Host 侧的路径比较口径:远端身份保留大小写,其余按运行平台折叠。
|
|
10170
|
-
* @param path 路径
|
|
10171
|
-
* @returns 比较键
|
|
10172
|
-
*/
|
|
10173
|
-
function pathKey$1(path) {
|
|
10174
|
-
return pathKeyFor(path, process.platform === "win32");
|
|
10175
|
-
}
|
|
10176
|
-
/**
|
|
10177
|
-
* @author ddj 2026年10月30号
|
|
10178
|
-
* @description: 判断两个路径是否指同一文件:比较键相同,或一方是另一方的工作区相对写法。
|
|
10179
|
-
* 面板传下来的路径可能是相对路径,而条目里存的是绝对路径。
|
|
10180
|
-
* @param left 路径
|
|
10181
|
-
* @param right 路径
|
|
10182
|
-
* @returns 是否同一文件
|
|
10183
|
-
*/
|
|
10184
|
-
function pendingSamePath(left, right) {
|
|
10185
|
-
const a = pathKey$1(left);
|
|
10186
|
-
const b = pathKey$1(right);
|
|
10187
|
-
if (!a || !b) return false;
|
|
10188
|
-
return a === b || a.endsWith("/" + b) || b.endsWith("/" + a);
|
|
10189
|
-
}
|
|
10190
|
-
/**
|
|
10191
|
-
* @author ddj 2026年10月30号
|
|
10192
|
-
* @description: 列表里的条目 id 带待审前缀用于区分通道,内核按原始身份存取,这里统一还原。
|
|
10193
|
-
* @param id 请求里的条目 id
|
|
10194
|
-
* @returns 待审内核使用的原始 id
|
|
10195
|
-
*/
|
|
10196
|
-
function pendingRawId(id) {
|
|
10197
|
-
return id.startsWith("pending:") ? id.slice(8) : id;
|
|
10198
|
-
}
|
|
10199
|
-
/**
|
|
10200
|
-
* @author ddj 2026年10月30号
|
|
10201
|
-
* @description: 列表显示用的短路径:工作区内显示相对路径,区外保留原文。
|
|
10202
|
-
* @param cwd 工作区
|
|
10203
|
-
* @param path 条目路径
|
|
10204
|
-
* @returns 显示用路径
|
|
10205
|
-
*/
|
|
10206
|
-
function pendingDisplay(cwd, path) {
|
|
10207
|
-
const root = cwd.replace(/[\\/]+$/, "");
|
|
10208
|
-
const unified = path.replace(/\\/g, "/");
|
|
10209
|
-
const base = root.replace(/\\/g, "/");
|
|
10210
|
-
if (base && (unified === base || unified.toLowerCase().startsWith(base.toLowerCase() + "/"))) return unified.slice(base.length + 1);
|
|
10211
|
-
return path;
|
|
10212
|
-
}
|
|
10213
|
-
/**
|
|
10214
|
-
* @author ddj 2026年10月29号
|
|
10215
|
-
* @description: 待审条目转面板列表项。
|
|
10216
|
-
* @param item 待审条目
|
|
10217
|
-
* @param cwd 工作区,用于显示相对路径;缺省显示原始路径
|
|
10218
|
-
* @returns 面板列表项
|
|
10219
|
-
*/
|
|
10220
|
-
function pendingItemOf(item, cwd) {
|
|
10221
|
-
return {
|
|
10222
|
-
id: PENDING_PREFIX + item.id,
|
|
10223
|
-
sessionId: item.sessionId,
|
|
10224
|
-
turn: 0,
|
|
10225
|
-
seq: stableSeq(item.id),
|
|
10226
|
-
path: item.path,
|
|
10227
|
-
currentPath: item.path,
|
|
10228
|
-
display: cwd ? pendingDisplay(cwd, item.path) : item.path,
|
|
10229
|
-
added: item.added,
|
|
10230
|
-
deleted: item.deleted,
|
|
10231
|
-
kind: item.oversized ? "oversized" : "text",
|
|
10232
|
-
decision: item.settled ? "accepted" : "pending",
|
|
10233
|
-
revision: item.revision,
|
|
10234
|
-
current: true
|
|
10235
|
-
};
|
|
10236
|
-
}
|
|
10237
|
-
/**
|
|
10238
|
-
* @author ddj 2026年10月29号
|
|
10239
|
-
* @description: 待审详情转面板详情;Revert 映射到面板的 Undo 能力位。
|
|
10240
|
-
* @param detail 待审详情
|
|
10241
|
-
* @param capturedAt 采集时间文案
|
|
10242
|
-
* @returns 面板详情
|
|
10243
|
-
*/
|
|
10244
|
-
function pendingDetailOf(detail, capturedAt, cwd) {
|
|
10245
|
-
return {
|
|
10246
|
-
item: pendingItemOf(detail.item, cwd),
|
|
10247
|
-
diff: detail.diff,
|
|
10248
|
-
capturedAt,
|
|
10249
|
-
blocks: detail.blocks,
|
|
10250
|
-
capability: {
|
|
10251
|
-
keep: detail.capability.keep,
|
|
10252
|
-
undo: detail.capability.revert,
|
|
10253
|
-
reason: detail.capability.reason
|
|
10254
|
-
}
|
|
10255
|
-
};
|
|
10256
|
-
}
|
|
10257
|
-
/**
|
|
10258
|
-
* @author ddj 2026年10月29号
|
|
10259
|
-
* @description: 记住/忘记"该路径由待审投影接管"。进程内集合,绝不在旧写入口上查库(避免撞上写事务锁)。
|
|
10260
|
-
* @param cwd 工作区
|
|
10261
|
-
* @param path 路径
|
|
10262
|
-
* @param covered 是否接管
|
|
10263
|
-
* @returns 无
|
|
10264
|
-
*/
|
|
10265
|
-
function trackPendingCover(cwd, path, covered) {
|
|
10266
|
-
const key = `${pathKey$1(cwd)}|${pathKey$1(path)}`;
|
|
10267
|
-
if (covered) coveredPaths.add(key);
|
|
10268
|
-
else coveredPaths.delete(key);
|
|
10269
|
-
}
|
|
10270
|
-
/**
|
|
10271
|
-
* @author ddj 2026年10月29号
|
|
10272
|
-
* @description: 判断某路径是否已由待审投影接管;仅读进程内集合,不产生任何 IO。
|
|
10273
|
-
* @param cwd 工作区
|
|
10274
|
-
* @param path 请求路径
|
|
10275
|
-
* @returns 是否已被接管
|
|
10276
|
-
*/
|
|
10277
|
-
function pendingCovers(cwd, path) {
|
|
10278
|
-
return coveredPaths.has(`${pathKey$1(cwd)}|${pathKey$1(path)}`);
|
|
10279
|
-
}
|
|
10280
|
-
/**
|
|
10281
|
-
* @author ddj 2026年10月29号
|
|
10282
|
-
* @description: 合并官方页与待审页:待审覆盖的路径从官方结果中剔除,其余原样保留。
|
|
10283
|
-
* @param official 官方页
|
|
10284
|
-
* @param pending 待审页
|
|
10285
|
-
* @param cwd 工作区,用于把待审行显示成相对路径
|
|
10286
|
-
* @returns 合并后的页
|
|
10287
|
-
*/
|
|
10288
|
-
function mergedPage(official, pending, cwd) {
|
|
10289
|
-
const covered = /* @__PURE__ */ new Set();
|
|
10290
|
-
for (const item of pending.items) {
|
|
10291
|
-
covered.add(pathKey$1(item.path));
|
|
10292
|
-
covered.add(pathKey$1(item.id));
|
|
10293
|
-
}
|
|
10294
|
-
const kept = official.items.filter((item) => !covered.has(pathKey$1(item.currentPath)) && !covered.has(pathKey$1(item.path)));
|
|
10295
|
-
return {
|
|
10296
|
-
items: [...pending.items.map((item) => pendingItemOf(item, cwd)), ...kept],
|
|
10297
|
-
pending: pending.pending + kept.filter((item) => item.decision === "pending").length,
|
|
10298
|
-
nextCursor: official.nextCursor,
|
|
10299
|
-
gaps: official.gaps,
|
|
10300
|
-
...official.overview ? { overview: official.overview } : {}
|
|
10301
|
-
};
|
|
10302
|
-
}
|
|
10303
|
-
//#endregion
|
|
10304
10900
|
//#region src/search/query.ts
|
|
10305
10901
|
/**
|
|
10306
10902
|
* 规范化用户搜索文本。
|
|
@@ -16040,6 +16636,7 @@ function finishUndo(cwd, request) {
|
|
|
16040
16636
|
}
|
|
16041
16637
|
//#endregion
|
|
16042
16638
|
//#region src/reviewRpc.ts
|
|
16639
|
+
/** Session-scoped official review RPC. @author ddj 2026年10月26号 */
|
|
16043
16640
|
/** 批量写入快照的硬上限:待审积压超过它说明状态异常,宁可拒绝也不做无界写循环。 */
|
|
16044
16641
|
const BATCH_LIMIT = 2e3;
|
|
16045
16642
|
/** 冷端 Undo 说明:Undo 会校验证据并写回源文件,只能在持有该会话的那一端执行。 */
|
|
@@ -16195,7 +16792,7 @@ function reviewHandlers(ctx, registry) {
|
|
|
16195
16792
|
*/
|
|
16196
16793
|
function pendingPageOf(cwd, sessionId) {
|
|
16197
16794
|
try {
|
|
16198
|
-
const page = loadPage(cwd, sessionId
|
|
16795
|
+
const page = loadPage(cwd, sessionId);
|
|
16199
16796
|
return page.items.some((item) => item.settled === true) ? {
|
|
16200
16797
|
...page,
|
|
16201
16798
|
items: page.items.filter((item) => item.settled !== true)
|
|
@@ -16209,6 +16806,99 @@ function reviewHandlers(ctx, registry) {
|
|
|
16209
16806
|
};
|
|
16210
16807
|
}
|
|
16211
16808
|
}
|
|
16809
|
+
/**
|
|
16810
|
+
* @private
|
|
16811
|
+
* @author ddj 2026年11月03号
|
|
16812
|
+
* @description: 按批次与条目派生幂等操作号:同一批次重放复用同一结果,不重复推进 revision。
|
|
16813
|
+
* @param batchId 客户端批次身份
|
|
16814
|
+
* @param id 条目身份
|
|
16815
|
+
* @param seq 事件序号
|
|
16816
|
+
* @returns 64 位 hex 操作号
|
|
16817
|
+
*/
|
|
16818
|
+
function operationOf(batchId, id, seq) {
|
|
16819
|
+
return createHash("sha256").update(`${batchId}|${id}|${seq}`).digest("hex");
|
|
16820
|
+
}
|
|
16821
|
+
/**
|
|
16822
|
+
* @private
|
|
16823
|
+
* @author ddj 2026年11月03号
|
|
16824
|
+
* @description: 取条目展示名,缺失时退回身份本身。
|
|
16825
|
+
* @param items 本批条目
|
|
16826
|
+
* @param id 条目身份
|
|
16827
|
+
* @returns 展示名
|
|
16828
|
+
*/
|
|
16829
|
+
function displayOf(items, id) {
|
|
16830
|
+
return items.find((item) => item.id === id)?.display ?? id;
|
|
16831
|
+
}
|
|
16832
|
+
/**
|
|
16833
|
+
* @private
|
|
16834
|
+
* @author ddj 2026年11月03号
|
|
16835
|
+
* @description: 官方段的批量保留。段级异常只记进 result.error,不取消待审段。
|
|
16836
|
+
* @param cwd 工作区
|
|
16837
|
+
* @param batchId 批次身份
|
|
16838
|
+
* @param items 官方条目
|
|
16839
|
+
* @param result 汇总结果
|
|
16840
|
+
* @returns 无
|
|
16841
|
+
*/
|
|
16842
|
+
function keepOfficialStage(cwd, batchId, items, result) {
|
|
16843
|
+
try {
|
|
16844
|
+
const outcome = keepOfficialAll(cwd, items.map((item) => ({
|
|
16845
|
+
id: item.id,
|
|
16846
|
+
seq: item.seq,
|
|
16847
|
+
revision: item.revision,
|
|
16848
|
+
operationId: operationOf(batchId, item.id, item.seq)
|
|
16849
|
+
})));
|
|
16850
|
+
result.completed += outcome.paths.length;
|
|
16851
|
+
result.paths.push(...outcome.paths);
|
|
16852
|
+
for (const skip of outcome.skipped) result.skipped.push({
|
|
16853
|
+
id: skip.id,
|
|
16854
|
+
display: displayOf(items, skip.id),
|
|
16855
|
+
reason: skip.reason
|
|
16856
|
+
});
|
|
16857
|
+
} catch (error) {
|
|
16858
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
16859
|
+
log.warn(`[review] keepAll 官方段失败: ${message}`);
|
|
16860
|
+
if (!result.error) result.error = message;
|
|
16861
|
+
}
|
|
16862
|
+
}
|
|
16863
|
+
/**
|
|
16864
|
+
* @private
|
|
16865
|
+
* @author ddj 2026年11月03号
|
|
16866
|
+
* @description: 待审段的批量保留。冷端只跳过待审条目(判定需要活跃会话),官方条目不受影响。
|
|
16867
|
+
* @param scope 已解析作用域
|
|
16868
|
+
* @param batchId 批次身份
|
|
16869
|
+
* @param items 待审条目(带 pending 前缀)
|
|
16870
|
+
* @param result 汇总结果
|
|
16871
|
+
* @returns 无
|
|
16872
|
+
*/
|
|
16873
|
+
async function keepPendingStage(scope, batchId, items, result) {
|
|
16874
|
+
if (!scope.live) {
|
|
16875
|
+
for (const item of items) result.skipped.push({
|
|
16876
|
+
id: item.id,
|
|
16877
|
+
display: item.display,
|
|
16878
|
+
reason: "待审条目需要在会话活跃的一端判定"
|
|
16879
|
+
});
|
|
16880
|
+
return;
|
|
16881
|
+
}
|
|
16882
|
+
const targets = items.map((item) => ({
|
|
16883
|
+
id: item.id.slice(8),
|
|
16884
|
+
display: item.display,
|
|
16885
|
+
operationId: operationOf(batchId, item.id, item.seq)
|
|
16886
|
+
}));
|
|
16887
|
+
try {
|
|
16888
|
+
const outcome = await keepAll(scope.cwd, targets);
|
|
16889
|
+
result.completed += outcome.kept.length;
|
|
16890
|
+
for (const entry of outcome.kept) result.paths.push(entry.path);
|
|
16891
|
+
for (const skip of outcome.skipped) result.skipped.push({
|
|
16892
|
+
id: PENDING_PREFIX + skip.id,
|
|
16893
|
+
display: skip.display,
|
|
16894
|
+
reason: skip.reason
|
|
16895
|
+
});
|
|
16896
|
+
} catch (error) {
|
|
16897
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
16898
|
+
log.warn(`[review] keepAll 待审段失败: ${message}`);
|
|
16899
|
+
if (!result.error) result.error = message;
|
|
16900
|
+
}
|
|
16901
|
+
}
|
|
16212
16902
|
return {
|
|
16213
16903
|
"edrv.review.list": async (args) => {
|
|
16214
16904
|
const { cwd } = await scopeOf(args.sessionId);
|
|
@@ -16242,8 +16932,7 @@ function reviewHandlers(ctx, registry) {
|
|
|
16242
16932
|
const scope = await scopeOf(args.sessionId);
|
|
16243
16933
|
const cwd = scope.cwd;
|
|
16244
16934
|
if (args.id.startsWith("pending:")) {
|
|
16245
|
-
const
|
|
16246
|
-
const detail = loadDetail(cwd, entryId, addedFiles(cwd, args.sessionId).has(entryId));
|
|
16935
|
+
const detail = loadDetail(cwd, args.id.slice(8));
|
|
16247
16936
|
if (!detail) return {
|
|
16248
16937
|
ok: false,
|
|
16249
16938
|
error: "待审条目不存在"
|
|
@@ -16291,7 +16980,7 @@ function reviewHandlers(ctx, registry) {
|
|
|
16291
16980
|
decision: args.decision,
|
|
16292
16981
|
scope: args.scope === "hunk" ? "block" : "file",
|
|
16293
16982
|
...args.blockId === void 0 ? {} : { blockId: args.blockId }
|
|
16294
|
-
});
|
|
16983
|
+
}, void 0, policyOf(ctx, sessionOf(ctx, args.sessionId)));
|
|
16295
16984
|
if (!result.ok) return {
|
|
16296
16985
|
ok: false,
|
|
16297
16986
|
error: result.reason
|
|
@@ -16325,6 +17014,43 @@ function reviewHandlers(ctx, registry) {
|
|
|
16325
17014
|
error: message
|
|
16326
17015
|
};
|
|
16327
17016
|
}
|
|
17017
|
+
},
|
|
17018
|
+
"edrv.review.keepAll": async (args) => {
|
|
17019
|
+
try {
|
|
17020
|
+
const scope = await scopeOf(args.sessionId);
|
|
17021
|
+
const items = Array.isArray(args.items) ? args.items : [];
|
|
17022
|
+
if (!items.length) return {
|
|
17023
|
+
ok: false,
|
|
17024
|
+
error: "没有可保留的待审记录"
|
|
17025
|
+
};
|
|
17026
|
+
if (items.length > BATCH_LIMIT) return {
|
|
17027
|
+
ok: false,
|
|
17028
|
+
error: `待审记录过多(${items.length} 条,上限 ${BATCH_LIMIT}),请先分批处理后重试`
|
|
17029
|
+
};
|
|
17030
|
+
const result = {
|
|
17031
|
+
completed: 0,
|
|
17032
|
+
total: items.length,
|
|
17033
|
+
paths: [],
|
|
17034
|
+
skipped: [],
|
|
17035
|
+
error: ""
|
|
17036
|
+
};
|
|
17037
|
+
const official = items.filter((item) => !item.id.startsWith(PENDING_PREFIX));
|
|
17038
|
+
const pending = items.filter((item) => item.id.startsWith(PENDING_PREFIX));
|
|
17039
|
+
if (official.length) keepOfficialStage(scope.cwd, args.batchId, official, result);
|
|
17040
|
+
if (pending.length) await keepPendingStage(scope, args.batchId, pending, result);
|
|
17041
|
+
log.info(`[review] keepAll live=${scope.live ? 1 : 0} total=${result.total} kept=${result.completed} skipped=${result.skipped.length}`);
|
|
17042
|
+
return {
|
|
17043
|
+
ok: true,
|
|
17044
|
+
result
|
|
17045
|
+
};
|
|
17046
|
+
} catch (error) {
|
|
17047
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
17048
|
+
log.warn(`[review] keepAll 失败: ${message}`);
|
|
17049
|
+
return {
|
|
17050
|
+
ok: false,
|
|
17051
|
+
error: message
|
|
17052
|
+
};
|
|
17053
|
+
}
|
|
16328
17054
|
}
|
|
16329
17055
|
};
|
|
16330
17056
|
}
|
|
@@ -16392,17 +17118,15 @@ function readHandlers(kit) {
|
|
|
16392
17118
|
return {
|
|
16393
17119
|
"edrv.pending.list": async (args) => {
|
|
16394
17120
|
const cwd = await kit.read(args.sessionId);
|
|
16395
|
-
const page = loadPage(cwd, args.sessionId
|
|
16396
|
-
|
|
17121
|
+
const page = loadPage(cwd, args.sessionId);
|
|
17122
|
+
setPendingCovers(cwd, args.sessionId, page.items.map((item) => item.path));
|
|
16397
17123
|
return {
|
|
16398
17124
|
ok: true,
|
|
16399
17125
|
page
|
|
16400
17126
|
};
|
|
16401
17127
|
},
|
|
16402
17128
|
"edrv.pending.detail": async (args) => {
|
|
16403
|
-
const
|
|
16404
|
-
const rawId = pendingRawId(args.id);
|
|
16405
|
-
const detail = loadDetail(cwd, rawId, addedFiles(cwd, args.sessionId).has(rawId));
|
|
17129
|
+
const detail = loadDetail(await kit.read(args.sessionId), pendingRawId(args.id));
|
|
16406
17130
|
return detail ? {
|
|
16407
17131
|
ok: true,
|
|
16408
17132
|
detail
|
|
@@ -16455,7 +17179,7 @@ function decideHandlers(kit) {
|
|
|
16455
17179
|
const id = pendingRawId(args.id);
|
|
16456
17180
|
const path = loadEntries(cwd).find((stored) => stored.entry.id === id)?.entry.path;
|
|
16457
17181
|
const removed = removeEntry(cwd, id, args.revision);
|
|
16458
|
-
if (removed && path) trackPendingCover(cwd, path, false);
|
|
17182
|
+
if (removed && path) trackPendingCover(cwd, path, false, args.sessionId);
|
|
16459
17183
|
return {
|
|
16460
17184
|
ok: true,
|
|
16461
17185
|
removed
|
|
@@ -16512,7 +17236,8 @@ function refreshHandlers(kit) {
|
|
|
16512
17236
|
includeUntracked: args.includeUntracked,
|
|
16513
17237
|
path: args.path
|
|
16514
17238
|
});
|
|
16515
|
-
|
|
17239
|
+
const page = loadPage(cwd, args.sessionId);
|
|
17240
|
+
setPendingCovers(cwd, args.sessionId, page.items.map((item) => item.path));
|
|
16516
17241
|
return {
|
|
16517
17242
|
ok: true,
|
|
16518
17243
|
imported: result.imported,
|
|
@@ -16795,15 +17520,16 @@ function recView(record, ws = null) {
|
|
|
16795
17520
|
};
|
|
16796
17521
|
}
|
|
16797
17522
|
/**
|
|
16798
|
-
*
|
|
16799
|
-
*
|
|
16800
|
-
* @
|
|
17523
|
+
* 旧写入口守卫:本会话可见的待审累计基线接管了该路径时一律拒绝,避免两套权威同时写同一文件。
|
|
17524
|
+
* 只按会话可见范围判定:别的会话在管、本会话看不见的条目不能拦人,否则用户无从处理。
|
|
17525
|
+
* @author ddj 2026年11月3号
|
|
17526
|
+
* @param sc 会话与工作区
|
|
16801
17527
|
* @param path 请求涉及的路径(可为空表示无法定位)
|
|
16802
17528
|
* @returns 拒绝原因,未接管时为 null
|
|
16803
17529
|
*/
|
|
16804
|
-
function legacyBlocked(
|
|
17530
|
+
function legacyBlocked(sc, path) {
|
|
16805
17531
|
if (!path) return null;
|
|
16806
|
-
if (!pendingCovers(cwd, path)) return null;
|
|
17532
|
+
if (!pendingCovers(sc.cwd, path, sc.session.id)) return null;
|
|
16807
17533
|
return "该文件已由待审累计基线接管,请使用面板 Keep/Revert(edrv.review.decide)";
|
|
16808
17534
|
}
|
|
16809
17535
|
/** 会话/工作区公共前置:返回 {session,cwd} 或错误文案。 */
|
|
@@ -17309,6 +18035,7 @@ function buildHandlers(ctx, registry, searcher = newSearcher(ctx), contentSearch
|
|
|
17309
18035
|
const staleStart = performance.now();
|
|
17310
18036
|
await autoArchiveStale(ctx, sc.session, sc.cwd, bucket);
|
|
17311
18037
|
await archiveCanceled(ctx, sc.session, sc.cwd, bucket);
|
|
18038
|
+
await retireCovered(ctx, sc.session, sc.cwd, bucket);
|
|
17312
18039
|
staleMs = performance.now() - staleStart;
|
|
17313
18040
|
}
|
|
17314
18041
|
}
|
|
@@ -17380,7 +18107,7 @@ function buildHandlers(ctx, registry, searcher = newSearcher(ctx), contentSearch
|
|
|
17380
18107
|
error: sc.err
|
|
17381
18108
|
};
|
|
17382
18109
|
const bucket = await bucketOf(registry, ctx, sc.cwd);
|
|
17383
|
-
const blocked = legacyBlocked(sc
|
|
18110
|
+
const blocked = legacyBlocked(sc, bucket.get(args.callId)?.path);
|
|
17384
18111
|
if (blocked) return {
|
|
17385
18112
|
ok: false,
|
|
17386
18113
|
error: blocked
|
|
@@ -17407,7 +18134,7 @@ function buildHandlers(ctx, registry, searcher = newSearcher(ctx), contentSearch
|
|
|
17407
18134
|
error: sc.err
|
|
17408
18135
|
};
|
|
17409
18136
|
const bucket = await bucketOf(registry, ctx, sc.cwd);
|
|
17410
|
-
const blocked = legacyBlocked(sc
|
|
18137
|
+
const blocked = legacyBlocked(sc, bucket.get(args.callId)?.path);
|
|
17411
18138
|
if (blocked) return {
|
|
17412
18139
|
ok: false,
|
|
17413
18140
|
error: blocked
|
|
@@ -17433,7 +18160,7 @@ function buildHandlers(ctx, registry, searcher = newSearcher(ctx), contentSearch
|
|
|
17433
18160
|
ok: false,
|
|
17434
18161
|
error: sc.err
|
|
17435
18162
|
};
|
|
17436
|
-
const blocked = legacyBlocked(sc
|
|
18163
|
+
const blocked = legacyBlocked(sc, args.path);
|
|
17437
18164
|
if (blocked) return {
|
|
17438
18165
|
ok: false,
|
|
17439
18166
|
error: blocked
|
|
@@ -17463,7 +18190,7 @@ function buildHandlers(ctx, registry, searcher = newSearcher(ctx), contentSearch
|
|
|
17463
18190
|
ok: false,
|
|
17464
18191
|
error: sc.err
|
|
17465
18192
|
};
|
|
17466
|
-
const blocked = legacyBlocked(sc
|
|
18193
|
+
const blocked = legacyBlocked(sc, missingPath(args.token));
|
|
17467
18194
|
if (blocked) return {
|
|
17468
18195
|
ok: false,
|
|
17469
18196
|
error: blocked
|
|
@@ -17489,7 +18216,7 @@ function buildHandlers(ctx, registry, searcher = newSearcher(ctx), contentSearch
|
|
|
17489
18216
|
};
|
|
17490
18217
|
const bucket = await bucketOf(registry, ctx, sc.cwd);
|
|
17491
18218
|
const items = Array.isArray(args.items) ? args.items : [];
|
|
17492
|
-
const blocked = items.map((item) => legacyBlocked(sc
|
|
18219
|
+
const blocked = items.map((item) => legacyBlocked(sc, bucket.get(item.callId)?.path)).find((reason) => reason !== null);
|
|
17493
18220
|
if (blocked) return {
|
|
17494
18221
|
ok: false,
|
|
17495
18222
|
error: blocked
|
|
@@ -29579,7 +30306,7 @@ function attachRuntime(ctx, runtime) {
|
|
|
29579
30306
|
});
|
|
29580
30307
|
const pending = installPending(ctx, (write) => {
|
|
29581
30308
|
try {
|
|
29582
|
-
trackPendingCover(write.cwd, write.path, true);
|
|
30309
|
+
trackPendingCover(write.cwd, write.path, true, write.sessionId);
|
|
29583
30310
|
} catch {}
|
|
29584
30311
|
try {
|
|
29585
30312
|
recordWrite(write);
|