dsh-vscode-mode 0.22.0 → 0.23.1

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.
@@ -29,93 +29,122 @@ export interface PendingCapture {
29
29
  dispose(): void
30
30
  }
31
31
 
32
+ /** 采集器的进程内状态:同一目标并发写入时只采信最外层那一次。 */
33
+ interface CaptureState {
34
+ busy: Set<string>
35
+ stopped: boolean
36
+ }
37
+
38
+ /** 工具的写工具参数形状;只取本采集关心的字段。 */
39
+ interface WriteExec {
40
+ name?: string
41
+ arguments?: { file_path?: unknown; path?: unknown }
42
+ agent?: { session?: { id?: string; cwd?: string } }
43
+ }
44
+
32
45
  /**
33
- * @author ddj 2026年10月29号
46
+ * @author ddj 2026年10月30号
34
47
  * @description: 安装写文件采集;读写失败只影响待审来源,绝不影响用户工具本身。
35
48
  * @param ctx Host 生命周期
36
49
  * @param sink 已确认写入的接收方
37
50
  * @returns 可释放的采集器
38
51
  */
39
- export function installPendingCapture(ctx: Ctx, sink: (write: PendingWrite) => void): PendingCapture {
40
- const busy = new Set<string>()
41
- let stopped = false
42
- ctx.on('tools/execute', aroundTool)
43
- return { dispose }
52
+ export function installPending(ctx: Ctx, sink: (write: PendingWrite) => void): PendingCapture {
53
+ const state: CaptureState = { busy: new Set<string>(), stopped: false }
54
+ ctx.on('tools/execute', (exec: WriteExec, next: () => Promise<unknown>) => aroundTool(ctx, state, sink, exec, next))
55
+ return {
56
+ dispose(): void {
57
+ state.stopped = true
58
+ state.busy.clear()
59
+ },
60
+ }
61
+ }
62
+
63
+ /**
64
+ * @author ddj 2026年10月30号
65
+ * @description: 判定本次调用是否值得采集;不值得时返回 null,调用方直接透传原始分发。
66
+ * @param ctx Host 生命周期
67
+ * @param state 采集状态
68
+ * @param exec 工具分发上下文
69
+ * @returns 采集计划或 null
70
+ */
71
+ async function planOf(ctx: Ctx, state: CaptureState, exec: WriteExec): Promise<CapturePlan | null> {
72
+ const session = exec.agent?.session
73
+ const requested = exec.arguments?.file_path ?? exec.arguments?.path
74
+ if (state.stopped || !session?.id || typeof requested !== 'string') return null
75
+ if (!WRITE_TOOLS.includes(String(exec.name))) return null
76
+ const cwd = cwdOf(session as never)
77
+ if (!cwd) return null
78
+ const fs = ctx.get('fs')
79
+ const target = await resolveTarget(ctx, session as never, requested)
80
+ const identity = fs.processPath(target)
81
+ if (typeof identity !== 'string' || !identity || state.busy.has(identity)) return null
82
+ const before = await readText(fs, target)
83
+ if (before !== null && before.length > MAX_TEXT) return null
84
+ return { cwd: String(cwd), sessionId: String(session.id), path: requested, identity, before, session: session as never }
85
+ }
86
+
87
+ /** 一次采集所需的固定信息。 */
88
+ interface CapturePlan {
89
+ cwd: string
90
+ sessionId: string
91
+ path: string
92
+ identity: string
93
+ before: string | null
94
+ session: never
95
+ }
44
96
 
45
- /**
46
- * @author ddj 2026年10月29号
47
- * @description: 写工具执行前后各读一次,内容变化才上报。
48
- * @param exec 工具分发上下文
49
- * @param next 原始分发
50
- * @returns 原始工具结果,永不因采集失败而改变
51
- */
52
- async function aroundTool(exec: any, next: () => Promise<any>): Promise<any> {
53
- const session = exec.agent?.session
54
- const path = exec.arguments?.file_path ?? exec.arguments?.path
55
- if (stopped || !session?.id || typeof path !== 'string' || !WRITE_TOOLS.includes(exec.name)) return next()
56
- const cwd = cwdOf(session)
57
- if (!cwd) return next()
58
- let before: string | null = null
59
- let identity: string | null = null
97
+ /**
98
+ * @author ddj 2026年10月30号
99
+ * @description: 写工具执行前后各读一次,内容变化才上报;任何采集异常都不改变工具结果。
100
+ * @param ctx Host 生命周期
101
+ * @param state 采集状态
102
+ * @param sink 已确认写入的接收方
103
+ * @param exec 工具分发上下文
104
+ * @param next 原始分发
105
+ * @returns 原始工具结果
106
+ */
107
+ async function aroundTool(
108
+ ctx: Ctx, state: CaptureState, sink: (write: PendingWrite) => void,
109
+ exec: WriteExec, next: () => Promise<unknown>,
110
+ ): Promise<unknown> {
111
+ let plan: CapturePlan | null = null
112
+ try {
113
+ plan = await planOf(ctx, state, exec)
114
+ } catch (error) {
115
+ log.debug('待审采集跳过:' + String(error))
116
+ }
117
+ if (!plan) return next()
118
+
119
+ state.busy.add(plan.identity)
120
+ try {
121
+ return await next()
122
+ } finally {
123
+ state.busy.delete(plan.identity)
60
124
  try {
61
- const fs = ctx.get('fs')
62
- const target = await resolveTarget(ctx, session, path)
63
- const key = fs.processPath(target)
64
- if (typeof key !== 'string' || !key) return next()
65
- if (busy.has(key)) return next()
66
- before = await readText(fs, target)
67
- if (before !== null && before.length > MAX_TEXT) return next()
68
- identity = key
69
- busy.add(key)
125
+ reportWrite(ctx, sink, plan)
70
126
  } catch (error) {
71
- log.debug('待审采集跳过:' + String(error))
72
- return next()
73
- }
74
- try {
75
- return await next()
76
- } finally {
77
- if (identity !== null) {
78
- const key: string = identity
79
- try { report(String(cwd), String(session.id), String(path), key, before, exec) } catch (error) {
80
- log.debug('待审上报失败:' + String(error))
81
- }
82
- busy.delete(key)
83
- }
127
+ log.debug('待审上报失败:' + String(error))
84
128
  }
85
129
  }
130
+ }
86
131
 
87
- /**
88
- * @author ddj 2026年10月29号
89
- * @description: 写后读回并比较,内容一致视为无净变化不产生条目。
90
- * @param cwd 工作区
91
- * @param sessionId 会话
92
- * @param path 展示路径
93
- * @param identity 目标身份
94
- * @param before 写前内容
95
- * @param exec 工具分发上下文
96
- * @returns 无
97
- */
98
- function report(
99
- cwd: string, sessionId: string, path: string, identity: string, before: string | null, exec: any,
100
- ): void {
101
- const fs = ctx.get('fs')
102
- void resolveTarget(ctx, exec.agent.session, path).then(async (target: any) => {
103
- const after = await readText(fs, target)
104
- if (after !== null && after.length > MAX_TEXT) return
105
- if (before === after) return
106
- sink({ cwd, sessionId, path, identity, before, after, now: Date.now() })
107
- }).catch((error: unknown) => log.debug('待审读回失败:' + String(error)))
108
- }
109
-
110
- /**
111
- * @author ddj 2026年10月29号
112
- * @description: 释放监听。
113
- * @returns 无
114
- */
115
- function dispose(): void {
116
- stopped = true
117
- busy.clear()
118
- }
132
+ /**
133
+ * @author ddj 2026年10月29号
134
+ * @description: 写后读回并比较,内容一致视为无净变化不产生条目。
135
+ * @param ctx Host 生命周期
136
+ * @param sink 已确认写入的接收方
137
+ * @param plan 采集计划
138
+ * @returns 无;读回失败只记日志
139
+ */
140
+ function reportWrite(ctx: Ctx, sink: (write: PendingWrite) => void, plan: CapturePlan): void {
141
+ const fs = ctx.get('fs')
142
+ void resolveTarget(ctx, plan.session, plan.path).then(async (target: unknown) => {
143
+ const after = await readText(fs, target)
144
+ if (after !== null && after.length > MAX_TEXT) return
145
+ if (plan.before === after) return
146
+ sink({ cwd: plan.cwd, sessionId: plan.sessionId, path: plan.path, identity: plan.identity, before: plan.before, after, now: Date.now() })
147
+ }).catch((error: unknown) => log.debug('待审读回失败:' + String(error)))
119
148
  }
120
149
 
121
150
  /**
@@ -76,7 +76,12 @@ CREATE TABLE IF NOT EXISTS pending_history (
76
76
  state TEXT NOT NULL DEFAULT 'applied');
77
77
  `
78
78
 
79
- /** v1 历史表补 state 列(rebuild 成本高于一次 ALTER)。 */
79
+ /**
80
+ * @author ddj 2026年10月29号
81
+ * @description: v1 历史表补 state 列(rebuild 成本高于一次 ALTER)。
82
+ * @param db 已打开的工作区库
83
+ * @returns 无
84
+ */
80
85
  function migrateHistory(db: DatabaseSync): void {
81
86
  const columns = db.prepare('PRAGMA table_info(pending_history)').all() as Row[]
82
87
  if (columns.some((column) => column.name === 'state')) return
@@ -21,7 +21,12 @@ import {
21
21
  readOp, saveBlocks, saveEntry, setHistoryState, writeOp,
22
22
  } from './persist.js'
23
23
 
24
- /** 统计一次差异的增删行数。 */
24
+ /**
25
+ * @author ddj 2026年10月29号
26
+ * @description: 统计一次差异的增删行数;非文本差异统一记 0/0。
27
+ * @param entry 待审条目
28
+ * @returns 增删行数
29
+ */
25
30
  function countsOf(entry: PendingEntry): { added: number; deleted: number } {
26
31
  const diff = diffOf(entry)
27
32
  if (diff.kind !== 'text') return { added: 0, deleted: 0 }
@@ -225,11 +230,11 @@ export function writerFor(
225
230
  // 撤销「Revert 删除新建文件」时文件已不存在,只有显式允许重建才放行
226
231
  const recreate = allowCreate && current === null
227
232
  if (!recreate && current !== expected) return { ok: false, reason: 'diverged' }
228
- const info = await fs.stat(target, signal).catch(() => undefined)
233
+ const currentStat = await fs.stat(target, signal).catch(() => undefined)
229
234
  try {
230
235
  // 第五个实参是沙箱策略;缺它时 provider 会以 FS_SANDBOX_DENIED 拒绝写入
231
236
  const written = await fs.writeText(
232
- target, content ?? '', info ? { kind: 'replaceIfVersion', version: info.version } : undefined, signal, policy,
237
+ target, content ?? '', currentStat ? { kind: 'replaceIfVersion', version: currentStat.version } : undefined, signal, policy,
233
238
  )
234
239
  const readBack = await readText(fs, target)
235
240
  if ((content ?? '') !== (readBack ?? '')) return { ok: false, reason: 'read-back-mismatch' }
@@ -412,8 +417,8 @@ async function deleteTarget(fs: any, cwd: string, identity: string, signal?: Abo
412
417
  } catch {
413
418
  return { ok: false, reason: 'resolve-failed' }
414
419
  }
415
- const info = await fs.stat(target, signal).catch(() => undefined)
416
- if (!info) return { ok: true, content: null }
420
+ const currentStat = await fs.stat(target, signal).catch(() => undefined)
421
+ if (!currentStat) return { ok: true, content: null }
417
422
  const absolute = String(fs.processPath(target))
418
423
  if (!insideWorkspace(cwd, absolute)) return { ok: false, reason: 'outside-workspace' }
419
424
  try {
@@ -373,8 +373,8 @@ async function runCommand(
373
373
  */
374
374
  async function readTextOf(path: string): Promise<string | null> {
375
375
  try {
376
- const info = await stat(path)
377
- if (!info.isFile()) return null
376
+ const fileStat = await stat(path)
377
+ if (!fileStat.isFile()) return null
378
378
  return await readFile(path, 'utf8')
379
379
  } catch {
380
380
  return null
@@ -5,6 +5,7 @@
5
5
  */
6
6
  import type { ReviewDetail, ReviewItem, ReviewPage } from './shared/review.js'
7
7
  import type { PendingDetail, PendingItem, PendingPage } from './shared/pendingReview.js'
8
+ import { pathKeyFor } from './shared/pathKey.js'
8
9
 
9
10
  /** 进程内"已被待审接管"的路径集合;旧写入口守卫只读它,绝不碰数据库。 */
10
11
  const coveredPaths = new Set<string>()
@@ -30,18 +31,12 @@ function stableSeq(id: string): number {
30
31
 
31
32
  /**
32
33
  * @author ddj 2026年10月29号
33
- * @description: 统一路径比较口径,消除分隔符与 Windows 大小写差异;远端身份保留原大小写。
34
+ * @description: Host 侧的路径比较口径:远端身份保留大小写,其余按运行平台折叠。
34
35
  * @param path 路径
35
36
  * @returns 比较键
36
37
  */
37
38
  function pathKey(path: string): string {
38
- const unified = path.replace(/\\/g, '/')
39
- // 远端仓库身份区分大小写,折叠成小写会把 Host/Repo 与 host/repo 混成同一个文件。
40
- if (/^[a-z]+:\/\//i.test(unified)) return unified
41
- // 盘符路径天生大小写不敏感(与宿主平台无关,客户端 diffPathKey 同口径);
42
- // POSIX 路径在 POSIX 上区分大小写,不能折叠。
43
- if (/^[a-z]:\//i.test(unified)) return unified.toLowerCase()
44
- return process.platform === 'win32' ? unified.toLowerCase() : unified
39
+ return pathKeyFor(path, process.platform === 'win32')
45
40
  }
46
41
 
47
42
  /**
package/src/pendingRpc.ts CHANGED
@@ -12,111 +12,172 @@ import type { ShellLike } from './pending/vcs.js'
12
12
  import type { Ctx } from './store.js'
13
13
  import { policyOf } from './store.js'
14
14
 
15
+ /** 处理器共享的作用域能力:读允许跨实例冷读,写必须本端 live 会话。 */
16
+ interface ScopeKit {
17
+ ctx: Ctx
18
+ read(sessionId: string): Promise<string>
19
+ write(sessionId: string): Promise<string>
20
+ }
21
+
15
22
  /**
16
- * @author ddj 2026年10月29号
17
- * @description: 待审投影处理器。读路径允许跨实例冷读,写路径必须本端 live 会话。
23
+ * @author ddj 2026年10月30号
24
+ * @description: 解析会话工作区并预热记录桶;读路径也走它,避免冷读时桶未就绪。
18
25
  * @param ctx Host 上下文
19
26
  * @param registry 工作区记录桶缓存
20
- * @returns 待审相关处理器表
27
+ * @param sessionId 请求作用域
28
+ * @returns 工作区路径
21
29
  */
22
- export function pendingHandlers(ctx: Ctx, registry: Registry): Partial<RpcHandlerMap> {
23
- /** @private @author ddj 2026年10月29号 @param sessionId 请求作用域 @returns 工作区 */
24
- async function workspace(sessionId: string): Promise<string> {
25
- const scope = await resolveScope(ctx, sessionId)
26
- if ('err' in scope) throw new Error('会话不存在或没有工作区')
27
- await bucketOf(registry, ctx, scope.cwd)
28
- return scope.cwd
29
- }
30
+ async function workspaceOf(ctx: Ctx, registry: Registry, sessionId: string): Promise<string> {
31
+ const scope = await resolveScope(ctx, sessionId)
32
+ if ('err' in scope) throw new Error('会话不存在或没有工作区')
33
+ await bucketOf(registry, ctx, scope.cwd)
34
+ return scope.cwd
35
+ }
30
36
 
31
- /** @private @author ddj 2026年10月29号 @param sessionId 请求作用域 @returns 本端 live 会话的工作区 */
32
- async function liveWorkspace(sessionId: string): Promise<string> {
33
- const cwd = await workspace(sessionId)
34
- if (!sessionOf(ctx, sessionId)) throw new Error('写入要求本端活跃会话')
35
- return cwd
36
- }
37
+ /**
38
+ * @author ddj 2026年10月30号
39
+ * @description: 写路径专用的工作区解析:本端没有该会话就直接拒绝,不做跨实例写。
40
+ * @param ctx Host 上下文
41
+ * @param registry 工作区记录桶缓存
42
+ * @param sessionId 请求作用域
43
+ * @returns 工作区路径
44
+ */
45
+ async function writeWorkspaceOf(ctx: Ctx, registry: Registry, sessionId: string): Promise<string> {
46
+ const cwd = await workspaceOf(ctx, registry, sessionId)
47
+ if (!sessionOf(ctx, sessionId)) throw new Error('写入要求本端活跃会话')
48
+ return cwd
49
+ }
50
+
51
+ /**
52
+ * @author ddj 2026年10月30号
53
+ * @description: 把异常归一成可播报文案,避免处理器各自重复三元表达式。
54
+ * @param error 捕获到的异常
55
+ * @returns 错误文案
56
+ */
57
+ function failureOf(error: unknown): string {
58
+ return error instanceof Error ? error.message : String(error)
59
+ }
37
60
 
38
- /**
39
- * @private
40
- * @author ddj 2026年10月30号
41
- * @description: 面板列表里的条目 id 带待审前缀(用于和官方条目区分),
42
- * 而待审内核按原始身份存取;各处理器统一走 pendingRawId 剥掉前缀。
43
- * @param id 请求里的条目 id
44
- * @returns 待审内核使用的原始 id
45
- */
46
- const rawId = pendingRawId
61
+ /**
62
+ * @author ddj 2026年10月30号
63
+ * @description: 按会话策略包装 shell:执行器默认按 workspace-write 跑,本机无可用沙箱后端会直接拒绝。
64
+ * @param raw 原始 shell 能力
65
+ * @param policy 会话沙箱策略
66
+ * @returns 带策略的 shell 视图
67
+ */
68
+ function shellOf(raw: ShellLike, policy: unknown): ShellLike {
69
+ return {
70
+ resolve: (request) => raw.resolve(policy ? { ...request, sandboxPolicy: policy } : request),
71
+ execute: (spec) => raw.execute(spec),
72
+ }
73
+ }
47
74
 
75
+ /**
76
+ * @author ddj 2026年10月30号
77
+ * @description: 读路径处理器:列表与详情,允许跨实例冷读。
78
+ * @param kit 作用域能力
79
+ * @returns 处理器表
80
+ */
81
+ function readHandlers(kit: ScopeKit): Partial<RpcHandlerMap> {
48
82
  return {
49
83
  'edrv.pending.list': async (args: PendingRequest) => {
50
- const cwd = await workspace(args.sessionId)
84
+ const cwd = await kit.read(args.sessionId)
51
85
  const page = loadPage(cwd, args.sessionId)
52
86
  // 列表即登记接管关系;旧写入口守卫之后只读内存集合,不再产生数据库读
53
87
  for (const item of page.items) trackPendingCover(cwd, item.path, true)
54
88
  return { ok: true, page }
55
89
  },
56
90
  'edrv.pending.detail': async (args: PendingRequest & { id: string }) => {
57
- const cwd = await workspace(args.sessionId)
58
- const detail = loadDetail(cwd, rawId(args.id))
91
+ const cwd = await kit.read(args.sessionId)
92
+ const detail = loadDetail(cwd, pendingRawId(args.id))
59
93
  return detail ? { ok: true, detail } : { ok: false, error: '待审条目不存在' }
60
94
  },
95
+ }
96
+ }
97
+
98
+ /**
99
+ * @author ddj 2026年10月30号
100
+ * @description: 判定处理器:Keep/Revert 与已解决条目移除,都会改变待审状态。
101
+ * @param kit 作用域能力
102
+ * @returns 处理器表
103
+ */
104
+ function decideHandlers(kit: ScopeKit): Partial<RpcHandlerMap> {
105
+ return {
61
106
  'edrv.pending.decide': async (args) => {
62
- const cwd = await liveWorkspace(args.sessionId)
107
+ const cwd = await kit.write(args.sessionId)
108
+ const id = pendingRawId(args.id)
63
109
  try {
64
- const id = rawId(args.id)
65
- const result = await applyDecide(ctx.get('fs'), cwd, { ...args, id }, undefined, policyOf(ctx, sessionOf(ctx, args.sessionId)))
110
+ const policy = policyOf(kit.ctx, sessionOf(kit.ctx, args.sessionId))
111
+ const result = await applyDecide(kit.ctx.get('fs'), cwd, { ...args, id }, undefined, policy)
66
112
  if (!result.ok) return { ok: false, error: result.reason }
67
113
  const detail = loadDetail(cwd, id)
68
- if (!detail) return { ok: false, error: '待审条目不存在' }
69
- return { ok: true, item: detail.item, wrote: result.wrote }
114
+ return detail ? { ok: true, item: detail.item, wrote: result.wrote } : { ok: false, error: '待审条目不存在' }
70
115
  } catch (error) {
71
- return { ok: false, error: error instanceof Error ? error.message : String(error) }
116
+ return { ok: false, error: failureOf(error) }
72
117
  }
73
118
  },
74
119
  'edrv.pending.remove': async (args) => {
75
- const cwd = await liveWorkspace(args.sessionId)
76
- const id = rawId(args.id)
120
+ const cwd = await kit.write(args.sessionId)
121
+ const id = pendingRawId(args.id)
77
122
  const path = loadEntries(cwd).find((stored) => stored.entry.id === id)?.entry.path
78
123
  const removed = removeEntry(cwd, id, args.revision)
79
124
  // 移除后该路径不再由待审投影接管,旧入口守卫随之放行
80
125
  if (removed && path) trackPendingCover(cwd, path, false)
81
126
  return { ok: true, removed }
82
127
  },
128
+ }
129
+ }
130
+
131
+ /**
132
+ * @author ddj 2026年10月30号
133
+ * @description: 历史处理器:撤销/重做一次决定。
134
+ * @param kit 作用域能力
135
+ * @returns 处理器表
136
+ */
137
+ function historyHandlers(kit: ScopeKit): Partial<RpcHandlerMap> {
138
+ return {
83
139
  'edrv.pending.history': async (args) => {
84
- const cwd = await liveWorkspace(args.sessionId)
140
+ const cwd = await kit.write(args.sessionId)
141
+ const policy = policyOf(kit.ctx, sessionOf(kit.ctx, args.sessionId))
85
142
  try {
86
- const result = await undoDecide(ctx.get('fs'), cwd, args, undefined, policyOf(ctx, sessionOf(ctx, args.sessionId)))
87
- if (!result.ok) return { ok: false, error: result.reason }
88
- return { ok: true, item: result.item, wrote: result.wrote }
143
+ const result = await undoDecide(kit.ctx.get('fs'), cwd, args, undefined, policy)
144
+ return result.ok ? { ok: true, item: result.item, wrote: result.wrote } : { ok: false, error: result.reason }
89
145
  } catch (error) {
90
- return { ok: false, error: error instanceof Error ? error.message : String(error) }
146
+ return { ok: false, error: failureOf(error) }
91
147
  }
92
148
  },
149
+ }
150
+ }
151
+
152
+ /**
153
+ * @author ddj 2026年10月30号
154
+ * @description: 投影维护处理器:从版本库导入、按磁盘现状刷新单条。
155
+ * @param kit 作用域能力
156
+ * @returns 处理器表
157
+ */
158
+ function refreshHandlers(kit: ScopeKit): Partial<RpcHandlerMap> {
159
+ return {
93
160
  'edrv.pending.import': async (args) => {
94
- const cwd = await liveWorkspace(args.sessionId)
95
- const raw = ctx.get('shell') as unknown as ShellLike | undefined
161
+ const cwd = await kit.write(args.sessionId)
162
+ const raw = kit.ctx.get('shell') as unknown as ShellLike | undefined
96
163
  if (!raw) return { ok: false, error: '当前环境没有 shell 能力,无法读取版本库基线' }
97
- // shell 执行器默认按 workspace-write 跑;本机没有可用沙箱后端时会直接拒绝,
98
- // 所以按会话策略显式下发(会话已放宽时命令才能跑起来)。
99
- const policy = policyOf(ctx, sessionOf(ctx, args.sessionId))
100
- const shell: ShellLike = {
101
- resolve: (request) => raw.resolve(policy ? { ...request, sandboxPolicy: policy } : request),
102
- execute: (spec) => raw.execute(spec),
103
- }
164
+ const policy = policyOf(kit.ctx, sessionOf(kit.ctx, args.sessionId))
104
165
  try {
166
+ const shell = shellOf(raw, policy)
105
167
  const result = await importFromVcs(shell, cwd, args.sessionId, { includeUntracked: args.includeUntracked, path: args.path })
106
- const page = loadPage(cwd, args.sessionId)
107
- for (const item of page.items) trackPendingCover(cwd, item.path, true)
168
+ for (const item of loadPage(cwd, args.sessionId).items) trackPendingCover(cwd, item.path, true)
108
169
  return { ok: true, imported: result.imported, changes: result.changes }
109
170
  } catch (error) {
110
- return { ok: false, error: error instanceof Error ? error.message : String(error) }
171
+ return { ok: false, error: failureOf(error) }
111
172
  }
112
173
  },
113
174
  'edrv.pending.refresh': async (args: PendingRequest & { id: string }) => {
114
- const cwd = await liveWorkspace(args.sessionId)
115
- const id = rawId(args.id)
175
+ const cwd = await kit.write(args.sessionId)
176
+ const id = pendingRawId(args.id)
116
177
  const stored = loadEntries(cwd).find((entry) => entry.entry.id === id)
117
178
  if (!stored) return { ok: false, error: '待审条目不存在' }
118
- const session = sessionOf(ctx, args.sessionId)
119
- const fs = ctx.get('fs')
179
+ const fs = kit.ctx.get('fs')
180
+ const session = sessionOf(kit.ctx, args.sessionId)
120
181
  const target = await fs.resolve(id, { cwd, agent: session })
121
182
  const content = await readText(fs, target).catch(() => undefined)
122
183
  if (content === undefined) return { ok: false, error: '读取失败' }
@@ -126,3 +187,24 @@ export function pendingHandlers(ctx: Ctx, registry: Registry): Partial<RpcHandle
126
187
  },
127
188
  }
128
189
  }
190
+
191
+ /**
192
+ * @author ddj 2026年10月29号
193
+ * @description: 待审投影处理器表:按用途拆成读/判定/历史/维护四组,便于分别收敛权限。
194
+ * @param ctx Host 上下文
195
+ * @param registry 工作区记录桶缓存
196
+ * @returns 待审相关处理器表
197
+ */
198
+ export function pendingHandlers(ctx: Ctx, registry: Registry): Partial<RpcHandlerMap> {
199
+ const kit: ScopeKit = {
200
+ ctx,
201
+ read: (sessionId) => workspaceOf(ctx, registry, sessionId),
202
+ write: (sessionId) => writeWorkspaceOf(ctx, registry, sessionId),
203
+ }
204
+ return {
205
+ ...readHandlers(kit),
206
+ ...decideHandlers(kit),
207
+ ...historyHandlers(kit),
208
+ ...refreshHandlers(kit),
209
+ }
210
+ }