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.
- package/README.md +7 -3
- package/lib/client.js +3387 -108
- package/lib/client.js.map +2 -2
- package/lib/index.js +1209 -266
- package/lib/index.js.map +1 -1
- package/package.json +1 -1
- package/src/client/index.ts +166 -0
- package/src/client/quickMenuHost.ts +46 -0
- package/src/client/quickMenuStore.ts +260 -0
- package/src/client/reviewBatch.ts +17 -21
- package/src/client/state/diffSummary.ts +9 -4
- package/src/client/styles/quickmenu.css +1049 -0
- package/src/client/ui/EditorView.ts +5 -1
- package/src/client/ui/QuickMenuDialog.tsx +835 -0
- package/src/client/ui/QuickMenuLauncher.tsx +792 -0
- package/src/client/ui/QuickMenuPickers.ts +577 -0
- package/src/index.ts +2 -2
- package/src/paths.ts +10 -0
- package/src/pending/capture.ts +104 -75
- package/src/pending/persist.ts +6 -1
- package/src/pending/store.ts +10 -5
- package/src/pending/vcs.ts +2 -2
- package/src/pendingCompat.ts +3 -8
- package/src/pendingRpc.ts +140 -58
- package/src/quickMenu.ts +269 -0
- package/src/reviewRpc.ts +40 -0
- package/src/rpc.ts +37 -0
- package/src/shared/pathKey.ts +54 -0
- package/src/shared/quickMenu.ts +1009 -0
- package/src/shared/quickPick.ts +67 -0
- package/src/shared/rpc.ts +11 -1
- package/src/shared/skillCatalog.ts +60 -0
- package/src/skillCatalog.ts +138 -0
package/src/pending/capture.ts
CHANGED
|
@@ -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月
|
|
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
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
-
|
|
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('
|
|
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
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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
|
/**
|
package/src/pending/persist.ts
CHANGED
|
@@ -76,7 +76,12 @@ CREATE TABLE IF NOT EXISTS pending_history (
|
|
|
76
76
|
state TEXT NOT NULL DEFAULT 'applied');
|
|
77
77
|
`
|
|
78
78
|
|
|
79
|
-
/**
|
|
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
|
package/src/pending/store.ts
CHANGED
|
@@ -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
|
|
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 ?? '',
|
|
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
|
|
416
|
-
if (!
|
|
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 {
|
package/src/pending/vcs.ts
CHANGED
|
@@ -373,8 +373,8 @@ async function runCommand(
|
|
|
373
373
|
*/
|
|
374
374
|
async function readTextOf(path: string): Promise<string | null> {
|
|
375
375
|
try {
|
|
376
|
-
const
|
|
377
|
-
if (!
|
|
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
|
package/src/pendingCompat.ts
CHANGED
|
@@ -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:
|
|
34
|
+
* @description: Host 侧的路径比较口径:远端身份保留大小写,其余按运行平台折叠。
|
|
34
35
|
* @param path 路径
|
|
35
36
|
* @returns 比较键
|
|
36
37
|
*/
|
|
37
38
|
function pathKey(path: string): string {
|
|
38
|
-
|
|
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月
|
|
17
|
-
* @description:
|
|
23
|
+
* @author ddj 2026年10月30号
|
|
24
|
+
* @description: 解析会话工作区并预热记录桶;读路径也走它,避免冷读时桶未就绪。
|
|
18
25
|
* @param ctx Host 上下文
|
|
19
26
|
* @param registry 工作区记录桶缓存
|
|
20
|
-
* @
|
|
27
|
+
* @param sessionId 请求作用域
|
|
28
|
+
* @returns 工作区路径
|
|
21
29
|
*/
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
|
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
|
|
58
|
-
const detail = loadDetail(cwd,
|
|
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
|
|
107
|
+
const cwd = await kit.write(args.sessionId)
|
|
108
|
+
const id = pendingRawId(args.id)
|
|
63
109
|
try {
|
|
64
|
-
const
|
|
65
|
-
const result = await applyDecide(ctx.get('fs'), cwd, { ...args, id }, undefined,
|
|
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
|
-
|
|
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:
|
|
116
|
+
return { ok: false, error: failureOf(error) }
|
|
72
117
|
}
|
|
73
118
|
},
|
|
74
119
|
'edrv.pending.remove': async (args) => {
|
|
75
|
-
const cwd = await
|
|
76
|
-
const 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
|
|
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,
|
|
87
|
-
|
|
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:
|
|
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
|
|
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
|
-
|
|
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
|
|
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:
|
|
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
|
|
115
|
-
const 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
|
|
119
|
-
const
|
|
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
|
+
}
|