dsh-vscode-mode 0.19.2 → 0.20.0
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 +6 -4
- package/lib/client.js +3868 -3486
- package/lib/client.js.map +1 -1
- package/lib/index.js +535 -42
- package/lib/index.js.map +1 -1
- package/package.json +1 -1
- package/src/archiveDb.ts +8 -8
- package/src/capture.ts +2 -2
- package/src/client/state/diffSummary.ts +30 -0
- package/src/client/state/reviewFeed.ts +60 -0
- package/src/client/ui/ConversationDiffDock.ts +9 -10
- package/src/client/ui/DiffBadge.ts +8 -6
- package/src/client/ui/DiffBox.ts +18 -0
- package/src/client/ui/DiffLauncher.ts +21 -5
- package/src/client/ui/EditorView.ts +17 -5
- package/src/client/ui/OfficialReview.ts +93 -0
- package/src/index.ts +3 -0
- package/src/model.ts +7 -3
- package/src/officialCapture.ts +199 -0
- package/src/reviewDb.ts +214 -0
- package/src/reviewRpc.ts +33 -0
- package/src/rpc.ts +14 -18
- package/src/shared/review.ts +91 -0
- package/src/shared/rpc.ts +7 -0
- package/src/store.ts +2 -2
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
/** Deferred official-event collection; no private snapshot access. @author ddj 2026年10月26号 */
|
|
2
|
+
import { captureState, repairPaths, saveSnapshot } from './reviewDb.js'
|
|
3
|
+
import type { OfficialDiff, OfficialSummary } from './shared/review.js'
|
|
4
|
+
import type { Ctx } from './store.js'
|
|
5
|
+
import { loadBucket } from './store.js'
|
|
6
|
+
import { log } from './log.js'
|
|
7
|
+
|
|
8
|
+
interface SourceEvent { type: string; seq: number; data: { turn?: number } }
|
|
9
|
+
interface SourceSession { id: string; inheritedEventCount?: number; header?: { cwd?: string } }
|
|
10
|
+
interface SourceService {
|
|
11
|
+
summary(sessionId: string, seq: number): OfficialSummary | undefined
|
|
12
|
+
diff(sessionId: string, seq: number, index: number, signal: AbortSignal): Promise<OfficialDiff | undefined>
|
|
13
|
+
}
|
|
14
|
+
interface CaptureTask { sessionId: string; turn: number; seq: number; cwd?: string }
|
|
15
|
+
export interface CaptureDeps {
|
|
16
|
+
source: () => SourceService | undefined
|
|
17
|
+
prepare(cwd: string): Promise<void>
|
|
18
|
+
identity(cwd: string, path: string): Promise<string>
|
|
19
|
+
persist: typeof saveSnapshot
|
|
20
|
+
state: typeof captureState
|
|
21
|
+
report(message: string): void
|
|
22
|
+
timeoutMs?: number
|
|
23
|
+
retryMs?: number
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** @public @author ddj 2026年10月26号 @param deps Official source and workspace adapters. @returns Bounded collector with cancellation and deferred event reads. */
|
|
27
|
+
export function createCollector(deps: CaptureDeps) {
|
|
28
|
+
const waiting: CaptureTask[] = []
|
|
29
|
+
const pending = new Set<string>()
|
|
30
|
+
const controller = new AbortController()
|
|
31
|
+
let active = 0
|
|
32
|
+
let stopped = false
|
|
33
|
+
const running = new Set<string>()
|
|
34
|
+
const tasks = new Set<Promise<void>>()
|
|
35
|
+
|
|
36
|
+
/** @private @author ddj 2026年10月26号 @param ms Delay; aborted on unload. @returns Deferred completion, not synchronous observer work. */
|
|
37
|
+
function pause(ms: number): Promise<void> {
|
|
38
|
+
return new Promise((resolve) => {
|
|
39
|
+
/** @private @author ddj 2026年10月26号 Release both timer and listener. */
|
|
40
|
+
function finish(): void { clearTimeout(timer); controller.signal.removeEventListener('abort', finish); resolve() }
|
|
41
|
+
const timer = setTimeout(finish, ms)
|
|
42
|
+
controller.signal.addEventListener('abort', finish, { once: true })
|
|
43
|
+
if (stopped) finish()
|
|
44
|
+
})
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** @private @author ddj 2026年10月26号 @param task Source identity. @returns Complete payload staged before one database transaction. */
|
|
48
|
+
async function collect(task: CaptureTask): Promise<void> {
|
|
49
|
+
await pause(0) // Official append publishes before summary registration.
|
|
50
|
+
if (stopped) return
|
|
51
|
+
// The session header may predate cd. Bind the durable inbox only after
|
|
52
|
+
// reading the official event cwd; header cwd is a gap-report fallback.
|
|
53
|
+
const source = deps.source()
|
|
54
|
+
if (!source) throw new Error('官方 workspaceChanges 服务不可用')
|
|
55
|
+
let summary: OfficialSummary | undefined
|
|
56
|
+
for (let attempt = 0; attempt < 4 && !stopped; attempt++) {
|
|
57
|
+
summary = source.summary(task.sessionId, task.seq)
|
|
58
|
+
if (summary) break
|
|
59
|
+
await pause(deps.retryMs ?? 100)
|
|
60
|
+
}
|
|
61
|
+
if (!summary || stopped) throw new Error('官方事件已公告,但 summary 已失效或尚不可用')
|
|
62
|
+
// Detach all source aliases before asynchronous work; never resolve against current session cwd.
|
|
63
|
+
summary = structuredClone(summary)
|
|
64
|
+
if (summary.turn !== task.turn) throw new Error('官方 summary 轮次与事件不一致')
|
|
65
|
+
task.cwd = summary.cwd
|
|
66
|
+
await deps.prepare(summary.cwd)
|
|
67
|
+
if (stopped) return
|
|
68
|
+
deps.state(summary.cwd, { ...task, error: '' }, 'queued')
|
|
69
|
+
try {
|
|
70
|
+
const files = []
|
|
71
|
+
for (const [index, file] of summary.files.entries()) {
|
|
72
|
+
if (stopped) return
|
|
73
|
+
const signal = AbortSignal.any([controller.signal, AbortSignal.timeout(deps.timeoutMs ?? 10000)])
|
|
74
|
+
const diff = await readDiff(source, task, index, signal)
|
|
75
|
+
if (!diff || diff.path !== file.path || diff.display !== file.display) throw new Error('官方文件差异缺失或身份不一致')
|
|
76
|
+
const identity = await deps.identity(summary.cwd, file.path)
|
|
77
|
+
files.push({ identity, diff: structuredClone(diff) })
|
|
78
|
+
}
|
|
79
|
+
if (!stopped) deps.persist(summary.cwd, { ...task, sourceKind: 'official', capturedAt: new Date().toISOString(), summary, files })
|
|
80
|
+
} catch (error) {
|
|
81
|
+
if (!stopped) deps.state(summary.cwd, { ...task, error: String(error) }, 'gap')
|
|
82
|
+
throw error
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** @private @author ddj 2026年10月26号 Starts at most two independent event reads, with each event's file reads serial. */
|
|
87
|
+
function drain(): void {
|
|
88
|
+
while (!stopped && active < 2 && waiting.length) {
|
|
89
|
+
const index = waiting.findIndex((task) => !running.has(JSON.stringify([task.sessionId, task.turn])))
|
|
90
|
+
if (index < 0) break
|
|
91
|
+
const task = waiting.splice(index, 1)[0]
|
|
92
|
+
const key = JSON.stringify([task.sessionId, task.turn, task.seq])
|
|
93
|
+
const turnKey = JSON.stringify([task.sessionId, task.turn])
|
|
94
|
+
active++; running.add(turnKey)
|
|
95
|
+
const work = collect(task).catch(async (error) => {
|
|
96
|
+
deps.report(`官方差异采集 ${key}: ${String(error)}`)
|
|
97
|
+
if (!stopped && task.cwd) {
|
|
98
|
+
try { await deps.prepare(task.cwd); if (!stopped) deps.state(task.cwd, { ...task, error: String(error) }, 'gap') }
|
|
99
|
+
catch (failure) { deps.report(`官方缺口落盘失败: ${String(failure)}`) }
|
|
100
|
+
}
|
|
101
|
+
}).finally(() => {
|
|
102
|
+
active--; pending.delete(key); running.delete(turnKey); tasks.delete(work); drain()
|
|
103
|
+
})
|
|
104
|
+
tasks.add(work)
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** @public @author ddj 2026年10月26号 @param session Source session. @param event Published event; inherited fork events are excluded. */
|
|
109
|
+
function enqueue(session: SourceSession, event: SourceEvent): void {
|
|
110
|
+
if (stopped || event.type !== 'workspace/changes' || !Number.isSafeInteger(event.data?.turn)
|
|
111
|
+
|| !Number.isSafeInteger(event.seq) || event.seq < (session.inheritedEventCount ?? 0)) return
|
|
112
|
+
const task = { sessionId: session.id, turn: event.data.turn!, seq: event.seq, cwd: session.header?.cwd }
|
|
113
|
+
const key = JSON.stringify([task.sessionId, task.turn, task.seq])
|
|
114
|
+
if (pending.has(key)) return
|
|
115
|
+
pending.add(key); waiting.push(task); drain()
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** @public @author ddj 2026年10月26号 @returns Bounded durability checkpoint including work queued while active tasks finish. */
|
|
119
|
+
async function flush(): Promise<void> {
|
|
120
|
+
const deadline = Date.now() + (deps.timeoutMs ?? 10000)
|
|
121
|
+
while (tasks.size && !stopped && Date.now() < deadline) {
|
|
122
|
+
let timer: ReturnType<typeof setTimeout> | undefined
|
|
123
|
+
await Promise.race([Promise.all([...tasks]), new Promise<void>((resolve) => { timer = setTimeout(resolve, Math.max(1, deadline - Date.now())) })])
|
|
124
|
+
clearTimeout(timer)
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** @public @author ddj 2026年10月26号 @returns Bounded drain, then cancel timers and reads; incomplete staging cannot publish. */
|
|
129
|
+
async function dispose(): Promise<void> {
|
|
130
|
+
await flush()
|
|
131
|
+
stopped = true; controller.abort(); waiting.length = 0; pending.clear()
|
|
132
|
+
// Never await an uncooperative external adapter after the deadline.
|
|
133
|
+
// Every publish path checks stopped; late completion cannot reopen the database.
|
|
134
|
+
}
|
|
135
|
+
return { enqueue, flush, dispose }
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** @private @author ddj 2026年10月26号 @param source Public service. @param task Event. @param index File position. @param signal Cancellation/deadline. @returns A read bounded even if an adapter ignores abort. */
|
|
139
|
+
async function readDiff(source: SourceService, task: CaptureTask, index: number, signal: AbortSignal): Promise<OfficialDiff | undefined> {
|
|
140
|
+
return new Promise((resolve, reject) => {
|
|
141
|
+
/** @private @author ddj 2026年10月26号 Propagate timeout or unload. */
|
|
142
|
+
function aborted(): void { reject(signal.reason) }
|
|
143
|
+
signal.addEventListener('abort', aborted, { once: true })
|
|
144
|
+
if (signal.aborted) { aborted(); return }
|
|
145
|
+
Promise.resolve().then(() => source.diff(task.sessionId, task.seq, index, signal))
|
|
146
|
+
.then(resolve, reject).finally(() => signal.removeEventListener('abort', aborted))
|
|
147
|
+
})
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** @public @author ddj 2026年10月26号 @param ctx Host lifecycle owner; services remain optional for older Hosts. */
|
|
151
|
+
export function installCapture(ctx: Ctx): () => Promise<void> {
|
|
152
|
+
const collector = createCollector({
|
|
153
|
+
source: () => ctx.get('workspaceChanges'),
|
|
154
|
+
prepare: async (cwd) => { await loadBucket(ctx, cwd); await repairPaths(cwd, resolvePath) },
|
|
155
|
+
identity: resolvePath,
|
|
156
|
+
persist: saveSnapshot, state: captureState, report: (message) => log.warn(message),
|
|
157
|
+
})
|
|
158
|
+
// Subscribe first: seed reads must not create a live-event race window.
|
|
159
|
+
ctx.on('session/event', collector.enqueue)
|
|
160
|
+
ctx.on('session/created', backfill)
|
|
161
|
+
ctx.on('session/flush', collector.flush)
|
|
162
|
+
for (const session of ctx.get('sessions').list()) void backfill(session)
|
|
163
|
+
return collector.dispose
|
|
164
|
+
|
|
165
|
+
/** @private @author ddj 2026年10月27号 @param cwd Source workspace. @param path Official relative path. @returns Backend-owned process path; never stringify opaque targets. @throws Invalid backend path. */
|
|
166
|
+
async function resolvePath(cwd: string, path: string): Promise<string> {
|
|
167
|
+
const fs = ctx.get('fs')
|
|
168
|
+
const target = await fs.resolve(path, { cwd })
|
|
169
|
+
const processPath = fs.processPath(target)
|
|
170
|
+
if (typeof processPath !== 'string' || !processPath || processPath === '[object Object]') {
|
|
171
|
+
throw new Error('官方文件路径解析失败')
|
|
172
|
+
}
|
|
173
|
+
return processPath
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** @private @author ddj 2026年10月26号 @param session Live session. Only public async reads, excluding inherited records. */
|
|
177
|
+
async function backfill(session: SourceSession): Promise<void> {
|
|
178
|
+
const query = ctx.get('sessionQuery')
|
|
179
|
+
if (!query?.listEvents || !query?.readEvent) {
|
|
180
|
+
log.warn('官方历史补采不可用:缺少 sessionQuery;后续实时事件仍会采集')
|
|
181
|
+
return
|
|
182
|
+
}
|
|
183
|
+
try {
|
|
184
|
+
// Repair durable history independently: official summaries do not survive Host restart.
|
|
185
|
+
// Each saved snapshot resolves against its own cwd, not the current session directory.
|
|
186
|
+
if (session.header?.cwd) {
|
|
187
|
+
await loadBucket(ctx, session.header.cwd)
|
|
188
|
+
await repairPaths(session.header.cwd, resolvePath)
|
|
189
|
+
}
|
|
190
|
+
// Semantic search omits structural events; only the raw metadata list contains workspace/changes.
|
|
191
|
+
const events = await query.listEvents(session.id)
|
|
192
|
+
for (const event of events) {
|
|
193
|
+
if (event.type !== 'workspace/changes' || event.seq < (session.inheritedEventCount ?? 0)) continue
|
|
194
|
+
const window = await query.readEvent({ sessionId: session.id, seq: event.seq })
|
|
195
|
+
collector.enqueue(session, window.target)
|
|
196
|
+
}
|
|
197
|
+
} catch (error) { log.warn('官方历史补采失败:' + String(error)) }
|
|
198
|
+
}
|
|
199
|
+
}
|
package/src/reviewDb.ts
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
/** Official history shares the archive connection, but never the legacy callId namespace. @author ddj 2026年10月26号 */
|
|
2
|
+
import { createHash } from 'node:crypto'
|
|
3
|
+
import type { DatabaseSync } from 'node:sqlite'
|
|
4
|
+
import { activeReady, dbFor } from './archiveDb.js'
|
|
5
|
+
import type { CaptureGap, DecideReview, OfficialDiff, ReviewDetail, ReviewItem, ReviewPage, ReviewSnapshot } from './shared/review.js'
|
|
6
|
+
|
|
7
|
+
type Row = Record<string, string | number | null>
|
|
8
|
+
|
|
9
|
+
/** @public @author ddj 2026年10月26号 @param cwd Migrated workspace. @returns Schema4 connection; DDL and version change roll back together. */
|
|
10
|
+
export function reviewDb(cwd: string): DatabaseSync {
|
|
11
|
+
if (!activeReady(cwd)) throw new Error('官方审查要求先完成活跃库迁移')
|
|
12
|
+
const db = dbFor(cwd)
|
|
13
|
+
const version = db.prepare("SELECT value FROM metadata WHERE name='schema'").get() as Row
|
|
14
|
+
if (version.value === '4') return db
|
|
15
|
+
db.exec('BEGIN IMMEDIATE')
|
|
16
|
+
try {
|
|
17
|
+
db.exec(`CREATE TABLE review_snapshots (
|
|
18
|
+
session_id TEXT NOT NULL, turn INTEGER NOT NULL, seq INTEGER NOT NULL,
|
|
19
|
+
payload TEXT NOT NULL, digest TEXT NOT NULL, current INTEGER NOT NULL DEFAULT 1,
|
|
20
|
+
PRIMARY KEY(session_id,turn,seq));
|
|
21
|
+
CREATE TABLE review_files (
|
|
22
|
+
id TEXT PRIMARY KEY, session_id TEXT NOT NULL, turn INTEGER NOT NULL, seq INTEGER NOT NULL,
|
|
23
|
+
identity TEXT NOT NULL, summary TEXT NOT NULL, diff TEXT NOT NULL,
|
|
24
|
+
decision TEXT NOT NULL DEFAULT 'pending', revision INTEGER NOT NULL DEFAULT 0,
|
|
25
|
+
current INTEGER NOT NULL DEFAULT 1);
|
|
26
|
+
CREATE INDEX review_pending ON review_files(current,decision,id);
|
|
27
|
+
CREATE INDEX review_turn ON review_files(session_id,turn,seq);
|
|
28
|
+
CREATE TABLE review_capture (
|
|
29
|
+
session_id TEXT NOT NULL, turn INTEGER NOT NULL, seq INTEGER NOT NULL,
|
|
30
|
+
state TEXT NOT NULL, error TEXT, PRIMARY KEY(session_id,turn,seq));
|
|
31
|
+
CREATE TABLE review_ops (
|
|
32
|
+
operation_id TEXT PRIMARY KEY, request TEXT NOT NULL, state TEXT NOT NULL, result TEXT,
|
|
33
|
+
evidence TEXT);
|
|
34
|
+
UPDATE metadata SET value='4' WHERE name='schema';`)
|
|
35
|
+
db.exec('COMMIT')
|
|
36
|
+
} catch (error) {
|
|
37
|
+
if (db.isTransaction) db.exec('ROLLBACK')
|
|
38
|
+
throw error
|
|
39
|
+
}
|
|
40
|
+
return db
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** @private @author ddj 2026年10月26号 @param text Canonical serialized input. @returns Collision-resistant identity. */
|
|
44
|
+
function digest(text: string): string { return createHash('sha256').update(text).digest('hex') }
|
|
45
|
+
|
|
46
|
+
/** @public @author ddj 2026年10月26号 @param cwd Workspace. @param snapshot Complete staged event. @returns False for late versions; commits facts, pointers and capture state atomically. */
|
|
47
|
+
export function saveSnapshot(cwd: string, snapshot: ReviewSnapshot): boolean {
|
|
48
|
+
const db = reviewDb(cwd)
|
|
49
|
+
const { sessionId, turn, seq } = snapshot
|
|
50
|
+
if (snapshot.summary.cwd !== cwd || snapshot.summary.turn !== turn || snapshot.files.length !== snapshot.summary.files.length) throw new Error('官方采集不完整')
|
|
51
|
+
const identities = new Set(snapshot.files.map((file) => file.identity))
|
|
52
|
+
if (identities.size !== snapshot.files.length) throw new Error('官方文件身份重复')
|
|
53
|
+
// capturedAt is acquisition metadata, not part of the immutable source identity.
|
|
54
|
+
const hash = digest(JSON.stringify({ ...snapshot, capturedAt: '' }))
|
|
55
|
+
db.exec('BEGIN IMMEDIATE')
|
|
56
|
+
try {
|
|
57
|
+
const prior = db.prepare('SELECT digest FROM review_snapshots WHERE session_id=? AND turn=? AND seq=?').get(sessionId, turn, seq) as Row | undefined
|
|
58
|
+
if (prior && prior.digest !== hash) throw new Error('同一官方事件的内容发生变化')
|
|
59
|
+
const latest = db.prepare('SELECT MAX(seq) AS seq FROM review_snapshots WHERE session_id=? AND turn=?').get(sessionId, turn) as Row
|
|
60
|
+
if (prior || (typeof latest.seq === 'number' && latest.seq > seq)) { db.exec('COMMIT'); return false }
|
|
61
|
+
insertSnapshot(db, snapshot, hash)
|
|
62
|
+
db.prepare("INSERT INTO review_capture VALUES(?,?,?,'complete',NULL) ON CONFLICT(session_id,turn,seq) DO UPDATE SET state='complete',error=NULL").run(sessionId, turn, seq)
|
|
63
|
+
db.exec('COMMIT')
|
|
64
|
+
return true
|
|
65
|
+
} catch (error) {
|
|
66
|
+
if (db.isTransaction) db.exec('ROLLBACK')
|
|
67
|
+
throw error
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** @public @author ddj 2026年10月27号 @param cwd Workspace. @param resolvePath Backend path adapter. @returns Repairs only historical object-string identities; preserves IDs, decisions and immutable official facts. @throws Resolution or concurrent payload changes. */
|
|
72
|
+
export async function repairPaths(cwd: string, resolvePath: (cwd: string, path: string) => Promise<string>): Promise<void> {
|
|
73
|
+
const db = reviewDb(cwd)
|
|
74
|
+
const broken = db.prepare(`SELECT DISTINCT s.* FROM review_snapshots s JOIN review_files f
|
|
75
|
+
USING(session_id,turn,seq) WHERE f.identity='[object Object]'`).all() as Row[]
|
|
76
|
+
for (const row of broken) {
|
|
77
|
+
const snapshot = JSON.parse(String(row.payload)) as ReviewSnapshot
|
|
78
|
+
for (const file of snapshot.files) {
|
|
79
|
+
if (file.identity !== '[object Object]') continue
|
|
80
|
+
file.identity = await resolvePath(snapshot.summary.cwd, file.diff.path)
|
|
81
|
+
if (!file.identity || file.identity === '[object Object]') throw new Error('官方文件路径修复失败')
|
|
82
|
+
}
|
|
83
|
+
if (new Set(snapshot.files.map((file) => file.identity)).size !== snapshot.files.length) throw new Error('官方文件身份重复')
|
|
84
|
+
|
|
85
|
+
// All IO is staged before locking; compare the persisted payload to avoid overwriting newer repairs.
|
|
86
|
+
db.exec('BEGIN IMMEDIATE')
|
|
87
|
+
try {
|
|
88
|
+
const current = db.prepare('SELECT payload FROM review_snapshots WHERE session_id=? AND turn=? AND seq=?')
|
|
89
|
+
.get(snapshot.sessionId, snapshot.turn, snapshot.seq) as Row | undefined
|
|
90
|
+
if (current?.payload === row.payload) {
|
|
91
|
+
for (const file of snapshot.files) {
|
|
92
|
+
db.prepare(`UPDATE review_files SET identity=? WHERE session_id=? AND turn=? AND seq=?
|
|
93
|
+
AND identity='[object Object]' AND json_extract(diff,'$.path')=?`)
|
|
94
|
+
.run(file.identity, snapshot.sessionId, snapshot.turn, snapshot.seq, file.diff.path)
|
|
95
|
+
}
|
|
96
|
+
db.prepare('UPDATE review_snapshots SET payload=?,digest=? WHERE session_id=? AND turn=? AND seq=?')
|
|
97
|
+
.run(JSON.stringify(snapshot), digest(JSON.stringify({ ...snapshot, capturedAt: '' })), snapshot.sessionId, snapshot.turn, snapshot.seq)
|
|
98
|
+
}
|
|
99
|
+
db.exec('COMMIT')
|
|
100
|
+
} catch (error) {
|
|
101
|
+
if (db.isTransaction) db.exec('ROLLBACK')
|
|
102
|
+
throw error
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** @private @author ddj 2026年10月26号 @param db Transaction. @param snapshot Source payload. @param hash Source digest. */
|
|
108
|
+
function insertSnapshot(db: DatabaseSync, snapshot: ReviewSnapshot, hash: string): void {
|
|
109
|
+
const { sessionId, turn, seq } = snapshot
|
|
110
|
+
const previous = db.prepare('SELECT identity,diff,decision FROM review_files WHERE session_id=? AND turn=? AND current=1').all(sessionId, turn) as Row[]
|
|
111
|
+
db.prepare('UPDATE review_snapshots SET current=0 WHERE session_id=? AND turn=?').run(sessionId, turn)
|
|
112
|
+
db.prepare('UPDATE review_files SET current=0 WHERE session_id=? AND turn=?').run(sessionId, turn)
|
|
113
|
+
db.prepare('INSERT INTO review_snapshots VALUES(?,?,?,?,?,1)').run(sessionId, turn, seq, JSON.stringify(snapshot), hash)
|
|
114
|
+
const insert = db.prepare('INSERT INTO review_files VALUES(?,?,?,?,?,?,?,?,0,1)')
|
|
115
|
+
for (const [index, file] of snapshot.files.entries()) {
|
|
116
|
+
const encoded = JSON.stringify(file.diff)
|
|
117
|
+
const unchanged = previous.find((item) => item.identity === file.identity && item.diff === encoded)
|
|
118
|
+
const id = digest(JSON.stringify([sessionId, turn, seq, file.identity]))
|
|
119
|
+
insert.run(id, sessionId, turn, seq, file.identity, JSON.stringify(snapshot.summary.files[index]), encoded, unchanged?.decision ?? 'pending')
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** @public @author ddj 2026年10月26号 @param cwd Workspace. @param gap Announced event identity. @param state Durable inbox or explicit failure. */
|
|
124
|
+
export function captureState(cwd: string, gap: CaptureGap, state: 'queued' | 'gap'): void {
|
|
125
|
+
reviewDb(cwd).prepare(`INSERT INTO review_capture VALUES(?,?,?,?,?) ON CONFLICT(session_id,turn,seq)
|
|
126
|
+
DO UPDATE SET state=excluded.state,error=excluded.error WHERE review_capture.state<>'complete'`).run(gap.sessionId, gap.turn, gap.seq, state, gap.error)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** @private @author ddj 2026年10月26号 @param row File and state. @returns Wire-safe summary without hunks. */
|
|
130
|
+
function itemOf(row: Row): ReviewItem {
|
|
131
|
+
const summary = JSON.parse(String(row.summary))
|
|
132
|
+
const kind = row.kind ?? (JSON.parse(String(row.diff)) as OfficialDiff).kind
|
|
133
|
+
return { id: String(row.id), sessionId: String(row.session_id), turn: Number(row.turn), seq: Number(row.seq),
|
|
134
|
+
...summary, currentPath: String(row.identity), kind: kind as ReviewItem['kind'], decision: row.decision as ReviewItem['decision'], revision: Number(row.revision), current: row.current === 1 }
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** @public @author ddj 2026年10月26号 @param cwd Workspace. @param cursor Page offset. @param history Include withdrawn/decided history. @returns Bounded metadata page; pending across all source sessions/turns. */
|
|
138
|
+
export function reviewPage(cwd: string, cursor = 0, history = false, path?: string): ReviewPage {
|
|
139
|
+
const db = reviewDb(cwd)
|
|
140
|
+
const offset = Number.isSafeInteger(cursor) && cursor >= 0 ? cursor : 0
|
|
141
|
+
const conditions = history ? [] : ["current=1 AND decision='pending'"]
|
|
142
|
+
if (path !== undefined) conditions.push('identity=?')
|
|
143
|
+
const clause = conditions.length ? `WHERE ${conditions.join(' AND ')}` : ''
|
|
144
|
+
const parameters = path === undefined ? [offset] : [path, offset]
|
|
145
|
+
const rows = db.prepare(`SELECT id,session_id,turn,seq,identity,summary,decision,revision,current,
|
|
146
|
+
json_extract(diff,'$.kind') AS kind FROM review_files ${clause} ORDER BY rowid DESC LIMIT 51 OFFSET ?`).all(...parameters) as Row[]
|
|
147
|
+
const count = db.prepare("SELECT COUNT(*) AS n FROM review_files WHERE current=1 AND decision='pending'").get() as Row
|
|
148
|
+
const gaps = db.prepare("SELECT * FROM review_capture WHERE state<>'complete' ORDER BY seq DESC LIMIT 50").all() as Row[]
|
|
149
|
+
const capped = db.prepare(`SELECT session_id,turn,seq,json_extract(payload,'$.summary.total') AS total,
|
|
150
|
+
json_array_length(payload,'$.summary.files') AS listed FROM review_snapshots WHERE current=1
|
|
151
|
+
AND json_extract(payload,'$.summary.total')>json_array_length(payload,'$.summary.files') LIMIT 50`).all() as Row[]
|
|
152
|
+
// Navigation is metadata-only and independent of the 50-row patch-history page.
|
|
153
|
+
const files = db.prepare(`SELECT identity,MIN(json_extract(summary,'$.display')) AS label,COUNT(*) AS n
|
|
154
|
+
FROM review_files WHERE current=1 AND decision='pending' GROUP BY identity ORDER BY identity`).all() as Row[]
|
|
155
|
+
const covered = db.prepare('SELECT DISTINCT identity FROM review_files').all() as Row[]
|
|
156
|
+
return { overview: { files: files.map((row) => ({ sourceKind: 'official', path: String(row.identity), label: String(row.label), pending: Number(row.n) })),
|
|
157
|
+
coveredPaths: covered.map((row) => String(row.identity)) },
|
|
158
|
+
items: rows.slice(0, 50).map(itemOf), pending: Number(count.n), nextCursor: rows.length > 50 ? offset + 50 : null,
|
|
159
|
+
gaps: [
|
|
160
|
+
...gaps.map((row) => ({ sessionId: String(row.session_id), turn: Number(row.turn), seq: Number(row.seq), error: String(row.error || '采集中或上次采集被中断') })),
|
|
161
|
+
...capped.map((row) => ({ sessionId: String(row.session_id), turn: Number(row.turn), seq: Number(row.seq), error: `官方列表已截断:总计 ${row.total} 个文件,仅提供 ${row.listed} 个` })),
|
|
162
|
+
] }
|
|
163
|
+
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** @public @author ddj 2026年10月26号 @param cwd Migrated workspace. @param path Legacy write target. @returns Whether any official version owns the file, including decided history. */
|
|
167
|
+
export function hasOfficial(cwd: string, path: string): boolean {
|
|
168
|
+
const db = dbFor(cwd)
|
|
169
|
+
// A read guard must not acquire a migration write lock on legacy-only databases.
|
|
170
|
+
if (!db.prepare("SELECT 1 FROM sqlite_master WHERE name='review_files' AND type='table'").get()) return false
|
|
171
|
+
if (/^[A-Za-z]:[\\/]/.test(path)) {
|
|
172
|
+
return !!db.prepare("SELECT 1 FROM review_files WHERE lower(replace(identity,char(92),'/'))=? LIMIT 1")
|
|
173
|
+
.get(path.replace(/\\/g, '/').toLowerCase())
|
|
174
|
+
}
|
|
175
|
+
return !!db.prepare('SELECT 1 FROM review_files WHERE identity=? LIMIT 1').get(path)
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** @public @author ddj 2026年10月26号 @param cwd Workspace. @param id Immutable file identity. @returns Historical patch, not a reconstruction from disk. */
|
|
179
|
+
export function reviewDetail(cwd: string, id: string): ReviewDetail | null {
|
|
180
|
+
const db = reviewDb(cwd)
|
|
181
|
+
const row = db.prepare(`SELECT f.*,json_extract(s.payload,'$.capturedAt') AS captured_at FROM review_files f JOIN review_snapshots s
|
|
182
|
+
USING(session_id,turn,seq) WHERE f.id=?`).get(id) as Row | undefined
|
|
183
|
+
if (!row) return null
|
|
184
|
+
return { item: itemOf(row), diff: JSON.parse(String(row.diff)), capturedAt: String(row.captured_at),
|
|
185
|
+
capability: { keep: row.current === 1 && row.decision === 'pending', undo: false,
|
|
186
|
+
reason: '官方仅提供差异块,缺少可验证的历史全文及原始字节;拒绝写回已禁用。可标记已审或打开当前文件继续编辑。' } }
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** @public @author ddj 2026年10月26号 @param cwd Workspace. @param request Frozen identity/revision. @returns Idempotent decision; rejection never guesses a target. */
|
|
190
|
+
export function decideReview(cwd: string, request: DecideReview): { revision: number } {
|
|
191
|
+
const db = reviewDb(cwd)
|
|
192
|
+
const encoded = JSON.stringify(request)
|
|
193
|
+
if (!request.operationId || request.operationId.length > 128) throw new Error('operationId 无效')
|
|
194
|
+
db.exec('BEGIN IMMEDIATE')
|
|
195
|
+
try {
|
|
196
|
+
const previous = db.prepare('SELECT request,result FROM review_ops WHERE operation_id=?').get(request.operationId) as Row | undefined
|
|
197
|
+
if (previous) {
|
|
198
|
+
if (previous.request !== encoded) throw new Error('operationId 已用于其他请求')
|
|
199
|
+
db.exec('COMMIT')
|
|
200
|
+
return JSON.parse(String(previous.result))
|
|
201
|
+
}
|
|
202
|
+
if (request.decision !== 'accepted') throw new Error('缺少可信全文证据,禁止拒绝写回')
|
|
203
|
+
const change = db.prepare(`UPDATE review_files SET decision='accepted',revision=revision+1
|
|
204
|
+
WHERE id=? AND seq=? AND revision=? AND current=1 AND decision='pending'`).run(request.id, request.seq, request.revision)
|
|
205
|
+
if (change.changes !== 1) throw new Error('审查版本冲突,请刷新后重试')
|
|
206
|
+
const result = { revision: request.revision + 1 }
|
|
207
|
+
db.prepare("INSERT INTO review_ops VALUES(?,?,'committed',?,NULL)").run(request.operationId, encoded, JSON.stringify(result))
|
|
208
|
+
db.exec('COMMIT')
|
|
209
|
+
return result
|
|
210
|
+
} catch (error) {
|
|
211
|
+
if (db.isTransaction) db.exec('ROLLBACK')
|
|
212
|
+
throw error
|
|
213
|
+
}
|
|
214
|
+
}
|
package/src/reviewRpc.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/** Session-scoped official review RPC. @author ddj 2026年10月26号 */
|
|
2
|
+
import type { RpcHandlerMap } from './shared/rpc.js'
|
|
3
|
+
import { decideReview, reviewDetail, reviewPage } from './reviewDb.js'
|
|
4
|
+
import { bucketOf, cwdOfSession, sessionOf } from './registry.js'
|
|
5
|
+
import type { Registry } from './registry.js'
|
|
6
|
+
import type { Ctx } from './store.js'
|
|
7
|
+
|
|
8
|
+
/** @public @author ddj 2026年10月26号 @param ctx Host. @param registry Workspace cache. @returns Handlers resolving workspace from the live authenticated session, never caller cwd. */
|
|
9
|
+
export function reviewHandlers(ctx: Ctx, registry: Registry): Partial<RpcHandlerMap> {
|
|
10
|
+
/** @private @author ddj 2026年10月26号 @param sessionId Request scope. @returns Migrated workspace, or explicit failure without fallback. */
|
|
11
|
+
async function workspace(sessionId: string): Promise<string> {
|
|
12
|
+
const session = sessionOf(ctx, sessionId)
|
|
13
|
+
const cwd = cwdOfSession(ctx, session)
|
|
14
|
+
if (!session || !cwd) throw new Error('会话不存在或没有工作区')
|
|
15
|
+
await bucketOf(registry, ctx, cwd)
|
|
16
|
+
return cwd
|
|
17
|
+
}
|
|
18
|
+
return {
|
|
19
|
+
'edrv.review.list': async (args) => {
|
|
20
|
+
const cwd = await workspace(args.sessionId)
|
|
21
|
+
return { ok: true, page: reviewPage(cwd, args.cursor, args.history, args.path) }
|
|
22
|
+
},
|
|
23
|
+
'edrv.review.detail': async (args) => {
|
|
24
|
+
const cwd = await workspace(args.sessionId)
|
|
25
|
+
const detail = reviewDetail(cwd, args.id)
|
|
26
|
+
return detail ? { ok: true, detail } : { ok: false, error: '官方审查记录不存在' }
|
|
27
|
+
},
|
|
28
|
+
'edrv.review.decide': async (args) => {
|
|
29
|
+
const cwd = await workspace(args.sessionId)
|
|
30
|
+
return { ok: true, ...decideReview(cwd, args) }
|
|
31
|
+
},
|
|
32
|
+
}
|
|
33
|
+
}
|
package/src/rpc.ts
CHANGED
|
@@ -23,6 +23,7 @@ import { cp, mkdir, readFile, rename, rm, stat, writeFile, readdir } from 'node:
|
|
|
23
23
|
import { basename, dirname, join } from 'node:path'
|
|
24
24
|
import { archiveEntryFor, markDecision, recordResolved, reconstructOriginal } from './model.js'
|
|
25
25
|
import { clearRevertState, listArchives, pageArchives, readArchives, revertState, setRevertState, startReverts } from './archiveDb.js'
|
|
26
|
+
import { hasOfficial } from './reviewDb.js'
|
|
26
27
|
import { log } from './log.js'
|
|
27
28
|
import type { Registry } from './registry.js'
|
|
28
29
|
import { bucketOf, cwdOfSession, sessionOf } from './registry.js'
|
|
@@ -57,6 +58,7 @@ import { patchHasPerfConfig, patchInsertPerfConfig, patchRemovePerfConfig, perfC
|
|
|
57
58
|
import type { FileVersions } from './fileVersions.js'
|
|
58
59
|
import { settingsDocPath } from './settingsDocPath.js'
|
|
59
60
|
import { createTextPages } from './textPages.js'
|
|
61
|
+
import { reviewHandlers } from './reviewRpc.js'
|
|
60
62
|
|
|
61
63
|
/** Process-local cursor signer; reads must stay on the Host filesystem capability. */
|
|
62
64
|
const readTextPage = createTextPages()
|
|
@@ -239,17 +241,8 @@ async function afterManualSave(
|
|
|
239
241
|
): Promise<void> {
|
|
240
242
|
// 手动保存后目录树可能变化(新建/删除文件):父目录+祖先进失效,后台自愈。
|
|
241
243
|
invalidateIndex(ctx, sc.cwd, path)
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
for (const rec of bucket.values()) {
|
|
245
|
-
if (rec.path === path && rec.superseded !== true) { rec.superseded = true; rec.at = new Date().toISOString(); changed = true }
|
|
246
|
-
}
|
|
247
|
-
if (changed) {
|
|
248
|
-
const done: DiffRecord[] = []
|
|
249
|
-
for (const rec of bucket.values()) if (rec.path === path && rec.superseded) done.push(rec)
|
|
250
|
-
if (done.length) await archiveRecords(ctx, sc.session, sc.cwd, bucket, done, '被手动编辑覆盖')
|
|
251
|
-
else await saveBucket(ctx, sc.cwd, bucket, sc.session, { upserts: [...bucket.values()].filter((rec) => rec.path === path), removed: [] })
|
|
252
|
-
}
|
|
244
|
+
// Editing disk is not a review decision. Keep all historical pending records;
|
|
245
|
+
// location checks may report conflict but must not silently accept or archive them.
|
|
253
246
|
}
|
|
254
247
|
|
|
255
248
|
/**
|
|
@@ -288,6 +281,10 @@ async function applyDecisions(
|
|
|
288
281
|
results.push({ callId: item.callId, ok: false, error: '记录不存在' })
|
|
289
282
|
continue
|
|
290
283
|
}
|
|
284
|
+
if (hasOfficial(cwd, record.path)) {
|
|
285
|
+
results.push({ callId: item.callId, ok: false, error: '该文件已有官方审查记录;旧工具记录禁止决策或回滚,请使用官方审查入口' })
|
|
286
|
+
continue
|
|
287
|
+
}
|
|
291
288
|
if (record.archived) {
|
|
292
289
|
results.push({ callId: item.callId, ok: false, error: '记录已归档,无法重复决策' })
|
|
293
290
|
continue
|
|
@@ -364,8 +361,8 @@ async function applyDecisions(
|
|
|
364
361
|
// #endregion
|
|
365
362
|
grouped.delete(index)
|
|
366
363
|
try {
|
|
367
|
-
if (outcome.ok
|
|
368
|
-
else if (outcome.error === '找不到该差异块' || outcome.error === '回滚失败:文件不存在'
|
|
364
|
+
if (outcome.ok) setRevertState(cwd, record.callId, scope, hunkIndex, 'applied')
|
|
365
|
+
else if (outcome.stale === true || outcome.error === '找不到该差异块' || outcome.error === '回滚失败:文件不存在'
|
|
369
366
|
|| outcome.error === '回滚失败:文件过大,无法安全定位') {
|
|
370
367
|
clearRevertState(cwd, record.callId, scope, hunkIndex)
|
|
371
368
|
}
|
|
@@ -375,11 +372,8 @@ async function applyDecisions(
|
|
|
375
372
|
continue
|
|
376
373
|
}
|
|
377
374
|
if (!outcome.ok) {
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
continue
|
|
381
|
-
}
|
|
382
|
-
revertedStale = true
|
|
375
|
+
results.push({ callId: item.callId, ok: false, error: outcome.error || '差异已偏移,未修改文件,保留待处理' })
|
|
376
|
+
continue
|
|
383
377
|
}
|
|
384
378
|
}
|
|
385
379
|
const wasResolved = recordResolved(record)
|
|
@@ -595,6 +589,7 @@ export function buildHandlers(
|
|
|
595
589
|
...((svnHandlers ?? {}) as RpcHandlerMap),
|
|
596
590
|
// edrv.dap.* 由 createDapRpc 提供(调试会话单例),这里并入。
|
|
597
591
|
...((dapHandlers ?? {}) as RpcHandlerMap),
|
|
592
|
+
...(reviewHandlers(ctx, registry) as RpcHandlerMap),
|
|
598
593
|
'edrv.list': async (args) => {
|
|
599
594
|
// #region debug log
|
|
600
595
|
const start = performance.now()
|
|
@@ -945,6 +940,7 @@ export function buildHandlers(
|
|
|
945
940
|
if (!fs) return { ok: false, error: '缺少 fs' }
|
|
946
941
|
const batch = args.batch
|
|
947
942
|
const bucket = await bucketOf(registry, ctx, sc.cwd)
|
|
943
|
+
if (hasOfficial(sc.cwd, args.path)) return { ok: false, error: '该文件已有官方审查记录,禁止使用旧版批次回滚覆盖当前文件' }
|
|
948
944
|
try {
|
|
949
945
|
let affected: DiffRecord[] = []
|
|
950
946
|
let beforeRec: DiffRecord | null = null
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/** Official facts are immutable; review decisions never imply a disk version. @author ddj 2026年10月26号 */
|
|
2
|
+
export interface ChangedFile {
|
|
3
|
+
path: string
|
|
4
|
+
display: string
|
|
5
|
+
added: number
|
|
6
|
+
deleted: number
|
|
7
|
+
binary?: true
|
|
8
|
+
oversized?: true
|
|
9
|
+
}
|
|
10
|
+
export interface ReviewHunk {
|
|
11
|
+
oldStart: number
|
|
12
|
+
oldLines: number
|
|
13
|
+
newStart: number
|
|
14
|
+
newLines: number
|
|
15
|
+
lines: string[]
|
|
16
|
+
}
|
|
17
|
+
export type OfficialDiff = { kind: 'text'; path: string; display: string; before: boolean; after: boolean; hunks: ReviewHunk[]; coarse: boolean }
|
|
18
|
+
| { kind: 'binary' | 'oversized'; path: string; display: string }
|
|
19
|
+
export interface OfficialSummary {
|
|
20
|
+
turn: number
|
|
21
|
+
cwd: string
|
|
22
|
+
files: ChangedFile[]
|
|
23
|
+
total: number
|
|
24
|
+
added: number
|
|
25
|
+
deleted: number
|
|
26
|
+
snapshot?: { before: string; after: string }
|
|
27
|
+
}
|
|
28
|
+
export interface ReviewSnapshot {
|
|
29
|
+
sourceKind: 'official'
|
|
30
|
+
sessionId: string
|
|
31
|
+
turn: number
|
|
32
|
+
seq: number
|
|
33
|
+
capturedAt: string
|
|
34
|
+
summary: OfficialSummary
|
|
35
|
+
files: { identity: string; diff: OfficialDiff }[]
|
|
36
|
+
}
|
|
37
|
+
export type ReviewDecision = 'pending' | 'accepted' | 'rejected'
|
|
38
|
+
export interface ReviewItem {
|
|
39
|
+
id: string
|
|
40
|
+
sessionId: string
|
|
41
|
+
turn: number
|
|
42
|
+
seq: number
|
|
43
|
+
path: string
|
|
44
|
+
currentPath: string
|
|
45
|
+
display: string
|
|
46
|
+
added: number
|
|
47
|
+
deleted: number
|
|
48
|
+
kind: OfficialDiff['kind']
|
|
49
|
+
decision: ReviewDecision
|
|
50
|
+
revision: number
|
|
51
|
+
current: boolean
|
|
52
|
+
}
|
|
53
|
+
export interface ReviewDetail {
|
|
54
|
+
item: ReviewItem
|
|
55
|
+
diff: OfficialDiff
|
|
56
|
+
capturedAt: string
|
|
57
|
+
capability: { keep: boolean; undo: boolean; reason: string }
|
|
58
|
+
}
|
|
59
|
+
export interface CaptureGap {
|
|
60
|
+
sessionId: string
|
|
61
|
+
turn: number
|
|
62
|
+
seq: number
|
|
63
|
+
error: string
|
|
64
|
+
}
|
|
65
|
+
export interface ReviewFile {
|
|
66
|
+
sourceKind: 'official'
|
|
67
|
+
path: string
|
|
68
|
+
label: string
|
|
69
|
+
pending: number
|
|
70
|
+
}
|
|
71
|
+
export interface ReviewOverview {
|
|
72
|
+
files: ReviewFile[]
|
|
73
|
+
/** Every official identity, including decided files: never revive legacy actions for them. */
|
|
74
|
+
coveredPaths: string[]
|
|
75
|
+
}
|
|
76
|
+
export interface ReviewPage {
|
|
77
|
+
overview?: ReviewOverview
|
|
78
|
+
items: ReviewItem[]
|
|
79
|
+
pending: number
|
|
80
|
+
nextCursor: number | null
|
|
81
|
+
gaps: CaptureGap[]
|
|
82
|
+
}
|
|
83
|
+
export interface ReviewRequest { sessionId: string }
|
|
84
|
+
export interface DecideReview extends ReviewRequest {
|
|
85
|
+
id: string
|
|
86
|
+
seq: number
|
|
87
|
+
revision: number
|
|
88
|
+
operationId: string
|
|
89
|
+
decision: 'accepted' | 'rejected'
|
|
90
|
+
}
|
|
91
|
+
export type ReviewResult<T> = ({ ok: true } & T) | { ok: false; error: string }
|
package/src/shared/rpc.ts
CHANGED
|
@@ -25,6 +25,7 @@ import { DAP_ACTIONS } from './dap.js'
|
|
|
25
25
|
import { appUrl } from './appUrl.js'
|
|
26
26
|
import type { LoggerLevel } from './logger.js'
|
|
27
27
|
import type { SvnViewRequest, SvnViewPayload } from './svnExplorer.js'
|
|
28
|
+
import type { DecideReview, ReviewDetail, ReviewPage, ReviewRequest } from './review.js'
|
|
28
29
|
|
|
29
30
|
/** webServer 精确路由。 */
|
|
30
31
|
export const RPC_PATH = '/edrv/rpc'
|
|
@@ -368,6 +369,9 @@ export interface UiStatePatchInput {
|
|
|
368
369
|
|
|
369
370
|
/** 每个方法的请求参数(sessionId 为公共可选字段)。 */
|
|
370
371
|
export interface RpcRequestMap {
|
|
372
|
+
'edrv.review.list': ReviewRequest & { cursor?: number; history?: boolean; path?: string }
|
|
373
|
+
'edrv.review.detail': ReviewRequest & { id: string }
|
|
374
|
+
'edrv.review.decide': DecideReview
|
|
371
375
|
'edrv.list': { sessionId?: string; callIds?: string[]; skipStale?: boolean }
|
|
372
376
|
'edrv.accept': { sessionId?: string; callId: string; scope?: RpcScope; hunkIndex?: number }
|
|
373
377
|
'edrv.reject': { sessionId?: string; callId: string; scope?: RpcScope; hunkIndex?: number }
|
|
@@ -537,6 +541,9 @@ export type RpcMethod = keyof RpcRequestMap
|
|
|
537
541
|
|
|
538
542
|
/** 每个方法成功(ok:true)时的附加载荷。 */
|
|
539
543
|
export interface RpcOkMap {
|
|
544
|
+
'edrv.review.list': { page: ReviewPage }
|
|
545
|
+
'edrv.review.detail': { detail: ReviewDetail }
|
|
546
|
+
'edrv.review.decide': { revision: number }
|
|
540
547
|
'edrv.list': { records: RecordView[] }
|
|
541
548
|
'edrv.accept': { record: RecordView }
|
|
542
549
|
'edrv.reject': { record: RecordView }
|