dsh-vscode-mode 0.14.2 → 0.14.4
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 +12 -9
- package/lib/client.js +906 -395
- package/lib/client.js.map +1 -1
- package/lib/index.js +1594 -607
- package/lib/index.js.map +1 -1
- package/package.json +7 -7
- package/skills/dsh-vscodemode-mcp/SKILL.md +9 -3
- package/src/archiveDb.ts +476 -0
- package/src/capture.ts +8 -1
- package/src/client/diffDockStore.ts +18 -0
- package/src/client/mcpForm.ts +79 -0
- package/src/client/mcpState.ts +179 -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/styles/mcp.css +6 -3
- 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/client/ui/McpSettings.ts +174 -137
- package/src/dshVersion.ts +4 -3
- package/src/index.ts +2 -0
- package/src/mcp.ts +131 -23
- package/src/mcpIsolation.ts +2 -1
- package/src/mcpProject.ts +74 -14
- package/src/mcpRuntime.ts +18 -3
- package/src/paths.ts +11 -0
- package/src/perf.ts +17 -24
- package/src/revert.ts +93 -1
- package/src/rpc.ts +243 -68
- package/src/shared/diff.ts +69 -20
- package/src/shared/mcpStatus.ts +30 -0
- package/src/shared/rpc.ts +10 -1
- package/src/store.ts +154 -55
package/src/revert.ts
CHANGED
|
@@ -7,6 +7,7 @@ import type { DiffRecord } from './shared/types.js'
|
|
|
7
7
|
import type { Ctx, Session } from './store.js'
|
|
8
8
|
import { policyOf, resolveTarget } from './store.js'
|
|
9
9
|
import { applyLocations, locateHunks, preciseHunk } from './shared/diff.js'
|
|
10
|
+
import { debugRecord } from './debugLog.js'
|
|
10
11
|
|
|
11
12
|
/** 回滚结果;stale=true 表示该 hunk 的新文本已不在文件中(无可回滚内容,不算失败)。 */
|
|
12
13
|
export type Result = { ok: true } | { ok: false; error: string; stale?: boolean }
|
|
@@ -82,25 +83,116 @@ export async function revertCall(ctx: Ctx, session: Session, record: DiffRecord)
|
|
|
82
83
|
}
|
|
83
84
|
|
|
84
85
|
/** 单 hunk 回滚:用 callHunk(edit 单 hunk 精确串)经 fs.editText 反替换。 */
|
|
85
|
-
export async function revertHunk(ctx: Ctx, session: Session, record: DiffRecord, idx: number): Promise<Result> {
|
|
86
|
+
export async function revertHunk(ctx: Ctx, session: Session, record: DiffRecord, idx: number, traceId?: string): Promise<Result> {
|
|
86
87
|
const fs = ctx.get('fs')
|
|
87
88
|
if (!fs) return { ok: false, error: '回滚不可用:缺少 fs' }
|
|
88
89
|
const precise = preciseHunk(record, idx)
|
|
89
90
|
if (!precise) return { ok: false, error: '找不到该差异块' }
|
|
91
|
+
// #region debug log
|
|
92
|
+
const started = traceId ? performance.now() : 0
|
|
93
|
+
let readMs = 0
|
|
94
|
+
let locateMs = 0
|
|
95
|
+
let stage = 'resolve'
|
|
96
|
+
// #endregion
|
|
90
97
|
try {
|
|
91
98
|
const target = await resolveTarget(ctx, session, record.path)
|
|
99
|
+
stage = 'stat'
|
|
92
100
|
const info = await fs.stat(target)
|
|
93
101
|
if (!info || info.type !== 'file') return { ok: false, error: '回滚失败:文件不存在' }
|
|
94
102
|
if ((info.size ?? 0) > 8 * 1024 * 1024) return { ok: false, error: '回滚失败:文件过大,无法安全定位' }
|
|
103
|
+
stage = 'read'
|
|
95
104
|
const content = await fs.readText(target)
|
|
105
|
+
// #region debug log
|
|
106
|
+
if (traceId) readMs = performance.now() - started
|
|
107
|
+
const locateStart = traceId ? performance.now() : 0
|
|
108
|
+
// #endregion
|
|
109
|
+
stage = 'locate'
|
|
96
110
|
const location = locateHunks(content, [precise])[0]
|
|
111
|
+
// #region debug log
|
|
112
|
+
if (traceId) locateMs = performance.now() - locateStart
|
|
113
|
+
// #endregion
|
|
97
114
|
// 新文本已不在文件中(被后续修改覆盖/撤销):不采纳的目标已达成,标记 stale 供调用方按"已回滚"记录决策
|
|
98
115
|
if (!location?.matched) return { ok: false, stale: true, error: '回滚失败:该区域在文件中已不存在(可能已被后续修改覆盖)' }
|
|
99
116
|
const result = applyLocations(content, [location], true)
|
|
117
|
+
stage = 'write'
|
|
100
118
|
await fs.writeText(target, result.content, void 0, void 0, policyOf(ctx, session))
|
|
101
119
|
return { ok: true }
|
|
102
120
|
} catch (error) {
|
|
103
121
|
return { ok: false, error: '回滚失败:该区域可能已被后续修改影响(' + String(error) + ')' }
|
|
122
|
+
} finally {
|
|
123
|
+
// #region debug log
|
|
124
|
+
if (traceId) {
|
|
125
|
+
try { debugRecord(ctx, session.header?.cwd ?? '', `[DEBUG diffRevert] trace=${traceId} mode=single stage=${stage} readMs=${readMs.toFixed(1)} locateMs=${locateMs.toFixed(1)} totalMs=${(performance.now() - started).toFixed(1)}`) } catch { /* 诊断不可影响回滚 */ }
|
|
126
|
+
}
|
|
127
|
+
// #endregion
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* 同一记录的连续 hunk 在内存里逐项回滚,只对文件读写各一次。
|
|
133
|
+
* 不替代单条回滚;调用方只传确定同路径且非远端的连续项。
|
|
134
|
+
* @author ddj 2026年09月29号
|
|
135
|
+
* @param ctx DSH 上下文
|
|
136
|
+
* @param session 会话
|
|
137
|
+
* @param record 差异记录
|
|
138
|
+
* @param indices 按决策顺序排列的 hunk 索引
|
|
139
|
+
* @param traceId 可选的诊断追踪号,仅开启诊断时传入
|
|
140
|
+
* @returns 每个 hunk 对应的独立执行结果
|
|
141
|
+
*/
|
|
142
|
+
export async function revertHunks(ctx: Ctx, session: Session, record: DiffRecord, indices: number[], traceId?: string): Promise<Result[]> {
|
|
143
|
+
const fs = ctx.get('fs')
|
|
144
|
+
if (!fs) return indices.map(() => ({ ok: false, error: '回滚不可用:缺少 fs' }))
|
|
145
|
+
const failAll = (error: string): Result[] => indices.map((idx) => preciseHunk(record, idx)
|
|
146
|
+
? { ok: false, error } : { ok: false, error: '找不到该差异块' })
|
|
147
|
+
try {
|
|
148
|
+
// #region debug log
|
|
149
|
+
const started = traceId ? performance.now() : 0
|
|
150
|
+
// #endregion
|
|
151
|
+
const target = await resolveTarget(ctx, session, record.path)
|
|
152
|
+
const info = await fs.stat(target)
|
|
153
|
+
if (!info || info.type !== 'file') return failAll('回滚失败:文件不存在')
|
|
154
|
+
if ((info.size ?? 0) > 8 * 1024 * 1024) return failAll('回滚失败:文件过大,无法安全定位')
|
|
155
|
+
let content = await fs.readText(target)
|
|
156
|
+
// #region debug log
|
|
157
|
+
const readMs = traceId ? performance.now() - started : 0
|
|
158
|
+
const locateStart = traceId ? performance.now() : 0
|
|
159
|
+
// #endregion
|
|
160
|
+
let size = info.size ?? Buffer.byteLength(content)
|
|
161
|
+
const results: Result[] = []
|
|
162
|
+
for (const idx of indices) {
|
|
163
|
+
const hunk = preciseHunk(record, idx)
|
|
164
|
+
if (!hunk) { results.push({ ok: false, error: '找不到该差异块' }); continue }
|
|
165
|
+
if (size > 8 * 1024 * 1024) {
|
|
166
|
+
results.push({ ok: false, error: '回滚失败:文件过大,无法安全定位' })
|
|
167
|
+
continue
|
|
168
|
+
}
|
|
169
|
+
const location = locateHunks(content, [hunk])[0]
|
|
170
|
+
if (!location?.matched) {
|
|
171
|
+
results.push({ ok: false, stale: true, error: '回滚失败:该区域在文件中已不存在(可能已被后续修改覆盖)' })
|
|
172
|
+
continue
|
|
173
|
+
}
|
|
174
|
+
size += Buffer.byteLength(hunk.oldText ?? '') - Buffer.byteLength(content.slice(location.start, location.end))
|
|
175
|
+
content = applyLocations(content, [location], true).content
|
|
176
|
+
results.push({ ok: true })
|
|
177
|
+
}
|
|
178
|
+
// #region debug log
|
|
179
|
+
const locateMs = traceId ? performance.now() - locateStart : 0
|
|
180
|
+
const writeStart = traceId ? performance.now() : 0
|
|
181
|
+
// #endregion
|
|
182
|
+
if (results.some((result) => result.ok)) {
|
|
183
|
+
await fs.writeText(target, content, void 0, void 0, policyOf(ctx, session))
|
|
184
|
+
}
|
|
185
|
+
// #region debug log
|
|
186
|
+
try {
|
|
187
|
+
if (traceId) {
|
|
188
|
+
const totalMs = performance.now() - started
|
|
189
|
+
debugRecord(ctx, session.header?.cwd ?? '', `[DEBUG diffRevert] trace=${traceId} mode=batch hunks=${indices.length} bytes=${info.size ?? 0} readMs=${readMs.toFixed(1)} locateMs=${locateMs.toFixed(1)} writeMs=${(performance.now() - writeStart).toFixed(1)} totalMs=${totalMs.toFixed(1)}`)
|
|
190
|
+
}
|
|
191
|
+
} catch { /* 诊断不可影响回滚 */ }
|
|
192
|
+
// #endregion
|
|
193
|
+
return results
|
|
194
|
+
} catch (error) {
|
|
195
|
+
return failAll('回滚失败:该区域可能已被后续修改影响(' + String(error) + ')')
|
|
104
196
|
}
|
|
105
197
|
}
|
|
106
198
|
|
package/src/rpc.ts
CHANGED
|
@@ -14,26 +14,26 @@ 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'
|
|
36
|
-
import { listProjects, projectRefresh, projectRemove, projectSave, projectToggle } from './mcpProject.js'
|
|
36
|
+
import { listProjects, projectRefresh, projectRemove, projectSave, projectToggle, mcpSnapshot } from './mcpProject.js'
|
|
37
37
|
import { normalizeFileOpenTool, FILE_OPEN_DEFAULT, FILE_OPEN_SETTINGS_NS, sectionOf, updateSection } from './fileOpenSettings.js'
|
|
38
38
|
import { INTEGRATION_BASE_DEFAULT } from './shared/integration.js'
|
|
39
39
|
import { shellMenuRegister, shellMenuRemove, shellMenuStatus } from './integrate.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
|
},
|
|
@@ -991,6 +1165,7 @@ export function buildHandlers(
|
|
|
991
1165
|
return { ok: true, from: r.from, to: r.to }
|
|
992
1166
|
},
|
|
993
1167
|
'mcp.list': async () => ({ ok: true, ...listMcp(ctx) }),
|
|
1168
|
+
'mcp.snapshot': async () => ({ ok: true, ...mcpSnapshot(ctx) }),
|
|
994
1169
|
'mcp.save': async (args) => {
|
|
995
1170
|
try { return { ok: true, server: await saveMcp(ctx, args.config) } }
|
|
996
1171
|
catch (error) { return { ok: false, error: String(error) } }
|