@dnalec/dsh-auto-approve 0.1.2 → 0.2.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/src/index.mjs CHANGED
@@ -1,11 +1,11 @@
1
1
  /**
2
2
  * dsh-auto-approve — Host 半。
3
3
  *
4
- * 职责:在 `approval/request` 瀑布上做自动审批门控;人工请求同时走网页框和 QQ。
5
- * 不注入 agent 会话;QQ 入站只进票据经纪。
6
- *
4
+ * 职责:在 `approval/request` 瀑布上做自动审批门控。
5
+ * 允许 / 拒绝直接返回 outcome;转人工则 `await next()` 交给原网页审批框。
6
+ * 不改 req,不 abort req.signal,不平行结算。
7
7
  * 管道(仅当会话预设为「自动审批」):
8
- * 1. 关键词:拒绝 > 人工 > 允许(只匹配工具名 + command + 路径)
8
+ * 1. 关键词:拒绝 > 人工 > 允许(只匹配工具名 + command + 路径 + workdir)
9
9
  * 2. 审核表:模型只输出类别 id,程序按表执行允许 / 拒绝 / 人工
10
10
  * 缺工具参数、解析失败、超时、插件异常 → 转人工,禁止在空卡片上自动放行。
11
11
  *
@@ -19,15 +19,11 @@ import {
19
19
  pathsFor,
20
20
  tryLoadJson,
21
21
  saveJson,
22
- saveSecretJson,
23
22
  audit as appendAudit,
24
23
  readEventsSince,
25
24
  maxEventId,
26
25
  appendEvent,
27
- maskSecret,
28
26
  ensureDir,
29
- forkAbortSignal,
30
- replaceRequestSignal,
31
27
  } from './util.mjs'
32
28
  import {
33
29
  DEFAULT_DENY_KEYWORDS,
@@ -37,6 +33,7 @@ import {
37
33
  normalizeJudgePromptLang,
38
34
  normalizeAllowlist,
39
35
  mergePluginConfig,
36
+ pickMigratablePluginConfig,
40
37
  parseReason,
41
38
  matchKeywordBuckets,
42
39
  buildJudgePrompt,
@@ -54,18 +51,27 @@ import {
54
51
  cloneAllowlist,
55
52
  copyAllowlistInto,
56
53
  mutateAllowlistOp,
54
+ fail,
57
55
  effectiveJudgeTimeoutMs,
58
56
  } from './rules.mjs'
59
57
  import { getSetupState, migratePresetCopy, setAutoApproveSandbox } from './preset-patch.mjs'
60
- import { createTicketBroker, parseApprovalReply, isBindConfirmText, formatPendingList, formatApprovalPush, formatApprovalKeyboard, formatOutcomeLabel } from './tickets.mjs'
61
- import { createQQBot } from './qqbot.mjs'
62
- import { startQqProvisioning } from './provisioning.mjs'
63
58
 
64
59
  export const name = NAME
65
60
  /** webServer 本身不用,只为等 web 起来再注册 RPC。 */
66
61
  export const inject = ['approval', 'permissionPresets', 'llm', 'timer', 'webServer']
67
62
 
68
- /** 设置页 rule-op:改草稿再写盘,失败不碰内存中的活对象。`other` 不能删。 */
63
+ /** Host Session 的工作目录在 header.cwd,没有 session.cwd。 */
64
+ export function readSessionCwd(session) {
65
+ const cwd = session && session.header && typeof session.header.cwd === 'string' ? session.header.cwd : ''
66
+ return cwd
67
+ }
68
+
69
+ /** Connection RPC 失败必须带 message,否则 client parseConnectionResponse 会 TypeError。 */
70
+ export function rpcFail(code, details) {
71
+ const c = String(code || 'err.internal')
72
+ const d = details && typeof details === 'object' && !Array.isArray(details) ? details : {}
73
+ return { ok: false, error: { code: c, message: c, details: d } }
74
+ }
69
75
 
70
76
  /**
71
77
  * 插件入口。热更新规则文件;权限预设写入 profile patch 后需重启才进会话下拉。
@@ -75,7 +81,6 @@ export const inject = ['approval', 'permissionPresets', 'llm', 'timer', 'webServ
75
81
  export function apply(ctx, rawConfig = {}) {
76
82
  const paths = pathsFor()
77
83
  ensureDir(paths.auto)
78
- ensureDir(paths.bridge)
79
84
  migratePresetCopy(paths.profilePatch)
80
85
 
81
86
  // 损坏的 allowlist 只用内存默认,绝不写盘,避免把用户规则清掉。
@@ -93,12 +98,21 @@ export function apply(ctx, rawConfig = {}) {
93
98
  const loadedPlugin = tryLoadJson(paths.pluginConfig)
94
99
  let pluginCfgCorrupt = false
95
100
  let pluginCfg
101
+ let migratedPlugin = false
96
102
  if (!loadedPlugin.ok) {
97
103
  console.error(`[${NAME}] 插件配置无法读取,本进程用默认且不覆盖磁盘`, loadedPlugin.error)
98
104
  pluginCfgCorrupt = true
99
105
  pluginCfg = mergePluginConfig(rawConfig, null)
106
+ } else if (loadedPlugin.missing) {
107
+ const legacy = tryLoadJson(paths.legacyPluginConfig)
108
+ const picked = (legacy.ok && !legacy.missing) ? pickMigratablePluginConfig(legacy.value) : null
109
+ pluginCfg = mergePluginConfig(rawConfig, picked)
110
+ if (picked && saveJson(paths.pluginConfig, pluginCfg)) {
111
+ migratedPlugin = true
112
+ console.log(`[${NAME}] 已从 approval-bridge/config.json 迁移判定配置到 auto-approve/config.json`)
113
+ }
100
114
  } else {
101
- pluginCfg = mergePluginConfig(rawConfig, loadedPlugin.missing ? null : loadedPlugin.value)
115
+ pluginCfg = mergePluginConfig(rawConfig, loadedPlugin.value)
102
116
  }
103
117
  if (loadedAllowlist.ok && loadedAllowlist.missing && Number(pluginCfg.judge.timeoutMs) > 0) {
104
118
  allowlist.judgeTimeoutMs = Number(pluginCfg.judge.timeoutMs)
@@ -109,14 +123,20 @@ export function apply(ctx, rawConfig = {}) {
109
123
  if (loadedAllowlist.ok && (loadedAllowlist.missing || diskVersion < allowlist.version)) {
110
124
  saveJson(paths.allowlist, allowlist)
111
125
  }
112
- const presetSetup = setAutoApproveSandbox(paths.profilePatch, pluginCfg.presetSandbox)
113
- if (presetSetup.ok && presetSetup.needRestart) {
114
- console.log(`[${NAME}] 已写入 auto-approve 权限预设(sandbox=${pluginCfg.presetSandbox});live patch 重载后会话权限会出现「自动审批」`)
115
- } else if (!presetSetup.ok) {
116
- console.error(`[${NAME}] 写入 auto-approve 预设失败`, presetSetup.error)
126
+ // 损坏配置不写沙箱。缺失配置若 patch 里已有 auto-approve,也不用默认 workspace-write 去加宽。
127
+ if (!pluginCfgCorrupt) {
128
+ const already = getSetupState(paths.profilePatch).configured
129
+ const writeSandbox = (loadedPlugin.ok && !loadedPlugin.missing) || migratedPlugin || !already
130
+ if (writeSandbox) {
131
+ const presetSetup = setAutoApproveSandbox(paths.profilePatch, pluginCfg.presetSandbox)
132
+ if (presetSetup.ok && presetSetup.needRestart) {
133
+ console.log(`[${NAME}] 已写入 auto-approve 权限预设(sandbox=${pluginCfg.presetSandbox});live patch 重载后会话权限会出现「自动审批」`)
134
+ } else if (!presetSetup.ok) {
135
+ console.error(`[${NAME}] 写入 auto-approve 预设失败`, presetSetup.code || presetSetup.error)
136
+ }
137
+ }
117
138
  }
118
139
  let eventSeq = maxEventId(paths.events)
119
- const tickets = createTicketBroker()
120
140
  let rpcTail = Promise.resolve()
121
141
  function enqueueRpc(fn) {
122
142
  const run = rpcTail.then(fn, fn)
@@ -129,13 +149,11 @@ export function apply(ctx, rawConfig = {}) {
129
149
  * key 为 sessionId:callId,避免多会话共用 call-0 互相覆盖。
130
150
  */
131
151
  const pendingCalls = new Map()
132
- /** @type {{ chatId: string, userId: string, username?: string, isGroup: boolean, ts: string, preview: string }[]} */
133
- const recentChats = []
134
- let qq = null
135
- let bindHintAt = 0
136
-
137
152
  const log = (line) => console.log(`[${NAME}] ${line}`)
138
153
  const audit = (line) => appendAudit(paths.audit, line)
154
+ const llm = ctx.llm
155
+ const permissionPresets = ctx.permissionPresets
156
+ const agentDefaultModel = ctx.get('agentDefaultModel')
139
157
 
140
158
  function reloadAllowlist() {
141
159
  const loaded = tryLoadJson(paths.allowlist)
@@ -170,12 +188,12 @@ export function apply(ctx, rawConfig = {}) {
170
188
 
171
189
  function applyRuleOp(op, kind, value) {
172
190
  if (allowlistCorrupt && op !== 'reset') {
173
- return { ok: false, error: '规则文件损坏,拒绝覆盖。请先「恢复默认」写回出厂规则,或修好磁盘上的 allowlist.json' }
191
+ return fail('err.allowlistCorrupt')
174
192
  }
175
193
  const draft = cloneAllowlist(allowlist)
176
194
  const result = mutateAllowlistOp(draft, op, kind, value)
177
195
  if (!result.ok) return result
178
- if (!saveJson(paths.allowlist, draft)) return { ok: false, error: '写入 allowlist 失败' }
196
+ if (!saveJson(paths.allowlist, draft)) return fail('err.allowlistWrite')
179
197
  copyAllowlistInto(allowlist, draft)
180
198
  allowlistCorrupt = false
181
199
  if (result.auditLine) audit(result.auditLine)
@@ -202,6 +220,10 @@ export function apply(ctx, rawConfig = {}) {
202
220
  put('reason', 600)
203
221
  put('raw', 800)
204
222
  put('error', 400)
223
+ put('errorCode', 80)
224
+ put('errorMs', 20)
225
+ put('errorDetail', 400)
226
+ put('errorEffort', 40)
205
227
  put('failed', 0, true)
206
228
  put('timedOut', 0, true)
207
229
  return Object.keys(out).length ? out : undefined
@@ -224,7 +246,6 @@ export function apply(ctx, rawConfig = {}) {
224
246
  if (o.category) ev.category = o.category
225
247
  if (o.judgeReason) ev.judgeReason = String(o.judgeReason).slice(0, 600)
226
248
  if (o.path) ev.path = o.path
227
- if (o.ticket !== undefined) ev.ticket = o.ticket
228
249
  if (o.source) ev.source = o.source
229
250
  if (o.cwd) ev.cwd = String(o.cwd).slice(0, 400)
230
251
  if (o.keyword) ev.keyword = String(o.keyword).slice(0, 120)
@@ -240,260 +261,28 @@ export function apply(ctx, rawConfig = {}) {
240
261
  return ev
241
262
  }
242
263
 
243
- let qqbotCorrupt = false
244
- function loadQqCreds() {
245
- const loaded = tryLoadJson(paths.qqbot)
246
- if (!loaded.ok) {
247
- qqbotCorrupt = true
248
- return { appId: '', appSecret: '', ownerUserOpenid: '' }
249
- }
250
- qqbotCorrupt = false
251
- const data = loaded.value || {}
252
- return {
253
- appId: String(data.appId || ''),
254
- appSecret: String(data.appSecret || ''),
255
- ownerUserOpenid: String(data.ownerUserOpenid || ''),
256
- }
257
- }
258
-
259
- function persistQqCreds(next) {
260
- saveSecretJson(paths.qqbot, next)
261
- qqbotCorrupt = false
262
- }
263
-
264
- function rememberChat(msg) {
265
- const preview = String(msg.text || '').slice(0, 80)
266
- const ts = new Date().toISOString()
267
- const idx = recentChats.findIndex((c) => c.chatId === msg.chatId && c.userId === msg.userId)
268
- const row = {
269
- chatId: msg.chatId,
270
- userId: msg.userId,
271
- username: msg.username,
272
- isGroup: Boolean(msg.isGroup),
273
- ts,
274
- preview,
275
- }
276
- if (idx >= 0) recentChats.splice(idx, 1)
277
- recentChats.unshift(row)
278
- if (recentChats.length > 20) recentChats.length = 20
279
- }
280
-
281
- async function qqSend(chatId, text, extra) {
282
- if (!qq || !chatId) return false
283
- try {
284
- await qq.send(chatId, text, extra)
285
- return true
286
- } catch (error) {
287
- console.error(`[${NAME}] QQ 发送失败`, error)
288
- return false
289
- }
290
- }
291
-
292
- function inboundAllowed(msg) {
293
- reloadPluginCfg()
294
- const { chatId, userId } = pluginCfg.notify
295
- if (!chatId) return { kind: 'unbound' }
296
- if (msg.chatId !== chatId) return { kind: 'ignore' }
297
- if (String(chatId).startsWith('g:')) {
298
- if (!userId || msg.userId !== userId) return { kind: 'ignore' }
299
- }
300
- return { kind: 'ok' }
301
- }
302
-
303
- async function handleInbound(msg) {
304
- rememberChat(msg)
305
- const gate = inboundAllowed(msg)
306
- if (gate.kind === 'ignore') return
307
-
308
- if (gate.kind === 'unbound') {
309
- // 私人 bot:未绑定 chatId 时,任意 C2C「是」绑定该聊天。文档已标明。
310
- if (msg.isGroup) return
311
- const yes = isBindConfirmText(msg.text)
312
- if (yes) {
313
- pluginCfg.notify.chatId = msg.chatId
314
- if (!persistPluginCfg()) {
315
- await qqSend(msg.chatId, '绑定失败:无法写入配置文件。')
316
- return
317
- }
318
- await qqSend(msg.chatId, '已将本聊天设为审批通知目标。之后人工审批会推到这里。')
319
- audit(`QQBIND chatId=${msg.chatId}`)
320
- return
321
- }
322
- const now = Date.now()
323
- if (now - bindHintAt > 60_000) {
324
- bindHintAt = now
325
- await qqSend(msg.chatId, '是否将本聊天用作审批通知?回复「是」确认。')
326
- }
327
- return
328
- }
329
-
330
- const parsed = parseApprovalReply(msg.text, tickets.pendingCount())
331
- if (!parsed) return
332
-
333
- const reply = async (text) => { await qqSend(msg.chatId, text) }
334
-
335
- if (parsed.kind === 'need-number') {
336
- await reply('有多条待处理审批,请带短号。' + formatPendingList(tickets.listPending()))
337
- return
338
- }
339
-
340
- let n = parsed.n
341
- if (parsed.kind === 'allow-bare' || parsed.kind === 'reject-bare') {
342
- const sole = tickets.solePending()
343
- if (!sole) {
344
- await reply('当前没有待处理审批')
345
- return
346
- }
347
- n = sole.n
348
- }
349
-
350
- const recent = tickets.recentOutcome(n)
351
- if (recent) {
352
- await reply(`#${n} 已处理`)
353
- return
354
- }
355
- const ticket = tickets.get(n)
356
- if (!ticket) {
357
- await reply(`没有待处理的 #${n},${formatPendingList(tickets.listPending())}`)
358
- return
359
- }
360
-
361
- const allow = parsed.kind === 'allow' || parsed.kind === 'allow-bare'
362
- const ok = tickets.answer(n, {
363
- outcome: allow ? 'allowed-once' : 'rejected',
364
- })
365
- if (!ok) await reply(`没有待处理的 #${n}`)
366
- }
367
-
368
- let qqGen = 0
369
- /** @type {{ controller: AbortController, handle?: { cancel: () => void }, status: string, qrDataUrl: string, expiresAt: number, error: string } | null} */
370
- let provisionAttempt = null
371
-
372
- function provisionSnapshot() {
373
- const a = provisionAttempt
374
- if (!a) return { status: '', qrDataUrl: '', expiresAt: 0, error: '' }
375
- return {
376
- status: a.status || '',
377
- qrDataUrl: a.qrDataUrl || '',
378
- expiresAt: a.expiresAt || 0,
379
- error: a.error || '',
380
- }
381
- }
382
-
383
- async function cancelProvisioning() {
384
- const attempt = provisionAttempt
385
- provisionAttempt = null
386
- if (!attempt) return
387
- try { attempt.controller.abort() } catch { /* ignore */ }
388
- try { await Promise.resolve(attempt.handle && attempt.handle.cancel()) } catch { /* ignore */ }
389
- }
390
-
391
- async function startProvisioning() {
392
- await cancelProvisioning()
393
- const controller = new AbortController()
394
- const attempt = {
395
- controller,
396
- status: '登录中',
397
- qrDataUrl: '',
398
- expiresAt: 0,
399
- error: '',
400
- }
401
- provisionAttempt = attempt
264
+ function emitDecision(leaf) {
402
265
  try {
403
- const handle = await startQqProvisioning({
404
- onQr(qr) {
405
- if (provisionAttempt !== attempt) return
406
- attempt.qrDataUrl = qr.dataUrl
407
- attempt.expiresAt = qr.expiresAt
408
- attempt.status = '等待扫码'
409
- },
410
- onStatus(status) {
411
- if (provisionAttempt === attempt) attempt.status = status
412
- },
413
- async onCredentials(credentials) {
414
- if (provisionAttempt !== attempt) return
415
- attempt.status = '保存凭据'
416
- const appId = String(credentials.appId || '')
417
- const appSecret = String(credentials.appSecret || '')
418
- const owner = String(credentials.ownerUserOpenid || '')
419
- const prev = loadQqCreds()
420
- persistQqCreds({
421
- appId,
422
- appSecret,
423
- ownerUserOpenid: owner || prev.ownerUserOpenid || '',
424
- })
425
- reloadPluginCfg()
426
- if (owner && !pluginCfg.notify.chatId) {
427
- pluginCfg.notify.chatId = owner
428
- persistPluginCfg()
429
- audit(`QQBIND chatId=${owner} (扫码者)`)
430
- }
431
- audit('CONFIG qqbot 扫码凭据已保存并重连')
432
- await startQq()
433
- if (provisionAttempt !== attempt) return
434
- attempt.status = '已连接'
435
- attempt.qrDataUrl = ''
436
- attempt.expiresAt = 0
437
- },
438
- onFailure(error) {
439
- if (provisionAttempt !== attempt) return
440
- const aborted = controller.signal.aborted
441
- const detail = error instanceof Error ? error.message : String(error)
442
- log(`扫码失败: ${detail}`)
443
- attempt.status = aborted ? '已取消' : '扫码失败'
444
- attempt.error = aborted ? '' : '平台扫码服务暂时不可用或二维码已过期,请重新扫码。'
445
- },
446
- }, controller.signal)
447
- if (provisionAttempt === attempt) attempt.handle = handle
448
- return { ok: true }
266
+ ctx.emit('auto-approve/decision', leaf)
449
267
  } catch (error) {
450
- const detail = error instanceof Error ? error.message : String(error)
451
- log(`扫码启动失败: ${detail}`)
452
- const missing = /未安装|ERR_MODULE_NOT_FOUND|Cannot find/i.test(detail)
453
- const message = missing
454
- ? '未安装扫码依赖。请在插件目录执行 npm install 后重试。'
455
- : '无法启动扫码流程,请检查网络后重试。'
456
- if (provisionAttempt === attempt) {
457
- attempt.status = '扫码启动失败'
458
- attempt.error = message
459
- }
460
- return { ok: false, error: message }
268
+ console.error(`[${NAME}] auto-approve/decision 失败`, error)
461
269
  }
462
270
  }
463
271
 
464
- async function startQq() {
465
- const gen = ++qqGen
466
- const prev = qq
467
- qq = null
468
- if (prev) {
469
- try { await prev.stop() } catch (error) {
470
- console.error(`[${NAME}] 停止旧 QQ 连接失败`, error)
471
- }
472
- }
473
- if (gen !== qqGen) return
474
- const creds = loadQqCreds()
475
- const bot = createQQBot(creds, log)
476
- bot.setMessageHandler((m) => { void handleInbound(m).catch((e) => console.error(`[${NAME}] inbound`, e)) })
477
- if (gen !== qqGen) {
478
- try { await bot.stop() } catch { /* superseded */ }
479
- return
480
- }
481
- qq = bot
482
- if (creds.appId && creds.appSecret) void bot.start()
272
+ function decisionLeaf(info, extra) {
273
+ const e = extra || {}
274
+ const leaf = {
275
+ sessionId: String(info.sessionId || ''),
276
+ tool: String(info.toolName || ''),
277
+ path: String(e.path || info.path || ''),
278
+ verdict: String(e.verdict || ''),
279
+ }
280
+ if (e.outcome) leaf.outcome = String(e.outcome)
281
+ if (e.category || info.category) leaf.category = String(e.category || info.category || '')
282
+ if (e.judgeReason || info.judgeReason) leaf.judgeReason = String(e.judgeReason || info.judgeReason || '').slice(0, 600)
283
+ return leaf
483
284
  }
484
285
 
485
- void startQq()
486
- ctx.effect(() => () => {
487
- qqGen += 1
488
- tickets.dispose()
489
- void cancelProvisioning()
490
- void qq?.stop()
491
- })
492
-
493
- const llm = ctx.llm
494
- const permissionPresets = ctx.permissionPresets
495
- const agentDefaultModel = ctx.get('agentDefaultModel')
496
-
497
286
  function fallbackSelection() {
498
287
  try {
499
288
  const sel = agentDefaultModel && typeof agentDefaultModel.currentSelection === 'function'
@@ -518,7 +307,7 @@ export function apply(ctx, rawConfig = {}) {
518
307
  async function resolveJudgeRoute() {
519
308
  const route = configuredRoute()
520
309
  if (!route.provider || !route.model) {
521
- return { ok: false, error: '未配置审核模型', ...route }
310
+ return { ok: false, code: 'err.judgeUnconfigured', ...route }
522
311
  }
523
312
  try {
524
313
  const info = await llm.resolveModelInfo(route.provider, route.model)
@@ -526,11 +315,11 @@ export function apply(ctx, rawConfig = {}) {
526
315
  ? info.reasoning.efforts.map((e) => e.id)
527
316
  : []
528
317
  if (route.reasoningEffort && efforts.length > 0 && !efforts.includes(route.reasoningEffort)) {
529
- return { ok: false, error: `reasoningEffort ${route.reasoningEffort} 不受支持`, ...route }
318
+ return { ok: false, code: 'err.judgeEffort', details: { effort: route.reasoningEffort }, ...route }
530
319
  }
531
320
  return { ok: true, ...route, info }
532
321
  } catch (error) {
533
- return { ok: false, error: String((error && error.message) || error), ...route }
322
+ return { ok: false, code: 'err.judgeUpstream', details: { error: String((error && error.message) || error) }, ...route }
534
323
  }
535
324
  }
536
325
 
@@ -557,7 +346,10 @@ export function apply(ctx, rawConfig = {}) {
557
346
  if (chunk.type === 'text-delta') text += chunk.text
558
347
  else if (chunk.type === 'finish' && (chunk.reason.kind === 'error' || chunk.reason.kind === 'aborted')) {
559
348
  const failure = chunk.reason.failure && chunk.reason.failure.message ? chunk.reason.failure.message : chunk.reason.kind
560
- throw new Error('审核模型调用失败: ' + failure)
349
+ const err = new Error('err.judgeCall')
350
+ err.code = 'err.judgeCall'
351
+ err.details = { error: String(failure) }
352
+ throw err
561
353
  }
562
354
  }
563
355
  return text
@@ -601,27 +393,37 @@ export function apply(ctx, rawConfig = {}) {
601
393
  try {
602
394
  const first = await runOnce()
603
395
  if (!first.timedOut) return first
604
- last = { failed: true, timedOut: true, error: `超时(${timeoutMs}ms)` }
396
+ last = { failed: true, timedOut: true, errorCode: 'err.judgeTimeout', error: 'err.judgeTimeout', errorMs: String(timeoutMs) }
605
397
  console.warn(`[${NAME}] ${label} 超时(${timeoutMs}ms),转人工`)
606
398
  return last
607
399
  } catch (error) {
608
400
  last = {
609
401
  failed: true,
610
- error: String((error && error.message) || error),
402
+ errorCode: error && error.code ? error.code : 'err.judgeFailed',
403
+ error: error && error.code ? error.code : 'err.judgeFailed',
404
+ errorDetail: error && error.details && error.details.error ? String(error.details.error) : '',
611
405
  raw: error && error.raw ? String(error.raw).slice(0, 800) : '',
612
406
  }
407
+ const code = error && error.code
408
+ if (code === 'err.judgeParse' || code === 'err.judgeEmpty') {
409
+ console.error(`[${NAME}] ${label} 输出无法解析,转人工`, error)
410
+ return last
411
+ }
613
412
  console.error(`[${NAME}] ${label} 异常,重试 1 次`, error)
614
413
  }
615
414
  try {
616
415
  const second = await runOnce()
617
416
  if (!second.timedOut) return second
618
- last = { failed: true, timedOut: true, error: `重试超时(${timeoutMs}ms)` }
417
+ last = { failed: true, timedOut: true, errorCode: 'err.judgeRetryTimeout', error: 'err.judgeRetryTimeout', errorMs: String(timeoutMs) }
619
418
  console.warn(`[${NAME}] ${label} 重试超时(${timeoutMs}ms)`)
620
419
  return last
621
420
  } catch (error) {
622
421
  last = {
623
422
  failed: true,
624
- error: String((error && error.message) || error),
423
+ errorCode: error && error.code ? error.code : 'err.judgeFailed',
424
+ error: error && error.code ? error.code : 'err.judgeFailed',
425
+ errorDetail: error && error.details && error.details.error ? String(error.details.error) : '',
426
+ errorMs: last.errorMs,
625
427
  raw: error && error.raw ? String(error.raw).slice(0, 800) : last.raw,
626
428
  }
627
429
  console.error(`[${NAME}] ${label} 重试仍异常`, error)
@@ -637,8 +439,8 @@ export function apply(ctx, rawConfig = {}) {
637
439
  effort: route.reasoningEffort || '',
638
440
  }
639
441
  if (!route.ok) {
640
- audit(`FAILED judge route: ${route.error}`)
641
- return { action: 'human', criterion: 'other', reason: '', failed: true, error: route.error, ...meta }
442
+ audit(`FAILED judge route: ${route.code || route.error || ''}`)
443
+ return { action: 'human', criterion: 'other', reason: '', failed: true, errorCode: route.code || 'err.judgeUnconfigured', error: route.code || 'err.judgeUnconfigured', errorDetail: route.details && route.details.error ? String(route.details.error) : '', errorEffort: route.details && route.details.effort ? String(route.details.effort) : '', ...meta }
642
444
  }
643
445
  const timeoutMs = effectiveJudgeTimeoutMs(allowlist, pluginCfg)
644
446
  const result = await withRetry(
@@ -653,7 +455,10 @@ export function apply(ctx, rawConfig = {}) {
653
455
  reason: '',
654
456
  failed: true,
655
457
  timedOut: Boolean(result.timedOut),
656
- error: result.error || '审核失败',
458
+ errorCode: result.errorCode || result.error || 'err.judgeFailed',
459
+ error: result.errorCode || result.error || 'err.judgeFailed',
460
+ errorMs: result.errorMs || '',
461
+ errorDetail: result.errorDetail || '',
657
462
  raw: result.raw || '',
658
463
  ...meta,
659
464
  }
@@ -662,12 +467,10 @@ export function apply(ctx, rawConfig = {}) {
662
467
  return { ...result, action: row.action, label: row.label, ...meta }
663
468
  }
664
469
 
665
- async function applyHumanOutcome(ctxInfo, outcome, extra) {
666
- const { sessionId, toolName, mode, reason, justification, category, path, ticket, args, cwd, judgeReason, judge } = ctxInfo
667
- const source = (extra && extra.source) || 'web'
668
- audit(`OUTCOME ${toolName} outcome=${outcome} source=${source} ticket=${ticket || '-'} | ${reason.slice(0, 80)}`)
669
- const detail = { category, path, ticket, source, args, cwd, judgeReason, judge }
670
-
470
+ function applyHumanOutcome(ctxInfo, outcome) {
471
+ const { sessionId, toolName, mode, reason, justification, category, path, args, cwd, judgeReason, judge } = ctxInfo
472
+ audit(`OUTCOME ${toolName} outcome=${outcome} source=web | ${reason.slice(0, 80)}`)
473
+ const detail = { category, path, source: 'web', args, cwd, judgeReason, judge }
671
474
  if (outcome === 'allowed-once') {
672
475
  recordEvent(sessionId, toolName, mode, reason, justification, 'manual-approved', {
673
476
  kind: 'manual-approved', ...detail,
@@ -685,108 +488,38 @@ export function apply(ctx, rawConfig = {}) {
685
488
  }
686
489
 
687
490
  /**
688
- * 网页 next() 与 QQ 票据竞速,谁先答谁赢。
689
- * 120s 只是 QQ 提醒,不是全局超时;网页框一直等到有人答或会话取消。
491
+ * 转人工:记 pending,再把同一条请求交给瀑布里的下一个 answerer(网页框)。
492
+ * 必须 await next() 并把 outcome 原样返回,观察者才能看到人工结果。
690
493
  */
691
- async function forwardToHuman(info, next, req) {
692
- reloadPluginCfg()
693
- const ticket = tickets.allocate(info)
494
+ async function forwardToHuman(info, next) {
694
495
  recordEvent(info.sessionId, info.toolName, info.mode, info.reason, info.justification, 'manual-pending', {
695
496
  kind: 'manual-pending',
696
497
  category: info.category || '',
697
498
  path: info.path,
698
- ticket: ticket.n,
699
499
  args: info.args,
700
500
  cwd: info.cwd,
701
501
  judgeReason: info.judgeReason,
702
502
  judge: info.judge,
703
503
  })
704
-
705
- const notify = pluginCfg.notify
706
- const timeoutSecs = Number(notify.timeoutSecs) > 0 ? Number(notify.timeoutSecs) : 120
707
- let pushed = false
708
- if (notify.enabled && qq && qq.connected() && notify.chatId) {
709
- const ok = await qqSend(notify.chatId, formatApprovalPush(ticket, timeoutSecs), {
710
- keyboard: formatApprovalKeyboard(
711
- ticket.n,
712
- String(notify.chatId).startsWith('g:') ? notify.userId : '',
713
- ),
714
- })
715
- if (ok) {
716
- pushed = true
717
- audit(`PUSH #${ticket.n} chat=${notify.chatId} ${info.toolName}`)
718
- } else {
719
- audit(`PUSH_FAIL #${ticket.n} ${info.toolName}`)
720
- }
721
- } else {
722
- audit(`PUSH_SKIP #${ticket.n} enabled=${notify.enabled} connected=${Boolean(qq && qq.connected())} chat=${notify.chatId || ''} ${info.toolName}`)
723
- }
724
-
725
- const onAbort = () => {
726
- const live = tickets.get(ticket.n)
727
- if (!live || live.settled) return
728
- tickets.discard(ticket.n, 'abort')
729
- if (pushed) void qqSend(notify.chatId, `#${ticket.n} 已取消`)
730
- }
731
- const requestSignal = req.signal
732
- // 网页框听这个 fork:QQ 先答时 abort 它即可关框,不能 abort 原始 req.signal(整单会 cancelled)。
733
- const webSignal = forkAbortSignal(requestSignal)
734
- replaceRequestSignal(req, webSignal.signal)
735
- if (requestSignal) {
736
- if (requestSignal.aborted) {
737
- onAbort()
738
- webSignal.abort()
739
- await applyHumanOutcome({ ...info, ticket: ticket.n }, 'cancelled', { source: 'abort' })
740
- return 'cancelled'
741
- }
742
- requestSignal.addEventListener('abort', onAbort, { once: true })
743
- }
744
-
745
- let timeoutHandle
746
- if (pushed) {
747
- timeoutHandle = ctx.timeout(() => {
748
- if (ticket.settled) return
749
- void qqSend(notify.chatId, `#${ticket.n} 请在网页继续`)
750
- }, timeoutSecs * 1000)
751
- }
752
-
504
+ emitDecision(decisionLeaf(info, { verdict: 'human', path: info.path }))
753
505
  try {
754
- const webP = Promise.resolve()
755
- .then(() => next())
756
- .then((outcome) => ({ source: 'web', outcome }))
757
- .catch((error) => {
758
- console.error(`[${NAME}] web next() 失败`, error)
759
- return { source: 'web', outcome: 'unavailable' }
760
- })
761
- const imP = ticket.wait.then((result) => result)
762
-
763
- const first = await Promise.race([webP, imP])
764
- if (first.source === 'qq' && first.outcome) {
765
- await applyHumanOutcome({ ...info, ticket: ticket.n }, first.outcome, {
766
- source: 'qq',
767
- })
768
- if (pushed) {
769
- void qqSend(notify.chatId, first.outcome === 'allowed-once'
770
- ? `#${ticket.n} 已批准`
771
- : `#${ticket.n} 已拒绝`)
772
- }
773
- return first.outcome
774
- }
775
- if (first.source === 'web') {
776
- tickets.discard(ticket.n, 'web')
777
- await applyHumanOutcome({ ...info, ticket: ticket.n }, first.outcome, { source: 'web' })
778
- if (pushed) void qqSend(notify.chatId, `#${ticket.n} 已在网页${formatOutcomeLabel(first.outcome)}`)
779
- return first.outcome
506
+ const outcome = await next()
507
+ try {
508
+ applyHumanOutcome(info, outcome)
509
+ emitDecision(decisionLeaf(info, { verdict: 'human', path: info.path, outcome }))
510
+ } catch (error) {
511
+ console.error(`[${NAME}] 记录人工结果失败`, error)
780
512
  }
781
- const outcome = first.outcome || 'cancelled'
782
- await applyHumanOutcome({ ...info, ticket: ticket.n }, outcome, { source: first.source || 'abort' })
783
513
  return outcome
784
- } finally {
785
- webSignal.abort()
786
- if (timeoutHandle && typeof timeoutHandle === 'function') {
787
- try { timeoutHandle() } catch { /* disposer */ }
514
+ } catch (error) {
515
+ console.error(`[${NAME}] 网页审批框失败`, error)
516
+ try {
517
+ applyHumanOutcome(info, 'unavailable')
518
+ emitDecision(decisionLeaf(info, { verdict: 'human', path: info.path, outcome: 'unavailable' }))
519
+ } catch (again) {
520
+ console.error(`[${NAME}] 记录 unavailable 失败`, again)
788
521
  }
789
- if (requestSignal) requestSignal.removeEventListener('abort', onAbort)
522
+ return 'unavailable'
790
523
  }
791
524
  }
792
525
 
@@ -807,6 +540,7 @@ export function apply(ctx, rawConfig = {}) {
807
540
 
808
541
  ctx.on('approval/request', async (req, next) => {
809
542
  let humanFallback = null
543
+ let forwarded = false
810
544
  try {
811
545
  // 非「自动审批」预设交给系统默认 ask,本插件不管。
812
546
  reloadAllowlist()
@@ -827,7 +561,8 @@ export function apply(ctx, rawConfig = {}) {
827
561
  const reason = String(req.reason || '')
828
562
  const { mode, justification } = parseReason(reason)
829
563
  const sessionId = typeof session.id === 'string' ? session.id : ''
830
- const sessionCwd = (typeof session.cwd === 'string' && session.cwd) ? session.cwd : ''
564
+ const sessionCwd = readSessionCwd(session)
565
+
831
566
  const cached = takeCachedCall(pendingCalls, sessionId, req.callId)
832
567
  const toolArgs = pickToolArgs(cached.args || {})
833
568
  const baseInfo = {
@@ -835,35 +570,37 @@ export function apply(ctx, rawConfig = {}) {
835
570
  args: toolArgs,
836
571
  }
837
572
 
838
- const toHuman = (path, category, extra) => forwardToHuman({
839
- ...baseInfo,
840
- path,
841
- category: category || '',
842
- key: extra && extra.key,
843
- confirmed: extra && extra.confirmed,
844
- threshold: extra && extra.threshold,
845
- judgeReason: extra && extra.judgeReason,
846
- judge: extra && extra.judge,
847
- }, next, req)
573
+ const toHuman = (path, category, extra) => {
574
+ forwarded = true
575
+ return forwardToHuman({
576
+ ...baseInfo,
577
+ path,
578
+ category: category || '',
579
+ judgeReason: extra && extra.judgeReason,
580
+ judge: extra && extra.judge,
581
+ }, next)
582
+ }
848
583
  humanFallback = toHuman
849
584
  // 没看见命令/路径就转人工,禁止关键词允许或模型标 safe。
850
585
  if (!cached.found || !hasToolPayload(toolArgs)) {
851
- const why = cached.found ? '工具参数不完整,转人工' : '未捕获工具参数,转人工'
586
+ const why = cached.found ? 'err.missingPayload' : 'err.missingPayloadUncaptured'
852
587
  audit(`HUMAN ${toolName} mode=${mode || 'none'} missing-payload | ${why}`)
853
588
  return toHuman('missing-payload', 'other', { judgeReason: why })
854
589
  }
855
- const hay = formatKeywordHay(toolName, reason, toolArgs)
590
+ const hay = formatKeywordHay(toolName, reason, toolArgs, sessionCwd)
856
591
  const kw = matchKeywordBuckets(hay, allowlist, formatAllowKeywordHay(toolArgs))
592
+
857
593
  const eventDetail = { args: toolArgs, cwd: sessionCwd }
858
594
  if (kw && kw.action === 'reject') {
859
595
  audit(`REJECT ${toolName} mode=${mode || 'none'} keyword | ${reason.slice(0, 160)}`)
860
596
  recordEvent(sessionId, toolName, mode, reason, justification, 'keyword-reject', {
861
597
  kind: 'auto', path: 'keyword-reject', ...eventDetail,
862
598
  })
599
+ emitDecision(decisionLeaf(baseInfo, { verdict: 'keyword-reject', path: 'keyword-reject', outcome: 'rejected' }))
863
600
  return 'rejected'
864
601
  }
865
602
  if (toolArgsTruncated(toolArgs)) {
866
- const why = '工具参数过长已截断,转人工(禁止按前缀自动放行)'
603
+ const why = 'err.truncatedPayload'
867
604
  audit(`HUMAN ${toolName} mode=${mode || 'none'} truncated-payload | ${why}`)
868
605
  return toHuman('truncated-payload', 'other', { judgeReason: why })
869
606
  }
@@ -873,6 +610,7 @@ export function apply(ctx, rawConfig = {}) {
873
610
  recordEvent(sessionId, toolName, mode, reason, justification, 'keyword-allow', {
874
611
  kind: 'auto', path: 'keyword-allow', ...eventDetail,
875
612
  })
613
+ emitDecision(decisionLeaf(baseInfo, { verdict: 'keyword-allow', path: 'keyword-allow', outcome: 'allowed-once' }))
876
614
  return 'allowed-once'
877
615
  }
878
616
  audit(`HUMAN ${toolName} mode=${mode || 'none'} keyword | ${reason.slice(0, 160)}`)
@@ -894,6 +632,9 @@ export function apply(ctx, rawConfig = {}) {
894
632
  recordEvent(sessionId, toolName, mode, reason, justification, 'criteria-reject', {
895
633
  kind: 'auto', category: criterion, path: 'criteria-reject', judgeReason, judge: judged, ...eventDetail,
896
634
  })
635
+ emitDecision(decisionLeaf(baseInfo, {
636
+ verdict: 'criteria-reject', path: 'criteria-reject', outcome: 'rejected', category: criterion, judgeReason,
637
+ }))
897
638
  return 'rejected'
898
639
  }
899
640
  if (judged.action === 'allow') {
@@ -901,16 +642,24 @@ export function apply(ctx, rawConfig = {}) {
901
642
  recordEvent(sessionId, toolName, mode, reason, justification, 'criteria-allow', {
902
643
  kind: 'auto', category: criterion, path: 'criteria-allow', judgeReason, judge: judged, ...eventDetail,
903
644
  })
645
+ emitDecision(decisionLeaf(baseInfo, {
646
+ verdict: 'criteria-allow', path: 'criteria-allow', outcome: 'allowed-once', category: criterion, judgeReason,
647
+ }))
904
648
  return 'allowed-once'
905
649
  }
906
650
  audit(`HUMAN ${toolName} mode=${mode || 'none'} criteria=${criterion} | ${judgeReason || reason.slice(0, 120)}`)
907
651
  return toHuman('criteria-human', criterion, { judgeReason, judge: judged })
908
652
  } catch (error) {
909
653
  console.error(`[${NAME}] 判断过程出错,回退人工`, error)
654
+ if (forwarded) return 'unavailable'
910
655
  if (humanFallback) {
911
656
  try {
912
657
  return await humanFallback('plugin-error', 'other', {
913
- judgeReason: String((error && error.message) || error),
658
+ judgeReason: (error && error.code) || 'err.pluginError',
659
+ judge: {
660
+ errorCode: (error && error.code) || 'err.pluginError',
661
+ errorDetail: String((error && error.message) || error),
662
+ },
914
663
  })
915
664
  } catch (again) {
916
665
  console.error(`[${NAME}] 转人工仍失败,交回系统默认`, again)
@@ -926,7 +675,7 @@ export function apply(ctx, rawConfig = {}) {
926
675
  ctx.inject(['connection'], (c) => {
927
676
  const connection = c.connection
928
677
  if (!connection || !connection.fetch || typeof connection.fetch.register !== 'function') {
929
- console.warn(`[${NAME}] connection.fetch 不可用,设置页/提示条 RPC 未注册(门控与 QQ 仍工作)`)
678
+ console.warn(`[${NAME}] connection.fetch 不可用,设置页/提示条 RPC 未注册(门控仍工作)`)
930
679
  return
931
680
  }
932
681
 
@@ -936,12 +685,11 @@ export function apply(ctx, rawConfig = {}) {
936
685
  reloadPluginCfg()
937
686
  const body = payload && typeof payload === 'object' ? payload : {}
938
687
  if (endpoint === 'snapshot') {
939
- const creds = loadQqCreds()
940
688
  return {
941
689
  ok: true,
942
690
  value: {
943
691
  config: {
944
- version: allowlist.version || 17,
692
+ version: allowlist.version || 18,
945
693
  corrupt: allowlistCorrupt,
946
694
  rejectKeywords: allowlist.rejectKeywords || [],
947
695
  humanKeywords: allowlist.humanKeywords || [],
@@ -959,15 +707,6 @@ export function apply(ctx, rawConfig = {}) {
959
707
  setup: getSetupState(paths.profilePatch),
960
708
  plugin: pluginCfg,
961
709
  pluginCorrupt: pluginCfgCorrupt,
962
- qq: {
963
- status: qq ? qq.status() : '未启动',
964
- connected: Boolean(qq && qq.connected()),
965
- hasSecret: Boolean(creds.appId && creds.appSecret),
966
- credsCorrupt: qqbotCorrupt,
967
- appIdMasked: maskSecret(creds.appId),
968
- recentChats,
969
- provisioning: provisionSnapshot(),
970
- },
971
710
  providers: (() => {
972
711
  try {
973
712
  return (llm.listProviders() || []).map((p) => ({ id: p.id, name: p.name || p.id }))
@@ -980,7 +719,8 @@ export function apply(ctx, rawConfig = {}) {
980
719
  if (endpoint === 'events') {
981
720
  const sessionId = String(body.sessionId || '')
982
721
  if (!sessionId) {
983
- return { ok: false, error: { code: 'events', message: '需要 sessionId', details: {} } }
722
+ return rpcFail('err.needSessionId')
723
+
984
724
  }
985
725
  const since = Number.parseInt(String(body.since || '0'), 10) || 0
986
726
  return { ok: true, value: { events: readEventsSince(paths.events, sessionId, since) } }
@@ -998,30 +738,43 @@ export function apply(ctx, rawConfig = {}) {
998
738
  pluginCfg.judge.timeoutMs = allowlist.judgeTimeoutMs
999
739
  persistPluginCfg()
1000
740
  }
1001
- return result.ok ? { ok: true, value: result } : { ok: false, error: { code: 'rule', message: result.error || '失败', details: {} } }
741
+ return result.ok ? { ok: true, value: result } : rpcFail(result.code || 'err.allowlistWrite', result.details || {})
742
+
1002
743
  }
1003
744
  if (endpoint === 'setup') {
1004
- return { ok: true, value: setAutoApproveSandbox(paths.profilePatch, pluginCfg.presetSandbox) }
745
+ const setupResult = setAutoApproveSandbox(paths.profilePatch, pluginCfg.presetSandbox)
746
+ if (!setupResult.ok) {
747
+ return rpcFail(setupResult.code || 'err.preset', setupResult.details || { error: String(setupResult.error || '') })
748
+
749
+ }
750
+ return { ok: true, value: setupResult }
1005
751
  }
1006
752
  if (endpoint === 'save-plugin') {
1007
753
  if (pluginCfgCorrupt && !body.overwriteCorrupt) {
1008
- return { ok: false, error: { code: 'save', message: '插件配置损坏,拒绝覆盖。请修好磁盘文件,或点「覆盖损坏配置」', details: {} } }
754
+ return rpcFail('err.pluginCorrupt')
755
+
1009
756
  }
1010
757
  const next = mergePluginConfig(pluginCfg, body)
1011
758
  const prev = pluginCfg
1012
759
  pluginCfg = next
1013
760
  if (!persistPluginCfg({ overwriteCorrupt: Boolean(body.overwriteCorrupt) })) {
1014
761
  pluginCfg = prev
1015
- return { ok: false, error: { code: 'save', message: '写入配置失败', details: {} } }
762
+ return rpcFail('err.pluginWrite')
763
+
1016
764
  }
1017
765
  let preset = null
1018
766
  if (body && Object.prototype.hasOwnProperty.call(body, 'presetSandbox')) {
1019
767
  preset = setAutoApproveSandbox(paths.profilePatch, pluginCfg.presetSandbox)
768
+ if (preset && !preset.ok) {
769
+ return rpcFail(preset.code || 'err.preset', preset.details || { error: String(preset.error || '') })
770
+
771
+ }
1020
772
  }
1021
773
  if (typeof body.judgeTimeoutMs === 'number') {
1022
774
  const timeoutResult = applyRuleOp('set', 'judgeTimeoutMs', body.judgeTimeoutMs)
1023
775
  if (!timeoutResult.ok) {
1024
- return { ok: false, error: { code: 'rule', message: timeoutResult.error || '写入超时失败', details: {} } }
776
+ return rpcFail(timeoutResult.code || 'err.allowlistWrite', timeoutResult.details || {})
777
+
1025
778
  }
1026
779
  pluginCfg.judge.timeoutMs = allowlist.judgeTimeoutMs
1027
780
  persistPluginCfg()
@@ -1029,51 +782,14 @@ export function apply(ctx, rawConfig = {}) {
1029
782
  audit('CONFIG plugin 已更新')
1030
783
  return { ok: true, value: { ok: true, plugin: pluginCfg, preset, setup: getSetupState(paths.profilePatch) } }
1031
784
  }
1032
- if (endpoint === 'save-qq-creds') {
1033
- await cancelProvisioning()
1034
- const appId = String(body.appId || '').trim()
1035
- const appSecret = String(body.appSecret || '').trim()
1036
- const prev = loadQqCreds()
1037
- const next = {
1038
- appId: appId || prev.appId,
1039
- appSecret: appSecret || prev.appSecret,
1040
- ownerUserOpenid: prev.ownerUserOpenid || '',
1041
- }
1042
- persistQqCreds(next)
1043
- await startQq()
1044
- audit('CONFIG qqbot 凭据已更新并重连')
1045
- return { ok: true, value: { ok: true, status: qq ? qq.status() : '未启动' } }
1046
- }
1047
- if (endpoint === 'qq-provision') {
1048
- const result = await startProvisioning()
1049
- return result.ok
1050
- ? { ok: true, value: { ok: true, status: provisionAttempt ? provisionAttempt.status : '' } }
1051
- : { ok: false, error: { code: 'provision', message: result.error || '扫码启动失败', details: {} } }
1052
- }
1053
- if (endpoint === 'qq-cancel-provision') {
1054
- await cancelProvisioning()
1055
- return { ok: true, value: { ok: true } }
1056
- }
1057
- if (endpoint === 'set-chat') {
1058
- pluginCfg.notify.chatId = String(body.chatId || '')
1059
- pluginCfg.notify.userId = String(body.userId || '')
1060
- if (!persistPluginCfg()) {
1061
- return { ok: false, error: { code: 'save', message: '写入配置失败', details: {} } }
1062
- }
1063
- audit(`CONFIG notify chatId=${pluginCfg.notify.chatId}`)
1064
- return { ok: true, value: { ok: true, notify: pluginCfg.notify } }
1065
- }
1066
- if (endpoint === 'reconnect-qq') {
1067
- await startQq()
1068
- return { ok: true, value: { ok: true, status: qq ? qq.status() : '未启动' } }
1069
- }
1070
785
  if (endpoint === 'judge-catalog') {
1071
786
  const provider = String(body.provider || configuredRoute().provider)
1072
787
  let models = []
1073
788
  try {
1074
789
  models = (await llm.listModels(provider)).map((m) => ({ id: m.id, name: m.name || m.id }))
1075
790
  } catch (error) {
1076
- return { ok: false, error: { code: 'catalog', message: String((error && error.message) || error), details: {} } }
791
+ return rpcFail('err.catalog', { error: String((error && error.message) || error) })
792
+
1077
793
  }
1078
794
  return { ok: true, value: { provider, models } }
1079
795
  }
@@ -1094,12 +810,15 @@ export function apply(ctx, rawConfig = {}) {
1094
810
  },
1095
811
  }
1096
812
  } catch (error) {
1097
- return { ok: false, error: { code: 'info', message: String((error && error.message) || error), details: {} } }
813
+ return rpcFail('err.info', { error: String((error && error.message) || error) })
814
+
1098
815
  }
1099
816
  }
1100
- return { ok: false, error: { code: 'unknown', message: `unknown endpoint ${endpoint}`, details: {} } }
817
+ return rpcFail('err.unknownEndpoint', { endpoint: String(endpoint || '') })
818
+
1101
819
  } catch (error) {
1102
- return { ok: false, error: { code: 'internal', message: String((error && error.message) || error), details: {} } }
820
+ return rpcFail('err.internal', { error: String((error && error.message) || error) })
821
+
1103
822
  }
1104
823
  }
1105
824
 
@@ -1109,7 +828,8 @@ export function apply(ctx, rawConfig = {}) {
1109
828
  try {
1110
829
  body = await request.json()
1111
830
  } catch {
1112
- return new Response('body is not JSON', { status: 400 })
831
+ return Response.json({ type: 'server-response', rpcId: 'invalid-request', result: rpcFail('err.badBody') })
832
+
1113
833
  }
1114
834
  const rpcId = body && typeof body.rpcId === 'string' ? body.rpcId : 'invalid-request'
1115
835
  const packed = body && body.payload && typeof body.payload === 'object' ? body.payload : {}
@@ -1119,7 +839,8 @@ export function apply(ctx, rawConfig = {}) {
1119
839
  const result = await enqueueRpc(() => dispatch(endpoint, payload))
1120
840
  return Response.json({ type: 'server-response', rpcId: rpcId, result: result })
1121
841
  } catch (error) {
1122
- return new Response('handler failure: ' + String(error), { status: 500 })
842
+ return Response.json({ type: 'server-response', rpcId: rpcId, result: rpcFail('err.internal', { error: String(error) }) })
843
+
1123
844
  }
1124
845
  }
1125
846
 
@@ -1139,5 +860,5 @@ export function apply(ctx, rawConfig = {}) {
1139
860
  }
1140
861
  })
1141
862
 
1142
- log(`已挂载:关键词→审核表 sandbox=${pluginCfg.presetSandbox};QQ=${qq ? qq.status() : '无'}`)
863
+ log(`已挂载:关键词→审核表 sandbox=${pluginCfg.presetSandbox}`)
1143
864
  }