dsh-vscode-mode 0.4.3 → 0.4.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 +18 -1
- package/lib/client.js +681 -26
- package/lib/client.js.map +1 -1
- package/lib/index.js +240 -21
- package/lib/index.js.map +1 -1
- package/package.json +1 -1
- package/src/client/events.ts +11 -0
- package/src/client/sidebar/panels/FileExplorer.ts +47 -0
- package/src/client/ui/EditorView.ts +285 -25
- package/src/client/ui/useFileWatch.ts +214 -0
- package/src/client/watchDecision.ts +274 -0
- package/src/compat.ts +1 -1
- package/src/dshVersion.ts +42 -10
- package/src/fileOpenSettings.ts +26 -2
- package/src/fileVersions.ts +167 -0
- package/src/index.ts +6 -1
- package/src/rpc.ts +50 -7
- package/src/shared/rpc.ts +23 -3
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-vscode-mode host — 文件磁盘新鲜度观察(外部改动检测的 host 半)。
|
|
3
|
+
*
|
|
4
|
+
* 用途:客户端(编辑区)在无法感知外部写盘的前提下会把陈旧缓冲当权威内容,
|
|
5
|
+
* 甚至在防抖自动保存时覆盖外部改动。本模块为客户端提供轻量批查:给一批路径
|
|
6
|
+
* 返回当前磁盘版本令牌(ctx.fs.stat 的 version)+ 类型,客户端与本地基线比对后
|
|
7
|
+
* 决定是否重载/提示/拒绝保存。
|
|
8
|
+
*
|
|
9
|
+
* 附带职责(文件树同步):对每次都变化的路径调用 treeIndex.invalidateIndex,
|
|
10
|
+
* 使「外部新增/删除文件」不再只依赖 60s TTL——已展开目录在下次展开时即为最新。
|
|
11
|
+
*
|
|
12
|
+
* 设计取舍:
|
|
13
|
+
* - 不注册 fs.watch:句柄占用、网络盘/长路径兼容性差且难单测;批 stat 由系统
|
|
14
|
+
* 缓存兜底,1–20 条/轮的代价可忽略,因此观察逻辑全部落在本模块且可确定性测试。
|
|
15
|
+
* - 基准版本表(path → version)只用于「是否变化」判定,不参与客户端语义;
|
|
16
|
+
* 每条记忆带 TTL(MEM_TTL_MS),超过即视为陈旧重新记基准;总量有上限兜底。
|
|
17
|
+
* - 单条失败不影响整批:解析失败/不存在以 type='missing' 返回,响应始终 ok。
|
|
18
|
+
*
|
|
19
|
+
* 作者 ddj 2026-09-15
|
|
20
|
+
*/
|
|
21
|
+
import type { FileVersionItem } from './shared/rpc.js'
|
|
22
|
+
import type { Ctx } from './store.js'
|
|
23
|
+
import { cwdOf, sessionOf } from './registry.js'
|
|
24
|
+
import { invalidateIndex } from './treeIndex.js'
|
|
25
|
+
import { log } from './log.js'
|
|
26
|
+
|
|
27
|
+
// --region 常量
|
|
28
|
+
|
|
29
|
+
/** 基准版本记忆上限:超出按写入序逐出最旧(防长时间运行无界增长)。 */
|
|
30
|
+
export const MEM_MAX = 500
|
|
31
|
+
/** 基准版本记忆新鲜期:超过即视为陈旧(重记基准,不报一次假变化)。 */
|
|
32
|
+
export const MEM_TTL_MS = 5 * 60_000
|
|
33
|
+
/** 批量路径上限:超出只处理前 N 条(客户端只发已打开页签,正常远小于此值)。 */
|
|
34
|
+
export const MAX_PATHS = 200
|
|
35
|
+
|
|
36
|
+
// --endregion
|
|
37
|
+
|
|
38
|
+
// --region 类型
|
|
39
|
+
|
|
40
|
+
/** 版本观察器实例。 */
|
|
41
|
+
export interface FileVersions {
|
|
42
|
+
/**
|
|
43
|
+
* 批查路径的磁盘版本。
|
|
44
|
+
* @param args 会话 id 与路径列表
|
|
45
|
+
*/
|
|
46
|
+
versions(args: { sessionId?: string; paths: string[] }): Promise<{ ok: true; items: FileVersionItem[] }>
|
|
47
|
+
/** 卸载收尾:清空基准表(会话销毁/插件卸载调用)。 */
|
|
48
|
+
dispose(): void
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** 一条基准记忆(版本 + 记录时间)。 */
|
|
52
|
+
interface Memo {
|
|
53
|
+
version: string
|
|
54
|
+
at: number
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// --endregion
|
|
58
|
+
|
|
59
|
+
// --region 基准表
|
|
60
|
+
|
|
61
|
+
/** 基准表键:工作区根 + 归一化路径。 */
|
|
62
|
+
function memoKey(cwd: string, path: string): string {
|
|
63
|
+
return String(cwd).replace(/\\/g, '/') + '\u0000' + String(path).replace(/\\/g, '/')
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** 清理超期/超量记忆(每次写入前调用,摊还 O(1))。 */
|
|
67
|
+
function sweep(memos: Map<string, Memo>, now: number): void {
|
|
68
|
+
if (memos.size === 0) return
|
|
69
|
+
for (const [key, memo] of memos) {
|
|
70
|
+
if (now - memo.at > MEM_TTL_MS) memos.delete(key)
|
|
71
|
+
}
|
|
72
|
+
while (memos.size > MEM_MAX) {
|
|
73
|
+
const oldest = memos.keys().next().value
|
|
74
|
+
if (oldest === undefined) break
|
|
75
|
+
memos.delete(oldest)
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// --endregion
|
|
80
|
+
|
|
81
|
+
// --region 观察器
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* 创建文件版本观察器(host 单例,随插件装配创建、卸载 dispose)。
|
|
85
|
+
* @author ddj 2026年09月15号
|
|
86
|
+
* @param ctx DSH host 上下文
|
|
87
|
+
* @returns 观察器实例
|
|
88
|
+
*/
|
|
89
|
+
export function createFileVersions(ctx: Ctx): FileVersions {
|
|
90
|
+
/** 基准表:memoKey → 上次观察到的版本(仅用于变化判定与树失效)。 */
|
|
91
|
+
const memos = new Map<string, Memo>()
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* 记录一次观察并按需失效目录树;返回本次是否为「变化」。
|
|
95
|
+
* @author ddj 2026年09月15号
|
|
96
|
+
* @param cwd 工作区根
|
|
97
|
+
* @param path 请求路径
|
|
98
|
+
* @param version 当前版本令牌(空串 = 后端不提供)
|
|
99
|
+
* @param present 磁盘上是否存在
|
|
100
|
+
* @returns 是否检测到变化
|
|
101
|
+
*/
|
|
102
|
+
const syncTree = (cwd: string, path: string, version: string, present: boolean): boolean => {
|
|
103
|
+
const now = Date.now()
|
|
104
|
+
const key = memoKey(cwd, path)
|
|
105
|
+
const prev = memos.get(key)
|
|
106
|
+
sweep(memos, now)
|
|
107
|
+
const changed = prev !== undefined && (prev.version !== version || !present)
|
|
108
|
+
memos.set(key, { version, at: now })
|
|
109
|
+
// 每个路径只在「检测到变化」时落一行(正常编辑频率,不会刷屏):外部改动没跟上时先看这条日志
|
|
110
|
+
if (changed) log.debug('检测到磁盘变化(已失效目录树):' + path)
|
|
111
|
+
if (changed) invalidateIndex(ctx, cwd, path)
|
|
112
|
+
return changed
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* 单条路径观察:解析 → stat → 组装条目(异常一律降级为 missing,不整批失败)。
|
|
117
|
+
* @author ddj 2026年09月15号
|
|
118
|
+
* @param fs host fs 服务
|
|
119
|
+
* @param cwd 工作区根(为空表示会话无工作区,跳过变化判定)
|
|
120
|
+
* @param path 请求路径
|
|
121
|
+
* @returns 版本条目
|
|
122
|
+
*/
|
|
123
|
+
const observe = async (fs: Ctx, cwd: string | null, path: string): Promise<FileVersionItem> => {
|
|
124
|
+
try {
|
|
125
|
+
const target = await fs.resolve(path, cwd ? { cwd } : {})
|
|
126
|
+
const info = await fs.stat(target)
|
|
127
|
+
if (!info || info.type === 'other') {
|
|
128
|
+
if (cwd) syncTree(cwd, path, '', false)
|
|
129
|
+
return { path, version: '', type: 'missing' }
|
|
130
|
+
}
|
|
131
|
+
const version = info.version ? String(info.version) : ''
|
|
132
|
+
if (cwd) syncTree(cwd, path, version, true)
|
|
133
|
+
return { path, version, size: typeof info.size === 'number' ? info.size : undefined, type: info.type }
|
|
134
|
+
} catch (error) {
|
|
135
|
+
if (cwd) syncTree(cwd, path, '', false)
|
|
136
|
+
return { path, version: '', type: 'missing', error: String(error) }
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* 批查入口(RPC handler 直接委托):同一会话的路径共享一次会话解析。
|
|
142
|
+
* @author ddj 2026年09月15号
|
|
143
|
+
* @param args 会话 id 与路径列表
|
|
144
|
+
* @returns 始终 ok 的逐条结果
|
|
145
|
+
*/
|
|
146
|
+
const versions = async (args: { sessionId?: string; paths: string[] }): Promise<{ ok: true; items: FileVersionItem[] }> => {
|
|
147
|
+
const fs = ctx.get('fs')
|
|
148
|
+
const paths = (Array.isArray(args.paths) ? args.paths : [])
|
|
149
|
+
.filter((p): p is string => typeof p === 'string' && p.length > 0)
|
|
150
|
+
.slice(0, MAX_PATHS)
|
|
151
|
+
if (!fs || !paths.length) return { ok: true, items: [] }
|
|
152
|
+
const session = sessionOf(ctx, args.sessionId)
|
|
153
|
+
const cwd = cwdOf(session)
|
|
154
|
+
const items: FileVersionItem[] = []
|
|
155
|
+
for (const path of paths) items.push(await observe(fs, cwd, path))
|
|
156
|
+
return { ok: true, items }
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return {
|
|
160
|
+
versions,
|
|
161
|
+
dispose() {
|
|
162
|
+
memos.clear()
|
|
163
|
+
},
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// --endregion
|
package/src/index.ts
CHANGED
|
@@ -25,6 +25,7 @@ import { createLspManager } from './lsp/manager.js'
|
|
|
25
25
|
import { createLspRpc } from './lsp/rpc.js'
|
|
26
26
|
import { disposeAllServers, hookExitReclaim } from './lsp/transport.js'
|
|
27
27
|
import { createAiRpc } from './ai/rpc.js'
|
|
28
|
+
import { createFileVersions } from './fileVersions.js'
|
|
28
29
|
import { installRulesSection } from './rules.js'
|
|
29
30
|
import { installSkillGroup } from './skills.js'
|
|
30
31
|
import type { RpcHandlerMap } from './shared/rpc.js'
|
|
@@ -65,6 +66,8 @@ export function apply(ctx: Ctx, config?: unknown): void {
|
|
|
65
66
|
/** AI 内联补全 RPC(settings 状态桥接 + llm 惰性获取)。 */
|
|
66
67
|
const aiRpc = createAiRpc({ ctx, settings: openSettings })
|
|
67
68
|
const aiHandlers: Partial<RpcHandlerMap> = aiRpc.handlers
|
|
69
|
+
/** 文件磁盘新鲜度观察器(客户端轮询 edrv.versions;变化时顺带失效目录树缓存)。 */
|
|
70
|
+
const fileVersions = createFileVersions(ctx)
|
|
68
71
|
|
|
69
72
|
ctx.on('tools/result', (exec: unknown, result: unknown) => {
|
|
70
73
|
void captureToolResult(ctx, registry, exec, result)
|
|
@@ -83,7 +86,7 @@ export function apply(ctx: Ctx, config?: unknown): void {
|
|
|
83
86
|
if (typeof sid === 'string') lspRpc.disposeSession(sid)
|
|
84
87
|
})
|
|
85
88
|
|
|
86
|
-
registerRoutes(ctx, config, (method, args) => handleRpc(ctx, registry, method, args, searcher, contentSearcher, lspHandlers, aiHandlers), (warning) => warnings.push(warning))
|
|
89
|
+
registerRoutes(ctx, config, (method, args) => handleRpc(ctx, registry, method, args, searcher, contentSearcher, lspHandlers, aiHandlers, fileVersions), (warning) => warnings.push(warning))
|
|
87
90
|
installIsolation(ctx)
|
|
88
91
|
// 系统集成生命周期:启动自动恢复右键菜单注册(marker 存在时);插件卸载/reload 清理注册痕迹
|
|
89
92
|
ctx.effect(() => shellMenuLifecycle(ctx))
|
|
@@ -95,6 +98,8 @@ export function apply(ctx: Ctx, config?: unknown): void {
|
|
|
95
98
|
void lspManager.disposeAll().catch(() => {})
|
|
96
99
|
disposeAllServers()
|
|
97
100
|
})
|
|
101
|
+
// 卸载时清空文件版本基准表(观察器为模块内单例,不清会跨装配残留陈旧版本)
|
|
102
|
+
ctx.effect(() => () => fileVersions.dispose())
|
|
98
103
|
// 宿主进程退出回收:ctx.effect 清理不覆盖进程退出,缺此注册会留下跨重启的孤儿服务器
|
|
99
104
|
hookExitReclaim()
|
|
100
105
|
|
package/src/rpc.ts
CHANGED
|
@@ -20,7 +20,7 @@ import {
|
|
|
20
20
|
resolveTarget,
|
|
21
21
|
saveBucket,
|
|
22
22
|
} from './store.js'
|
|
23
|
-
import { mkdir, readFile, rm, writeFile, readdir } from 'node:fs/promises'
|
|
23
|
+
import { mkdir, readFile, rm, stat, writeFile, readdir } from 'node:fs/promises'
|
|
24
24
|
import { basename, join } from 'node:path'
|
|
25
25
|
import { archiveEntryFor, markDecision, recordResolved, reconstructOriginal } from './model.js'
|
|
26
26
|
import type { Registry } from './registry.js'
|
|
@@ -48,6 +48,7 @@ import { dshHome } from './paths.js'
|
|
|
48
48
|
import { debugRecord } from './debugLog.js'
|
|
49
49
|
import { markActiveSessions, moveOutSessions, planMoveOut, purgeArchive, restoreSession, scanSessionInventory, sessionsArchiveRoot, sessionSizeOf, sidecarSummaryOf } from './perf.js'
|
|
50
50
|
import { patchHasPerfConfig, patchInsertPerfConfig, patchRemovePerfConfig, perfConfigBlock } from './perfPatch.js'
|
|
51
|
+
import type { FileVersions } from './fileVersions.js'
|
|
51
52
|
|
|
52
53
|
/** cwd → 上次 stale 自动清理时间:全量轮询(EditorView/DiffBadge/Dock 各自 5s)节流,避免每轮都读文件算指纹。 */
|
|
53
54
|
const staleCheckedAt = new Map<string, number>()
|
|
@@ -105,6 +106,32 @@ function snippetTargetOf(path: string): string | null {
|
|
|
105
106
|
return isSnippetFilePath(path) ? path.replace(/\\/g, '/') : null
|
|
106
107
|
}
|
|
107
108
|
|
|
109
|
+
/**
|
|
110
|
+
* 本地文件系统版本令牌(mtime+size):与 ctx.fs 的不透明版本串用途相同,
|
|
111
|
+
* 仅供全局片段文件(走 node:fs 直读、不经 ctx.fs)在 edrv.read 时回带。
|
|
112
|
+
* @author ddj 2026年09月15号
|
|
113
|
+
* @param info node fs.Stats(stat 失败传 null)
|
|
114
|
+
* @returns 版本令牌;取不到 → 空串(不启用护栏)
|
|
115
|
+
*/
|
|
116
|
+
function fileVersionOf(info: { mtimeMs?: number; size?: number } | null): string {
|
|
117
|
+
if (!info || typeof info.mtimeMs !== 'number' || typeof info.size !== 'number') return ''
|
|
118
|
+
return info.mtimeMs + ':' + info.size
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* 判定错误是否为「读取后文件已被外部修改」(版本守卫拒绝写入)。
|
|
123
|
+
* 不依赖 host fs 的 FsError 类(外置模块,instanceof 不可靠),按错误码与文案双重判定。
|
|
124
|
+
* @author ddj 2026年09月15号
|
|
125
|
+
* @param error 捕获到的异常
|
|
126
|
+
* @returns 是否为版本冲突
|
|
127
|
+
*/
|
|
128
|
+
function isStaleVersion(error: unknown): boolean {
|
|
129
|
+
const err = error as { code?: unknown; message?: unknown } | null
|
|
130
|
+
if (err && err.code === 'FS_STALE_VERSION') return true
|
|
131
|
+
const message = err && typeof err.message === 'string' ? err.message : String(error)
|
|
132
|
+
return message.includes('changed since it was read')
|
|
133
|
+
}
|
|
134
|
+
|
|
108
135
|
/**
|
|
109
136
|
* 手动保存收尾(edrv.save / edrv.saveBinary 共用):目录树失效 + 该路径 pending
|
|
110
137
|
* 文本差异记录标记 superseded 并归档(旧文本 diff 不得再应用到新内容上)。
|
|
@@ -227,6 +254,7 @@ export function buildHandlers(
|
|
|
227
254
|
contentSearcher = newContentSearcher(ctx),
|
|
228
255
|
lspHandlers?: Partial<RpcHandlerMap>,
|
|
229
256
|
aiHandlers?: Partial<RpcHandlerMap>,
|
|
257
|
+
fileVersions?: FileVersions,
|
|
230
258
|
): RpcHandlerMap {
|
|
231
259
|
return {
|
|
232
260
|
// edrv.lsp.* 由 createLspRpc 一次性提供(tracker 跨请求保留),这里并入。
|
|
@@ -289,7 +317,9 @@ export function buildHandlers(
|
|
|
289
317
|
if (snippetTarget && args.encoding !== 'base64') {
|
|
290
318
|
try {
|
|
291
319
|
const content = await readFile(snippetTarget, 'utf8')
|
|
292
|
-
|
|
320
|
+
// 版本令牌随读带回:客户端据此判断「读后是否被外部改过」(片段文件同理)
|
|
321
|
+
const info = await stat(snippetTarget).catch(() => null)
|
|
322
|
+
return { ok: true, content, size: content.length, version: fileVersionOf(info) }
|
|
293
323
|
} catch (error) {
|
|
294
324
|
return { ok: false, error: '读取片段文件失败:' + String(error), resolvedPath: snippetTarget }
|
|
295
325
|
}
|
|
@@ -310,15 +340,20 @@ export function buildHandlers(
|
|
|
310
340
|
// 图片/PDF 等二进制预览:readBytes 无解码、无二进制拒绝;超上限已由上方 stat 拦截
|
|
311
341
|
const bytes = await fs.readBytes(target, undefined, BINARY_READ_CAP)
|
|
312
342
|
const content = Buffer.from(bytes).toString('base64')
|
|
313
|
-
return { ok: true, content, size: bytes.byteLength, encoding: 'base64', mime: binaryMimeOf(args.path) }
|
|
343
|
+
return { ok: true, content, size: bytes.byteLength, encoding: 'base64', mime: binaryMimeOf(args.path), version: String(info.version ?? '') }
|
|
314
344
|
}
|
|
315
345
|
if ((info.size ?? 0) > READ_CAP) return { ok: false, error: '文件过大(>8MB),不支持整文件预览' }
|
|
316
346
|
const content = await fs.readText(target)
|
|
317
|
-
return { ok: true, content, size: content.length }
|
|
347
|
+
return { ok: true, content, size: content.length, version: String(info.version ?? '') }
|
|
318
348
|
} catch (error) {
|
|
319
349
|
return { ok: false, error: '读取失败:' + String(error) }
|
|
320
350
|
}
|
|
321
351
|
},
|
|
352
|
+
'edrv.versions': async (args) => {
|
|
353
|
+
// 外部改动检测:批量返回已打开文件的磁盘版本(观察器同时失效变化的目录树)
|
|
354
|
+
if (!fileVersions) return { ok: false, error: '文件版本观察器未装配' }
|
|
355
|
+
return fileVersions.versions(args)
|
|
356
|
+
},
|
|
322
357
|
'edrv.original': async (args) => {
|
|
323
358
|
// 重建"本批次修改前"内容:DiffEditor 原始侧。仅反解 pending 块;
|
|
324
359
|
// 全部反解失败时回退到最早记录的整体 before(若存在)。
|
|
@@ -361,10 +396,17 @@ export function buildHandlers(
|
|
|
361
396
|
if (!fs) return { ok: false, error: '缺少 fs' }
|
|
362
397
|
try {
|
|
363
398
|
const target = await resolveTarget(ctx, sc.session, args.path)
|
|
364
|
-
|
|
399
|
+
// 版本守卫(客户端带回上次读取的版本):文件在读取后被外部改过则拒绝写入,
|
|
400
|
+
// 避免用陈旧缓冲静默覆盖外部改动;客户端未带 rev(旧版/无版本后端)时行为不变。
|
|
401
|
+
const rev = typeof args.rev === 'string' && args.rev ? args.rev : null
|
|
402
|
+
const intent = rev ? { kind: 'replaceIfVersion', version: rev } : undefined
|
|
403
|
+
const outcome = await fs.writeText(target, args.content, intent, void 0, policyOf(ctx, sc.session))
|
|
365
404
|
await afterManualSave(ctx, registry, sc, args.path)
|
|
366
|
-
return { ok: true }
|
|
405
|
+
return { ok: true, rev: outcome?.version ? String(outcome.version) : rev ?? undefined }
|
|
367
406
|
} catch (error) {
|
|
407
|
+
if (isStaleVersion(error)) {
|
|
408
|
+
return { ok: false, conflict: true, error: '文件已被外部修改,请先重新加载(或用编辑器内容覆盖)' }
|
|
409
|
+
}
|
|
368
410
|
return { ok: false, error: '保存失败:' + String(error) }
|
|
369
411
|
}
|
|
370
412
|
},
|
|
@@ -871,8 +913,9 @@ export async function handleRpc<M extends RpcMethod>(
|
|
|
871
913
|
contentSearcher = newContentSearcher(ctx),
|
|
872
914
|
lspHandlers?: Partial<RpcHandlerMap>,
|
|
873
915
|
aiHandlers?: Partial<RpcHandlerMap>,
|
|
916
|
+
fileVersions?: FileVersions,
|
|
874
917
|
): Promise<RpcResult<M>> {
|
|
875
|
-
const handlers = buildHandlers(ctx, registry, searcher, contentSearcher, lspHandlers, aiHandlers)
|
|
918
|
+
const handlers = buildHandlers(ctx, registry, searcher, contentSearcher, lspHandlers, aiHandlers, fileVersions)
|
|
876
919
|
const handler = handlers[method]
|
|
877
920
|
if (!handler) return { ok: false, error: '未知方法: ' + String(method) } as RpcResult<M>
|
|
878
921
|
return handler(args)
|
package/src/shared/rpc.ts
CHANGED
|
@@ -137,6 +137,24 @@ export interface SidecarPerfSummary {
|
|
|
137
137
|
}
|
|
138
138
|
// --endregion
|
|
139
139
|
|
|
140
|
+
/**
|
|
141
|
+
* 单个路径的磁盘新鲜度(edrv.versions 逐条结果)。
|
|
142
|
+
* version 为 host fs 的不透明新鲜度令牌(本地后端 = dev:ino:size:mtimeNs:ctimeNs):
|
|
143
|
+
* 客户端只做字符串相等比较判「是否变化」,不解析其内部结构。
|
|
144
|
+
*/
|
|
145
|
+
export interface FileVersionItem {
|
|
146
|
+
/** 请求时的原始路径(原样回带,客户端按此对齐基线)。 */
|
|
147
|
+
path: string
|
|
148
|
+
/** 当前磁盘版本令牌;空串 = 该后端不提供版本,调用方应停用护栏。 */
|
|
149
|
+
version: string
|
|
150
|
+
/** 文件字节数(后端可报时)。 */
|
|
151
|
+
size?: number
|
|
152
|
+
/** 目标类型;'missing' = 已不存在(外部删除或路径解析失败)。 */
|
|
153
|
+
type?: 'file' | 'directory' | 'other' | 'missing'
|
|
154
|
+
/** 单条失败原因(仅诊断用;解析失败/缺失也可能只体现在 type='missing')。 */
|
|
155
|
+
error?: string
|
|
156
|
+
}
|
|
157
|
+
|
|
140
158
|
/** 一条批量决策项(decideBatch 的 items 元素,与 accept/reject 单条参数同构)。 */
|
|
141
159
|
export interface DecideItem {
|
|
142
160
|
callId: string
|
|
@@ -162,8 +180,9 @@ export interface RpcRequestMap {
|
|
|
162
180
|
'edrv.reject': { sessionId?: string; callId: string; scope?: RpcScope; hunkIndex?: number }
|
|
163
181
|
'edrv.decideBatch': { sessionId?: string; items: DecideItem[] }
|
|
164
182
|
'edrv.read': { sessionId?: string; path: string; encoding?: 'base64' }
|
|
183
|
+
'edrv.versions': { sessionId?: string; paths: string[] }
|
|
165
184
|
'edrv.original': { sessionId?: string; path: string }
|
|
166
|
-
'edrv.save': { sessionId?: string; path: string; content: string }
|
|
185
|
+
'edrv.save': { sessionId?: string; path: string; content: string; rev?: string }
|
|
167
186
|
'edrv.saveBinary': { sessionId?: string; path: string; content: string; encoding: 'base64' }
|
|
168
187
|
'edrv.archiveList': { sessionId?: string }
|
|
169
188
|
'edrv.archiveRead': { sessionId?: string; path?: string }
|
|
@@ -254,9 +273,10 @@ export interface RpcOkMap {
|
|
|
254
273
|
'edrv.accept': { record: RecordView }
|
|
255
274
|
'edrv.reject': { record: RecordView }
|
|
256
275
|
'edrv.decideBatch': { results: DecideResult[] }
|
|
257
|
-
'edrv.read': { content: string; size: number; encoding?: 'base64'; mime?: string }
|
|
276
|
+
'edrv.read': { content: string; size: number; encoding?: 'base64'; mime?: string; version?: string }
|
|
277
|
+
'edrv.versions': { items: FileVersionItem[] }
|
|
258
278
|
'edrv.original': { content: string; size: number; stale: StaleHunk[]; fallback: boolean }
|
|
259
|
-
'edrv.save':
|
|
279
|
+
'edrv.save': { rev?: string; conflict?: boolean }
|
|
260
280
|
'edrv.saveBinary': object
|
|
261
281
|
'edrv.archiveList': { entries: ArchiveEntry[] }
|
|
262
282
|
'edrv.archiveRead': { batches: ArchiveBatch[] }
|