dsh-vscode-mode 0.23.1 → 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 +3 -0
- package/lib/client.js +234 -47
- package/lib/client.js.map +2 -2
- package/lib/index.js +221 -46
- package/lib/index.js.map +1 -1
- package/package.json +1 -1
- package/src/client/missingReview.ts +24 -0
- package/src/client/reviewBatch.ts +5 -0
- package/src/client/ui/EditorView.ts +172 -19
- package/src/client/ui/OfficialDiffBar.tsx +18 -3
- package/src/client/ui/SidecarDiffBar.tsx +3 -2
- package/src/client/ui/reviewAllRow.tsx +5 -4
- package/src/missingReview.ts +44 -5
- package/src/officialCapture.ts +11 -0
- package/src/reviewDb.ts +23 -0
- package/src/reviewRpc.ts +140 -37
- package/src/rpc.ts +6 -2
- package/src/shared/review.ts +2 -1
- package/src/shared/rpc.ts +4 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-vscode-mode",
|
|
3
|
-
"version": "0.23.
|
|
3
|
+
"version": "0.23.3",
|
|
4
4
|
"description": "DSH 上的类 VSCode 编码体验:Monaco 编辑器(文件页签/QuickOpen/状态栏,可驻 DSH 0.1.5+ 官方右侧 Sidebar 与对话同屏)+ 命令栏(Ctrl+Shift+P)与可扩展指令系统 + VS Code 兼容代码片段(.code-snippets 全局/项目,IntelliSense 展开)+ Agent 编辑差异审查(整文件差异/采纳/拒绝/归档/回滚)+ SVN 集成(侧栏变更面板/日志弹窗/并排差异)+ 语言服务器(LSP)智能(跳转定义/引用/hover/大纲 + VSIX 扩展市场安装),状态持久化到工作区旁车,界面状态(页签/光标/侧栏/编辑 Tab/导航历史)镜像到 DSH home 跨重启恢复",
|
|
5
5
|
"icon": "assets/icon.svg",
|
|
6
6
|
"keywords": [
|
|
@@ -1,6 +1,30 @@
|
|
|
1
1
|
import type { MissingReview } from '../shared/types.js'
|
|
2
2
|
import { splitLines } from '../shared/diff.js'
|
|
3
3
|
|
|
4
|
+
/** 缺失文件终态:文件已在磁盘删除,且没有可展示的整文件预览。 */
|
|
5
|
+
export interface GoneState {
|
|
6
|
+
title: string
|
|
7
|
+
message: string
|
|
8
|
+
/** 该文件还有官方待审改动:Keep 可在差异栏结案,Undo 不可用。 */
|
|
9
|
+
official: boolean
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* @public
|
|
14
|
+
* @author ddj 2026年10月31号
|
|
15
|
+
* @description: 把 missingReview 的失败归并成「文件已删除」终态。只有确认删除的拒绝码才是终态;
|
|
16
|
+
* 会话缺失、权限、文件重新出现等失败返回 null,调用方继续走通用加载失败(可重试、保留跨端提示)。
|
|
17
|
+
* @param failure RPC 失败结果(error 必填,code 可选)
|
|
18
|
+
* @returns 终态信息或 null
|
|
19
|
+
*/
|
|
20
|
+
export function goneStateOf(failure: { error?: unknown; code?: unknown } | null | undefined): GoneState | null {
|
|
21
|
+
const code = failure?.code
|
|
22
|
+
const official = code === 'official-covered'
|
|
23
|
+
if (!official && code !== 'official-decided' && code !== 'no-records' && code !== 'no-evidence' && code !== 'too-large') return null
|
|
24
|
+
const message = String(failure?.error ?? '文件已在磁盘删除').replace(/^Error:\s*/, '')
|
|
25
|
+
return { title: official ? '文件已在磁盘删除 · 有官方待审改动' : '文件已在磁盘删除', message, official }
|
|
26
|
+
}
|
|
27
|
+
|
|
4
28
|
/** @public @author ddj 2026年10月28号 @param review Frozen complete snapshot. @returns One whole-file deletion, including empty files; no invented current lines. */
|
|
5
29
|
export function missingRegion(review: MissingReview) {
|
|
6
30
|
return {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/** Official bulk decisions use persisted metadata, never the visible history page. @author ddj 2026年10月27号 */
|
|
2
2
|
import type { ReviewDetail, ReviewItem, ReviewPage, ReviewResult } from '../shared/review.js'
|
|
3
|
+
import { PENDING_PREFIX } from '../shared/pendingReview.js'
|
|
3
4
|
|
|
4
5
|
export interface BatchPort {
|
|
5
6
|
snapshot: () => Promise<ReviewPage>
|
|
@@ -41,6 +42,10 @@ export async function runReviewBatch(port: BatchPort, reject: boolean): Promise<
|
|
|
41
42
|
for (const item of items) {
|
|
42
43
|
port.guard()
|
|
43
44
|
const detail = await port.detail(item.id)
|
|
45
|
+
// 待审条目判定要在会话活跃的一端做:冷端能力位已关闭,这里给出比「版本变化」更准确的原因
|
|
46
|
+
if (detail.capability.live === false && item.id.startsWith(PENDING_PREFIX)) {
|
|
47
|
+
throw new Error(`${item.display}:待审条目需要在会话活跃的一端判定,请先在那一端处理或在本端打开该会话`)
|
|
48
|
+
}
|
|
44
49
|
if (!detail.item.current || detail.item.seq !== item.seq || detail.item.revision !== item.revision
|
|
45
50
|
|| !detail.capability.keep) throw new Error(`${item.display}:审查版本变化,请刷新后重试`)
|
|
46
51
|
if (reject && !detail.capability.undo) throw new Error(`${item.display}:${detail.capability.reason}`)
|
|
@@ -62,7 +62,7 @@ import { applyOfficial, editorFont, registerThemes, themeNameOf } from '../monac
|
|
|
62
62
|
import { createDiffRenderer } from '../monaco/diffRender.js'
|
|
63
63
|
import { ST, callIdAttr, noopHunk, summarize } from '../state/records.js'
|
|
64
64
|
import { diffRegions } from '../state/regions.js'
|
|
65
|
-
import { missingBatch, missingRegion } from '../missingReview.js'
|
|
65
|
+
import { goneStateOf, missingBatch, missingRegion } from '../missingReview.js'
|
|
66
66
|
import { QuickOpen } from './QuickOpen.js'
|
|
67
67
|
import { attachHorizontalWheel } from './horizontalWheel.js'
|
|
68
68
|
import { CommandPalette } from './CommandPalette.js'
|
|
@@ -343,6 +343,11 @@ export function EditorView(props) {
|
|
|
343
343
|
const aiStatusTimer = React.useRef(null)
|
|
344
344
|
const [error, setError] = React.useState(null)
|
|
345
345
|
const [loadError, setLoadError] = React.useState(null)
|
|
346
|
+
// 缺失文件的终态:{ path, sessionId, title, message, official }。
|
|
347
|
+
// 与 loadError 区分开——加载失败可以重试,磁盘删除是终态,不能一直卡在进度条 0%。
|
|
348
|
+
const [goneFile, setGoneFile] = React.useState(null)
|
|
349
|
+
const missingReadRef = React.useRef(new Map()) // path → 已重读次数(防 文件缺失↔重现 抖动死循环)
|
|
350
|
+
const goneNow = goneFile && goneFile.path === active && goneFile.sessionId === sessionId ? goneFile : null
|
|
346
351
|
const [largeCandidate, setLargeCandidate] = React.useState(null)
|
|
347
352
|
const [pagedFile, setPagedFile] = React.useState(null)
|
|
348
353
|
const [pageChanged, setPageChanged] = React.useState(null)
|
|
@@ -569,6 +574,8 @@ export function EditorView(props) {
|
|
|
569
574
|
}, [records, active, cwd, review.page])
|
|
570
575
|
|
|
571
576
|
const reviewOpsRef = React.useRef(new Map())
|
|
577
|
+
// 已自动结案的记录 id:防止「改动已不在文件中」的记录在刷新期间被反复提交判定
|
|
578
|
+
const settledRef = React.useRef(new Set())
|
|
572
579
|
const [officialDetail, setOfficialDetail] = React.useState(null)
|
|
573
580
|
const [officialRecords, setOfficialRecords] = React.useState([])
|
|
574
581
|
const [selectedReview, setSelectedReview] = React.useState(null)
|
|
@@ -588,6 +595,9 @@ export function EditorView(props) {
|
|
|
588
595
|
.find((path) => sameFile(path, active))
|
|
589
596
|
const coveredPath = review.page?.overview?.coveredPaths.find((path) => sameFile(path, active))
|
|
590
597
|
const reviewPath = coveredPath ?? pendingPath
|
|
598
|
+
dbg(sessionId, `[DEBUG reviewPath] active=${active ?? '(null)'} covered=${coveredPath ?? '(none)'}`
|
|
599
|
+
+ ` pending=${pendingPath ?? '(none)'} coveredCount=${review.page?.overview?.coveredPaths.length ?? -1}`
|
|
600
|
+
+ ` items=${review.page?.items?.length ?? -1} overview=${review.page?.overview ? 1 : 0}`)
|
|
591
601
|
if (!active || !reviewPath) {
|
|
592
602
|
setOfficialDetail(null)
|
|
593
603
|
setOfficialRecords([])
|
|
@@ -601,9 +611,15 @@ export function EditorView(props) {
|
|
|
601
611
|
if (controller.signal.aborted) return
|
|
602
612
|
if (!page.ok) throw new Error(page.error)
|
|
603
613
|
const selected = page.page.items.find((item) => item.id === selectedReview) ?? page.page.items[0]
|
|
614
|
+
dbg(sessionId, `[DEBUG reviewList] path=${reviewPath} items=${page.page.items.length}`
|
|
615
|
+
+ ` waiting=${page.page.pending} selected=${selected?.id ?? '(none)'}`)
|
|
604
616
|
if (!selected) { setOfficialRecords([]); setOfficialDetail(null); return }
|
|
605
617
|
const result = await rpc('edrv.review.detail', { sessionId, id: selected.id }, controller.signal)
|
|
606
618
|
if (controller.signal.aborted) return
|
|
619
|
+
dbg(sessionId, `[DEBUG reviewDetail] id=${selected.id} ok=${result.ok ? 1 : 0}`
|
|
620
|
+
+ ` keep=${result.ok ? (result.detail.capability?.keep === true ? 1 : 0) : '-'}`
|
|
621
|
+
+ ` live=${result.ok ? String(result.detail.capability?.live) : '-'}`
|
|
622
|
+
+ ` err=${result.ok ? '' : String(result.error).slice(0, 120)}`)
|
|
607
623
|
if (!result.ok) throw new Error(result.error)
|
|
608
624
|
setOfficialRecords((previous) => JSON.stringify(previous) === JSON.stringify(page.page.items) ? previous : page.page.items)
|
|
609
625
|
setOfficialDetail((previous) => JSON.stringify(previous) === JSON.stringify(result.detail) ? previous : result.detail)
|
|
@@ -672,6 +688,15 @@ export function EditorView(props) {
|
|
|
672
688
|
const noBlocks = !!officialDetail && (officialDetail.diff.kind !== 'text' || officialDetail.diff.hunks.length === 0)
|
|
673
689
|
// 单文件 Keep/Undo 的作用域:可定位差异 + 无法定位的冲突差异(后者否则永久留在待处理列表)
|
|
674
690
|
const decideRegions = React.useMemo(() => [...pendingRegions, ...staleRegions], [pendingRegions, staleRegions])
|
|
691
|
+
// 记录的改动已全部不在当前文件中时没有可见差异可审:自动结案,不再让「改动已失效」占用差异栏。
|
|
692
|
+
React.useEffect(() => {
|
|
693
|
+
if (!officialDetail || contentReady !== true || batchBusyRef.current) return
|
|
694
|
+
if (pendingRegions.length > 0 || staleRegions.length === 0) return
|
|
695
|
+
if (noBlocks || officialDetail.capability?.keep !== true) return
|
|
696
|
+
if (dirtyRef.current[active]) return
|
|
697
|
+
|
|
698
|
+
void settleInvisibleRecord(officialDetail)
|
|
699
|
+
}, [officialDetail, contentReady, pendingRegions, staleRegions, noBlocks, active])
|
|
675
700
|
hoverRegionsRef.current = pendingRegions
|
|
676
701
|
// 行 → 差异区域 映射(hover O(1) 命中;每行归属其区域)
|
|
677
702
|
const lineRegionMap = React.useMemo(() => {
|
|
@@ -1153,17 +1178,49 @@ export function EditorView(props) {
|
|
|
1153
1178
|
* @param force 强制走 RPC(reloadFile 用,跳过 model 复用)
|
|
1154
1179
|
* @param version 已知磁盘版本(缺省走 RPC 返回值)
|
|
1155
1180
|
*/
|
|
1156
|
-
/**
|
|
1181
|
+
/**
|
|
1182
|
+
* 把「文件已在磁盘删除」记成终态面板:不占进度条、不反复重试,并允许直接关闭页签。
|
|
1183
|
+
* 只有确认删除的原因才走终态;会话/权限类失败仍回退通用加载失败,保留跨端提示。
|
|
1184
|
+
* @private @author ddj 2026年10月31号
|
|
1185
|
+
* @param path 目标文件
|
|
1186
|
+
* @param sid 会话 id
|
|
1187
|
+
* @param seq 加载序号(过期响应丢弃)
|
|
1188
|
+
* @param failure 宿主拒绝原因(可带稳定 code)
|
|
1189
|
+
*/
|
|
1190
|
+
function settleGone(path, sid, seq, failure) {
|
|
1191
|
+
if (!loadCurrent(path, seq)) return
|
|
1192
|
+
const error = failure && typeof failure === 'object' ? failure : { error: failure }
|
|
1193
|
+
const gone = goneStateOf(error)
|
|
1194
|
+
?? (error.deleted === true ? { title: '文件已在磁盘删除', official: false, message: String(error.error ?? '') } : null)
|
|
1195
|
+
if (!gone) { failLoad(path, sid, seq, String(error.error ?? error)); return }
|
|
1196
|
+
stampLoad(path, sid, seq, gone.message)
|
|
1197
|
+
setLoadError(null)
|
|
1198
|
+
setError(null)
|
|
1199
|
+
setGoneFile({ ...gone, path, sessionId: sid })
|
|
1200
|
+
setStatus(gone.official ? '文件已删除 · 差异栏可 Keep 结案' : '文件已删除')
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
/** @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. */
|
|
1157
1204
|
async function loadMissing(path, sid, seq) {
|
|
1158
1205
|
try {
|
|
1159
1206
|
const result = await rpc('edrv.missingReview', { sessionId: sid, path })
|
|
1160
1207
|
if (!loadCurrent(path, seq)) return
|
|
1161
|
-
if (!result.ok) {
|
|
1162
|
-
if (!result.review) {
|
|
1208
|
+
if (!result.ok) { settleGone(path, sid, seq, result); return }
|
|
1209
|
+
if (!result.review) {
|
|
1210
|
+
// 宿主观测到文件仍在磁盘(与读取时的 file-missing 竞态):重读一次即可;
|
|
1211
|
+
// 同一文件反复抖动时收尾为终态,绝不再递归重读。
|
|
1212
|
+
const attempts = (missingReadRef.current.get(path) ?? 0) + 1
|
|
1213
|
+
missingReadRef.current.set(path, attempts)
|
|
1214
|
+
if (attempts <= 2) { loadContent(path, sid, true); return }
|
|
1215
|
+
settleGone(path, sid, seq, { deleted: true, error: '文件状态在磁盘上反复变化,请稍后重试,或关闭该标签页' })
|
|
1216
|
+
return
|
|
1217
|
+
}
|
|
1218
|
+
missingReadRef.current.delete(path)
|
|
1163
1219
|
if (dirtyRef.current[path] || savingRef.current.has(path)) { failLoad(path, sid, seq, '文件已删除,但有未保存编辑;请先处理本地缓冲'); return }
|
|
1164
1220
|
const preview = { ...result.review, sessionId: sid, keepId: crypto.randomUUID(), undoId: crypto.randomUUID() }
|
|
1165
1221
|
missingRef.current.set(path, preview)
|
|
1166
1222
|
setMissingPreview(preview)
|
|
1223
|
+
setGoneFile(null)
|
|
1167
1224
|
setContent('')
|
|
1168
1225
|
setContentPath(path)
|
|
1169
1226
|
setLoadError(null)
|
|
@@ -1178,6 +1235,7 @@ export function EditorView(props) {
|
|
|
1178
1235
|
const seq = ++loadSeqRef.current
|
|
1179
1236
|
setReceiptLoad({ path, cwd, sessionId: sid, sequence: seq, status: 'loading' })
|
|
1180
1237
|
setLoadError(null)
|
|
1238
|
+
setGoneFile(null)
|
|
1181
1239
|
setLargeCandidate(null)
|
|
1182
1240
|
setPagedFile(null)
|
|
1183
1241
|
setPageChanged(null)
|
|
@@ -3303,33 +3361,84 @@ export function EditorView(props) {
|
|
|
3303
3361
|
*/
|
|
3304
3362
|
const itemOf = (r, reject) => ({ callId: r.callId, scope: r.create ? 'call' : 'hunk', hunkIndex: r.idx, decision: reject ? 'rejected' : 'accepted' })
|
|
3305
3363
|
|
|
3364
|
+
/**
|
|
3365
|
+
* @private
|
|
3366
|
+
* @author ddj 2026年10月31号
|
|
3367
|
+
* @description: 提交一条官方记录的判定;operationId 按决策键复用,网络重试保持幂等。
|
|
3368
|
+
* @param detail 目标记录(含冻结的 seq / revision)
|
|
3369
|
+
* @param reject true=Undo
|
|
3370
|
+
* @param blockId 可选的连续块;缺省为整文件
|
|
3371
|
+
* @returns 成功返回提交结果,失败抛出 host 给出的原因
|
|
3372
|
+
*/
|
|
3373
|
+
async function submitOfficialDecision(detail, reject, blockId) {
|
|
3374
|
+
const source = detail.item
|
|
3375
|
+
const opKey = JSON.stringify([sessionId, source.id, source.seq, source.revision, blockId, reject])
|
|
3376
|
+
let operationId = reviewOpsRef.current.get(opKey)
|
|
3377
|
+
if (!operationId) { operationId = crypto.randomUUID(); reviewOpsRef.current.set(opKey, operationId) }
|
|
3378
|
+
|
|
3379
|
+
const result = await rpc('edrv.review.decide', { sessionId, id: source.id, seq: source.seq,
|
|
3380
|
+
revision: source.revision, operationId, scope: blockId ? 'hunk' : 'file', blockId,
|
|
3381
|
+
decision: reject ? 'rejected' : 'accepted' })
|
|
3382
|
+
if (!result.ok) throw new Error(result.error)
|
|
3383
|
+
|
|
3384
|
+
reviewOpsRef.current.delete(opKey)
|
|
3385
|
+
return result
|
|
3386
|
+
}
|
|
3387
|
+
|
|
3388
|
+
/**
|
|
3389
|
+
* @private
|
|
3390
|
+
* @author ddj 2026年10月31号
|
|
3391
|
+
* @description: 结案一条改动已不在当前文件中的官方记录:等价于 Keep(只标记已采纳,不改写磁盘)。
|
|
3392
|
+
* 这类记录没有可见差异可审,留在差异栏只会让用户去处理一个不存在的东西。
|
|
3393
|
+
* @param detail 待结案的记录
|
|
3394
|
+
*/
|
|
3395
|
+
async function settleInvisibleRecord(detail) {
|
|
3396
|
+
if (batchBusyRef.current) return
|
|
3397
|
+
|
|
3398
|
+
const settled = settledRef.current
|
|
3399
|
+
if (settled.has(detail.item.id)) return
|
|
3400
|
+
settled.add(detail.item.id)
|
|
3401
|
+
|
|
3402
|
+
batchBusyRef.current = true
|
|
3403
|
+
try {
|
|
3404
|
+
await submitOfficialDecision(detail, false)
|
|
3405
|
+
refreshReview(sessionId)
|
|
3406
|
+
} catch (cause) {
|
|
3407
|
+
// 结案失败不打扰用户:记录仍留在待审列表,下次打开该文件再试
|
|
3408
|
+
settled.delete(detail.item.id)
|
|
3409
|
+
dbg(sessionId, `[DEBUG settleInvisible] id=${detail.item.id} err=${String(cause).slice(0, 160)}`)
|
|
3410
|
+
} finally { batchBusyRef.current = false }
|
|
3411
|
+
}
|
|
3412
|
+
|
|
3306
3413
|
/** @private @author ddj 2026年10月27号 @param reject Undo instead of Keep. @param blockId Optional contiguous block. @returns Persisted decision result; never reloads over a dirty buffer. */
|
|
3307
3414
|
async function decideOfficial(reject, blockId) {
|
|
3308
|
-
|
|
3415
|
+
dbg(sessionId, `[DEBUG decideOfficial] reject=${reject ? 1 : 0} detail=${officialDetail ? 1 : 0}`
|
|
3416
|
+
+ ` busy=${batchBusyRef.current ? 1 : 0} active=${active ?? '(null)'}`)
|
|
3417
|
+
if (!officialDetail || batchBusyRef.current) {
|
|
3418
|
+
// 详情缺失时按钮仍可点(能力位来自列表数据),必须给可读原因而不是静默返回
|
|
3419
|
+
if (!officialDetail && !batchBusyRef.current) setError('官方差异详情尚未加载完成,请稍后重试;若一直如此,请切换文件或刷新页面。')
|
|
3420
|
+
return false
|
|
3421
|
+
}
|
|
3309
3422
|
if (reject && (dirtyRef.current[active] || savingRef.current.has(active) || saveTimerRef.current.isPending())) {
|
|
3310
3423
|
setError('存在未保存或正在保存的修改,请等待完成后重试'); return false
|
|
3311
3424
|
}
|
|
3312
3425
|
batchBusyRef.current = true
|
|
3313
|
-
const
|
|
3426
|
+
const detail = officialDetail
|
|
3314
3427
|
const undoPath = active
|
|
3315
3428
|
const undoModel = editorRef.current?.getModel?.()
|
|
3316
3429
|
const undoVersion = undoModel?.getVersionId?.()
|
|
3317
|
-
const opKey = JSON.stringify([sessionId, source.id, source.seq, source.revision, blockId, reject])
|
|
3318
|
-
let operationId = reviewOpsRef.current.get(opKey)
|
|
3319
|
-
if (!operationId) { operationId = crypto.randomUUID(); reviewOpsRef.current.set(opKey, operationId) }
|
|
3320
3430
|
try {
|
|
3321
|
-
|
|
3322
|
-
revision: source.revision, operationId, scope: blockId ? 'hunk' : 'file', blockId,
|
|
3323
|
-
decision: reject ? 'rejected' : 'accepted' })
|
|
3324
|
-
if (!result.ok) throw new Error(result.error)
|
|
3325
|
-
reviewOpsRef.current.delete(opKey)
|
|
3431
|
+
await submitOfficialDecision(detail, reject, blockId)
|
|
3326
3432
|
if (reject && activeRef.current === undoPath && !dirtyRef.current[undoPath]
|
|
3327
3433
|
&& !savingRef.current.has(undoPath) && undoModel?.getVersionId?.() === undoVersion) reloadFile(true)
|
|
3328
3434
|
else if (reject) setError('Undo 已完成,编辑器在等待期间变化;未覆盖当前缓冲区,请核对磁盘后刷新')
|
|
3329
3435
|
refreshReview(sessionId)
|
|
3330
3436
|
setStatus(reject ? '已 Undo 选中差异' : '已 Keep 选中差异')
|
|
3331
3437
|
return true
|
|
3332
|
-
} catch (cause) {
|
|
3438
|
+
} catch (cause) {
|
|
3439
|
+
dbg(sessionId, `[DEBUG decideOfficialFail] id=${detail.item.id} reject=${reject ? 1 : 0} err=${String(cause).slice(0, 160)}`)
|
|
3440
|
+
setError(String(cause)); return false
|
|
3441
|
+
}
|
|
3333
3442
|
finally { batchBusyRef.current = false }
|
|
3334
3443
|
}
|
|
3335
3444
|
|
|
@@ -3466,10 +3575,26 @@ export function EditorView(props) {
|
|
|
3466
3575
|
return operationId
|
|
3467
3576
|
}
|
|
3468
3577
|
|
|
3578
|
+
/**
|
|
3579
|
+
* @private
|
|
3580
|
+
* @author ddj 2026年10月31号
|
|
3581
|
+
* @description: 批量操作要求官方待审列表已就绪;切换会话后列表还在载入时,
|
|
3582
|
+
* 先触发一次刷新并给出可读提示,而不是直接报错中止(用户看到红色「尚未就绪」会以为功能坏了)。
|
|
3583
|
+
* @param action 面向用户的动作名
|
|
3584
|
+
* @returns 列表已就绪时为 true
|
|
3585
|
+
*/
|
|
3586
|
+
function reviewReadyForBatch(action) {
|
|
3587
|
+
if (review.page?.overview) return true
|
|
3588
|
+
|
|
3589
|
+
refreshReview(sessionId)
|
|
3590
|
+
setStatus(`官方待审列表正在载入,载入完成后请再次点击${action}`)
|
|
3591
|
+
return false
|
|
3592
|
+
}
|
|
3593
|
+
|
|
3469
3594
|
/** @private @author ddj 2026年10月27号 @param reject Undo or Keep all workspace pending records, including previous rounds. @returns Explicit partial results; legacy proceeds only when official stage fully succeeds. */
|
|
3470
3595
|
async function actReviewAll(reject) {
|
|
3471
3596
|
if (batchBusyRef.current) return
|
|
3472
|
-
if (!
|
|
3597
|
+
if (!reviewReadyForBatch(reject ? 'Undo All' : 'Keep All')) return
|
|
3473
3598
|
if (!sum.pendingFiles.length) return
|
|
3474
3599
|
const token = reviewLifeRef.current
|
|
3475
3600
|
const undoPath = activeRef.current
|
|
@@ -3504,7 +3629,7 @@ export function EditorView(props) {
|
|
|
3504
3629
|
/** @private @author ddj 2026年10月27号 @param reject Whether to undo pending files. @description Open a DSH confirmation without starting any mutations. */
|
|
3505
3630
|
function askReviewAll(reject) {
|
|
3506
3631
|
if (batchBusyRef.current || !sum.pendingFiles.length) return
|
|
3507
|
-
if (!
|
|
3632
|
+
if (!reviewReadyForBatch(reject ? 'Undo All' : 'Keep All')) return
|
|
3508
3633
|
setReviewAllPrompt({ reject, count: sum.pendingFiles.length, token: reviewLifeRef.current })
|
|
3509
3634
|
}
|
|
3510
3635
|
|
|
@@ -4581,6 +4706,25 @@ export function EditorView(props) {
|
|
|
4581
4706
|
React.createElement('div', { className: 'edrv-loading-percent' }, Math.round(progress) + '%'),
|
|
4582
4707
|
retry ? React.createElement('button', { className: 'edrv-pill edrv-pill-ghost', onClick: retry }, '重试') : null)
|
|
4583
4708
|
|
|
4709
|
+
/**
|
|
4710
|
+
* 渲染缺失文件终态面板:说明 + 重试 + 关闭页签(不显示进度条,避免卡在 0%)。
|
|
4711
|
+
* @private @author ddj 2026年10月31号
|
|
4712
|
+
* @param gone 终态信息
|
|
4713
|
+
* @param retry 重新读取回调
|
|
4714
|
+
* @param close 关闭该标签页回调
|
|
4715
|
+
* @returns 终态面板 React 元素
|
|
4716
|
+
*/
|
|
4717
|
+
const goneBody = (gone, retry, close) => React.createElement('div', { className: 'edrv-empty edrv-loading', 'aria-live': 'polite', role: 'status' },
|
|
4718
|
+
React.createElement('div', { className: 'edrv-loading-title' }, gone.title),
|
|
4719
|
+
React.createElement('div', { style: { fontSize: 12, maxWidth: 460, lineHeight: 1.6 } }, gone.message),
|
|
4720
|
+
gone.official
|
|
4721
|
+
? React.createElement('div', { style: { fontSize: 12, maxWidth: 460, lineHeight: 1.6, opacity: 0.8 } },
|
|
4722
|
+
'待审改动在差异栏:Keep 保留删除结果并结案;Undo 需要该文件的原始内容,磁盘已删除时不可用。')
|
|
4723
|
+
: null,
|
|
4724
|
+
React.createElement('div', { style: { display: 'flex', gap: 8 } },
|
|
4725
|
+
React.createElement('button', { className: 'edrv-pill edrv-pill-ghost', onClick: retry }, '重试'),
|
|
4726
|
+
React.createElement('button', { className: 'edrv-pill edrv-pill-ghost', onClick: close }, '关闭标签页')))
|
|
4727
|
+
|
|
4584
4728
|
let body
|
|
4585
4729
|
if (activePlan) {
|
|
4586
4730
|
const labels = PLAN_MD_LABELS
|
|
@@ -4609,6 +4753,12 @@ export function EditorView(props) {
|
|
|
4609
4753
|
body = React.createElement('div', { className: 'edrv-empty' },
|
|
4610
4754
|
React.createElement('div', null, '暂无打开的文件'),
|
|
4611
4755
|
React.createElement('div', { style: { fontSize: 12 } }, '使用右上搜索框 (' + (chordOf('edrv.quickOpen') ?? 'Ctrl+P') + ') 打开工作区文件;' + (chordOf('edrv.showCommands') ?? 'Ctrl+Shift+P') + ' 打开命令栏;agent 修改文件后顶部会出现差异角标'))
|
|
4756
|
+
} else if (goneNow) {
|
|
4757
|
+
// 终态:文件已在磁盘删除且没有可用预览。不再显示 0% 进度条,也不无限重试。
|
|
4758
|
+
body = goneBody(goneNow, () => {
|
|
4759
|
+
missingReadRef.current.delete(goneNow.path)
|
|
4760
|
+
loadContent(active, sessionId, true)
|
|
4761
|
+
}, () => { void closeTab(goneNow.path) })
|
|
4612
4762
|
} else if (content === null && loadError && isSessionMissingError(loadError)) {
|
|
4613
4763
|
// 跨端会话缺失:终态提示而非加载失败卡死(会话仅在一端激活,本端 RPC 均被拒)
|
|
4614
4764
|
body = React.createElement('div', { className: 'edrv-empty edrv-loading', 'aria-live': 'polite' },
|
|
@@ -4697,6 +4847,9 @@ export function EditorView(props) {
|
|
|
4697
4847
|
const sessionDown = isSessionMissingError(rawReviewError)
|
|
4698
4848
|
const reviewErrorTone = sessionDown ? 'info' : 'error'
|
|
4699
4849
|
const reviewErrorText = !rawReviewError ? undefined : sessionDown ? SESSION_DOWN_NOTICE : sessionErrorText(rawReviewError)
|
|
4850
|
+
// 官方元数据未到位且没有报错 = 首次载入中:此时还不知道哪些历史记录会被官方通道接管,
|
|
4851
|
+
// 直接按历史记录报数会在每次重启时闪出几十个「待处理文件」,故先显示占位、批量也先不放开。
|
|
4852
|
+
const reviewLoading = review.page === null && !review.error
|
|
4700
4853
|
const buildDockSnapshot = () => ({
|
|
4701
4854
|
mode: editorDockMode(active),
|
|
4702
4855
|
missingDeletion: !!missingNow,
|
|
@@ -4718,7 +4871,7 @@ export function EditorView(props) {
|
|
|
4718
4871
|
onUndoFile: undoFile,
|
|
4719
4872
|
onAcceptAllFiles: acceptAllFiles,
|
|
4720
4873
|
onUndoAllFiles: undoAllFiles,
|
|
4721
|
-
allPendingCount: allPending.length + (review.page?.pending ?? 0),
|
|
4874
|
+
allPendingCount: reviewLoading ? 0 : allPending.length + (review.page?.pending ?? 0),
|
|
4722
4875
|
reviewBatchBusy,
|
|
4723
4876
|
batchUnavailable: !review.page?.overview ? '官方待审列表尚未就绪' : undefined,
|
|
4724
4877
|
onRollback: rollbackFile,
|
|
@@ -4732,7 +4885,7 @@ export function EditorView(props) {
|
|
|
4732
4885
|
contentReady,
|
|
4733
4886
|
diffTotal: displayDiffTotal(contentReady, pendingRegions.length, sum.files.find((file) => sameFile(file.path, active))?.pending ?? 0),
|
|
4734
4887
|
fileIdx,
|
|
4735
|
-
fileTotal: sum.pendingFiles.length,
|
|
4888
|
+
fileTotal: reviewLoading ? null : sum.pendingFiles.length,
|
|
4736
4889
|
onPrevDiff: () => gotoDiff(-1),
|
|
4737
4890
|
onNextDiff: () => gotoDiff(1),
|
|
4738
4891
|
onPrevFile: () => gotoFile(-1),
|
|
@@ -15,7 +15,8 @@ export interface OfficialBarProps {
|
|
|
15
15
|
diffIdx?: number
|
|
16
16
|
/** 编辑器内容是否就绪;未就绪时不得断言差异失效 */
|
|
17
17
|
contentReady?: boolean
|
|
18
|
-
|
|
18
|
+
/** 待审文件总数;null 表示官方元数据尚未就绪,栏内显示占位 */
|
|
19
|
+
fileTotal?: number | null
|
|
19
20
|
fileIdx?: number
|
|
20
21
|
allPendingCount?: number
|
|
21
22
|
reviewBatchBusy?: boolean
|
|
@@ -51,6 +52,20 @@ export function barIcon(mark: Mark): React.ReactElement {
|
|
|
51
52
|
return <svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth={1.5} strokeLinecap="round" strokeLinejoin="round" aria-hidden="true"><path d={marks[mark]} /></svg>
|
|
52
53
|
}
|
|
53
54
|
|
|
55
|
+
/**
|
|
56
|
+
* @private
|
|
57
|
+
* @author ddj 2026年10月31号
|
|
58
|
+
* @description: Keep 按钮提示。会话不在本端活跃时 Keep 仍可执行(只写共享审查库),
|
|
59
|
+
* 提示里补一句说明,免得用户把它当成跨端不可用的按钮。
|
|
60
|
+
* @param hint 差异栏给出的 Keep 说明
|
|
61
|
+
* @param live 会话是否在本端活跃
|
|
62
|
+
* @returns 按钮 title 文本
|
|
63
|
+
*/
|
|
64
|
+
function keepTitle(hint: string, live: boolean): string {
|
|
65
|
+
if (live) return hint
|
|
66
|
+
return `${hint}(会话在另一端激活:Keep 写入共享审查库,仍可执行)`
|
|
67
|
+
}
|
|
68
|
+
|
|
54
69
|
/** @public @author ddj 2026年10月27号 @param props Existing review callbacks and immutable record metadata. @returns Responsive review toolbar; no disk or decision side effects until an explicit action. */
|
|
55
70
|
export function OfficialDiffBar(props: OfficialBarProps): React.ReactElement {
|
|
56
71
|
const { official, pendingRegions = [], staleRegions = [], diffIdx = 0 } = props
|
|
@@ -75,7 +90,7 @@ export function OfficialDiffBar(props: OfficialBarProps): React.ReactElement {
|
|
|
75
90
|
|
|
76
91
|
return <div className="edrv-diffbar edrv-diffbar-dock edrv-reviewbar" data-edrv-diffbox="1" data-edrv-diff-source="official" role="region" aria-label="文件差异审查">
|
|
77
92
|
<ReviewAllRow
|
|
78
|
-
total={props.fileTotal ??
|
|
93
|
+
total={props.fileTotal ?? null}
|
|
79
94
|
current={(props.fileTotal ?? 0) && (props.fileIdx ?? -1) >= 0 ? (props.fileIdx ?? 0) + 1 : 0}
|
|
80
95
|
busy={props.reviewBatchBusy}
|
|
81
96
|
unavailable={props.batchUnavailable}
|
|
@@ -104,7 +119,7 @@ export function OfficialDiffBar(props: OfficialBarProps): React.ReactElement {
|
|
|
104
119
|
</div>
|
|
105
120
|
<div className="edrv-review-actions">
|
|
106
121
|
<Button size="sm" variant="ghost" className="edrv-review-undo" icon={barIcon('undo')} onClick={props.onUndoFile} disabled={props.reviewBatchBusy || !detail?.capability.undo} title={detail?.capability.reason}>Undo</Button>
|
|
107
|
-
<Button size="sm" variant="primary" className="edrv-review-keep" icon={barIcon('check')} onClick={props.onAcceptFile} disabled={props.reviewBatchBusy || !detail?.capability.keep} title={detail?.capability.keep ? view.keepHint : detail?.capability.reason}>Keep</Button>
|
|
122
|
+
<Button size="sm" variant="primary" className="edrv-review-keep" icon={barIcon('check')} onClick={props.onAcceptFile} disabled={props.reviewBatchBusy || !detail?.capability.keep} title={detail?.capability.keep ? keepTitle(view.keepHint, detail.capability.live !== false) : detail?.capability.reason}>Keep</Button>
|
|
108
123
|
<Menu open={menu === 'more'} anchor={moreTrigger} items={[{ id: 'pending', label: '差异总览' }, { id: 'archive', label: '归档 / 历史' }]} onSelect={openOverview} onClose={closeMenu} side="top" align="end" portal compact autoFocus />
|
|
109
124
|
</div>
|
|
110
125
|
</div>
|
|
@@ -29,7 +29,8 @@ export interface SidecarBarProps {
|
|
|
29
29
|
/** 编辑器内容是否就绪;未就绪时不得断言差异失效 */
|
|
30
30
|
contentReady?: boolean
|
|
31
31
|
fileIdx?: number
|
|
32
|
-
|
|
32
|
+
/** 待审文件总数;null 表示官方元数据尚未就绪,栏内显示占位 */
|
|
33
|
+
fileTotal?: number | null
|
|
33
34
|
allPendingCount?: number
|
|
34
35
|
reviewBatchBusy?: boolean
|
|
35
36
|
/** 官方元数据通道失败时的可操作提示 */
|
|
@@ -86,7 +87,7 @@ export function SidecarDiffBar(props: SidecarBarProps): React.ReactElement {
|
|
|
86
87
|
|
|
87
88
|
return <div className="edrv-diffbar edrv-diffbar-dock edrv-reviewbar" data-edrv-diffbox="1" data-edrv-diff-source="sidecar" role="region" aria-label="文件差异审查">
|
|
88
89
|
<ReviewAllRow
|
|
89
|
-
total={props.fileTotal ??
|
|
90
|
+
total={props.fileTotal ?? null}
|
|
90
91
|
current={(props.fileTotal ?? 0) && (props.fileIdx ?? -1) >= 0 ? (props.fileIdx ?? 0) + 1 : 0}
|
|
91
92
|
busy={busy}
|
|
92
93
|
pending={props.allPendingCount}
|
|
@@ -5,8 +5,8 @@ import { Button } from '@deepseek-ai/dsh-client-ui-primitives'
|
|
|
5
5
|
export interface ReviewAllRowProps {
|
|
6
6
|
/** 当前文件序号(1 基,0 表示未定位) */
|
|
7
7
|
current: number
|
|
8
|
-
/**
|
|
9
|
-
total: number
|
|
8
|
+
/** 差异文件总数;null 表示官方元数据尚未就绪、归属还分不清,显示占位而不报个数 */
|
|
9
|
+
total: number | null
|
|
10
10
|
/** 批量操作进行中 */
|
|
11
11
|
busy?: boolean
|
|
12
12
|
/** 批量不可用原因(悬浮提示) */
|
|
@@ -34,12 +34,13 @@ export interface ReviewAllRowProps {
|
|
|
34
34
|
* @returns 持久化的全工作区控制条
|
|
35
35
|
*/
|
|
36
36
|
export function ReviewAllRow(props: ReviewAllRowProps): React.ReactElement {
|
|
37
|
-
const
|
|
37
|
+
const known = typeof props.total === 'number'
|
|
38
|
+
const total = known ? props.total as number : 0
|
|
38
39
|
const current = total && props.current > 0 ? props.current : 0
|
|
39
40
|
const disabled = props.busy || !!props.unavailable || !props.pending
|
|
40
41
|
|
|
41
42
|
return <div className="edrv-review-global" role="group" aria-label="全部待审文件">
|
|
42
|
-
<Button size="sm" variant="ghost" className="edrv-review-total" onClick={props.onOpen} title="打开当前工作区全部待审差异文件">差异文件 <strong>{current} / {total}</strong></Button>
|
|
43
|
+
<Button size="sm" variant="ghost" className="edrv-review-total" onClick={props.onOpen} title="打开当前工作区全部待审差异文件">差异文件 <strong>{known ? `${current} / ${total}` : '—'}</strong></Button>
|
|
43
44
|
<span className="edrv-review-all-actions">
|
|
44
45
|
<Button size="sm" variant="ghost" onClick={props.onKeepAll} disabled={disabled || !props.onKeepAll} title={props.unavailable || props.keepTitle || 'Keep 当前工作区全部待审文件'}>Keep All</Button>
|
|
45
46
|
<Button size="sm" variant="ghost" className="edrv-review-undo" onClick={props.onUndoAll} disabled={disabled || !props.onUndoAll} title={props.unavailable || props.undoTitle || 'Undo 当前工作区全部待审文件'}>Undo All</Button>
|
package/src/missingReview.ts
CHANGED
|
@@ -2,9 +2,10 @@
|
|
|
2
2
|
import { createHash, randomUUID } from 'node:crypto'
|
|
3
3
|
import { resolve } from 'node:path'
|
|
4
4
|
import type { DiffRecord, MissingReview } from './shared/types.js'
|
|
5
|
+
import type { MissingRefusal } from './shared/rpc.js'
|
|
5
6
|
import type { Ctx, Session } from './store.js'
|
|
6
7
|
import { archiveRecords, policyOf, READ_CAP, resolveTargetIn, targetPathOf } from './store.js'
|
|
7
|
-
import { hasOfficial } from './reviewDb.js'
|
|
8
|
+
import { hasOfficial, officialPending } from './reviewDb.js'
|
|
8
9
|
import { recordResolved } from './model.js'
|
|
9
10
|
|
|
10
11
|
interface FrozenReview {
|
|
@@ -18,6 +19,32 @@ interface FrozenReview {
|
|
|
18
19
|
const frozen = new Map<string, FrozenReview>()
|
|
19
20
|
const locks = new Set<string>()
|
|
20
21
|
|
|
22
|
+
/**
|
|
23
|
+
* @public
|
|
24
|
+
* @author ddj 2026年10月31号
|
|
25
|
+
* @description: 给缺失文件预览的可预期拒绝挂稳定错误码。客户端据此显示终态说明,
|
|
26
|
+
* 而不是把内部错误原文当「加载失败」反复重试;也不让上游把 SQLite 报错原文透给编辑器。
|
|
27
|
+
* @param code 稳定错误码(official-covered / no-records / no-evidence / too-large)
|
|
28
|
+
* @param message 可读原因
|
|
29
|
+
* @returns 带 code 的 Error
|
|
30
|
+
*/
|
|
31
|
+
export function refusal(code: MissingRefusal, message: string): Error {
|
|
32
|
+
return Object.assign(new Error(message), { code })
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* @public
|
|
37
|
+
* @author ddj 2026年10月31号
|
|
38
|
+
* @description: 从任意错误里取回缺失文件预览的稳定拒绝码;不是本模块抛出的错误一律返回 undefined。
|
|
39
|
+
* @param error 捕获到的错误
|
|
40
|
+
* @returns 稳定拒绝码或 undefined
|
|
41
|
+
*/
|
|
42
|
+
export function refusalCodeOf(error: unknown): MissingRefusal | undefined {
|
|
43
|
+
const code = (error as { code?: unknown } | null)?.code
|
|
44
|
+
return code === 'official-covered' || code === 'official-decided' || code === 'no-records'
|
|
45
|
+
|| code === 'no-evidence' || code === 'too-large' ? code : undefined
|
|
46
|
+
}
|
|
47
|
+
|
|
21
48
|
/** @private @author ddj 2026年10月28号 @param target Resolved absolute path. @returns Platform file identity. */
|
|
22
49
|
function fileKey(target: string): string {
|
|
23
50
|
const normalized = resolve(target).replace(/\\/g, '/')
|
|
@@ -67,13 +94,25 @@ export async function missingReview(ctx: Ctx, session: Session, cwd: string, buc
|
|
|
67
94
|
// 解析结果可能是不透明 FsTarget,凡按路径字符串使用的分支都必须先还原。
|
|
68
95
|
const abs = targetPathOf(ctx, target)
|
|
69
96
|
if (!abs) throw new Error('无法解析目标文件路径')
|
|
70
|
-
|
|
97
|
+
// 官方记录覆盖该文件时不再给整文件删除预览:删除内容只能用差异栏的 Keep 结案,
|
|
98
|
+
// 但必须带稳定错误码,客户端才能显示「已删除」终态而不是卡在加载失败。
|
|
99
|
+
if (hasOfficial(cwd, path) || hasOfficial(cwd, abs)) {
|
|
100
|
+
if (!officialPending(cwd, abs) && !officialPending(cwd, path)) {
|
|
101
|
+
throw refusal('official-decided', '该文件已在磁盘删除,官方审查记录均已处理:可直接关闭该标签页')
|
|
102
|
+
}
|
|
103
|
+
throw refusal('official-covered', '该文件已有官方审查记录(磁盘上已删除):请在差异栏对该待审条目执行 Keep 结案;'
|
|
104
|
+
+ '整文件删除预览只适用于待审历史记录')
|
|
105
|
+
}
|
|
71
106
|
if (!await fileAbsent(ctx, cwd, abs)) throw new Error('文件已重新出现,请刷新')
|
|
72
107
|
const records = await fileRecords(ctx, cwd, bucket, abs)
|
|
73
108
|
const latest = records.at(-1)
|
|
74
|
-
if (!records.some((record) => !record.archived && !recordResolved(record)))
|
|
75
|
-
|
|
76
|
-
|
|
109
|
+
if (!records.some((record) => !record.archived && !recordResolved(record))) {
|
|
110
|
+
throw refusal('no-records', '磁盘上已没有该文件,且没有待审历史记录:可直接关闭该标签页')
|
|
111
|
+
}
|
|
112
|
+
if (!latest || typeof latest.after !== 'string') {
|
|
113
|
+
throw refusal('no-evidence', '磁盘上已没有该文件,且缺少可靠的完整历史快照,无法展示或恢复;不会用局部差异拼接文件')
|
|
114
|
+
}
|
|
115
|
+
if (Buffer.byteLength(latest.after, 'utf8') > READ_CAP) throw refusal('too-large', '磁盘上已没有该文件,且历史快照过大,无法整文件预览')
|
|
77
116
|
|
|
78
117
|
for (const [token, item] of frozen) if (item.expires < Date.now()) frozen.delete(token)
|
|
79
118
|
if (frozen.size >= 256) throw new Error('缺失文件预览过多,请稍后重试')
|
package/src/officialCapture.ts
CHANGED
|
@@ -184,6 +184,11 @@ export function installCapture(ctx: Ctx, onSnapshot?: (cwd: string, snapshot: Re
|
|
|
184
184
|
log.warn('官方历史补采不可用:缺少 sessionQuery;后续实时事件仍会采集')
|
|
185
185
|
return
|
|
186
186
|
}
|
|
187
|
+
const source = ctx.get('workspaceChanges')
|
|
188
|
+
if (!source?.summary) {
|
|
189
|
+
log.warn('官方历史补采不可用:缺少 workspaceChanges;后续实时事件仍会采集')
|
|
190
|
+
return
|
|
191
|
+
}
|
|
187
192
|
try {
|
|
188
193
|
// Repair durable history independently: official summaries do not survive Host restart.
|
|
189
194
|
// Each saved snapshot resolves against its own cwd, not the current session directory.
|
|
@@ -193,11 +198,17 @@ export function installCapture(ctx: Ctx, onSnapshot?: (cwd: string, snapshot: Re
|
|
|
193
198
|
}
|
|
194
199
|
// Semantic search omits structural events; only the raw metadata list contains workspace/changes.
|
|
195
200
|
const events = await query.listEvents(session.id)
|
|
201
|
+
let expired = 0
|
|
196
202
|
for (const event of events) {
|
|
197
203
|
if (event.type !== 'workspace/changes' || event.seq < (session.inheritedEventCount ?? 0)) continue
|
|
204
|
+
// Official summaries live only in Host memory, so a restart makes every
|
|
205
|
+
// historical event unreadable. Probe before enqueue: queueing them records
|
|
206
|
+
// one durable gap per turn that can never be repaired or replayed.
|
|
207
|
+
if (!source.summary(session.id, event.seq)) { expired += 1; continue }
|
|
198
208
|
const window = await query.readEvent({ sessionId: session.id, seq: event.seq })
|
|
199
209
|
collector.enqueue(session, window.target)
|
|
200
210
|
}
|
|
211
|
+
if (expired) log.info(`官方历史补采跳过 ${expired} 轮已失效事件:官方 summary 不跨宿主重启保留,实时事件照常采集`)
|
|
201
212
|
} catch (error) { log.warn('官方历史补采失败:' + String(error)) }
|
|
202
213
|
}
|
|
203
214
|
}
|
package/src/reviewDb.ts
CHANGED
|
@@ -205,6 +205,29 @@ export function reviewPage(cwd: string, cursor = 0, history = false, path?: stri
|
|
|
205
205
|
|
|
206
206
|
}
|
|
207
207
|
|
|
208
|
+
/**
|
|
209
|
+
* @public
|
|
210
|
+
* @author ddj 2026年10月31号
|
|
211
|
+
* @description: 该文件是否还有未处理的官方待审改动。缺失文件预览据此区分「需要去差异栏 Keep 结案」
|
|
212
|
+
* 与「记录都已处理、可直接关闭标签页」,避免已结案的删除文件继续提示待审。
|
|
213
|
+
* @param cwd Migrated workspace
|
|
214
|
+
* @param path Host path of the file
|
|
215
|
+
* @returns 存在未决官方记录时为 true
|
|
216
|
+
*/
|
|
217
|
+
export function officialPending(cwd: string, path: string): boolean {
|
|
218
|
+
if (typeof path !== 'string') return false
|
|
219
|
+
const db = dbFor(cwd)
|
|
220
|
+
if (!db.prepare("SELECT 1 FROM sqlite_master WHERE name='review_files' AND type='table'").get()) return false
|
|
221
|
+
// Windows 盘符路径按「反斜杠折成正斜杠 + 折大小写」比较(与 hasOfficial 的 SQL 归一化等价);
|
|
222
|
+
// 其余平台按身份精确比较,别把大小写不同的两个文件当成同一个。
|
|
223
|
+
if (path.length > 1 && path[1] === ':') {
|
|
224
|
+
const folded = path.split(String.fromCharCode(92)).join('/').toLowerCase()
|
|
225
|
+
return !!db.prepare("SELECT 1 FROM review_files WHERE current=1 AND decision='pending'"
|
|
226
|
+
+ " AND lower(replace(identity,char(92),'/'))=? LIMIT 1").get(folded)
|
|
227
|
+
}
|
|
228
|
+
return !!db.prepare("SELECT 1 FROM review_files WHERE current=1 AND decision='pending' AND identity=? LIMIT 1").get(path)
|
|
229
|
+
}
|
|
230
|
+
|
|
208
231
|
/** @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. */
|
|
209
232
|
export function hasOfficial(cwd: string, path: string): boolean {
|
|
210
233
|
// 防御:解析结果若被误当成路径传进来(不透明对象),按「无官方记录」处理,
|