dsh-vscode-mode 0.23.2 → 0.23.3
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 +1 -0
- package/lib/client.js +105 -3
- package/lib/client.js.map +2 -2
- package/lib/index.js +97 -6
- package/lib/index.js.map +1 -1
- package/package.json +1 -1
- package/src/client/missingReview.ts +24 -0
- package/src/client/ui/EditorView.ts +67 -4
- package/src/missingReview.ts +44 -5
- package/src/officialCapture.ts +11 -0
- package/src/reviewDb.ts +23 -0
- package/src/reviewRpc.ts +32 -2
- package/src/rpc.ts +6 -2
- package/src/shared/rpc.ts +4 -1
package/README.md
CHANGED
|
@@ -880,6 +880,7 @@ Keep All / Undo All 却是亮的。修复:单文件 Keep / Undo 覆盖冲突
|
|
|
880
880
|
完整变更见 [GitHub Releases](https://github.com/Lenonss/DSH_VsCodeMode/releases)。
|
|
881
881
|
近期关键版本:
|
|
882
882
|
|
|
883
|
+
- **v0.23.3**:修复 issue #13(已删除文件的差异条目把人卡死)与 #12(重启后成串「采集不完整」)。①文件在磁盘上被删掉时,客户端申请整文件删除预览被「已有官方审查记录」拒绝,而拒绝只是一条普通 `Error`,于是落到通用加载失败分支——「文件加载失败:…」+ 进度钉在 0%、重试同错、标签页关不掉;`edrv.review.detail` 还把 Undo 不可用解释成「缺少可验证的原始字节」,而真实原因是文件已不在。现缺失文件预览的可预期拒绝带稳定码(`official-covered` / `official-decided` / `no-records` / `no-evidence` / `too-large`),客户端据此显示「文件已在磁盘删除(· 有官方待审改动)」终态:标题 + 原因 + 重试 / 关闭标签页,不再画 0% 进度条;官方待审记录在磁盘已删除时给出「Keep 按删除结案、Undo 需要读写该文件」的准确原因(Keep 语义不变,仍只写共享审查库),记录都已结案时提示可直接关闭;`!review` 的磁盘状态抖动改为最多重读 2 次后给终态,消除缺失↔重现的无限递归。v0.22.0 起 `hasOfficial()` 已用 `typeof path !== 'string'` 守卫堵掉把不透明 FsTarget 绑进 SQL 导致的 `Unknown named parameter 'targetKey'`,本版不再复现。②官方 `workspace/changes` 的 summary 只存在宿主内存里,插件启动补采原先无条件把历史事件全部入队,重启后每一轮都必然抛「summary 已失效或尚不可用」并写成不可修复的 `gap`(面板成串「轮次 N 采集不完整」);现补采先 `summary()` 探测再入队,失效事件只记一条汇总日志。验证:typecheck 与基线同为 37 个既有错误(`fflate/browser` 依赖缺失)、零新增;定向单测 111/111(含新增 `tests/deletedFileReview.test.ts` 5 例与 `tests/officialBackfill.test.ts` 2 例)、EditorView 相关 82/82;`npm run build` 四个 bundle 通过。见 [发布说明](docs/release-0.23.3.md)。
|
|
883
884
|
- **v0.23.2**:修复 Keep 在非活跃端(桌面端)点了没反应,两处成因。①`edrv.review.decide` 原先一律要求本端活跃会话,而能力位只按审查库推导,会话在另一实例时按钮亮着、点击后抛错被兜底成 HTTP 500。现按写入副作用区分:官方记录 Keep 只写共享审查库(`BEGIN IMMEDIATE` + 幂等键 + 版本 CAS),冷端放行;官方 Undo(校验证据并写回源文件)与待审投影条目(本机 JSON 无跨进程锁)仍限活跃端,返回可读跨端原因而非 500;详情新增 `capability.live`,冷端禁用 Undo 并说明、Keep 提示补「写入共享审查库,仍可执行」。②已结算待审投影的 `capability.keep` 恒为假,却因排在 `items[0]`、同路径官方待审行被历史过滤,把官方记录挤出差异栏(表现为「待审 0」+Keep 禁用+点击无反应);现 `pendingPageOf()` 剔除已结算投影,官方记录重新成为审查目标。另详情未加载时点 Keep 不再静默失败;打开文件时若某条官方记录的改动已全部不在当前文件中(无可定位差异且缓冲区干净),自动按 Keep 结案(只标记不写盘),差异栏不再显示「本记录 N 处改动均已不在当前文件中」;官方元数据首次载入期间差异栏显示 `—` 而非按历史记录报数,重启瞬间不再闪出几十条假待审。验证:typecheck 0 error、目标单测 57/57、build 通过、真实审查库探针确认首条回到官方记录且 `capability={keep:true,undo:true,live:true}`;桌面端 GUI 点按验收因重启后恢复的是另一个会话未完成,缺口保留。发布不自动部署,升级后重启 DSH 并刷新页面。见 [发布说明](docs/release-0.23.2.md)。
|
|
884
885
|
- **v0.23.1**:修复 Keep All / Undo All 在待审超过一页时必定中止——批量按钮原先复用分页接口并把每页的展示用计数当全量总数校验,官方待审 150 条时先报「待审记录在分页期间变化」或最终报「待审记录数量不完整」,实际 0 项被决策。现改为一次性完整快照(`edrv.review.list` 新增 `all: true`,服务端循环取全并与历史改动投影去重,`nextCursor: null`、`pending` 为真实总数),客户端单次取快照、校验唯一且仍待审后逐项决策;超过 2000 条明确拒绝并提示分批,途中变化仍整批中止不部分写入。差异栏浏览仍走原分页接口。本版未在重启后的 GUI 点按验证,缺口保留;发布不自动部署,升级后重启 DSH 并刷新页面。见 [发布说明](docs/release-0.23.1.md)。
|
|
885
886
|
- **v0.23.0**:新增快捷菜单——可拖动悬浮按钮 + 自定义菜单树(组/按钮,最多 5 层),按钮携带提示词并「直接发送给 AI」或「只填入输入框」;配置存 `<DSH_HOME>/dsh-vscode-mode/data/quick-menu.json`,带 `revision` 乐观锁、冲突重拉不覆盖、损坏降级。节点可限定工作区(默认全部、子节点继承),按钮可绑定技能与规则,发送时按会话可见性校验后只拼装通过的绑定(规则支持引用/内联)。新增 `skills.list` 只读 RPC 枚举全局技能 + 当前工作区技能;配置界面工作区改下拉弹层、技能/规则改分组多选下拉并移除手输技能名,菜单设置重排四段、统一按钮盒模型并修掉「导入 JSON」下移不居中。本版 GUI 未真机验收(pnpm store 损坏、插件未部署),缺口已获用户接受;发布不自动部署,升级后重启 DSH 并刷新页面。见 [发布说明](docs/release-0.23.0.md)。
|
package/lib/client.js
CHANGED
|
@@ -377892,6 +377892,25 @@ td.UniverTableCell, th.UniverTableCell{mso-padding-alt:0cm 5.4pt 0cm 5.4pt;}
|
|
|
377892
377892
|
}
|
|
377893
377893
|
//#endregion
|
|
377894
377894
|
//#region src/client/missingReview.ts
|
|
377895
|
+
/**
|
|
377896
|
+
* @public
|
|
377897
|
+
* @author ddj 2026年10月31号
|
|
377898
|
+
* @description: 把 missingReview 的失败归并成「文件已删除」终态。只有确认删除的拒绝码才是终态;
|
|
377899
|
+
* 会话缺失、权限、文件重新出现等失败返回 null,调用方继续走通用加载失败(可重试、保留跨端提示)。
|
|
377900
|
+
* @param failure RPC 失败结果(error 必填,code 可选)
|
|
377901
|
+
* @returns 终态信息或 null
|
|
377902
|
+
*/
|
|
377903
|
+
function goneStateOf(failure) {
|
|
377904
|
+
const code = failure?.code;
|
|
377905
|
+
const official = code === "official-covered";
|
|
377906
|
+
if (!official && code !== "official-decided" && code !== "no-records" && code !== "no-evidence" && code !== "too-large") return null;
|
|
377907
|
+
const message = String(failure?.error ?? "文件已在磁盘删除").replace(/^Error:\s*/, "");
|
|
377908
|
+
return {
|
|
377909
|
+
title: official ? "文件已在磁盘删除 · 有官方待审改动" : "文件已在磁盘删除",
|
|
377910
|
+
message,
|
|
377911
|
+
official
|
|
377912
|
+
};
|
|
377913
|
+
}
|
|
377895
377914
|
/** @public @author ddj 2026年10月28号 @param review Frozen complete snapshot. @returns One whole-file deletion, including empty files; no invented current lines. */
|
|
377896
377915
|
function missingRegion(review) {
|
|
377897
377916
|
return {
|
|
@@ -393573,6 +393592,9 @@ td.UniverTableCell, th.UniverTableCell{mso-padding-alt:0cm 5.4pt 0cm 5.4pt;}
|
|
|
393573
393592
|
const aiStatusTimer = react$1.default.useRef(null);
|
|
393574
393593
|
const [error, setError] = react$1.default.useState(null);
|
|
393575
393594
|
const [loadError, setLoadError] = react$1.default.useState(null);
|
|
393595
|
+
const [goneFile, setGoneFile] = react$1.default.useState(null);
|
|
393596
|
+
const missingReadRef = react$1.default.useRef(/* @__PURE__ */ new Map());
|
|
393597
|
+
const goneNow = goneFile && goneFile.path === active && goneFile.sessionId === sessionId ? goneFile : null;
|
|
393576
393598
|
const [largeCandidate, setLargeCandidate] = react$1.default.useState(null);
|
|
393577
393599
|
const [pagedFile, setPagedFile] = react$1.default.useState(null);
|
|
393578
393600
|
const [pageChanged, setPageChanged] = react$1.default.useState(null);
|
|
@@ -394459,7 +394481,38 @@ td.UniverTableCell, th.UniverTableCell{mso-padding-alt:0cm 5.4pt 0cm 5.4pt;}
|
|
|
394459
394481
|
* @param force 强制走 RPC(reloadFile 用,跳过 model 复用)
|
|
394460
394482
|
* @param version 已知磁盘版本(缺省走 RPC 返回值)
|
|
394461
394483
|
*/
|
|
394462
|
-
/**
|
|
394484
|
+
/**
|
|
394485
|
+
* 把「文件已在磁盘删除」记成终态面板:不占进度条、不反复重试,并允许直接关闭页签。
|
|
394486
|
+
* 只有确认删除的原因才走终态;会话/权限类失败仍回退通用加载失败,保留跨端提示。
|
|
394487
|
+
* @private @author ddj 2026年10月31号
|
|
394488
|
+
* @param path 目标文件
|
|
394489
|
+
* @param sid 会话 id
|
|
394490
|
+
* @param seq 加载序号(过期响应丢弃)
|
|
394491
|
+
* @param failure 宿主拒绝原因(可带稳定 code)
|
|
394492
|
+
*/
|
|
394493
|
+
function settleGone(path, sid, seq, failure) {
|
|
394494
|
+
if (!loadCurrent(path, seq)) return;
|
|
394495
|
+
const error = failure && typeof failure === "object" ? failure : { error: failure };
|
|
394496
|
+
const gone = goneStateOf(error) ?? (error.deleted === true ? {
|
|
394497
|
+
title: "文件已在磁盘删除",
|
|
394498
|
+
official: false,
|
|
394499
|
+
message: String(error.error ?? "")
|
|
394500
|
+
} : null);
|
|
394501
|
+
if (!gone) {
|
|
394502
|
+
failLoad(path, sid, seq, String(error.error ?? error));
|
|
394503
|
+
return;
|
|
394504
|
+
}
|
|
394505
|
+
stampLoad(path, sid, seq, gone.message);
|
|
394506
|
+
setLoadError(null);
|
|
394507
|
+
setError(null);
|
|
394508
|
+
setGoneFile({
|
|
394509
|
+
...gone,
|
|
394510
|
+
path,
|
|
394511
|
+
sessionId: sid
|
|
394512
|
+
});
|
|
394513
|
+
setStatus(gone.official ? "文件已删除 · 差异栏可 Keep 结案" : "文件已删除");
|
|
394514
|
+
}
|
|
394515
|
+
/** @private @author ddj 2026年10月28号 / 2026年10月31号 @param path Missing text file. @param sid Owning session. @param seq Loader generation. @sideEffects Loads deletion evidence without creating or saving the file. */
|
|
394463
394516
|
async function loadMissing(path, sid, seq) {
|
|
394464
394517
|
try {
|
|
394465
394518
|
const result = await rpc("edrv.missingReview", {
|
|
@@ -394468,13 +394521,23 @@ td.UniverTableCell, th.UniverTableCell{mso-padding-alt:0cm 5.4pt 0cm 5.4pt;}
|
|
|
394468
394521
|
});
|
|
394469
394522
|
if (!loadCurrent(path, seq)) return;
|
|
394470
394523
|
if (!result.ok) {
|
|
394471
|
-
|
|
394524
|
+
settleGone(path, sid, seq, result);
|
|
394472
394525
|
return;
|
|
394473
394526
|
}
|
|
394474
394527
|
if (!result.review) {
|
|
394475
|
-
|
|
394528
|
+
const attempts = (missingReadRef.current.get(path) ?? 0) + 1;
|
|
394529
|
+
missingReadRef.current.set(path, attempts);
|
|
394530
|
+
if (attempts <= 2) {
|
|
394531
|
+
loadContent(path, sid, true);
|
|
394532
|
+
return;
|
|
394533
|
+
}
|
|
394534
|
+
settleGone(path, sid, seq, {
|
|
394535
|
+
deleted: true,
|
|
394536
|
+
error: "文件状态在磁盘上反复变化,请稍后重试,或关闭该标签页"
|
|
394537
|
+
});
|
|
394476
394538
|
return;
|
|
394477
394539
|
}
|
|
394540
|
+
missingReadRef.current.delete(path);
|
|
394478
394541
|
if (dirtyRef.current[path] || savingRef.current.has(path)) {
|
|
394479
394542
|
failLoad(path, sid, seq, "文件已删除,但有未保存编辑;请先处理本地缓冲");
|
|
394480
394543
|
return;
|
|
@@ -394487,6 +394550,7 @@ td.UniverTableCell, th.UniverTableCell{mso-padding-alt:0cm 5.4pt 0cm 5.4pt;}
|
|
|
394487
394550
|
};
|
|
394488
394551
|
missingRef.current.set(path, preview);
|
|
394489
394552
|
setMissingPreview(preview);
|
|
394553
|
+
setGoneFile(null);
|
|
394490
394554
|
setContent("");
|
|
394491
394555
|
setContentPath(path);
|
|
394492
394556
|
setLoadError(null);
|
|
@@ -394511,6 +394575,7 @@ td.UniverTableCell, th.UniverTableCell{mso-padding-alt:0cm 5.4pt 0cm 5.4pt;}
|
|
|
394511
394575
|
status: "loading"
|
|
394512
394576
|
});
|
|
394513
394577
|
setLoadError(null);
|
|
394578
|
+
setGoneFile(null);
|
|
394514
394579
|
setLargeCandidate(null);
|
|
394515
394580
|
setPagedFile(null);
|
|
394516
394581
|
setPageChanged(null);
|
|
@@ -398542,6 +398607,37 @@ td.UniverTableCell, th.UniverTableCell{mso-padding-alt:0cm 5.4pt 0cm 5.4pt;}
|
|
|
398542
398607
|
className: "edrv-pill edrv-pill-ghost",
|
|
398543
398608
|
onClick: retry
|
|
398544
398609
|
}, "重试") : null);
|
|
398610
|
+
/**
|
|
398611
|
+
* 渲染缺失文件终态面板:说明 + 重试 + 关闭页签(不显示进度条,避免卡在 0%)。
|
|
398612
|
+
* @private @author ddj 2026年10月31号
|
|
398613
|
+
* @param gone 终态信息
|
|
398614
|
+
* @param retry 重新读取回调
|
|
398615
|
+
* @param close 关闭该标签页回调
|
|
398616
|
+
* @returns 终态面板 React 元素
|
|
398617
|
+
*/
|
|
398618
|
+
const goneBody = (gone, retry, close) => react$1.default.createElement("div", {
|
|
398619
|
+
className: "edrv-empty edrv-loading",
|
|
398620
|
+
"aria-live": "polite",
|
|
398621
|
+
role: "status"
|
|
398622
|
+
}, react$1.default.createElement("div", { className: "edrv-loading-title" }, gone.title), react$1.default.createElement("div", { style: {
|
|
398623
|
+
fontSize: 12,
|
|
398624
|
+
maxWidth: 460,
|
|
398625
|
+
lineHeight: 1.6
|
|
398626
|
+
} }, gone.message), gone.official ? react$1.default.createElement("div", { style: {
|
|
398627
|
+
fontSize: 12,
|
|
398628
|
+
maxWidth: 460,
|
|
398629
|
+
lineHeight: 1.6,
|
|
398630
|
+
opacity: .8
|
|
398631
|
+
} }, "待审改动在差异栏:Keep 保留删除结果并结案;Undo 需要该文件的原始内容,磁盘已删除时不可用。") : null, react$1.default.createElement("div", { style: {
|
|
398632
|
+
display: "flex",
|
|
398633
|
+
gap: 8
|
|
398634
|
+
} }, react$1.default.createElement("button", {
|
|
398635
|
+
className: "edrv-pill edrv-pill-ghost",
|
|
398636
|
+
onClick: retry
|
|
398637
|
+
}, "重试"), react$1.default.createElement("button", {
|
|
398638
|
+
className: "edrv-pill edrv-pill-ghost",
|
|
398639
|
+
onClick: close
|
|
398640
|
+
}, "关闭标签页")));
|
|
398545
398641
|
let body;
|
|
398546
398642
|
if (activePlan) {
|
|
398547
398643
|
const labels = PLAN_MD_LABELS;
|
|
@@ -398582,6 +398678,12 @@ td.UniverTableCell, th.UniverTableCell{mso-padding-alt:0cm 5.4pt 0cm 5.4pt;}
|
|
|
398582
398678
|
}
|
|
398583
398679
|
}, "重试"));
|
|
398584
398680
|
else if (!active) body = react$1.default.createElement("div", { className: "edrv-empty" }, react$1.default.createElement("div", null, "暂无打开的文件"), react$1.default.createElement("div", { style: { fontSize: 12 } }, "使用右上搜索框 (" + (chordOf("edrv.quickOpen") ?? "Ctrl+P") + ") 打开工作区文件;" + (chordOf("edrv.showCommands") ?? "Ctrl+Shift+P") + " 打开命令栏;agent 修改文件后顶部会出现差异角标"));
|
|
398681
|
+
else if (goneNow) body = goneBody(goneNow, () => {
|
|
398682
|
+
missingReadRef.current.delete(goneNow.path);
|
|
398683
|
+
loadContent(active, sessionId, true);
|
|
398684
|
+
}, () => {
|
|
398685
|
+
closeTab(goneNow.path);
|
|
398686
|
+
});
|
|
398585
398687
|
else if (content === null && loadError && isSessionMissingError(loadError)) body = react$1.default.createElement("div", {
|
|
398586
398688
|
className: "edrv-empty edrv-loading",
|
|
398587
398689
|
"aria-live": "polite"
|
package/lib/client.js.map
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
[diffend] Oversized file quarantined before diffing.
|
|
2
2
|
name: package/lib/client.js.map
|
|
3
|
-
size:
|
|
4
|
-
sha256:
|
|
3
|
+
size: 35677585 bytes
|
|
4
|
+
sha256: df1afa8466a7852d100cfbc6a1f44e61beabd3b1808a262562e920ac06b96865
|
package/lib/index.js
CHANGED
|
@@ -7649,6 +7649,25 @@ function reviewPage(cwd, cursor = 0, history = false, path) {
|
|
|
7649
7649
|
}))]
|
|
7650
7650
|
};
|
|
7651
7651
|
}
|
|
7652
|
+
/**
|
|
7653
|
+
* @public
|
|
7654
|
+
* @author ddj 2026年10月31号
|
|
7655
|
+
* @description: 该文件是否还有未处理的官方待审改动。缺失文件预览据此区分「需要去差异栏 Keep 结案」
|
|
7656
|
+
* 与「记录都已处理、可直接关闭标签页」,避免已结案的删除文件继续提示待审。
|
|
7657
|
+
* @param cwd Migrated workspace
|
|
7658
|
+
* @param path Host path of the file
|
|
7659
|
+
* @returns 存在未决官方记录时为 true
|
|
7660
|
+
*/
|
|
7661
|
+
function officialPending(cwd, path) {
|
|
7662
|
+
if (typeof path !== "string") return false;
|
|
7663
|
+
const db = dbFor(cwd);
|
|
7664
|
+
if (!db.prepare("SELECT 1 FROM sqlite_master WHERE name='review_files' AND type='table'").get()) return false;
|
|
7665
|
+
if (path.length > 1 && path[1] === ":") {
|
|
7666
|
+
const folded = path.split(String.fromCharCode(92)).join("/").toLowerCase();
|
|
7667
|
+
return !!db.prepare("SELECT 1 FROM review_files WHERE current=1 AND decision='pending' AND lower(replace(identity,char(92),'/'))=? LIMIT 1").get(folded);
|
|
7668
|
+
}
|
|
7669
|
+
return !!db.prepare("SELECT 1 FROM review_files WHERE current=1 AND decision='pending' AND identity=? LIMIT 1").get(path);
|
|
7670
|
+
}
|
|
7652
7671
|
/** @public @author ddj 2026年10月26号 @param cwd Migrated workspace. @param path Legacy write target. @returns Whether any official version owns the file, including decided history. */
|
|
7653
7672
|
function hasOfficial(cwd, path) {
|
|
7654
7673
|
if (typeof path !== "string") return false;
|
|
@@ -7926,20 +7945,31 @@ function installCapture(ctx, onSnapshot) {
|
|
|
7926
7945
|
log.warn("官方历史补采不可用:缺少 sessionQuery;后续实时事件仍会采集");
|
|
7927
7946
|
return;
|
|
7928
7947
|
}
|
|
7948
|
+
const source = ctx.get("workspaceChanges");
|
|
7949
|
+
if (!source?.summary) {
|
|
7950
|
+
log.warn("官方历史补采不可用:缺少 workspaceChanges;后续实时事件仍会采集");
|
|
7951
|
+
return;
|
|
7952
|
+
}
|
|
7929
7953
|
try {
|
|
7930
7954
|
if (session.header?.cwd) {
|
|
7931
7955
|
await loadBucket(ctx, session.header.cwd);
|
|
7932
7956
|
await repairPaths(session.header.cwd, resolvePath);
|
|
7933
7957
|
}
|
|
7934
7958
|
const events = await query.listEvents(session.id);
|
|
7959
|
+
let expired = 0;
|
|
7935
7960
|
for (const event of events) {
|
|
7936
7961
|
if (event.type !== "workspace/changes" || event.seq < (session.inheritedEventCount ?? 0)) continue;
|
|
7962
|
+
if (!source.summary(session.id, event.seq)) {
|
|
7963
|
+
expired += 1;
|
|
7964
|
+
continue;
|
|
7965
|
+
}
|
|
7937
7966
|
const window = await query.readEvent({
|
|
7938
7967
|
sessionId: session.id,
|
|
7939
7968
|
seq: event.seq
|
|
7940
7969
|
});
|
|
7941
7970
|
collector.enqueue(session, window.target);
|
|
7942
7971
|
}
|
|
7972
|
+
if (expired) log.info(`官方历史补采跳过 ${expired} 轮已失效事件:官方 summary 不跨宿主重启保留,实时事件照常采集`);
|
|
7943
7973
|
} catch (error) {
|
|
7944
7974
|
log.warn("官方历史补采失败:" + String(error));
|
|
7945
7975
|
}
|
|
@@ -16015,6 +16045,39 @@ function batchSnapshot(cwd, pending) {
|
|
|
16015
16045
|
};
|
|
16016
16046
|
}
|
|
16017
16047
|
/**
|
|
16048
|
+
* @private
|
|
16049
|
+
* @author ddj 2026年10月31号
|
|
16050
|
+
* @description: 文件已在磁盘删除时修正官方记录的能力说明。Keep 只写审查库、仍可结案,
|
|
16051
|
+
* Undo 需要读写源文件、必然不可用:把「缺少原始字节」这种误导性原因换成可执行说明。
|
|
16052
|
+
* 只改文案与 undo 位,不参与判定写入。
|
|
16053
|
+
* @param ctx Host 能力
|
|
16054
|
+
* @param cwd 工作区
|
|
16055
|
+
* @param detail 官方记录详情
|
|
16056
|
+
* @param capability 已按冷/热端调整过的能力
|
|
16057
|
+
* @returns 修正后的能力;无法确认删除时原样返回
|
|
16058
|
+
*/
|
|
16059
|
+
async function absenceCapability(ctx, cwd, detail, capability) {
|
|
16060
|
+
if (!capability.keep) return capability;
|
|
16061
|
+
if (detail.diff.kind === "text" && !detail.diff.after) return {
|
|
16062
|
+
...capability,
|
|
16063
|
+
undo: false,
|
|
16064
|
+
reason: "该记录删除了这个文件:Keep 按删除结案;Undo 需要完整文件内容,官方差异只有片段,不可用。"
|
|
16065
|
+
};
|
|
16066
|
+
const fs = ctx.get("fs");
|
|
16067
|
+
if (!fs) return capability;
|
|
16068
|
+
try {
|
|
16069
|
+
const target = await resolveTargetIn(ctx, cwd, detail.item.currentPath);
|
|
16070
|
+
if (await fs.stat(target)) return capability;
|
|
16071
|
+
} catch {
|
|
16072
|
+
return capability;
|
|
16073
|
+
}
|
|
16074
|
+
return {
|
|
16075
|
+
...capability,
|
|
16076
|
+
undo: false,
|
|
16077
|
+
reason: "文件已在磁盘删除:Keep 按删除结案;Undo 需要读写该文件,当前不可用。"
|
|
16078
|
+
};
|
|
16079
|
+
}
|
|
16080
|
+
/**
|
|
16018
16081
|
* @public
|
|
16019
16082
|
* @author ddj 2026年10月26号 / 2026年10月28号
|
|
16020
16083
|
* @description: 审查 RPC 处理器。工作区一律由 host 解析,绝不采信调用方 cwd:
|
|
@@ -16160,7 +16223,7 @@ function reviewHandlers(ctx, registry) {
|
|
|
16160
16223
|
error: "官方审查记录不存在"
|
|
16161
16224
|
};
|
|
16162
16225
|
}
|
|
16163
|
-
const capability = liveCapabilityOf(detail.capability, scope.live, false);
|
|
16226
|
+
const capability = await absenceCapability(ctx, cwd, detail, liveCapabilityOf(detail.capability, scope.live, false));
|
|
16164
16227
|
log.debug(`[review] detail live=${scope.live ? 1 : 0} id=${args.id} found=1 keep=${capability.keep ? 1 : 0} undo=${capability.undo ? 1 : 0}`);
|
|
16165
16228
|
return {
|
|
16166
16229
|
ok: true,
|
|
@@ -16470,6 +16533,29 @@ function pendingHandlers(ctx, registry) {
|
|
|
16470
16533
|
/** Missing historical files: frozen, whole-file deletion reviews. */
|
|
16471
16534
|
const frozen = /* @__PURE__ */ new Map();
|
|
16472
16535
|
const locks = /* @__PURE__ */ new Set();
|
|
16536
|
+
/**
|
|
16537
|
+
* @public
|
|
16538
|
+
* @author ddj 2026年10月31号
|
|
16539
|
+
* @description: 给缺失文件预览的可预期拒绝挂稳定错误码。客户端据此显示终态说明,
|
|
16540
|
+
* 而不是把内部错误原文当「加载失败」反复重试;也不让上游把 SQLite 报错原文透给编辑器。
|
|
16541
|
+
* @param code 稳定错误码(official-covered / no-records / no-evidence / too-large)
|
|
16542
|
+
* @param message 可读原因
|
|
16543
|
+
* @returns 带 code 的 Error
|
|
16544
|
+
*/
|
|
16545
|
+
function refusal(code, message) {
|
|
16546
|
+
return Object.assign(new Error(message), { code });
|
|
16547
|
+
}
|
|
16548
|
+
/**
|
|
16549
|
+
* @public
|
|
16550
|
+
* @author ddj 2026年10月31号
|
|
16551
|
+
* @description: 从任意错误里取回缺失文件预览的稳定拒绝码;不是本模块抛出的错误一律返回 undefined。
|
|
16552
|
+
* @param error 捕获到的错误
|
|
16553
|
+
* @returns 稳定拒绝码或 undefined
|
|
16554
|
+
*/
|
|
16555
|
+
function refusalCodeOf(error) {
|
|
16556
|
+
const code = error?.code;
|
|
16557
|
+
return code === "official-covered" || code === "official-decided" || code === "no-records" || code === "no-evidence" || code === "too-large" ? code : void 0;
|
|
16558
|
+
}
|
|
16473
16559
|
/** @private @author ddj 2026年10月28号 @param target Resolved absolute path. @returns Platform file identity. */
|
|
16474
16560
|
function fileKey(target) {
|
|
16475
16561
|
const normalized = resolve(target).replace(/\\/g, "/");
|
|
@@ -16520,13 +16606,16 @@ async function fileAbsent(ctx, cwd, path) {
|
|
|
16520
16606
|
async function missingReview(ctx, session, cwd, bucket, path) {
|
|
16521
16607
|
const abs = targetPathOf(ctx, await resolveTargetIn(ctx, cwd, path));
|
|
16522
16608
|
if (!abs) throw new Error("无法解析目标文件路径");
|
|
16523
|
-
if (hasOfficial(cwd, path) || hasOfficial(cwd, abs))
|
|
16609
|
+
if (hasOfficial(cwd, path) || hasOfficial(cwd, abs)) {
|
|
16610
|
+
if (!officialPending(cwd, abs) && !officialPending(cwd, path)) throw refusal("official-decided", "该文件已在磁盘删除,官方审查记录均已处理:可直接关闭该标签页");
|
|
16611
|
+
throw refusal("official-covered", "该文件已有官方审查记录(磁盘上已删除):请在差异栏对该待审条目执行 Keep 结案;整文件删除预览只适用于待审历史记录");
|
|
16612
|
+
}
|
|
16524
16613
|
if (!await fileAbsent(ctx, cwd, abs)) throw new Error("文件已重新出现,请刷新");
|
|
16525
16614
|
const records = await fileRecords(ctx, cwd, bucket, abs);
|
|
16526
16615
|
const latest = records.at(-1);
|
|
16527
|
-
if (!records.some((record) => !record.archived && !recordResolved(record))) throw
|
|
16528
|
-
if (!latest || typeof latest.after !== "string") throw
|
|
16529
|
-
if (Buffer.byteLength(latest.after, "utf8") > 8388608) throw
|
|
16616
|
+
if (!records.some((record) => !record.archived && !recordResolved(record))) throw refusal("no-records", "磁盘上已没有该文件,且没有待审历史记录:可直接关闭该标签页");
|
|
16617
|
+
if (!latest || typeof latest.after !== "string") throw refusal("no-evidence", "磁盘上已没有该文件,且缺少可靠的完整历史快照,无法展示或恢复;不会用局部差异拼接文件");
|
|
16618
|
+
if (Buffer.byteLength(latest.after, "utf8") > 8388608) throw refusal("too-large", "磁盘上已没有该文件,且历史快照过大,无法整文件预览");
|
|
16530
16619
|
for (const [token, item] of frozen) if (item.expires < Date.now()) frozen.delete(token);
|
|
16531
16620
|
if (frozen.size >= 256) throw new Error("缺失文件预览过多,请稍后重试");
|
|
16532
16621
|
const view = {
|
|
@@ -17316,9 +17405,11 @@ function buildHandlers(ctx, registry, searcher = newSearcher(ctx), contentSearch
|
|
|
17316
17405
|
review: await missingReview(ctx, sc.session, sc.cwd, bucket, args.path)
|
|
17317
17406
|
};
|
|
17318
17407
|
} catch (error) {
|
|
17408
|
+
const code = refusalCodeOf(error);
|
|
17319
17409
|
return {
|
|
17320
17410
|
ok: false,
|
|
17321
|
-
error: String(error)
|
|
17411
|
+
error: String(error),
|
|
17412
|
+
...code ? { code } : {}
|
|
17322
17413
|
};
|
|
17323
17414
|
}
|
|
17324
17415
|
},
|