@dnalec/dsh-auto-approve 0.1.3 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +85 -0
- package/README.md +44 -94
- package/README.zh.md +34 -85
- package/client.js +400 -265
- package/locales.mjs +204 -132
- package/package.json +6 -9
- package/src/index.mjs +276 -472
- package/src/preset-patch.mjs +333 -78
- package/src/rules.mjs +365 -120
- package/src/util.mjs +48 -71
- 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
|
*
|
|
@@ -17,17 +17,14 @@
|
|
|
17
17
|
import {
|
|
18
18
|
NAME,
|
|
19
19
|
pathsFor,
|
|
20
|
+
resolveProfilePatchPath,
|
|
20
21
|
tryLoadJson,
|
|
21
22
|
saveJson,
|
|
22
|
-
saveSecretJson,
|
|
23
23
|
audit as appendAudit,
|
|
24
24
|
readEventsSince,
|
|
25
25
|
maxEventId,
|
|
26
26
|
appendEvent,
|
|
27
|
-
maskSecret,
|
|
28
27
|
ensureDir,
|
|
29
|
-
forkAbortSignal,
|
|
30
|
-
replaceRequestSignal,
|
|
31
28
|
} from './util.mjs'
|
|
32
29
|
import {
|
|
33
30
|
DEFAULT_DENY_KEYWORDS,
|
|
@@ -37,15 +34,19 @@ import {
|
|
|
37
34
|
normalizeJudgePromptLang,
|
|
38
35
|
normalizeAllowlist,
|
|
39
36
|
mergePluginConfig,
|
|
37
|
+
pickMigratablePluginConfig,
|
|
40
38
|
parseReason,
|
|
41
39
|
matchKeywordBuckets,
|
|
42
40
|
buildJudgePrompt,
|
|
41
|
+
resolveJudgePromptTemplate,
|
|
42
|
+
shippedJudgePromptTemplate,
|
|
43
43
|
parseJudgeClassify,
|
|
44
44
|
pickToolArgs,
|
|
45
45
|
toolArgsTruncated,
|
|
46
46
|
clipToolArgsForEvent,
|
|
47
47
|
formatKeywordHay,
|
|
48
48
|
formatAllowKeywordHay,
|
|
49
|
+
formatPathKeywordHay,
|
|
49
50
|
formatJudgeCard,
|
|
50
51
|
hasToolPayload,
|
|
51
52
|
rememberCachedCall,
|
|
@@ -54,18 +55,40 @@ import {
|
|
|
54
55
|
cloneAllowlist,
|
|
55
56
|
copyAllowlistInto,
|
|
56
57
|
mutateAllowlistOp,
|
|
58
|
+
fail,
|
|
57
59
|
effectiveJudgeTimeoutMs,
|
|
60
|
+
judgeMaxTokens,
|
|
58
61
|
} from './rules.mjs'
|
|
59
|
-
import {
|
|
60
|
-
import {
|
|
61
|
-
|
|
62
|
-
|
|
62
|
+
import { dirname } from 'node:path'
|
|
63
|
+
import {
|
|
64
|
+
getSetupState,
|
|
65
|
+
migratePresetCopy,
|
|
66
|
+
presetDrift,
|
|
67
|
+
readBasePresetKeys,
|
|
68
|
+
setAutoApproveSandbox,
|
|
69
|
+
} from './preset-patch.mjs'
|
|
63
70
|
|
|
64
71
|
export const name = NAME
|
|
65
|
-
/**
|
|
66
|
-
|
|
72
|
+
/**
|
|
73
|
+
* 只列门控真正需要的服务。**不要**放 `webServer`:cordis 把插件 `inject` 当必需服务,
|
|
74
|
+
* 缺一个就停在 PENDING、apply 完全不执行(`vendor/cordis/src/fiber.ts`),
|
|
75
|
+
* 而 `webserver` 行只在 web-app bundle 里 —— headless / acp / sdk 组合下会连审批门控一起失踪。
|
|
76
|
+
* RPC 侧自己用 `ctx.inject(['connection'], …)`,`connection.fetch.register` 不需要 webServer。
|
|
77
|
+
*/
|
|
78
|
+
export const inject = ['approval', 'permissionPresets', 'llm', 'timer']
|
|
79
|
+
|
|
80
|
+
/** Host Session 的工作目录在 header.cwd,没有 session.cwd。 */
|
|
81
|
+
export function readSessionCwd(session) {
|
|
82
|
+
const cwd = session && session.header && typeof session.header.cwd === 'string' ? session.header.cwd : ''
|
|
83
|
+
return cwd
|
|
84
|
+
}
|
|
67
85
|
|
|
68
|
-
/**
|
|
86
|
+
/** Connection RPC 失败必须带 message,否则 client parseConnectionResponse 会 TypeError。 */
|
|
87
|
+
export function rpcFail(code, details) {
|
|
88
|
+
const c = String(code || 'err.internal')
|
|
89
|
+
const d = details && typeof details === 'object' && !Array.isArray(details) ? details : {}
|
|
90
|
+
return { ok: false, error: { code: c, message: c, details: d } }
|
|
91
|
+
}
|
|
69
92
|
|
|
70
93
|
/**
|
|
71
94
|
* 插件入口。热更新规则文件;权限预设写入 profile patch 后需重启才进会话下拉。
|
|
@@ -74,8 +97,9 @@ export const inject = ['approval', 'permissionPresets', 'llm', 'timer', 'webServ
|
|
|
74
97
|
*/
|
|
75
98
|
export function apply(ctx, rawConfig = {}) {
|
|
76
99
|
const paths = pathsFor()
|
|
100
|
+
// 真实 profile 目录从 ctx.baseUrl 推导;拿不到才回落到 profiles/web。
|
|
101
|
+
paths.profilePatch = resolveProfilePatchPath(ctx, rawConfig, paths.profilePatch)
|
|
77
102
|
ensureDir(paths.auto)
|
|
78
|
-
ensureDir(paths.bridge)
|
|
79
103
|
migratePresetCopy(paths.profilePatch)
|
|
80
104
|
|
|
81
105
|
// 损坏的 allowlist 只用内存默认,绝不写盘,避免把用户规则清掉。
|
|
@@ -93,12 +117,21 @@ export function apply(ctx, rawConfig = {}) {
|
|
|
93
117
|
const loadedPlugin = tryLoadJson(paths.pluginConfig)
|
|
94
118
|
let pluginCfgCorrupt = false
|
|
95
119
|
let pluginCfg
|
|
120
|
+
let migratedPlugin = false
|
|
96
121
|
if (!loadedPlugin.ok) {
|
|
97
122
|
console.error(`[${NAME}] 插件配置无法读取,本进程用默认且不覆盖磁盘`, loadedPlugin.error)
|
|
98
123
|
pluginCfgCorrupt = true
|
|
99
124
|
pluginCfg = mergePluginConfig(rawConfig, null)
|
|
125
|
+
} else if (loadedPlugin.missing) {
|
|
126
|
+
const legacy = tryLoadJson(paths.legacyPluginConfig)
|
|
127
|
+
const picked = (legacy.ok && !legacy.missing) ? pickMigratablePluginConfig(legacy.value) : null
|
|
128
|
+
pluginCfg = mergePluginConfig(rawConfig, picked)
|
|
129
|
+
if (picked && saveJson(paths.pluginConfig, pluginCfg)) {
|
|
130
|
+
migratedPlugin = true
|
|
131
|
+
console.log(`[${NAME}] 已从 approval-bridge/config.json 迁移判定配置到 auto-approve/config.json`)
|
|
132
|
+
}
|
|
100
133
|
} else {
|
|
101
|
-
pluginCfg = mergePluginConfig(rawConfig, loadedPlugin.
|
|
134
|
+
pluginCfg = mergePluginConfig(rawConfig, loadedPlugin.value)
|
|
102
135
|
}
|
|
103
136
|
if (loadedAllowlist.ok && loadedAllowlist.missing && Number(pluginCfg.judge.timeoutMs) > 0) {
|
|
104
137
|
allowlist.judgeTimeoutMs = Number(pluginCfg.judge.timeoutMs)
|
|
@@ -109,14 +142,34 @@ export function apply(ctx, rawConfig = {}) {
|
|
|
109
142
|
if (loadedAllowlist.ok && (loadedAllowlist.missing || diskVersion < allowlist.version)) {
|
|
110
143
|
saveJson(paths.allowlist, allowlist)
|
|
111
144
|
}
|
|
112
|
-
|
|
113
|
-
if (
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
145
|
+
// 损坏配置不写沙箱。缺失配置若 patch 里已有 auto-approve,也不用默认 workspace-write 去加宽。
|
|
146
|
+
if (!pluginCfgCorrupt) {
|
|
147
|
+
const already = getSetupState(paths.profilePatch).configured
|
|
148
|
+
const writeSandbox = (loadedPlugin.ok && !loadedPlugin.missing) || migratedPlugin || !already
|
|
149
|
+
if (writeSandbox) {
|
|
150
|
+
const presetSetup = setAutoApproveSandbox(paths.profilePatch, pluginCfg.presetSandbox)
|
|
151
|
+
if (presetSetup.ok && presetSetup.needRestart) {
|
|
152
|
+
console.log(`[${NAME}] 已写入 auto-approve 权限预设(sandbox=${pluginCfg.presetSandbox});live patch 重载后会话权限会出现「自动审批」`)
|
|
153
|
+
} else if (!presetSetup.ok) {
|
|
154
|
+
console.error(
|
|
155
|
+
`[${NAME}] 写入 auto-approve 预设失败(${presetSetup.code || presetSetup.error || 'err.preset'}):${paths.profilePatch}`,
|
|
156
|
+
presetSetup.details || '',
|
|
157
|
+
)
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
{
|
|
162
|
+
// 出厂预设表比本 profile 那份新:只警告,不自动改写用户文件(patch 里 config 是整块替换)。
|
|
163
|
+
const drift = presetSetupState().drift
|
|
164
|
+
if (drift.baseKnown && drift.missing.length) {
|
|
165
|
+
console.warn(
|
|
166
|
+
`[${NAME}] DSH 出厂权限预设表多了 ${drift.missing.join(', ')},`
|
|
167
|
+
+ `但 profile 的 permission 行是本插件写入的副本(patch 整块替换 config),不会自动包含;`
|
|
168
|
+
+ `请更新插件或手工合并 ${paths.profilePatch}`,
|
|
169
|
+
)
|
|
170
|
+
}
|
|
117
171
|
}
|
|
118
172
|
let eventSeq = maxEventId(paths.events)
|
|
119
|
-
const tickets = createTicketBroker()
|
|
120
173
|
let rpcTail = Promise.resolve()
|
|
121
174
|
function enqueueRpc(fn) {
|
|
122
175
|
const run = rpcTail.then(fn, fn)
|
|
@@ -129,14 +182,24 @@ export function apply(ctx, rawConfig = {}) {
|
|
|
129
182
|
* key 为 sessionId:callId,避免多会话共用 call-0 互相覆盖。
|
|
130
183
|
*/
|
|
131
184
|
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
185
|
const log = (line) => console.log(`[${NAME}] ${line}`)
|
|
138
186
|
const audit = (line) => appendAudit(paths.audit, line)
|
|
139
187
|
|
|
188
|
+
/**
|
|
189
|
+
* setup 状态 + 预设表漂移。
|
|
190
|
+
* 插件写进 profile patch 的 `permission` 行会整块替换 base 的 config(patch 语义不做深合并),
|
|
191
|
+
* 所以 DSH 出厂表新增预设时本 profile 不会有:读 base 的 patch 比一比,只提示、不自动改写。
|
|
192
|
+
*/
|
|
193
|
+
function presetSetupState() {
|
|
194
|
+
const setup = getSetupState(paths.profilePatch)
|
|
195
|
+
const base = readBasePresetKeys(dirname(paths.profilePatch))
|
|
196
|
+
if (!base.ok) return { ...setup, drift: { baseKnown: false, missing: [], extra: [] } }
|
|
197
|
+
return { ...setup, drift: { baseKnown: true, baseKeys: base.keys, ...presetDrift(base.keys, setup.presets) } }
|
|
198
|
+
}
|
|
199
|
+
const llm = ctx.llm
|
|
200
|
+
const permissionPresets = ctx.permissionPresets
|
|
201
|
+
const agentDefaultModel = ctx.get('agentDefaultModel')
|
|
202
|
+
|
|
140
203
|
function reloadAllowlist() {
|
|
141
204
|
const loaded = tryLoadJson(paths.allowlist)
|
|
142
205
|
if (!loaded.ok) {
|
|
@@ -170,12 +233,12 @@ export function apply(ctx, rawConfig = {}) {
|
|
|
170
233
|
|
|
171
234
|
function applyRuleOp(op, kind, value) {
|
|
172
235
|
if (allowlistCorrupt && op !== 'reset') {
|
|
173
|
-
return
|
|
236
|
+
return fail('err.allowlistCorrupt')
|
|
174
237
|
}
|
|
175
238
|
const draft = cloneAllowlist(allowlist)
|
|
176
239
|
const result = mutateAllowlistOp(draft, op, kind, value)
|
|
177
240
|
if (!result.ok) return result
|
|
178
|
-
if (!saveJson(paths.allowlist, draft)) return
|
|
241
|
+
if (!saveJson(paths.allowlist, draft)) return fail('err.allowlistWrite')
|
|
179
242
|
copyAllowlistInto(allowlist, draft)
|
|
180
243
|
allowlistCorrupt = false
|
|
181
244
|
if (result.auditLine) audit(result.auditLine)
|
|
@@ -202,6 +265,10 @@ export function apply(ctx, rawConfig = {}) {
|
|
|
202
265
|
put('reason', 600)
|
|
203
266
|
put('raw', 800)
|
|
204
267
|
put('error', 400)
|
|
268
|
+
put('errorCode', 80)
|
|
269
|
+
put('errorMs', 20)
|
|
270
|
+
put('errorDetail', 400)
|
|
271
|
+
put('errorEffort', 40)
|
|
205
272
|
put('failed', 0, true)
|
|
206
273
|
put('timedOut', 0, true)
|
|
207
274
|
return Object.keys(out).length ? out : undefined
|
|
@@ -224,7 +291,6 @@ export function apply(ctx, rawConfig = {}) {
|
|
|
224
291
|
if (o.category) ev.category = o.category
|
|
225
292
|
if (o.judgeReason) ev.judgeReason = String(o.judgeReason).slice(0, 600)
|
|
226
293
|
if (o.path) ev.path = o.path
|
|
227
|
-
if (o.ticket !== undefined) ev.ticket = o.ticket
|
|
228
294
|
if (o.source) ev.source = o.source
|
|
229
295
|
if (o.cwd) ev.cwd = String(o.cwd).slice(0, 400)
|
|
230
296
|
if (o.keyword) ev.keyword = String(o.keyword).slice(0, 120)
|
|
@@ -240,260 +306,28 @@ export function apply(ctx, rawConfig = {}) {
|
|
|
240
306
|
return ev
|
|
241
307
|
}
|
|
242
308
|
|
|
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
|
|
309
|
+
function emitDecision(leaf) {
|
|
402
310
|
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 }
|
|
311
|
+
ctx.emit('auto-approve/decision', leaf)
|
|
449
312
|
} 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 }
|
|
313
|
+
console.error(`[${NAME}] auto-approve/decision 失败`, error)
|
|
461
314
|
}
|
|
462
315
|
}
|
|
463
316
|
|
|
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()
|
|
317
|
+
function decisionLeaf(info, extra) {
|
|
318
|
+
const e = extra || {}
|
|
319
|
+
const leaf = {
|
|
320
|
+
sessionId: String(info.sessionId || ''),
|
|
321
|
+
tool: String(info.toolName || ''),
|
|
322
|
+
path: String(e.path || info.path || ''),
|
|
323
|
+
verdict: String(e.verdict || ''),
|
|
324
|
+
}
|
|
325
|
+
if (e.outcome) leaf.outcome = String(e.outcome)
|
|
326
|
+
if (e.category || info.category) leaf.category = String(e.category || info.category || '')
|
|
327
|
+
if (e.judgeReason || info.judgeReason) leaf.judgeReason = String(e.judgeReason || info.judgeReason || '').slice(0, 600)
|
|
328
|
+
return leaf
|
|
483
329
|
}
|
|
484
330
|
|
|
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
331
|
function fallbackSelection() {
|
|
498
332
|
try {
|
|
499
333
|
const sel = agentDefaultModel && typeof agentDefaultModel.currentSelection === 'function'
|
|
@@ -518,7 +352,7 @@ export function apply(ctx, rawConfig = {}) {
|
|
|
518
352
|
async function resolveJudgeRoute() {
|
|
519
353
|
const route = configuredRoute()
|
|
520
354
|
if (!route.provider || !route.model) {
|
|
521
|
-
return { ok: false,
|
|
355
|
+
return { ok: false, code: 'err.judgeUnconfigured', ...route }
|
|
522
356
|
}
|
|
523
357
|
try {
|
|
524
358
|
const info = await llm.resolveModelInfo(route.provider, route.model)
|
|
@@ -526,20 +360,20 @@ export function apply(ctx, rawConfig = {}) {
|
|
|
526
360
|
? info.reasoning.efforts.map((e) => e.id)
|
|
527
361
|
: []
|
|
528
362
|
if (route.reasoningEffort && efforts.length > 0 && !efforts.includes(route.reasoningEffort)) {
|
|
529
|
-
return { ok: false,
|
|
363
|
+
return { ok: false, code: 'err.judgeEffort', details: { effort: route.reasoningEffort }, ...route }
|
|
530
364
|
}
|
|
531
365
|
return { ok: true, ...route, info }
|
|
532
366
|
} catch (error) {
|
|
533
|
-
return { ok: false, error: String((error && error.message) || error), ...route }
|
|
367
|
+
return { ok: false, code: 'err.judgeUpstream', details: { error: String((error && error.message) || error) }, ...route }
|
|
534
368
|
}
|
|
535
369
|
}
|
|
536
370
|
|
|
537
371
|
/**
|
|
538
|
-
* 调用审核模型。不要传 messages.system
|
|
539
|
-
* 分类提示全部折进 user
|
|
372
|
+
* 调用审核模型。不要传 messages.system:部分 OpenAI 兼容网关会把 system 映射成 developer 导致 400。
|
|
373
|
+
* 分类提示全部折进 user 文本。带推理档位时输出预算要留出推理 token,否则空文本会被当成解析失败。
|
|
540
374
|
*/
|
|
541
375
|
async function callJudge(userText, signal, route, system) {
|
|
542
|
-
const prompt = system || buildJudgePrompt(allowlist.criteria, pluginCfg.judgePromptLang)
|
|
376
|
+
const prompt = system || buildJudgePrompt(allowlist.criteria, pluginCfg.judgePromptLang, resolveJudgePromptTemplate(pluginCfg, pluginCfg.judgePromptLang))
|
|
543
377
|
const opts = {
|
|
544
378
|
provider: route.provider,
|
|
545
379
|
model: route.model,
|
|
@@ -548,7 +382,7 @@ export function apply(ctx, rawConfig = {}) {
|
|
|
548
382
|
content: [{ type: 'text', text: prompt + '\n\n' + userText }],
|
|
549
383
|
}],
|
|
550
384
|
temperature: 0,
|
|
551
|
-
maxTokens:
|
|
385
|
+
maxTokens: judgeMaxTokens(route.reasoningEffort),
|
|
552
386
|
signal,
|
|
553
387
|
}
|
|
554
388
|
if (route.reasoningEffort) opts.reasoningEffort = route.reasoningEffort
|
|
@@ -557,7 +391,10 @@ export function apply(ctx, rawConfig = {}) {
|
|
|
557
391
|
if (chunk.type === 'text-delta') text += chunk.text
|
|
558
392
|
else if (chunk.type === 'finish' && (chunk.reason.kind === 'error' || chunk.reason.kind === 'aborted')) {
|
|
559
393
|
const failure = chunk.reason.failure && chunk.reason.failure.message ? chunk.reason.failure.message : chunk.reason.kind
|
|
560
|
-
|
|
394
|
+
const err = new Error('err.judgeCall')
|
|
395
|
+
err.code = 'err.judgeCall'
|
|
396
|
+
err.details = { error: String(failure) }
|
|
397
|
+
throw err
|
|
561
398
|
}
|
|
562
399
|
}
|
|
563
400
|
return text
|
|
@@ -567,7 +404,7 @@ export function apply(ctx, rawConfig = {}) {
|
|
|
567
404
|
const criteria = allowlist.criteria || DEFAULT_CRITERIA
|
|
568
405
|
const lang = normalizeJudgePromptLang(pluginCfg.judgePromptLang)
|
|
569
406
|
const user = formatJudgeCard(toolName, mode, justification, args, cwd, lang)
|
|
570
|
-
const text = await callJudge(user, signal, route, buildJudgePrompt(criteria, lang))
|
|
407
|
+
const text = await callJudge(user, signal, route, buildJudgePrompt(criteria, lang, resolveJudgePromptTemplate(pluginCfg, lang)))
|
|
571
408
|
try {
|
|
572
409
|
return { ...parseJudgeClassify(text, criteria), raw: String(text || '').slice(0, 800) }
|
|
573
410
|
} catch (error) {
|
|
@@ -576,10 +413,24 @@ export function apply(ctx, rawConfig = {}) {
|
|
|
576
413
|
}
|
|
577
414
|
}
|
|
578
415
|
|
|
579
|
-
|
|
416
|
+
/**
|
|
417
|
+
* 单次判定 + 超时 + 重试。
|
|
418
|
+
* `outerSignal` 是审批请求自己的取消信号:请求被取消后不该继续烧模型调用。
|
|
419
|
+
* 注意**不要**去 abort `req.signal`,这里只观察它。
|
|
420
|
+
*/
|
|
421
|
+
async function withRetry(runFn, label, timeoutMs, outerSignal) {
|
|
422
|
+
const cancelled = () => Boolean(outerSignal && outerSignal.aborted)
|
|
580
423
|
const runOnce = async () => {
|
|
424
|
+
if (cancelled()) return { aborted: true }
|
|
581
425
|
const controller = new AbortController()
|
|
582
426
|
let cancelTimer
|
|
427
|
+
const onOuterAbort = () => controller.abort(`${NAME}: ${label} 请求已取消`)
|
|
428
|
+
const linkable = outerSignal && typeof outerSignal.addEventListener === 'function'
|
|
429
|
+
if (linkable) {
|
|
430
|
+
outerSignal.addEventListener('abort', onOuterAbort, { once: true })
|
|
431
|
+
// 竞态窗口:cancelled() 之后、addEventListener 之前就 abort 了,事件不会再触发。
|
|
432
|
+
if (outerSignal.aborted) onOuterAbort()
|
|
433
|
+
}
|
|
583
434
|
const timed = new Promise((resolve) => {
|
|
584
435
|
cancelTimer = ctx.timeout(() => resolve({ timedOut: true }), timeoutMs)
|
|
585
436
|
})
|
|
@@ -588,40 +439,56 @@ export function apply(ctx, rawConfig = {}) {
|
|
|
588
439
|
.then((r) => ({ ...r, timedOut: false }))
|
|
589
440
|
.catch((error) => ({ judgeError: error }))
|
|
590
441
|
const result = await Promise.race([call, timed])
|
|
442
|
+
if (cancelled()) return { aborted: true }
|
|
591
443
|
if (result.judgeError) throw result.judgeError
|
|
592
444
|
return result
|
|
593
445
|
} finally {
|
|
594
446
|
if (typeof cancelTimer === 'function') {
|
|
595
447
|
try { cancelTimer() } catch { /* disposer */ }
|
|
596
448
|
}
|
|
449
|
+
if (linkable && typeof outerSignal.removeEventListener === 'function') {
|
|
450
|
+
outerSignal.removeEventListener('abort', onOuterAbort)
|
|
451
|
+
}
|
|
597
452
|
controller.abort(`${NAME}: ${label} 结束`)
|
|
598
453
|
}
|
|
599
454
|
}
|
|
600
455
|
let last = { failed: true }
|
|
601
456
|
try {
|
|
602
457
|
const first = await runOnce()
|
|
458
|
+
if (first.aborted) return first
|
|
603
459
|
if (!first.timedOut) return first
|
|
604
|
-
last = { failed: true, timedOut: true, error:
|
|
460
|
+
last = { failed: true, timedOut: true, errorCode: 'err.judgeTimeout', error: 'err.judgeTimeout', errorMs: String(timeoutMs) }
|
|
605
461
|
console.warn(`[${NAME}] ${label} 超时(${timeoutMs}ms),转人工`)
|
|
606
462
|
return last
|
|
607
463
|
} catch (error) {
|
|
608
464
|
last = {
|
|
609
465
|
failed: true,
|
|
610
|
-
|
|
466
|
+
errorCode: error && error.code ? error.code : 'err.judgeFailed',
|
|
467
|
+
error: error && error.code ? error.code : 'err.judgeFailed',
|
|
468
|
+
errorDetail: error && error.details && error.details.error ? String(error.details.error) : '',
|
|
611
469
|
raw: error && error.raw ? String(error.raw).slice(0, 800) : '',
|
|
612
470
|
}
|
|
471
|
+
const code = error && error.code
|
|
472
|
+
if (code === 'err.judgeParse' || code === 'err.judgeEmpty') {
|
|
473
|
+
console.error(`[${NAME}] ${label} 输出无法解析,转人工`, error)
|
|
474
|
+
return last
|
|
475
|
+
}
|
|
613
476
|
console.error(`[${NAME}] ${label} 异常,重试 1 次`, error)
|
|
614
477
|
}
|
|
615
478
|
try {
|
|
616
479
|
const second = await runOnce()
|
|
480
|
+
if (second.aborted) return second
|
|
617
481
|
if (!second.timedOut) return second
|
|
618
|
-
last = { failed: true, timedOut: true, error:
|
|
482
|
+
last = { failed: true, timedOut: true, errorCode: 'err.judgeRetryTimeout', error: 'err.judgeRetryTimeout', errorMs: String(timeoutMs) }
|
|
619
483
|
console.warn(`[${NAME}] ${label} 重试超时(${timeoutMs}ms)`)
|
|
620
484
|
return last
|
|
621
485
|
} catch (error) {
|
|
622
486
|
last = {
|
|
623
487
|
failed: true,
|
|
624
|
-
|
|
488
|
+
errorCode: error && error.code ? error.code : 'err.judgeFailed',
|
|
489
|
+
error: error && error.code ? error.code : 'err.judgeFailed',
|
|
490
|
+
errorDetail: error && error.details && error.details.error ? String(error.details.error) : '',
|
|
491
|
+
errorMs: last.errorMs,
|
|
625
492
|
raw: error && error.raw ? String(error.raw).slice(0, 800) : last.raw,
|
|
626
493
|
}
|
|
627
494
|
console.error(`[${NAME}] ${label} 重试仍异常`, error)
|
|
@@ -629,7 +496,7 @@ export function apply(ctx, rawConfig = {}) {
|
|
|
629
496
|
}
|
|
630
497
|
}
|
|
631
498
|
|
|
632
|
-
async function judgeOperation(toolName, mode, justification, args, cwd) {
|
|
499
|
+
async function judgeOperation(toolName, mode, justification, args, cwd, requestSignal) {
|
|
633
500
|
const route = await resolveJudgeRoute()
|
|
634
501
|
const meta = {
|
|
635
502
|
provider: route.provider || '',
|
|
@@ -637,15 +504,19 @@ export function apply(ctx, rawConfig = {}) {
|
|
|
637
504
|
effort: route.reasoningEffort || '',
|
|
638
505
|
}
|
|
639
506
|
if (!route.ok) {
|
|
640
|
-
audit(`FAILED judge route: ${route.error}`)
|
|
641
|
-
return { action: 'human', criterion: 'other', reason: '', failed: true, error: route.error, ...meta }
|
|
507
|
+
audit(`FAILED judge route: ${route.code || route.error || ''}`)
|
|
508
|
+
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
509
|
}
|
|
643
510
|
const timeoutMs = effectiveJudgeTimeoutMs(allowlist, pluginCfg)
|
|
644
511
|
const result = await withRetry(
|
|
645
512
|
(signal) => judgeOnce(toolName, mode, justification, args, signal, route, cwd),
|
|
646
513
|
'审核模型',
|
|
647
514
|
timeoutMs,
|
|
515
|
+
requestSignal,
|
|
648
516
|
)
|
|
517
|
+
if (result.aborted) {
|
|
518
|
+
return { aborted: true, criterion: 'other', reason: '', ...meta }
|
|
519
|
+
}
|
|
649
520
|
if (result.failed) {
|
|
650
521
|
return {
|
|
651
522
|
action: 'human',
|
|
@@ -653,7 +524,10 @@ export function apply(ctx, rawConfig = {}) {
|
|
|
653
524
|
reason: '',
|
|
654
525
|
failed: true,
|
|
655
526
|
timedOut: Boolean(result.timedOut),
|
|
656
|
-
|
|
527
|
+
errorCode: result.errorCode || result.error || 'err.judgeFailed',
|
|
528
|
+
error: result.errorCode || result.error || 'err.judgeFailed',
|
|
529
|
+
errorMs: result.errorMs || '',
|
|
530
|
+
errorDetail: result.errorDetail || '',
|
|
657
531
|
raw: result.raw || '',
|
|
658
532
|
...meta,
|
|
659
533
|
}
|
|
@@ -662,12 +536,10 @@ export function apply(ctx, rawConfig = {}) {
|
|
|
662
536
|
return { ...result, action: row.action, label: row.label, ...meta }
|
|
663
537
|
}
|
|
664
538
|
|
|
665
|
-
|
|
666
|
-
const { sessionId, toolName, mode, reason, justification, category, path,
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
const detail = { category, path, ticket, source, args, cwd, judgeReason, judge }
|
|
670
|
-
|
|
539
|
+
function applyHumanOutcome(ctxInfo, outcome) {
|
|
540
|
+
const { sessionId, toolName, mode, reason, justification, category, path, args, cwd, judgeReason, judge } = ctxInfo
|
|
541
|
+
audit(`OUTCOME ${toolName} outcome=${outcome} source=web | ${reason.slice(0, 80)}`)
|
|
542
|
+
const detail = { category, path, source: 'web', args, cwd, judgeReason, judge }
|
|
671
543
|
if (outcome === 'allowed-once') {
|
|
672
544
|
recordEvent(sessionId, toolName, mode, reason, justification, 'manual-approved', {
|
|
673
545
|
kind: 'manual-approved', ...detail,
|
|
@@ -685,108 +557,38 @@ export function apply(ctx, rawConfig = {}) {
|
|
|
685
557
|
}
|
|
686
558
|
|
|
687
559
|
/**
|
|
688
|
-
*
|
|
689
|
-
*
|
|
560
|
+
* 转人工:记 pending,再把同一条请求交给瀑布里的下一个 answerer(网页框)。
|
|
561
|
+
* 必须 await next() 并把 outcome 原样返回,观察者才能看到人工结果。
|
|
690
562
|
*/
|
|
691
|
-
async function forwardToHuman(info, next
|
|
692
|
-
reloadPluginCfg()
|
|
693
|
-
const ticket = tickets.allocate(info)
|
|
563
|
+
async function forwardToHuman(info, next) {
|
|
694
564
|
recordEvent(info.sessionId, info.toolName, info.mode, info.reason, info.justification, 'manual-pending', {
|
|
695
565
|
kind: 'manual-pending',
|
|
696
566
|
category: info.category || '',
|
|
697
567
|
path: info.path,
|
|
698
|
-
ticket: ticket.n,
|
|
699
568
|
args: info.args,
|
|
700
569
|
cwd: info.cwd,
|
|
701
570
|
judgeReason: info.judgeReason,
|
|
702
571
|
judge: info.judge,
|
|
703
572
|
})
|
|
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
|
-
|
|
573
|
+
emitDecision(decisionLeaf(info, { verdict: 'human', path: info.path }))
|
|
753
574
|
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
|
|
575
|
+
const outcome = await next()
|
|
576
|
+
try {
|
|
577
|
+
applyHumanOutcome(info, outcome)
|
|
578
|
+
emitDecision(decisionLeaf(info, { verdict: 'human', path: info.path, outcome }))
|
|
579
|
+
} catch (error) {
|
|
580
|
+
console.error(`[${NAME}] 记录人工结果失败`, error)
|
|
780
581
|
}
|
|
781
|
-
const outcome = first.outcome || 'cancelled'
|
|
782
|
-
await applyHumanOutcome({ ...info, ticket: ticket.n }, outcome, { source: first.source || 'abort' })
|
|
783
582
|
return outcome
|
|
784
|
-
}
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
583
|
+
} catch (error) {
|
|
584
|
+
console.error(`[${NAME}] 网页审批框失败`, error)
|
|
585
|
+
try {
|
|
586
|
+
applyHumanOutcome(info, 'unavailable')
|
|
587
|
+
emitDecision(decisionLeaf(info, { verdict: 'human', path: info.path, outcome: 'unavailable' }))
|
|
588
|
+
} catch (again) {
|
|
589
|
+
console.error(`[${NAME}] 记录 unavailable 失败`, again)
|
|
788
590
|
}
|
|
789
|
-
|
|
591
|
+
return 'unavailable'
|
|
790
592
|
}
|
|
791
593
|
}
|
|
792
594
|
|
|
@@ -807,6 +609,7 @@ export function apply(ctx, rawConfig = {}) {
|
|
|
807
609
|
|
|
808
610
|
ctx.on('approval/request', async (req, next) => {
|
|
809
611
|
let humanFallback = null
|
|
612
|
+
let forwarded = false
|
|
810
613
|
try {
|
|
811
614
|
// 非「自动审批」预设交给系统默认 ask,本插件不管。
|
|
812
615
|
reloadAllowlist()
|
|
@@ -827,7 +630,8 @@ export function apply(ctx, rawConfig = {}) {
|
|
|
827
630
|
const reason = String(req.reason || '')
|
|
828
631
|
const { mode, justification } = parseReason(reason)
|
|
829
632
|
const sessionId = typeof session.id === 'string' ? session.id : ''
|
|
830
|
-
const sessionCwd = (
|
|
633
|
+
const sessionCwd = readSessionCwd(session)
|
|
634
|
+
|
|
831
635
|
const cached = takeCachedCall(pendingCalls, sessionId, req.callId)
|
|
832
636
|
const toolArgs = pickToolArgs(cached.args || {})
|
|
833
637
|
const baseInfo = {
|
|
@@ -835,35 +639,38 @@ export function apply(ctx, rawConfig = {}) {
|
|
|
835
639
|
args: toolArgs,
|
|
836
640
|
}
|
|
837
641
|
|
|
838
|
-
const toHuman = (path, category, extra) =>
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
}
|
|
642
|
+
const toHuman = (path, category, extra) => {
|
|
643
|
+
forwarded = true
|
|
644
|
+
return forwardToHuman({
|
|
645
|
+
...baseInfo,
|
|
646
|
+
path,
|
|
647
|
+
category: category || '',
|
|
648
|
+
judgeReason: extra && extra.judgeReason,
|
|
649
|
+
judge: extra && extra.judge,
|
|
650
|
+
}, next)
|
|
651
|
+
}
|
|
848
652
|
humanFallback = toHuman
|
|
849
653
|
// 没看见命令/路径就转人工,禁止关键词允许或模型标 safe。
|
|
850
654
|
if (!cached.found || !hasToolPayload(toolArgs)) {
|
|
851
|
-
const why = cached.found ? '
|
|
655
|
+
const why = cached.found ? 'err.missingPayload' : 'err.missingPayloadUncaptured'
|
|
852
656
|
audit(`HUMAN ${toolName} mode=${mode || 'none'} missing-payload | ${why}`)
|
|
853
657
|
return toHuman('missing-payload', 'other', { judgeReason: why })
|
|
854
658
|
}
|
|
855
|
-
const hay = formatKeywordHay(toolName, reason, toolArgs)
|
|
856
|
-
const
|
|
659
|
+
const hay = formatKeywordHay(toolName, reason, toolArgs, sessionCwd)
|
|
660
|
+
const pathHay = formatPathKeywordHay(toolArgs, sessionCwd)
|
|
661
|
+
const kw = matchKeywordBuckets(hay, allowlist, formatAllowKeywordHay(toolArgs), pathHay)
|
|
662
|
+
|
|
857
663
|
const eventDetail = { args: toolArgs, cwd: sessionCwd }
|
|
858
664
|
if (kw && kw.action === 'reject') {
|
|
859
665
|
audit(`REJECT ${toolName} mode=${mode || 'none'} keyword | ${reason.slice(0, 160)}`)
|
|
860
666
|
recordEvent(sessionId, toolName, mode, reason, justification, 'keyword-reject', {
|
|
861
667
|
kind: 'auto', path: 'keyword-reject', ...eventDetail,
|
|
862
668
|
})
|
|
669
|
+
emitDecision(decisionLeaf(baseInfo, { verdict: 'keyword-reject', path: 'keyword-reject', outcome: 'rejected' }))
|
|
863
670
|
return 'rejected'
|
|
864
671
|
}
|
|
865
672
|
if (toolArgsTruncated(toolArgs)) {
|
|
866
|
-
const why = '
|
|
673
|
+
const why = 'err.truncatedPayload'
|
|
867
674
|
audit(`HUMAN ${toolName} mode=${mode || 'none'} truncated-payload | ${why}`)
|
|
868
675
|
return toHuman('truncated-payload', 'other', { judgeReason: why })
|
|
869
676
|
}
|
|
@@ -873,15 +680,22 @@ export function apply(ctx, rawConfig = {}) {
|
|
|
873
680
|
recordEvent(sessionId, toolName, mode, reason, justification, 'keyword-allow', {
|
|
874
681
|
kind: 'auto', path: 'keyword-allow', ...eventDetail,
|
|
875
682
|
})
|
|
683
|
+
emitDecision(decisionLeaf(baseInfo, { verdict: 'keyword-allow', path: 'keyword-allow', outcome: 'allowed-once' }))
|
|
876
684
|
return 'allowed-once'
|
|
877
685
|
}
|
|
878
686
|
audit(`HUMAN ${toolName} mode=${mode || 'none'} keyword | ${reason.slice(0, 160)}`)
|
|
879
687
|
return toHuman('keyword-human', '')
|
|
880
688
|
}
|
|
881
689
|
|
|
882
|
-
const judged = await judgeOperation(toolName, mode, justification, toolArgs, sessionCwd)
|
|
690
|
+
const judged = await judgeOperation(toolName, mode, justification, toolArgs, sessionCwd, req.signal)
|
|
883
691
|
const criterion = judged.criterion || 'other'
|
|
884
692
|
const judgeReason = judged.reason || ''
|
|
693
|
+
if (judged.aborted) {
|
|
694
|
+
// 请求已被取消(工具执行 signal abort)。审批服务会以 cancelled 结算并丢弃迟到结果,
|
|
695
|
+
// 这里不再占坑、也不再调 next(),避免取消后还弹人工框。
|
|
696
|
+
audit(`CANCEL ${toolName} mode=${mode || 'none'} judge aborted`)
|
|
697
|
+
return 'cancelled'
|
|
698
|
+
}
|
|
885
699
|
if (judged.failed) {
|
|
886
700
|
audit(`FAILED ${toolName} mode=${mode || 'none'} → 人工 | ${judged.error || reason.slice(0, 120)}`)
|
|
887
701
|
return toHuman('judge-failed', criterion, {
|
|
@@ -894,6 +708,9 @@ export function apply(ctx, rawConfig = {}) {
|
|
|
894
708
|
recordEvent(sessionId, toolName, mode, reason, justification, 'criteria-reject', {
|
|
895
709
|
kind: 'auto', category: criterion, path: 'criteria-reject', judgeReason, judge: judged, ...eventDetail,
|
|
896
710
|
})
|
|
711
|
+
emitDecision(decisionLeaf(baseInfo, {
|
|
712
|
+
verdict: 'criteria-reject', path: 'criteria-reject', outcome: 'rejected', category: criterion, judgeReason,
|
|
713
|
+
}))
|
|
897
714
|
return 'rejected'
|
|
898
715
|
}
|
|
899
716
|
if (judged.action === 'allow') {
|
|
@@ -901,16 +718,24 @@ export function apply(ctx, rawConfig = {}) {
|
|
|
901
718
|
recordEvent(sessionId, toolName, mode, reason, justification, 'criteria-allow', {
|
|
902
719
|
kind: 'auto', category: criterion, path: 'criteria-allow', judgeReason, judge: judged, ...eventDetail,
|
|
903
720
|
})
|
|
721
|
+
emitDecision(decisionLeaf(baseInfo, {
|
|
722
|
+
verdict: 'criteria-allow', path: 'criteria-allow', outcome: 'allowed-once', category: criterion, judgeReason,
|
|
723
|
+
}))
|
|
904
724
|
return 'allowed-once'
|
|
905
725
|
}
|
|
906
726
|
audit(`HUMAN ${toolName} mode=${mode || 'none'} criteria=${criterion} | ${judgeReason || reason.slice(0, 120)}`)
|
|
907
727
|
return toHuman('criteria-human', criterion, { judgeReason, judge: judged })
|
|
908
728
|
} catch (error) {
|
|
909
729
|
console.error(`[${NAME}] 判断过程出错,回退人工`, error)
|
|
730
|
+
if (forwarded) return 'unavailable'
|
|
910
731
|
if (humanFallback) {
|
|
911
732
|
try {
|
|
912
733
|
return await humanFallback('plugin-error', 'other', {
|
|
913
|
-
judgeReason:
|
|
734
|
+
judgeReason: (error && error.code) || 'err.pluginError',
|
|
735
|
+
judge: {
|
|
736
|
+
errorCode: (error && error.code) || 'err.pluginError',
|
|
737
|
+
errorDetail: String((error && error.message) || error),
|
|
738
|
+
},
|
|
914
739
|
})
|
|
915
740
|
} catch (again) {
|
|
916
741
|
console.error(`[${NAME}] 转人工仍失败,交回系统默认`, again)
|
|
@@ -926,7 +751,7 @@ export function apply(ctx, rawConfig = {}) {
|
|
|
926
751
|
ctx.inject(['connection'], (c) => {
|
|
927
752
|
const connection = c.connection
|
|
928
753
|
if (!connection || !connection.fetch || typeof connection.fetch.register !== 'function') {
|
|
929
|
-
console.warn(`[${NAME}] connection.fetch 不可用,设置页/提示条 RPC
|
|
754
|
+
console.warn(`[${NAME}] connection.fetch 不可用,设置页/提示条 RPC 未注册(门控仍工作)`)
|
|
930
755
|
return
|
|
931
756
|
}
|
|
932
757
|
|
|
@@ -936,12 +761,11 @@ export function apply(ctx, rawConfig = {}) {
|
|
|
936
761
|
reloadPluginCfg()
|
|
937
762
|
const body = payload && typeof payload === 'object' ? payload : {}
|
|
938
763
|
if (endpoint === 'snapshot') {
|
|
939
|
-
const creds = loadQqCreds()
|
|
940
764
|
return {
|
|
941
765
|
ok: true,
|
|
942
766
|
value: {
|
|
943
767
|
config: {
|
|
944
|
-
version: allowlist.version ||
|
|
768
|
+
version: allowlist.version || 18,
|
|
945
769
|
corrupt: allowlistCorrupt,
|
|
946
770
|
rejectKeywords: allowlist.rejectKeywords || [],
|
|
947
771
|
humanKeywords: allowlist.humanKeywords || [],
|
|
@@ -955,19 +779,14 @@ export function apply(ctx, rawConfig = {}) {
|
|
|
955
779
|
rejectKeywords: shippedRejectKeywords(),
|
|
956
780
|
humanKeywords: [],
|
|
957
781
|
criteria: shippedCriteria(pluginCfg.judgePromptLang),
|
|
782
|
+
judgePrompts: {
|
|
783
|
+
zh: shippedJudgePromptTemplate('zh'),
|
|
784
|
+
en: shippedJudgePromptTemplate('en'),
|
|
785
|
+
},
|
|
958
786
|
},
|
|
959
|
-
setup:
|
|
787
|
+
setup: presetSetupState(),
|
|
960
788
|
plugin: pluginCfg,
|
|
961
789
|
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
790
|
providers: (() => {
|
|
972
791
|
try {
|
|
973
792
|
return (llm.listProviders() || []).map((p) => ({ id: p.id, name: p.name || p.id }))
|
|
@@ -980,7 +799,8 @@ export function apply(ctx, rawConfig = {}) {
|
|
|
980
799
|
if (endpoint === 'events') {
|
|
981
800
|
const sessionId = String(body.sessionId || '')
|
|
982
801
|
if (!sessionId) {
|
|
983
|
-
return
|
|
802
|
+
return rpcFail('err.needSessionId')
|
|
803
|
+
|
|
984
804
|
}
|
|
985
805
|
const since = Number.parseInt(String(body.since || '0'), 10) || 0
|
|
986
806
|
return { ok: true, value: { events: readEventsSince(paths.events, sessionId, since) } }
|
|
@@ -998,74 +818,52 @@ export function apply(ctx, rawConfig = {}) {
|
|
|
998
818
|
pluginCfg.judge.timeoutMs = allowlist.judgeTimeoutMs
|
|
999
819
|
persistPluginCfg()
|
|
1000
820
|
}
|
|
1001
|
-
return result.ok ? { ok: true, value: result } :
|
|
821
|
+
return result.ok ? { ok: true, value: result } : rpcFail(result.code || 'err.allowlistWrite', result.details || {})
|
|
822
|
+
|
|
1002
823
|
}
|
|
1003
824
|
if (endpoint === 'setup') {
|
|
1004
|
-
|
|
825
|
+
const setupResult = setAutoApproveSandbox(paths.profilePatch, pluginCfg.presetSandbox)
|
|
826
|
+
if (!setupResult.ok) {
|
|
827
|
+
return rpcFail(setupResult.code || 'err.preset', setupResult.details || { error: String(setupResult.error || '') })
|
|
828
|
+
|
|
829
|
+
}
|
|
830
|
+
return { ok: true, value: setupResult }
|
|
1005
831
|
}
|
|
1006
832
|
if (endpoint === 'save-plugin') {
|
|
1007
833
|
if (pluginCfgCorrupt && !body.overwriteCorrupt) {
|
|
1008
|
-
return
|
|
834
|
+
return rpcFail('err.pluginCorrupt')
|
|
835
|
+
|
|
1009
836
|
}
|
|
1010
837
|
const next = mergePluginConfig(pluginCfg, body)
|
|
1011
838
|
const prev = pluginCfg
|
|
1012
839
|
pluginCfg = next
|
|
1013
840
|
if (!persistPluginCfg({ overwriteCorrupt: Boolean(body.overwriteCorrupt) })) {
|
|
1014
841
|
pluginCfg = prev
|
|
1015
|
-
return
|
|
842
|
+
return rpcFail('err.pluginWrite')
|
|
843
|
+
|
|
1016
844
|
}
|
|
1017
845
|
let preset = null
|
|
1018
846
|
if (body && Object.prototype.hasOwnProperty.call(body, 'presetSandbox')) {
|
|
1019
847
|
preset = setAutoApproveSandbox(paths.profilePatch, pluginCfg.presetSandbox)
|
|
848
|
+
if (preset && !preset.ok) {
|
|
849
|
+
// 沙箱没写进 patch 就整个回滚:不能让 config.json 说 read-only、patch 还是全权限。
|
|
850
|
+
pluginCfg = prev
|
|
851
|
+
persistPluginCfg({ overwriteCorrupt: Boolean(body.overwriteCorrupt) })
|
|
852
|
+
return rpcFail(preset.code || 'err.preset', preset.details || { error: String(preset.error || '') })
|
|
853
|
+
|
|
854
|
+
}
|
|
1020
855
|
}
|
|
1021
856
|
if (typeof body.judgeTimeoutMs === 'number') {
|
|
1022
857
|
const timeoutResult = applyRuleOp('set', 'judgeTimeoutMs', body.judgeTimeoutMs)
|
|
1023
858
|
if (!timeoutResult.ok) {
|
|
1024
|
-
return
|
|
859
|
+
return rpcFail(timeoutResult.code || 'err.allowlistWrite', timeoutResult.details || {})
|
|
860
|
+
|
|
1025
861
|
}
|
|
1026
862
|
pluginCfg.judge.timeoutMs = allowlist.judgeTimeoutMs
|
|
1027
863
|
persistPluginCfg()
|
|
1028
864
|
}
|
|
1029
865
|
audit('CONFIG plugin 已更新')
|
|
1030
|
-
return { ok: true, value: { ok: true, plugin: pluginCfg, preset, setup:
|
|
1031
|
-
}
|
|
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() : '未启动' } }
|
|
866
|
+
return { ok: true, value: { ok: true, plugin: pluginCfg, preset, setup: presetSetupState() } }
|
|
1069
867
|
}
|
|
1070
868
|
if (endpoint === 'judge-catalog') {
|
|
1071
869
|
const provider = String(body.provider || configuredRoute().provider)
|
|
@@ -1073,7 +871,8 @@ export function apply(ctx, rawConfig = {}) {
|
|
|
1073
871
|
try {
|
|
1074
872
|
models = (await llm.listModels(provider)).map((m) => ({ id: m.id, name: m.name || m.id }))
|
|
1075
873
|
} catch (error) {
|
|
1076
|
-
return
|
|
874
|
+
return rpcFail('err.catalog', { error: String((error && error.message) || error) })
|
|
875
|
+
|
|
1077
876
|
}
|
|
1078
877
|
return { ok: true, value: { provider, models } }
|
|
1079
878
|
}
|
|
@@ -1094,12 +893,15 @@ export function apply(ctx, rawConfig = {}) {
|
|
|
1094
893
|
},
|
|
1095
894
|
}
|
|
1096
895
|
} catch (error) {
|
|
1097
|
-
return
|
|
896
|
+
return rpcFail('err.info', { error: String((error && error.message) || error) })
|
|
897
|
+
|
|
1098
898
|
}
|
|
1099
899
|
}
|
|
1100
|
-
return
|
|
900
|
+
return rpcFail('err.unknownEndpoint', { endpoint: String(endpoint || '') })
|
|
901
|
+
|
|
1101
902
|
} catch (error) {
|
|
1102
|
-
return
|
|
903
|
+
return rpcFail('err.internal', { error: String((error && error.message) || error) })
|
|
904
|
+
|
|
1103
905
|
}
|
|
1104
906
|
}
|
|
1105
907
|
|
|
@@ -1109,7 +911,8 @@ export function apply(ctx, rawConfig = {}) {
|
|
|
1109
911
|
try {
|
|
1110
912
|
body = await request.json()
|
|
1111
913
|
} catch {
|
|
1112
|
-
return
|
|
914
|
+
return Response.json({ type: 'server-response', rpcId: 'invalid-request', result: rpcFail('err.badBody') })
|
|
915
|
+
|
|
1113
916
|
}
|
|
1114
917
|
const rpcId = body && typeof body.rpcId === 'string' ? body.rpcId : 'invalid-request'
|
|
1115
918
|
const packed = body && body.payload && typeof body.payload === 'object' ? body.payload : {}
|
|
@@ -1119,7 +922,8 @@ export function apply(ctx, rawConfig = {}) {
|
|
|
1119
922
|
const result = await enqueueRpc(() => dispatch(endpoint, payload))
|
|
1120
923
|
return Response.json({ type: 'server-response', rpcId: rpcId, result: result })
|
|
1121
924
|
} catch (error) {
|
|
1122
|
-
return
|
|
925
|
+
return Response.json({ type: 'server-response', rpcId: rpcId, result: rpcFail('err.internal', { error: String(error) }) })
|
|
926
|
+
|
|
1123
927
|
}
|
|
1124
928
|
}
|
|
1125
929
|
|
|
@@ -1139,5 +943,5 @@ export function apply(ctx, rawConfig = {}) {
|
|
|
1139
943
|
}
|
|
1140
944
|
})
|
|
1141
945
|
|
|
1142
|
-
log(`已挂载:关键词→审核表 sandbox=${pluginCfg.presetSandbox}
|
|
946
|
+
log(`已挂载:关键词→审核表 sandbox=${pluginCfg.presetSandbox} profilePatch=${paths.profilePatch}`)
|
|
1143
947
|
}
|