dsh-vscode-mode 0.14.2 → 0.14.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 +9 -8
- package/lib/client.js +412 -209
- package/lib/client.js.map +1 -1
- package/lib/index.js +1284 -486
- package/lib/index.js.map +1 -1
- package/package.json +1 -1
- package/src/archiveDb.ts +476 -0
- package/src/capture.ts +8 -1
- package/src/client/diffDockStore.ts +13 -0
- package/src/client/monaco/diffRender.ts +3 -0
- package/src/client/state/regions.ts +21 -1
- package/src/client/styles/editor.css +12 -0
- package/src/client/ui/ConversationDiffDock.ts +2 -2
- package/src/client/ui/DiffLauncher.ts +76 -17
- package/src/client/ui/EditorView.ts +90 -7
- package/src/index.ts +2 -0
- package/src/paths.ts +11 -0
- package/src/perf.ts +17 -24
- package/src/revert.ts +93 -1
- package/src/rpc.ts +241 -67
- package/src/shared/diff.ts +69 -20
- package/src/shared/rpc.ts +8 -1
- package/src/store.ts +154 -55
package/src/rpc.ts
CHANGED
|
@@ -14,22 +14,22 @@ import {
|
|
|
14
14
|
appendArchiveEntries,
|
|
15
15
|
archiveRecords,
|
|
16
16
|
autoArchiveStale,
|
|
17
|
-
parseArchive,
|
|
18
17
|
policyOf,
|
|
19
|
-
readArchiveText,
|
|
20
18
|
resolveTarget,
|
|
21
19
|
saveBucket,
|
|
22
20
|
} from './store.js'
|
|
23
21
|
import { cp, mkdir, readFile, rename, rm, stat, writeFile, readdir } from 'node:fs/promises'
|
|
24
22
|
import { basename, dirname, join } from 'node:path'
|
|
25
23
|
import { archiveEntryFor, markDecision, recordResolved, reconstructOriginal } from './model.js'
|
|
24
|
+
import { clearRevertState, listArchives, pageArchives, readArchives, revertState, setRevertState, startReverts } from './archiveDb.js'
|
|
25
|
+
import { log } from './log.js'
|
|
26
26
|
import type { Registry } from './registry.js'
|
|
27
27
|
import { bucketOf, cwdOf, sessionOf } from './registry.js'
|
|
28
28
|
import type { SearchOrchestrator } from './search/orchestrator.js'
|
|
29
29
|
import { newSearcher } from './search/orchestrator.js'
|
|
30
30
|
import type { ContentSearcher } from './search/content.js'
|
|
31
31
|
import { newContentSearcher } from './search/content.js'
|
|
32
|
-
import { restoreFile, revertCall, revertHunk } from './revert.js'
|
|
32
|
+
import { restoreFile, revertCall, revertHunk, revertHunks, type Result } from './revert.js'
|
|
33
33
|
import { rulesList, rulesRead, rulesRemove, rulesSave, rulesToggle } from './rules.js'
|
|
34
34
|
import { isSnippetFilePath, snippetsEntries, snippetsList, snippetsRead, snippetsRemove, snippetsSave } from './snippets.js'
|
|
35
35
|
import { listMcp, refreshMcp, removeMcp, saveMcp, toggleMcp } from './mcp.js'
|
|
@@ -58,6 +58,10 @@ import { settingsDocPath } from './settingsDocPath.js'
|
|
|
58
58
|
const staleCheckedAt = new Map<string, number>()
|
|
59
59
|
/** stale 自动清理最小间隔。 */
|
|
60
60
|
const STALE_CHECK_MIN_MS = 10_000
|
|
61
|
+
// #region debug log
|
|
62
|
+
/** 诊断由 client 的 edrv.debug 激活,过期后不再产生 Host 耗时日志。 */
|
|
63
|
+
const diffTraceUntil = new Map<string, { until: number; id: string }>()
|
|
64
|
+
// #endregion
|
|
61
65
|
/**
|
|
62
66
|
* 无会话期诊断的伪 cwd:冷启动早期本插件客户端就会上报界面状态镜像事件
|
|
63
67
|
* (此时 `sessions` 可能尚未就绪,`requireSession` 会失败),退回本固定值落盘
|
|
@@ -237,7 +241,7 @@ async function afterManualSave(
|
|
|
237
241
|
const done: DiffRecord[] = []
|
|
238
242
|
for (const rec of bucket.values()) if (rec.path === path && rec.superseded) done.push(rec)
|
|
239
243
|
if (done.length) await archiveRecords(ctx, sc.session, sc.cwd, bucket, done, '被手动编辑覆盖')
|
|
240
|
-
else await saveBucket(ctx, sc.cwd, bucket, sc.session)
|
|
244
|
+
else await saveBucket(ctx, sc.cwd, bucket, sc.session, { upserts: [...bucket.values()].filter((rec) => rec.path === path), removed: [] })
|
|
241
245
|
}
|
|
242
246
|
}
|
|
243
247
|
|
|
@@ -259,18 +263,110 @@ async function applyDecisions(
|
|
|
259
263
|
): Promise<DecideResult[]> {
|
|
260
264
|
const results: DecideResult[] = []
|
|
261
265
|
const resolved: DiffRecord[] = []
|
|
266
|
+
const touched = new Map<string, DiffRecord>()
|
|
262
267
|
const ws = findRemoteWs(cwd)
|
|
263
268
|
let changed = false
|
|
264
|
-
|
|
269
|
+
const grouped = new Map<number, Result>()
|
|
270
|
+
// #region debug log
|
|
271
|
+
const trace = diffTraceUntil.get(cwd)
|
|
272
|
+
const traceId = trace && trace.until > Date.now() ? trace.id : null
|
|
273
|
+
const started = traceId ? performance.now() : 0
|
|
274
|
+
let revertMs = 0
|
|
275
|
+
let revertGroups = 0
|
|
276
|
+
// #endregion
|
|
277
|
+
for (let index = 0; index < items.length; index++) {
|
|
278
|
+
const item = items[index]
|
|
265
279
|
const record = bucket.get(item.callId)
|
|
266
280
|
if (!record) {
|
|
267
281
|
results.push({ callId: item.callId, ok: false, error: '记录不存在' })
|
|
268
282
|
continue
|
|
269
283
|
}
|
|
284
|
+
if (record.archived) {
|
|
285
|
+
results.push({ callId: item.callId, ok: false, error: '记录已归档,无法重复决策' })
|
|
286
|
+
continue
|
|
287
|
+
}
|
|
288
|
+
// 重试已执行的决定只补持久化,不能再次修改磁盘上的同一个 hunk。
|
|
289
|
+
const previous = item.scope === 'hunk'
|
|
290
|
+
? record.decisions.perHunk[item.hunkIndex ?? -1] : record.decisions.call
|
|
291
|
+
const alreadyDone = previous === item.decision
|
|
292
|
+
if (alreadyDone) {
|
|
293
|
+
if (recordResolved(record) && !record.archived) resolved.push(record)
|
|
294
|
+
changed = true
|
|
295
|
+
touched.set(record.callId, record)
|
|
296
|
+
results.push({ callId: item.callId, ok: true, record: recView(record, ws) })
|
|
297
|
+
continue
|
|
298
|
+
}
|
|
270
299
|
let revertedStale = false
|
|
271
|
-
if (item.decision === 'rejected') {
|
|
300
|
+
if (item.decision === 'rejected' && !alreadyDone) {
|
|
272
301
|
const scope: RpcScope = item.scope ?? 'call'
|
|
273
|
-
const
|
|
302
|
+
const hunkIndex = scope === 'hunk' ? item.hunkIndex ?? -1 : -1
|
|
303
|
+
let existingIntent: ReturnType<typeof revertState>
|
|
304
|
+
try { existingIntent = grouped.has(index) ? null : revertState(cwd, record.callId, scope, hunkIndex) }
|
|
305
|
+
catch (error) {
|
|
306
|
+
results.push({ callId: item.callId, ok: false, error: '回滚状态读取失败,未修改文件:' + String(error) })
|
|
307
|
+
continue
|
|
308
|
+
}
|
|
309
|
+
if (existingIntent === 'started') {
|
|
310
|
+
results.push({ callId: item.callId, ok: false, error: '回滚结果不确定,请人工核对文件后处理;已阻止再次修改文件' })
|
|
311
|
+
continue
|
|
312
|
+
}
|
|
313
|
+
if (existingIntent === 'applied') {
|
|
314
|
+
markDecision(record, scope, item.hunkIndex, item.decision)
|
|
315
|
+
changed = true
|
|
316
|
+
touched.set(record.callId, record)
|
|
317
|
+
if (recordResolved(record)) resolved.push(record)
|
|
318
|
+
results.push({ callId: item.callId, ok: true, record: recView(record, ws) })
|
|
319
|
+
continue
|
|
320
|
+
}
|
|
321
|
+
if (!grouped.has(index)) {
|
|
322
|
+
let last = index + 1
|
|
323
|
+
if (scope === 'hunk' && !ws && record.toolName !== 'remote_ssh_write') {
|
|
324
|
+
while (last < items.length && items[last].callId === item.callId
|
|
325
|
+
&& items[last].decision === 'rejected' && items[last].scope === 'hunk') last++
|
|
326
|
+
}
|
|
327
|
+
let groupReady = false
|
|
328
|
+
try {
|
|
329
|
+
groupReady = last > index + 1 && items.slice(index + 1, last).every((next) =>
|
|
330
|
+
revertState(cwd, record.callId, 'hunk', next.hunkIndex ?? -1) === null)
|
|
331
|
+
if (groupReady) startReverts(cwd, record.callId, items.slice(index, last).map((next) => next.hunkIndex ?? -1))
|
|
332
|
+
else setRevertState(cwd, record.callId, scope, hunkIndex, 'started')
|
|
333
|
+
} catch (error) {
|
|
334
|
+
results.push({ callId: item.callId, ok: false, error: '回滚状态无法登记,未修改文件:' + String(error) })
|
|
335
|
+
continue
|
|
336
|
+
}
|
|
337
|
+
if (groupReady) {
|
|
338
|
+
// #region debug log
|
|
339
|
+
const revertStart = traceId ? performance.now() : 0
|
|
340
|
+
// #endregion
|
|
341
|
+
const outcomes = await revertHunks(ctx, session, record, items.slice(index, last).map((next) => next.hunkIndex ?? -1), traceId ?? undefined)
|
|
342
|
+
// #region debug log
|
|
343
|
+
if (traceId) revertMs += performance.now() - revertStart
|
|
344
|
+
revertGroups++
|
|
345
|
+
// #endregion
|
|
346
|
+
outcomes.forEach((outcome, offset) => grouped.set(index + offset, outcome))
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
// #region debug log
|
|
350
|
+
const revertStart = traceId ? performance.now() : 0
|
|
351
|
+
// #endregion
|
|
352
|
+
const outcome = grouped.get(index) ?? (scope === 'hunk'
|
|
353
|
+
? await revertHunk(ctx, session, record, item.hunkIndex ?? -1, traceId ?? undefined)
|
|
354
|
+
: await revertCall(ctx, session, record))
|
|
355
|
+
// #region debug log
|
|
356
|
+
if (traceId && !grouped.has(index)) revertMs += performance.now() - revertStart
|
|
357
|
+
// #endregion
|
|
358
|
+
grouped.delete(index)
|
|
359
|
+
try {
|
|
360
|
+
if (outcome.ok || outcome.stale === true) setRevertState(cwd, record.callId, scope, hunkIndex, 'applied')
|
|
361
|
+
else if (outcome.error === '找不到该差异块' || outcome.error === '回滚失败:文件不存在'
|
|
362
|
+
|| outcome.error === '回滚失败:文件过大,无法安全定位') {
|
|
363
|
+
clearRevertState(cwd, record.callId, scope, hunkIndex)
|
|
364
|
+
}
|
|
365
|
+
// 其余失败可能发生在写盘途中:保留 started,阻止不确定状态下重复回滚。
|
|
366
|
+
} catch (error) {
|
|
367
|
+
results.push({ callId: item.callId, ok: false, error: '回滚状态保存失败,请人工核对文件:' + String(error) })
|
|
368
|
+
continue
|
|
369
|
+
}
|
|
274
370
|
if (!outcome.ok) {
|
|
275
371
|
if (scope !== 'hunk' || outcome.stale !== true) {
|
|
276
372
|
results.push({ callId: item.callId, ok: false, error: outcome.error })
|
|
@@ -282,16 +378,62 @@ async function applyDecisions(
|
|
|
282
378
|
const wasResolved = recordResolved(record)
|
|
283
379
|
markDecision(record, item.scope ?? 'call', item.hunkIndex, item.decision)
|
|
284
380
|
changed = true
|
|
381
|
+
touched.set(record.callId, record)
|
|
285
382
|
if (!wasResolved && recordResolved(record)) resolved.push(record)
|
|
286
383
|
const result: DecideResult = { callId: item.callId, ok: true, record: recView(record, ws) }
|
|
287
384
|
if (revertedStale) result.stale = true
|
|
288
385
|
results.push(result)
|
|
289
386
|
}
|
|
387
|
+
// #region debug log
|
|
388
|
+
const decisionMs = traceId ? performance.now() - started : 0
|
|
389
|
+
const archiveStart = traceId ? performance.now() : 0
|
|
390
|
+
// #endregion
|
|
290
391
|
if (resolved.length) {
|
|
291
392
|
const reason = items.some((item) => item.decision === 'rejected') ? '已处理(回滚)' : '已处理'
|
|
292
|
-
|
|
393
|
+
try {
|
|
394
|
+
await archiveRecords(ctx, session, cwd, bucket, resolved, reason, { deferSave: true, traceId: traceId ?? undefined, changed: [...touched.values()] })
|
|
395
|
+
} catch (error) {
|
|
396
|
+
const failed = new Set(resolved.map((record) => record.callId))
|
|
397
|
+
for (const result of results) {
|
|
398
|
+
if (!failed.has(result.callId) || !result.ok) continue
|
|
399
|
+
result.ok = false
|
|
400
|
+
result.error = '归档失败:' + String(error)
|
|
401
|
+
delete result.record
|
|
402
|
+
}
|
|
403
|
+
// 决策保留于内存并尝试写回 sidecar,Undo 不再重复反替换。
|
|
404
|
+
if (changed) {
|
|
405
|
+
try { await saveBucket(ctx, cwd, bucket, session, { upserts: [...touched.values()], removed: [] }) }
|
|
406
|
+
catch (saveError) { log.error('归档失败后的 SQLite 补写失败:' + String(saveError)) }
|
|
407
|
+
}
|
|
408
|
+
return results
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
// #region debug log
|
|
412
|
+
const archiveMs = traceId ? performance.now() - archiveStart : 0
|
|
413
|
+
const saveStart = traceId ? performance.now() : 0
|
|
414
|
+
// #endregion
|
|
415
|
+
if (changed && !resolved.length) {
|
|
416
|
+
try {
|
|
417
|
+
await saveBucket(ctx, cwd, bucket, session, { upserts: [...touched.values()], removed: [] })
|
|
418
|
+
} catch (error) {
|
|
419
|
+
for (const result of results) {
|
|
420
|
+
if (!result.ok) continue
|
|
421
|
+
result.ok = false
|
|
422
|
+
result.error = '决策记录保存失败:' + String(error)
|
|
423
|
+
delete result.record
|
|
424
|
+
}
|
|
425
|
+
return results
|
|
426
|
+
}
|
|
293
427
|
}
|
|
294
|
-
|
|
428
|
+
// #region debug log
|
|
429
|
+
try {
|
|
430
|
+
if (traceId) {
|
|
431
|
+
const saveMs = performance.now() - saveStart
|
|
432
|
+
const totalMs = performance.now() - started
|
|
433
|
+
debugRecord(ctx, cwd, `[DEBUG diffDecision] trace=${traceId} items=${items.length} rejected=${items.filter((item) => item.decision === 'rejected').length} ok=${results.filter((result) => result.ok).length} stale=${results.filter((result) => result.stale === true).length} grouped=${revertGroups} loopMs=${decisionMs.toFixed(1)} revertMs=${revertMs.toFixed(1)} archiveMs=${archiveMs.toFixed(1)} saveMs=${saveMs.toFixed(1)} totalMs=${totalMs.toFixed(1)}`)
|
|
434
|
+
}
|
|
435
|
+
} catch { /* 诊断不可影响决策 */ }
|
|
436
|
+
// #endregion
|
|
295
437
|
return results
|
|
296
438
|
}
|
|
297
439
|
|
|
@@ -447,9 +589,17 @@ export function buildHandlers(
|
|
|
447
589
|
// edrv.dap.* 由 createDapRpc 提供(调试会话单例),这里并入。
|
|
448
590
|
...((dapHandlers ?? {}) as RpcHandlerMap),
|
|
449
591
|
'edrv.list': async (args) => {
|
|
592
|
+
// #region debug log
|
|
593
|
+
const start = performance.now()
|
|
594
|
+
let staleMs = 0
|
|
595
|
+
let staleChecked = false
|
|
596
|
+
// #endregion
|
|
450
597
|
const sc = await requireSession(ctx, args.sessionId)
|
|
451
598
|
if ('err' in sc) return { ok: false, error: sc.err }
|
|
452
599
|
const bucket = await bucketOf(registry, ctx, sc.cwd)
|
|
600
|
+
// #region debug log
|
|
601
|
+
const loadMs = performance.now() - start
|
|
602
|
+
// #endregion
|
|
453
603
|
const want = Array.isArray(args.callIds) ? new Set(args.callIds) : null
|
|
454
604
|
// 全量轮询时自动清理 stale 幽灵差异(批量决策后的即时刷新可 skipStale 跳过);
|
|
455
605
|
// 三个组件各自 5s 全量 list,stale 检查节流到 STALE_CHECK_MIN_MS 一次,
|
|
@@ -458,7 +608,14 @@ export function buildHandlers(
|
|
|
458
608
|
const now = Date.now()
|
|
459
609
|
if (now - (staleCheckedAt.get(sc.cwd) ?? 0) > STALE_CHECK_MIN_MS) {
|
|
460
610
|
staleCheckedAt.set(sc.cwd, now)
|
|
611
|
+
// #region debug log
|
|
612
|
+
staleChecked = true
|
|
613
|
+
const staleStart = performance.now()
|
|
614
|
+
// #endregion
|
|
461
615
|
await autoArchiveStale(ctx, sc.session, sc.cwd, bucket)
|
|
616
|
+
// #region debug log
|
|
617
|
+
staleMs = performance.now() - staleStart
|
|
618
|
+
// #endregion
|
|
462
619
|
}
|
|
463
620
|
}
|
|
464
621
|
const out: RecordView[] = []
|
|
@@ -470,6 +627,13 @@ export function buildHandlers(
|
|
|
470
627
|
out.push(recView(rec, ws))
|
|
471
628
|
}
|
|
472
629
|
out.sort((a, b) => (a.at < b.at ? -1 : 1))
|
|
630
|
+
// #region debug log
|
|
631
|
+
try {
|
|
632
|
+
const totalMs = performance.now() - start
|
|
633
|
+
const trace = diffTraceUntil.get(sc.cwd)
|
|
634
|
+
if (trace && trace.until > Date.now() && (totalMs >= 20 || staleMs >= 10)) debugRecord(ctx, sc.cwd, `[DEBUG diffListHost] trace=${trace.id} count=${out.length} bucket=${bucket.size} checked=${staleChecked} loadMs=${loadMs.toFixed(1)} staleMs=${staleMs.toFixed(1)} totalMs=${totalMs.toFixed(1)}`)
|
|
635
|
+
} catch { /* 诊断不可影响列表 */ }
|
|
636
|
+
// #endregion
|
|
473
637
|
return { ok: true, records: out }
|
|
474
638
|
},
|
|
475
639
|
/**
|
|
@@ -715,38 +879,38 @@ export function buildHandlers(
|
|
|
715
879
|
'edrv.archiveList': async (args) => {
|
|
716
880
|
const sc = await requireSession(ctx, args.sessionId)
|
|
717
881
|
if ('err' in sc) return { ok: false, error: sc.err }
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
})
|
|
741
|
-
entries.sort((a, b) => (Number(b.batch ?? -1) - Number(a.batch ?? -1)) || (a.at < b.at ? 1 : -1))
|
|
742
|
-
return { ok: true, entries }
|
|
882
|
+
try {
|
|
883
|
+
const ws = findRemoteWs(sc.cwd)
|
|
884
|
+
const entries = listArchives(sc.cwd).map((item) => ({
|
|
885
|
+
...item, ...(ws ? { displayPath: remoteDisplayOf(item.path, ws) ?? undefined } : {}),
|
|
886
|
+
}))
|
|
887
|
+
return { ok: true, entries }
|
|
888
|
+
} catch (error) {
|
|
889
|
+
return { ok: false, error: '归档列表读取失败:' + String(error) }
|
|
890
|
+
}
|
|
891
|
+
},
|
|
892
|
+
'edrv.archivePage': async (args) => {
|
|
893
|
+
const sc = await requireSession(ctx, args.sessionId)
|
|
894
|
+
if ('err' in sc) return { ok: false, error: sc.err }
|
|
895
|
+
try {
|
|
896
|
+
const ws = findRemoteWs(sc.cwd)
|
|
897
|
+
const page = pageArchives(sc.cwd, args.cursor, args.limit)
|
|
898
|
+
return { ok: true, entries: page.entries.map((item) => ({
|
|
899
|
+
...item, ...(ws ? { displayPath: remoteDisplayOf(item.path, ws) ?? undefined } : {}),
|
|
900
|
+
})), nextCursor: page.nextCursor }
|
|
901
|
+
} catch (error) {
|
|
902
|
+
return { ok: false, error: '归档分页读取失败:' + String(error) }
|
|
903
|
+
}
|
|
743
904
|
},
|
|
744
905
|
'edrv.archiveRead': async (args) => {
|
|
745
906
|
const sc = await requireSession(ctx, args.sessionId)
|
|
746
907
|
if ('err' in sc) return { ok: false, error: sc.err }
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
908
|
+
try {
|
|
909
|
+
if (args.batch !== undefined && (!args.path || (args.batch !== null && !Number.isInteger(args.batch)))) return { ok: false, error: '归档批次参数无效' }
|
|
910
|
+
return { ok: true, batches: readArchives(sc.cwd, args.path, args.batch) }
|
|
911
|
+
} catch (error) {
|
|
912
|
+
return { ok: false, error: '归档详情读取失败:' + String(error) }
|
|
913
|
+
}
|
|
750
914
|
},
|
|
751
915
|
'edrv.rollback': async (args) => {
|
|
752
916
|
const sc = await requireSession(ctx, args.sessionId)
|
|
@@ -755,37 +919,41 @@ export function buildHandlers(
|
|
|
755
919
|
if (!fs) return { ok: false, error: '缺少 fs' }
|
|
756
920
|
const batch = args.batch
|
|
757
921
|
const bucket = await bucketOf(registry, ctx, sc.cwd)
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
922
|
+
try {
|
|
923
|
+
let affected: DiffRecord[] = []
|
|
924
|
+
let beforeRec: DiffRecord | null = null
|
|
925
|
+
if (batch !== undefined) {
|
|
926
|
+
const all = readArchives(sc.cwd, args.path, batch)
|
|
927
|
+
const recs: DiffRecord[] = []
|
|
928
|
+
for (const b of all) if (b.batch === batch) for (const r of (b.records || [])) recs.push(r as unknown as DiffRecord)
|
|
929
|
+
if (!recs.length) return { ok: false, error: '归档中找不到该批次' }
|
|
930
|
+
recs.sort((a, b2) => (a.at < b2.at ? -1 : 1))
|
|
931
|
+
beforeRec = recs[0]
|
|
932
|
+
affected = recs
|
|
933
|
+
} else {
|
|
934
|
+
for (const r of bucket.values()) if (r.path === args.path && !r.archived) affected.push(r)
|
|
935
|
+
if (!affected.length) return { ok: false, error: '该文件没有可回滚的差异' }
|
|
936
|
+
affected.sort((a, b2) => (a.at < b2.at ? -1 : 1))
|
|
937
|
+
beforeRec = affected[0]
|
|
938
|
+
}
|
|
939
|
+
const rerr = await restoreFile(ctx, sc.session, beforeRec)
|
|
940
|
+
if (rerr) return { ok: false, error: rerr }
|
|
941
|
+
if (batch === undefined) {
|
|
942
|
+
await archiveRecords(ctx, sc.session, sc.cwd, bucket, affected, '已回滚')
|
|
943
|
+
} else {
|
|
944
|
+
const all = readArchives(sc.cwd, args.path, batch)
|
|
945
|
+
const sumRecs = all.reduce((s, b) => s + ((b.records || []).length), 0)
|
|
946
|
+
const logRec = Object.assign({}, affected[0], { note: (affected[0].note ? affected[0].note + ';' : '') + '回滚至本批次前(批次 ' + batch + ')' })
|
|
947
|
+
await appendArchiveEntries(ctx, sc.cwd, [archiveEntryFor([logRec], sc.cwd, '已回滚(批次 ' + batch + ',' + sumRecs + ' 条)')], sc.session)
|
|
948
|
+
// 批次回滚恢复的是旧内容,文件当前活跃差异已失效,一并归档
|
|
949
|
+
const activeRecs: DiffRecord[] = []
|
|
950
|
+
for (const r of bucket.values()) if (r.path === args.path && !r.archived) activeRecs.push(r)
|
|
951
|
+
if (activeRecs.length) await archiveRecords(ctx, sc.session, sc.cwd, bucket, activeRecs, '已回滚(批次回滚覆盖)')
|
|
952
|
+
}
|
|
953
|
+
return { ok: true, path: args.path, batch: batch ?? null }
|
|
954
|
+
} catch (error) {
|
|
955
|
+
return { ok: false, error: '回滚/归档失败:' + String(error) }
|
|
787
956
|
}
|
|
788
|
-
return { ok: true, path: args.path, batch: batch ?? null }
|
|
789
957
|
},
|
|
790
958
|
'edrv.debug': async (args) => {
|
|
791
959
|
// 诊断日志:client 上报 → debugLog 模块缓冲批量落盘 ~/.dsh/dsh-vscode-mode/logs/
|
|
@@ -798,6 +966,12 @@ export function buildHandlers(
|
|
|
798
966
|
debugRecord(ctx, UI_AUDIT_CWD, text, args.level)
|
|
799
967
|
return { ok: true }
|
|
800
968
|
}
|
|
969
|
+
// #region debug log
|
|
970
|
+
try {
|
|
971
|
+
const match = /^\[DEBUG diffAction\] stage=start trace=(\d+-\d+) /.exec(text)
|
|
972
|
+
if (match) diffTraceUntil.set(sc.cwd, { until: Date.now() + 15_000, id: match[1] })
|
|
973
|
+
} catch { /* 诊断不可影响日志路由 */ }
|
|
974
|
+
// #endregion
|
|
801
975
|
debugRecord(ctx, sc.cwd, text, args.level)
|
|
802
976
|
return { ok: true }
|
|
803
977
|
},
|
package/src/shared/diff.ts
CHANGED
|
@@ -127,18 +127,43 @@ export function lineBefore(text: string, index: number): number {
|
|
|
127
127
|
return n
|
|
128
128
|
}
|
|
129
129
|
|
|
130
|
-
/**
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
130
|
+
/** 每种搜索串只推进一次,保持原来从头开始的非重叠匹配顺序。 */
|
|
131
|
+
interface MatchState {
|
|
132
|
+
ranges: TextRange[]
|
|
133
|
+
next: number
|
|
134
|
+
done: boolean
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* 懒扫描候选:先找 cursor 后未占用项,找不到再回退至首个未占用项。
|
|
139
|
+
* @author ddj 2026年09月29号
|
|
140
|
+
* @param content 当前内容
|
|
141
|
+
* @param needle 搜索串
|
|
142
|
+
* @param cursor 前一块的结束位置
|
|
143
|
+
* @param occupied 已选区间
|
|
144
|
+
* @param state 此搜索串已扫描的候选
|
|
145
|
+
* @returns 与完整候选列表双轮查找相同的匹配
|
|
146
|
+
*/
|
|
147
|
+
function findCandidate(content: string, needle: string, cursor: number, occupied: TextRange[], state: MatchState): TextRange | undefined {
|
|
148
|
+
if (!needle.length) return undefined
|
|
149
|
+
let fallback: TextRange | undefined
|
|
150
|
+
for (let i = 0; i < state.ranges.length; i++) {
|
|
151
|
+
const range = state.ranges[i]
|
|
152
|
+
if (occupied.some((used) => overlaps(range, used))) continue
|
|
153
|
+
if (range.start >= cursor) return range
|
|
154
|
+
if (!fallback) fallback = range
|
|
155
|
+
}
|
|
156
|
+
while (!state.done) {
|
|
157
|
+
const at = content.indexOf(needle, state.next)
|
|
158
|
+
if (at < 0) { state.done = true; break }
|
|
159
|
+
const range = { start: at, end: at + needle.length }
|
|
160
|
+
state.ranges.push(range)
|
|
161
|
+
state.next = range.end
|
|
162
|
+
if (occupied.some((used) => overlaps(range, used))) continue
|
|
163
|
+
if (range.start >= cursor) return range
|
|
164
|
+
if (!fallback) fallback = range
|
|
140
165
|
}
|
|
141
|
-
return
|
|
166
|
+
return fallback
|
|
142
167
|
}
|
|
143
168
|
|
|
144
169
|
/**
|
|
@@ -153,6 +178,7 @@ export function locateHunks(content: string, hunks: Hunk[]): HunkLocation[] {
|
|
|
153
178
|
const locations: HunkLocation[] = []
|
|
154
179
|
let cursor = 0
|
|
155
180
|
const occupied: TextRange[] = []
|
|
181
|
+
const matches = new Map<string, MatchState>()
|
|
156
182
|
for (let idx = 0; idx < hunks.length; idx++) {
|
|
157
183
|
const hunk = hunks[idx]
|
|
158
184
|
const newText = hunk.newText
|
|
@@ -163,9 +189,14 @@ export function locateHunks(content: string, hunks: Hunk[]): HunkLocation[] {
|
|
|
163
189
|
&& (newText.length === 0 || content.slice(snapshot.start, snapshot.end) === newText)
|
|
164
190
|
? snapshot
|
|
165
191
|
: null
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
192
|
+
let candidate: TextRange | undefined
|
|
193
|
+
if (snapshotMatches) {
|
|
194
|
+
if (!occupied.some((used) => overlaps(snapshotMatches, used))) candidate = snapshotMatches
|
|
195
|
+
} else if (newText.length) {
|
|
196
|
+
let state = matches.get(newText)
|
|
197
|
+
if (!state) { state = { ranges: [], next: 0, done: false }; matches.set(newText, state) }
|
|
198
|
+
candidate = findCandidate(content, newText, cursor, occupied, state)
|
|
199
|
+
}
|
|
169
200
|
if (candidate) {
|
|
170
201
|
locations.push({ idx, hunk, ...candidate, matched: true })
|
|
171
202
|
occupied.push(candidate)
|
|
@@ -205,15 +236,33 @@ export function replaceRange(content: string, location: HunkLocation, reverse =
|
|
|
205
236
|
return content.slice(0, location.start) + replacement + content.slice(location.end)
|
|
206
237
|
}
|
|
207
238
|
|
|
208
|
-
/**
|
|
239
|
+
/**
|
|
240
|
+
* 按位置倒序收集片段,最后一次拼接;非互斥区间退回原替换语义。
|
|
241
|
+
* @author ddj 2026年09月29号
|
|
242
|
+
* @param content 当前文件内容
|
|
243
|
+
* @param locations 已定位区间
|
|
244
|
+
* @param reverse 是否反向应用
|
|
245
|
+
* @returns 合成内容及失效块索引
|
|
246
|
+
*/
|
|
209
247
|
export function applyLocations(content: string, locations: HunkLocation[], reverse = false): { content: string; stale: number[] } {
|
|
210
248
|
const stale = locations.filter((location) => !location.matched).map((location) => location.idx)
|
|
211
|
-
let out = content
|
|
212
249
|
const matched = locations.filter((location) => location.matched).sort((a, b) => b.start - a.start)
|
|
250
|
+
const parts: string[] = []
|
|
251
|
+
let end = content.length
|
|
213
252
|
for (const location of matched) {
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
253
|
+
if (location.start < 0 || location.end > end) {
|
|
254
|
+
let out = content
|
|
255
|
+
for (const item of matched) {
|
|
256
|
+
const next = replaceRange(out, item, reverse)
|
|
257
|
+
if (next === null) stale.push(item.idx)
|
|
258
|
+
else out = next
|
|
259
|
+
}
|
|
260
|
+
return { content: out, stale: stale.sort((a, b) => a - b) }
|
|
261
|
+
}
|
|
262
|
+
parts.push(content.slice(location.end, end), reverse ? (location.hunk.oldText ?? '') : location.hunk.newText)
|
|
263
|
+
end = location.start
|
|
217
264
|
}
|
|
218
|
-
|
|
265
|
+
parts.push(content.slice(0, end))
|
|
266
|
+
parts.reverse()
|
|
267
|
+
return { content: parts.join(''), stale: stale.sort((a, b) => a - b) }
|
|
219
268
|
}
|
package/src/shared/rpc.ts
CHANGED
|
@@ -275,7 +275,12 @@ export interface PerfMoveFailure {
|
|
|
275
275
|
export interface SidecarPerfSummary {
|
|
276
276
|
active: number
|
|
277
277
|
pendingByFile: { path: string; pending: number }[]
|
|
278
|
+
/** 新版 SQLite 主库 + WAL 文件字节数。 */
|
|
278
279
|
archiveBytes: number
|
|
280
|
+
/** 旧 JSON 历史体积;不计入新归档。 */
|
|
281
|
+
legacyArchiveBytes: number
|
|
282
|
+
/** 旧活跃 JSON 只读快照体积。 */
|
|
283
|
+
legacyActiveBytes: number
|
|
279
284
|
}
|
|
280
285
|
// --endregion
|
|
281
286
|
|
|
@@ -362,7 +367,8 @@ export interface RpcRequestMap {
|
|
|
362
367
|
'edrv.save': { sessionId?: string; path: string; content: string; rev?: string }
|
|
363
368
|
'edrv.saveBinary': { sessionId?: string; path: string; content: string; encoding: 'base64' }
|
|
364
369
|
'edrv.archiveList': { sessionId?: string }
|
|
365
|
-
'edrv.
|
|
370
|
+
'edrv.archivePage': { sessionId?: string; cursor?: string; limit?: number }
|
|
371
|
+
'edrv.archiveRead': { sessionId?: string; path?: string; batch?: number | null }
|
|
366
372
|
'edrv.rollback': { sessionId?: string; path: string; batch?: number }
|
|
367
373
|
'edrv.debug': { sessionId?: string; text: string; level?: LoggerLevel }
|
|
368
374
|
'edrv.dlog.list': { sessionId?: string }
|
|
@@ -520,6 +526,7 @@ export interface RpcOkMap {
|
|
|
520
526
|
'edrv.save': { rev?: string; conflict?: boolean }
|
|
521
527
|
'edrv.saveBinary': object
|
|
522
528
|
'edrv.archiveList': { entries: ArchiveEntry[] }
|
|
529
|
+
'edrv.archivePage': { entries: ArchiveEntry[]; nextCursor: string | null }
|
|
523
530
|
'edrv.archiveRead': { batches: ArchiveBatch[] }
|
|
524
531
|
'edrv.rollback': { path: string; batch: number | null }
|
|
525
532
|
'edrv.debug': object
|