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/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
|
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { MpcServer } from './mcp.js'
|
|
2
|
+
|
|
3
|
+
type ServerStatus = Pick<MpcServer, 'enabled' | 'status'>
|
|
4
|
+
export type McpTone = 'connected' | 'connecting' | 'disabled' | 'error' | 'neutral'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @public
|
|
8
|
+
* @author ddj 2026年10月08号
|
|
9
|
+
* @description Display lifecycle evidence; enabled only expresses configuration intent, not connectivity.
|
|
10
|
+
* @param server Server lifecycle snapshot.
|
|
11
|
+
* @returns User-facing status label.
|
|
12
|
+
*/
|
|
13
|
+
export function statusOf(server: ServerStatus): string {
|
|
14
|
+
if (!server.enabled || server.status === 'disabled') return '已禁用'
|
|
15
|
+
const labels = { connected: '在线', connecting: '装配中', configured: '已配置,等待会话', unverified: '已加载,连接未确认', error: '错误' }
|
|
16
|
+
return labels[server.status] ?? '状态未知'
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* @public
|
|
21
|
+
* @author ddj 2026年10月08号
|
|
22
|
+
* @description Use neutral styling unless lifecycle evidence explicitly identifies online, loading or error.
|
|
23
|
+
* @param server Server lifecycle snapshot.
|
|
24
|
+
* @returns CSS tone, independent of configuration-enabled state.
|
|
25
|
+
*/
|
|
26
|
+
export function statusTone(server: ServerStatus): McpTone {
|
|
27
|
+
if (!server.enabled || server.status === 'disabled') return 'disabled'
|
|
28
|
+
if (server.status === 'connected' || server.status === 'connecting' || server.status === 'error') return server.status
|
|
29
|
+
return 'neutral'
|
|
30
|
+
}
|
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 }
|
|
@@ -380,6 +386,7 @@ export interface RpcRequestMap {
|
|
|
380
386
|
'edrv.fsCopy': { sessionId?: string; from: string; toDir: string }
|
|
381
387
|
'edrv.fsMove': { sessionId?: string; from: string; toDir: string }
|
|
382
388
|
'mcp.list': {}
|
|
389
|
+
'mcp.snapshot': {}
|
|
383
390
|
'mcp.save': { config: MpcConfig }
|
|
384
391
|
'mcp.remove': { id: string }
|
|
385
392
|
'mcp.toggle': { id: string; enabled: boolean }
|
|
@@ -520,6 +527,7 @@ export interface RpcOkMap {
|
|
|
520
527
|
'edrv.save': { rev?: string; conflict?: boolean }
|
|
521
528
|
'edrv.saveBinary': object
|
|
522
529
|
'edrv.archiveList': { entries: ArchiveEntry[] }
|
|
530
|
+
'edrv.archivePage': { entries: ArchiveEntry[]; nextCursor: string | null }
|
|
523
531
|
'edrv.archiveRead': { batches: ArchiveBatch[] }
|
|
524
532
|
'edrv.rollback': { path: string; batch: number | null }
|
|
525
533
|
'edrv.debug': object
|
|
@@ -538,6 +546,7 @@ export interface RpcOkMap {
|
|
|
538
546
|
'edrv.fsCopy': { from: string; to: string }
|
|
539
547
|
'edrv.fsMove': { from: string; to: string }
|
|
540
548
|
'mcp.list': { servers: MpcServer[] }
|
|
549
|
+
'mcp.snapshot': { servers: MpcServer[]; projects: MpcProject[] }
|
|
541
550
|
'mcp.save': { server: MpcServer }
|
|
542
551
|
'mcp.remove': object
|
|
543
552
|
'mcp.toggle': { server: MpcServer }
|
package/src/store.ts
CHANGED
|
@@ -3,17 +3,21 @@
|
|
|
3
3
|
* 迁移自原 src/index.ts 的 fs/ctx IO 部分,语义一字不改。
|
|
4
4
|
* 作者 ddj 2026-08-20
|
|
5
5
|
*/
|
|
6
|
-
import type { ArchiveBatch,
|
|
6
|
+
import type { ArchiveBatch, DiffRecord, SidecarData } from './shared/types.js'
|
|
7
7
|
import type { ReadState } from './shared/diff.js'
|
|
8
8
|
import { fingerprint, isNoopHunk, locateHunks, normalizeForCompare, normalizeHunk, preciseHunk } from './shared/diff.js'
|
|
9
|
-
import { archiveEntryFor,
|
|
9
|
+
import { archiveEntryFor, normalizeRecord } from './model.js'
|
|
10
10
|
import { SIDECAR, SIDECAR_ARCHIVE } from './paths.js'
|
|
11
11
|
import { log } from './log.js'
|
|
12
|
+
import { debugRecord } from './debugLog.js'
|
|
13
|
+
import { activeReady, appendArchive, checkActiveSource, commitActive, committedRecord, migrateActive, restoreReverts, type ActiveChange } from './archiveDb.js'
|
|
12
14
|
|
|
13
15
|
export { SIDECAR, SIDECAR_ARCHIVE }
|
|
14
16
|
export const READ_CAP = 8 * 1024 * 1024
|
|
15
17
|
/** 二进制预览/写回上限(PDF 等整文件 base64 传输;文本侧仍用 READ_CAP)。 */
|
|
16
18
|
export const BINARY_READ_CAP = 32 * 1024 * 1024
|
|
19
|
+
/** 旧文件首次全量指纹核验后,每次操作优先检查 DSH fs stat 身份。 */
|
|
20
|
+
const sourceStats = new Map<string, string>()
|
|
17
21
|
|
|
18
22
|
/** DSH 上下文与会话类型较宽松:本地无 dsh 类型声明,显式 any + 文档约束。 */
|
|
19
23
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
@@ -76,33 +80,88 @@ export function parseSidecar(text: string | null): SidecarData | null {
|
|
|
76
80
|
}
|
|
77
81
|
}
|
|
78
82
|
|
|
79
|
-
/**
|
|
83
|
+
/**
|
|
84
|
+
* 严格读取旧 sidecar:仅确认确实不存在时才允许从空记录迁移。
|
|
85
|
+
* @author ddj 2026年09月30号
|
|
86
|
+
* @param ctx DSH 上下文
|
|
87
|
+
* @param cwd 工作区
|
|
88
|
+
* @returns 旧文件内容或确认缺失
|
|
89
|
+
*/
|
|
90
|
+
async function migrationText(ctx: Ctx, cwd: string): Promise<string | null> {
|
|
91
|
+
const fs = ctx.get('fs')
|
|
92
|
+
if (!fs) throw new Error('无法迁移差异记录:缺少 fs')
|
|
93
|
+
const target = await fs.resolve(SIDECAR, { cwd })
|
|
94
|
+
const info = await fs.stat(target)
|
|
95
|
+
if (!info) return null
|
|
96
|
+
if (info.type !== 'file') throw new Error('旧活跃 sidecar 非文件')
|
|
97
|
+
return fs.readText(target)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* 读取旧 sidecar 文件的版本身份;缺少可靠版本令牌时返回 null 走全文核验。
|
|
102
|
+
* @author ddj 2026年09月30号
|
|
103
|
+
* @param ctx DSH 上下文
|
|
104
|
+
* @param cwd 工作区
|
|
105
|
+
* @returns 文件身份或 null
|
|
106
|
+
*/
|
|
107
|
+
async function sourceStat(ctx: Ctx, cwd: string): Promise<string | null> {
|
|
108
|
+
const fs = ctx.get('fs')
|
|
109
|
+
if (!fs) throw new Error('无法核验旧 sidecar:缺少 fs')
|
|
110
|
+
const info = await fs.stat(await fs.resolve(SIDECAR, { cwd }))
|
|
111
|
+
if (!info) return 'absent'
|
|
112
|
+
const version = info.version ?? info.rev
|
|
113
|
+
return typeof version === 'string' && version.length ? version : null
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* 首次读取旧文件全文确认数据库来源,后续只在 stat 版本变动时重新核验。
|
|
118
|
+
* @author ddj 2026年09月30号
|
|
119
|
+
* @param ctx DSH 上下文
|
|
120
|
+
* @param cwd 工作区
|
|
121
|
+
*/
|
|
122
|
+
async function checkSource(ctx: Ctx, cwd: string): Promise<void> {
|
|
123
|
+
const now = await sourceStat(ctx, cwd)
|
|
124
|
+
if (now !== null && sourceStats.get(cwd) === now) return
|
|
125
|
+
checkActiveSource(cwd, await migrationText(ctx, cwd))
|
|
126
|
+
if (now !== null) sourceStats.set(cwd, now)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** 加载某工作区的记录桶(内存 Map;首次在隔离切换窗口严格迁移)。 */
|
|
80
130
|
export async function loadBucket(ctx: Ctx, cwd: string): Promise<Map<string, DiffRecord>> {
|
|
81
|
-
const
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
131
|
+
const source = await migrationText(ctx, cwd)
|
|
132
|
+
const map = await migrateActive(cwd, source, () => migrationText(ctx, cwd))
|
|
133
|
+
await checkSource(ctx, cwd)
|
|
134
|
+
for (const rec of map.values()) {
|
|
135
|
+
if (rec.archived) continue
|
|
136
|
+
const previous = JSON.stringify(rec)
|
|
137
|
+
restoreReverts(cwd, rec)
|
|
138
|
+
const committed = committedRecord(cwd, rec.callId, rec.path, rec.batch ?? null)
|
|
139
|
+
if (committed) {
|
|
140
|
+
rec.decisions = committed.decisions
|
|
141
|
+
rec.superseded = committed.superseded
|
|
142
|
+
rec.archived = true
|
|
88
143
|
}
|
|
144
|
+
if (JSON.stringify(rec) !== previous) commitActive(cwd, { upserts: [rec], removed: [] })
|
|
89
145
|
}
|
|
90
146
|
return map
|
|
91
147
|
}
|
|
92
148
|
|
|
93
|
-
/**
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
149
|
+
/**
|
|
150
|
+
* 仅把明确变动的记录行写入 SQLite;旧 sidecar 不再写入。
|
|
151
|
+
* @author ddj 2026年09月30号
|
|
152
|
+
* @param ctx DSH 上下文
|
|
153
|
+
* @param cwd 工作区
|
|
154
|
+
* @param recsMap 供当前调用点核对的记录桶
|
|
155
|
+
* @param session 会话(保留旧 API)
|
|
156
|
+
* @param changes 明确的更新和删除集合
|
|
157
|
+
*/
|
|
158
|
+
export async function saveBucket(ctx: Ctx, cwd: string, recsMap: Map<string, DiffRecord>, session?: Session, changes?: ActiveChange): Promise<void> {
|
|
159
|
+
if (!changes) throw new Error('增量保存缺少变更集合')
|
|
160
|
+
for (const rec of changes.upserts) if (recsMap.get(rec.callId) !== rec) throw new Error('增量记录不在工作区桶内')
|
|
97
161
|
try {
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
existing.updatedAt = new Date().toISOString()
|
|
102
|
-
await fs.writeText(target, JSON.stringify(existing), void 0, void 0, policyOf(ctx, session))
|
|
103
|
-
} catch (error) {
|
|
104
|
-
log.error('saveBucket failed: ' + String(error))
|
|
105
|
-
}
|
|
162
|
+
await checkSource(ctx, cwd)
|
|
163
|
+
commitActive(cwd, changes)
|
|
164
|
+
} catch (error) { log.error('saveBucket failed: ' + String(error)); throw error }
|
|
106
165
|
}
|
|
107
166
|
|
|
108
167
|
/**
|
|
@@ -172,8 +231,18 @@ export async function autoArchiveStale(ctx: Ctx, session: Session, cwd: string,
|
|
|
172
231
|
if (await recordIsStale(ctx, session, cache, rec)) stale.push(rec)
|
|
173
232
|
}
|
|
174
233
|
if (!stale.length) return 0
|
|
234
|
+
const original = stale.map((rec) => ({ rec, at: rec.at, superseded: rec.superseded }))
|
|
175
235
|
for (const r of stale) { r.superseded = true; r.at = new Date().toISOString() }
|
|
176
|
-
|
|
236
|
+
try {
|
|
237
|
+
await archiveRecords(ctx, session, cwd, bucket, stale, '差异无法定位(已被后续修改覆盖),自动归档')
|
|
238
|
+
} catch (error) {
|
|
239
|
+
for (const item of original) {
|
|
240
|
+
if (item.rec.archived) continue
|
|
241
|
+
item.rec.superseded = item.superseded
|
|
242
|
+
item.rec.at = item.at
|
|
243
|
+
}
|
|
244
|
+
throw error
|
|
245
|
+
}
|
|
177
246
|
return stale.length
|
|
178
247
|
}
|
|
179
248
|
|
|
@@ -198,40 +267,49 @@ export function parseArchive(text: string | null): ArchiveBatch[] {
|
|
|
198
267
|
return []
|
|
199
268
|
}
|
|
200
269
|
|
|
201
|
-
/**
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
270
|
+
/**
|
|
271
|
+
* 追加归档条目(按 cwd+path+batch 合并,同 callId 去重)。
|
|
272
|
+
* @author ddj 2026年09月30号
|
|
273
|
+
* @param ctx DSH 上下文
|
|
274
|
+
* @param cwd 工作区
|
|
275
|
+
* @param entries 待归档批次
|
|
276
|
+
* @param session 会话
|
|
277
|
+
* @param traceId 可选诊断追踪标识;有值时输出分段耗时
|
|
278
|
+
*/
|
|
279
|
+
export async function appendArchiveEntries(ctx: Ctx, cwd: string, entries: Array<ReturnType<typeof archiveEntryFor>>, session?: Session, traceId?: string): Promise<void> {
|
|
280
|
+
if (!cwd || !entries.length) return
|
|
281
|
+
// #region debug log
|
|
282
|
+
const started = traceId ? performance.now() : 0
|
|
283
|
+
// #endregion
|
|
205
284
|
try {
|
|
206
|
-
|
|
207
|
-
const
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
if (
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
for (const rec of e.records) {
|
|
214
|
-
if (!seen.has(String(rec.callId))) {
|
|
215
|
-
exist.records.push(rec as unknown as ArchiveBatch['records'][number])
|
|
216
|
-
seen.add(String(rec.callId))
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
exist.lastAt = e.at
|
|
220
|
-
exist.reason = e.reason
|
|
221
|
-
} else {
|
|
222
|
-
existing.push(e as unknown as ArchiveBatch)
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
const data: ArchiveData = { version: 1, updatedAt: new Date().toISOString(), batches: existing }
|
|
226
|
-
await fs.writeText(target, JSON.stringify(data), void 0, void 0, policyOf(ctx, session))
|
|
285
|
+
await checkSource(ctx, cwd)
|
|
286
|
+
const added = appendArchive(cwd, entries as unknown as ArchiveBatch[])
|
|
287
|
+
// #region debug log
|
|
288
|
+
try {
|
|
289
|
+
if (traceId) debugRecord(ctx, cwd, `[DEBUG diffArchive] trace=${traceId} stage=done mode=sqlite entries=${entries.length} insertedBatches=${added} dbMs=${(performance.now() - started).toFixed(1)} totalMs=${(performance.now() - started).toFixed(1)}`)
|
|
290
|
+
} catch { /* 诊断不可影响归档 */ }
|
|
291
|
+
// #endregion
|
|
227
292
|
} catch (error) {
|
|
293
|
+
// #region debug log
|
|
294
|
+
try {
|
|
295
|
+
if (traceId) debugRecord(ctx, cwd, `[DEBUG diffArchive] trace=${traceId} stage=error mode=sqlite totalMs=${(performance.now() - started).toFixed(1)}`)
|
|
296
|
+
} catch { /* 诊断不可影响归档 */ }
|
|
297
|
+
// #endregion
|
|
228
298
|
log.error('appendArchiveEntries failed: ' + String(error))
|
|
299
|
+
throw error
|
|
229
300
|
}
|
|
230
301
|
}
|
|
231
302
|
|
|
232
303
|
/**
|
|
233
304
|
* 归档记录:标记 archived、按批次写入归档、落盘工作区桶。
|
|
234
|
-
* @
|
|
305
|
+
* @author ddj 2026年09月30号
|
|
306
|
+
* @param ctx DSH 上下文
|
|
307
|
+
* @param session 会话
|
|
308
|
+
* @param cwd 工作区
|
|
309
|
+
* @param bucket 当前工作区记录桶
|
|
310
|
+
* @param recs 待归档记录
|
|
311
|
+
* @param reason 归档原因
|
|
312
|
+
* @param opts deferSave 为 true 时只写归档、不落盘桶;traceId 用于可选分段计时
|
|
235
313
|
*/
|
|
236
314
|
export async function archiveRecords(
|
|
237
315
|
ctx: Ctx,
|
|
@@ -240,13 +318,34 @@ export async function archiveRecords(
|
|
|
240
318
|
bucket: Map<string, DiffRecord>,
|
|
241
319
|
recs: DiffRecord[],
|
|
242
320
|
reason: string,
|
|
243
|
-
opts?: { deferSave?: boolean },
|
|
321
|
+
opts?: { deferSave?: boolean; traceId?: string; changed?: DiffRecord[] },
|
|
244
322
|
): Promise<void> {
|
|
245
323
|
if (!recs.length) return
|
|
246
324
|
const fresh = recs.filter((r) => !r.archived)
|
|
247
|
-
|
|
248
|
-
const
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
325
|
+
const byFile = new Map<string, DiffRecord[]>()
|
|
326
|
+
for (const rec of recs) {
|
|
327
|
+
const key = JSON.stringify([rec.path, Number.isInteger(rec.batch) ? rec.batch : null])
|
|
328
|
+
const list = byFile.get(key) ?? []
|
|
329
|
+
list.push(rec)
|
|
330
|
+
byFile.set(key, list)
|
|
331
|
+
}
|
|
332
|
+
const entries = [...byFile.values()].map((list) => archiveEntryFor(list, cwd, reason))
|
|
333
|
+
const changed = new Map((opts?.changed ?? []).map((rec) => [rec.callId, rec]))
|
|
334
|
+
for (const rec of recs) changed.set(rec.callId, rec)
|
|
335
|
+
const start = opts?.traceId ? performance.now() : 0
|
|
336
|
+
await checkSource(ctx, cwd)
|
|
337
|
+
const original = recs.map((rec) => rec.archived)
|
|
338
|
+
for (const rec of recs) rec.archived = true
|
|
339
|
+
try {
|
|
340
|
+
const added = commitActive(cwd, { upserts: [...changed.values()], removed: [] }, entries as unknown as ArchiveBatch[])
|
|
341
|
+
// #region debug log
|
|
342
|
+
try {
|
|
343
|
+
if (opts?.traceId) debugRecord(ctx, cwd, `[DEBUG diffArchive] trace=${opts.traceId} stage=done mode=sqlite entries=${entries.length} insertedBatches=${added} dbMs=${(performance.now() - start).toFixed(1)} totalMs=${(performance.now() - start).toFixed(1)}`)
|
|
344
|
+
} catch { /* 诊断不可影响归档 */ }
|
|
345
|
+
// #endregion
|
|
346
|
+
} catch (error) {
|
|
347
|
+
recs.forEach((rec, idx) => { rec.archived = original[idx] })
|
|
348
|
+
throw error
|
|
349
|
+
}
|
|
350
|
+
if (fresh.length && !opts?.deferSave) return
|
|
252
351
|
}
|