dsh-vscode-mode 0.1.17 → 0.1.19

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/src/rpc.ts CHANGED
@@ -4,7 +4,7 @@
4
4
  * 新功能 = shared/rpc.ts 加方法 + 这里加一个 handler,不改其他模块。
5
5
  * 作者 ddj 2026-08-20
6
6
  */
7
- import type { RpcHandlerMap, RpcMethod, RpcRequestMap, RpcResult } from './shared/rpc.js'
7
+ import type { DecideItem, DecideResult, RpcHandlerMap, RpcMethod, RpcRequestMap, RpcResult, RpcScope } from './shared/rpc.js'
8
8
  import type { DiffRecord, RecordView } from './shared/types.js'
9
9
  import type { Ctx, Session } from './store.js'
10
10
  import {
@@ -66,6 +66,52 @@ async function requireSession(ctx: Ctx, sessionId: string | undefined): Promise<
66
66
  return { session, cwd }
67
67
  }
68
68
 
69
+ /**
70
+ * 批量决策核心:一次会话/桶解析 + 逐项处理 + 统一落盘(accept/reject 单条与 decideBatch 共用)。
71
+ * rejected 项先做回滚,失败记入该项 error 且不改决策;本批次新增"已解决"记录一次性归档,
72
+ * 有任何成功项则整桶只写盘一次,避免逐条读写整个 sidecar。
73
+ * @author ddj 2026年08月25号
74
+ * @param items 决策项(按数组顺序处理,rejected 的先后即回滚顺序)
75
+ * @returns 逐项结果(ok 项含更新后的记录视图)
76
+ */
77
+ async function applyDecisions(
78
+ ctx: Ctx,
79
+ session: Session,
80
+ cwd: string,
81
+ bucket: Map<string, DiffRecord>,
82
+ items: DecideItem[],
83
+ ): Promise<DecideResult[]> {
84
+ const results: DecideResult[] = []
85
+ const resolved: DiffRecord[] = []
86
+ let changed = false
87
+ for (const item of items) {
88
+ const record = bucket.get(item.callId)
89
+ if (!record) {
90
+ results.push({ callId: item.callId, ok: false, error: '记录不存在' })
91
+ continue
92
+ }
93
+ if (item.decision === 'rejected') {
94
+ const scope: RpcScope = item.scope ?? 'call'
95
+ const outcome = scope === 'hunk' ? await revertHunk(ctx, session, record, item.hunkIndex ?? -1) : await revertCall(ctx, session, record)
96
+ if (!outcome.ok) {
97
+ results.push({ callId: item.callId, ok: false, error: outcome.error })
98
+ continue
99
+ }
100
+ }
101
+ const wasResolved = recordResolved(record)
102
+ markDecision(record, item.scope ?? 'call', item.hunkIndex, item.decision)
103
+ changed = true
104
+ if (!wasResolved && recordResolved(record)) resolved.push(record)
105
+ results.push({ callId: item.callId, ok: true, record: recView(record) })
106
+ }
107
+ if (resolved.length) {
108
+ const reason = items.some((item) => item.decision === 'rejected') ? '已处理(回滚)' : '已处理'
109
+ await archiveRecords(ctx, session, cwd, bucket, resolved, reason, { deferSave: true })
110
+ }
111
+ if (changed) await saveBucket(ctx, cwd, bucket, session)
112
+ return results
113
+ }
114
+
69
115
  /** 各方法 handler 表(类型由 shared/rpc 的 RpcHandlerMap 约束)。 */
70
116
  export function buildHandlers(ctx: Ctx, registry: Registry, searcher = newSearcher(ctx)): RpcHandlerMap {
71
117
  return {
@@ -74,7 +120,7 @@ export function buildHandlers(ctx: Ctx, registry: Registry, searcher = newSearch
74
120
  if ('err' in sc) return { ok: false, error: sc.err }
75
121
  const bucket = await bucketOf(registry, ctx, sc.cwd)
76
122
  const want = Array.isArray(args.callIds) ? new Set(args.callIds) : null
77
- if (!want) await autoArchiveStale(ctx, sc.session, sc.cwd, bucket) // 全量轮询时自动清理 stale 幽灵差异
123
+ if (!want && args.skipStale !== true) await autoArchiveStale(ctx, sc.session, sc.cwd, bucket) // 全量轮询时自动清理 stale 幽灵差异(批量决策后的即时刷新可 skipStale 跳过)
78
124
  const out: RecordView[] = []
79
125
  for (const rec of bucket.values()) {
80
126
  // 面板全量查询过滤已归档;聊天条按 callId 查询保留(状态徽章仍需正确显示)
@@ -89,27 +135,26 @@ export function buildHandlers(ctx: Ctx, registry: Registry, searcher = newSearch
89
135
  const sc = await requireSession(ctx, args.sessionId)
90
136
  if ('err' in sc) return { ok: false, error: sc.err }
91
137
  const bucket = await bucketOf(registry, ctx, sc.cwd)
92
- const record = bucket.get(args.callId)
93
- if (!record) return { ok: false, error: '记录不存在' }
94
- markDecision(record, args.scope ?? 'call', args.hunkIndex, 'accepted')
95
- if (recordResolved(record)) await archiveRecords(ctx, sc.session, sc.cwd, bucket, [record], '已处理')
96
- else await saveBucket(ctx, sc.cwd, bucket, sc.session)
97
- return { ok: true, record: recView(record) }
138
+ const results = await applyDecisions(ctx, sc.session, sc.cwd, bucket, [{ callId: args.callId, scope: args.scope, hunkIndex: args.hunkIndex, decision: 'accepted' }])
139
+ const item = results[0]
140
+ if (!item?.ok) return { ok: false, error: item?.error ?? '操作失败' }
141
+ return { ok: true, record: item.record! }
98
142
  },
99
143
  'edrv.reject': async (args) => {
100
144
  const sc = await requireSession(ctx, args.sessionId)
101
145
  if ('err' in sc) return { ok: false, error: sc.err }
102
146
  const bucket = await bucketOf(registry, ctx, sc.cwd)
103
- const record = bucket.get(args.callId)
104
- if (!record) return { ok: false, error: '记录不存在' }
105
- const scope = args.scope ?? 'call'
106
- const outcome = scope === 'hunk' ? await revertHunk(ctx, sc.session, record, args.hunkIndex ?? -1) : await revertCall(ctx, sc.session, record)
107
- if (!outcome.ok) return { ok: false, error: outcome.error }
108
- markDecision(record, scope, args.hunkIndex, 'rejected')
109
- record.at = new Date().toISOString()
110
- if (recordResolved(record)) await archiveRecords(ctx, sc.session, sc.cwd, bucket, [record], '已处理(回滚)')
111
- else await saveBucket(ctx, sc.cwd, bucket, sc.session)
112
- return { ok: true, record: recView(record) }
147
+ const results = await applyDecisions(ctx, sc.session, sc.cwd, bucket, [{ callId: args.callId, scope: args.scope, hunkIndex: args.hunkIndex, decision: 'rejected' }])
148
+ const item = results[0]
149
+ if (!item?.ok) return { ok: false, error: item?.error ?? '操作失败' }
150
+ return { ok: true, record: item.record! }
151
+ },
152
+ 'edrv.decideBatch': async (args) => {
153
+ const sc = await requireSession(ctx, args.sessionId)
154
+ if ('err' in sc) return { ok: false, error: sc.err }
155
+ const bucket = await bucketOf(registry, ctx, sc.cwd)
156
+ const results = await applyDecisions(ctx, sc.session, sc.cwd, bucket, Array.isArray(args.items) ? args.items : [])
157
+ return { ok: true, results }
113
158
  },
114
159
  'edrv.read': async (args) => {
115
160
  const sc = await requireSession(ctx, args.sessionId)
package/src/shared/rpc.ts CHANGED
@@ -21,11 +21,28 @@ export const RPC_PATH = '/edrv/rpc'
21
21
  /** 决策作用域:call=整条记录,hunk=单个差异块。 */
22
22
  export type RpcScope = 'call' | 'hunk'
23
23
 
24
+ /** 一条批量决策项(decideBatch 的 items 元素,与 accept/reject 单条参数同构)。 */
25
+ export interface DecideItem {
26
+ callId: string
27
+ scope?: RpcScope
28
+ hunkIndex?: number
29
+ decision: 'accepted' | 'rejected'
30
+ }
31
+
32
+ /** 批量决策逐项结果。 */
33
+ export interface DecideResult {
34
+ callId: string
35
+ ok: boolean
36
+ error?: string
37
+ record?: RecordView
38
+ }
39
+
24
40
  /** 每个方法的请求参数(sessionId 为公共可选字段)。 */
25
41
  export interface RpcRequestMap {
26
- 'edrv.list': { sessionId?: string; callIds?: string[] }
42
+ 'edrv.list': { sessionId?: string; callIds?: string[]; skipStale?: boolean }
27
43
  'edrv.accept': { sessionId?: string; callId: string; scope?: RpcScope; hunkIndex?: number }
28
44
  'edrv.reject': { sessionId?: string; callId: string; scope?: RpcScope; hunkIndex?: number }
45
+ 'edrv.decideBatch': { sessionId?: string; items: DecideItem[] }
29
46
  'edrv.read': { sessionId?: string; path: string }
30
47
  'edrv.original': { sessionId?: string; path: string }
31
48
  'edrv.save': { sessionId?: string; path: string; content: string }
@@ -58,6 +75,7 @@ export interface RpcOkMap {
58
75
  'edrv.list': { records: RecordView[] }
59
76
  'edrv.accept': { record: RecordView }
60
77
  'edrv.reject': { record: RecordView }
78
+ 'edrv.decideBatch': { results: DecideResult[] }
61
79
  'edrv.read': { content: string; size: number }
62
80
  'edrv.original': { content: string; size: number; stale: StaleHunk[]; fallback: boolean }
63
81
  'edrv.save': object
package/src/store.ts CHANGED
@@ -223,7 +223,10 @@ export async function appendArchiveEntries(ctx: Ctx, cwd: string, entries: Array
223
223
  }
224
224
  }
225
225
 
226
- /** 归档记录:标记 archived、按批次写入归档、落盘工作区桶。 */
226
+ /**
227
+ * 归档记录:标记 archived、按批次写入归档、落盘工作区桶。
228
+ * @param opts.deferSave 为 true 时只写归档、不落盘桶(由批量调用方统一 saveBucket 一次)
229
+ */
227
230
  export async function archiveRecords(
228
231
  ctx: Ctx,
229
232
  session: Session,
@@ -231,6 +234,7 @@ export async function archiveRecords(
231
234
  bucket: Map<string, DiffRecord>,
232
235
  recs: DiffRecord[],
233
236
  reason: string,
237
+ opts?: { deferSave?: boolean },
234
238
  ): Promise<void> {
235
239
  if (!recs.length) return
236
240
  const fresh = recs.filter((r) => !r.archived)
@@ -238,5 +242,5 @@ export async function archiveRecords(
238
242
  const entries: Array<ReturnType<typeof archiveEntryFor>> = []
239
243
  for (const list of groupByBatch(recs).values()) entries.push(archiveEntryFor(list, cwd, reason))
240
244
  await appendArchiveEntries(ctx, cwd, entries, session)
241
- if (fresh.length) await saveBucket(ctx, cwd, bucket, session)
245
+ if (fresh.length && !opts?.deferSave) await saveBucket(ctx, cwd, bucket, session)
242
246
  }