@dnalec/dsh-auto-approve 0.1.3 → 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/CHANGELOG.md +21 -0
- package/README.md +34 -93
- package/README.zh.md +23 -85
- package/client.js +106 -217
- package/locales.mjs +164 -128
- package/package.json +5 -9
- package/src/index.mjs +179 -458
- package/src/preset-patch.mjs +3 -3
- package/src/rules.mjs +155 -54
- package/src/util.mjs +5 -69
- package/src/provisioning.mjs +0 -131
- package/src/qqbot.mjs +0 -313
- package/src/tickets.mjs +0 -277
package/src/index.mjs
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* dsh-auto-approve — Host 半。
|
|
3
3
|
*
|
|
4
|
-
* 职责:在 `approval/request`
|
|
5
|
-
*
|
|
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
|
-
/**
|
|
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.
|
|
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
|
-
|
|
113
|
-
if (
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
465
|
-
const
|
|
466
|
-
const
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
if (
|
|
474
|
-
|
|
475
|
-
|
|
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,
|
|
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,
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
666
|
-
const { sessionId, toolName, mode, reason, justification, category, path,
|
|
667
|
-
|
|
668
|
-
|
|
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
|
-
*
|
|
689
|
-
*
|
|
491
|
+
* 转人工:记 pending,再把同一条请求交给瀑布里的下一个 answerer(网页框)。
|
|
492
|
+
* 必须 await next() 并把 outcome 原样返回,观察者才能看到人工结果。
|
|
690
493
|
*/
|
|
691
|
-
async function forwardToHuman(info, next
|
|
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
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
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
|
-
}
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
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
|
-
|
|
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 = (
|
|
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) =>
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
}
|
|
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:
|
|
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
|
|
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 ||
|
|
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
|
|
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 } :
|
|
741
|
+
return result.ok ? { ok: true, value: result } : rpcFail(result.code || 'err.allowlistWrite', result.details || {})
|
|
742
|
+
|
|
1002
743
|
}
|
|
1003
744
|
if (endpoint === 'setup') {
|
|
1004
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
813
|
+
return rpcFail('err.info', { error: String((error && error.message) || error) })
|
|
814
|
+
|
|
1098
815
|
}
|
|
1099
816
|
}
|
|
1100
|
-
return
|
|
817
|
+
return rpcFail('err.unknownEndpoint', { endpoint: String(endpoint || '') })
|
|
818
|
+
|
|
1101
819
|
} catch (error) {
|
|
1102
|
-
return
|
|
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
|
|
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
|
|
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}
|
|
863
|
+
log(`已挂载:关键词→审核表 sandbox=${pluginCfg.presetSandbox}`)
|
|
1143
864
|
}
|