dsh-ant 0.6.1 → 0.6.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/client.js +669 -194
- package/conversation.js +175 -80
- package/delegation.js +228 -49
- package/index.js +38 -7
- package/locale/messages.json +643 -144
- package/package.json +1 -1
package/delegation.js
CHANGED
|
@@ -5,14 +5,25 @@ import { dirname, join, resolve } from 'node:path'
|
|
|
5
5
|
import { spawn } from 'node:child_process'
|
|
6
6
|
import { CONVERSATION_TOOL_NAMES, TASK_DRAFT_TTL_MS, cancelSession, conversationKey,
|
|
7
7
|
buildTaskContext, cardReviewers, createConversationEngine, giteaPullRequestContext, isHelpRequest,
|
|
8
|
-
modelFor, requestsPrivateReply, teamModelProvider, truncateTaskOutput,
|
|
9
|
-
promptAndCollectSessionTurn, taskErrorText } from './conversation.js'
|
|
8
|
+
modelFor, prompt, requestsPrivateReply, restrictedSession, teamModelProvider, truncateTaskOutput,
|
|
9
|
+
promptAndCollectSessionTurn, taskErrorText, waitForSignal } from './conversation.js'
|
|
10
10
|
|
|
11
11
|
const MAX_DIFF_BYTES = 240_000
|
|
12
12
|
const MAX_GITEA_JSON_BYTES = 64_000
|
|
13
13
|
const MAX_REPLY_LENGTH = 3_800
|
|
14
14
|
const LEDGER_EVENT_TTL_MS = 30 * 86_400_000
|
|
15
15
|
const MAX_LEDGER_EVENTS = 1_000
|
|
16
|
+
const OWNER_APPROVAL_TTL_MS = 90 * 86_400_000
|
|
17
|
+
const MAX_OWNER_APPROVAL_ENTRIES = 1_000
|
|
18
|
+
const OWNER_APPROVAL_COMPACT_STATUSES = new Set([
|
|
19
|
+
'done', 'failed', 'canceled', 'rejected', 'expired', 'completed',
|
|
20
|
+
])
|
|
21
|
+
const OWNER_APPROVAL_TERMINAL_STATUSES = new Set(['done', 'failed', 'canceled', 'rejected', 'expired'])
|
|
22
|
+
const OWNER_APPROVAL_RETAINED_FIELDS = new Set([
|
|
23
|
+
'key', 'kind', 'status', 'outcome', 'approval_id', 'request_sha256', 'task_id',
|
|
24
|
+
'service_url', 'scope', 'actor_id', 'conversation_kind', 'conversation_id',
|
|
25
|
+
'botId', 'fingerprint', 'route', 'created_at', 'failure_reason', 'finish_error',
|
|
26
|
+
])
|
|
16
27
|
const TASK_PRESET = 'ant-delegated-task'
|
|
17
28
|
const MODEL_TIMEOUT_MS = 180_000
|
|
18
29
|
const REVIEW_EXECUTORS = new Set(['dsh', 'codex', 'claude-code'])
|
|
@@ -161,6 +172,13 @@ export async function writeLocalDelegationSettings(patch, settingsPath = LOCAL_S
|
|
|
161
172
|
function pruneLedgerEntries(entries, now) {
|
|
162
173
|
const next = new Map(entries)
|
|
163
174
|
for (const [key, saved] of next) {
|
|
175
|
+
if (saved.kind === 'owner_approval' && OWNER_APPROVAL_COMPACT_STATUSES.has(saved.status)) {
|
|
176
|
+
next.set(key, compactOwnerApproval(saved))
|
|
177
|
+
if (!OWNER_APPROVAL_TERMINAL_STATUSES.has(saved.status)) continue
|
|
178
|
+
const age = now - Date.parse(saved.created_at)
|
|
179
|
+
if (Number.isFinite(age) && age > OWNER_APPROVAL_TTL_MS) next.delete(key)
|
|
180
|
+
continue
|
|
181
|
+
}
|
|
164
182
|
if (!['conversation_input', 'conversation_ignored', 'group_notice', 'chat_reply',
|
|
165
183
|
'card_draft', 'card_task', 'legacy_pr_task'].includes(saved.kind)) continue
|
|
166
184
|
const age = now - Date.parse(saved.created_at)
|
|
@@ -172,6 +190,10 @@ function pruneLedgerEntries(entries, now) {
|
|
|
172
190
|
next.delete(key)
|
|
173
191
|
}
|
|
174
192
|
}
|
|
193
|
+
const ownerApprovals = [...next.values()].filter((saved) => saved.kind === 'owner_approval'
|
|
194
|
+
&& OWNER_APPROVAL_TERMINAL_STATUSES.has(saved.status))
|
|
195
|
+
.sort((a, b) => (b.created_at || '').localeCompare(a.created_at || ''))
|
|
196
|
+
for (const saved of ownerApprovals.slice(MAX_OWNER_APPROVAL_ENTRIES)) next.delete(saved.key)
|
|
175
197
|
const recentChats = [...next.values()].filter((saved) => saved.kind === 'chat_reply'
|
|
176
198
|
&& saved.status === 'done').sort((a, b) => (b.created_at || '').localeCompare(a.created_at || ''))
|
|
177
199
|
for (const saved of recentChats.slice(1_000)) next.delete(saved.key)
|
|
@@ -189,6 +211,14 @@ function pruneLedgerEntries(entries, now) {
|
|
|
189
211
|
return next
|
|
190
212
|
}
|
|
191
213
|
|
|
214
|
+
function compactOwnerApproval(entry) {
|
|
215
|
+
const retainedFields = entry.status === 'done' ? OWNER_APPROVAL_RETAINED_FIELDS
|
|
216
|
+
: new Set([...OWNER_APPROVAL_RETAINED_FIELDS, 'reply'])
|
|
217
|
+
const compact = Object.fromEntries(Object.entries(entry)
|
|
218
|
+
.filter(([key]) => retainedFields.has(key)))
|
|
219
|
+
return Object.keys(compact).length === Object.keys(entry).length ? entry : compact
|
|
220
|
+
}
|
|
221
|
+
|
|
192
222
|
export class DelegationLedger {
|
|
193
223
|
constructor(path = join(dshRoot(), 'ant-delegation', 'events.json')) {
|
|
194
224
|
this.path = path
|
|
@@ -223,7 +253,8 @@ export class DelegationLedger {
|
|
|
223
253
|
}
|
|
224
254
|
}
|
|
225
255
|
const next = pruneLedgerEntries(loaded, Date.now())
|
|
226
|
-
if (next.size !== loaded.size
|
|
256
|
+
if (next.size !== loaded.size
|
|
257
|
+
|| [...next.entries()].some(([key, entry]) => loaded.get(key) !== entry)) changed = true
|
|
227
258
|
if (changed) await this.writeSnapshot(next)
|
|
228
259
|
else this.entries = next
|
|
229
260
|
return this
|
|
@@ -274,7 +305,8 @@ function statusErrorText(value) {
|
|
|
274
305
|
export async function recentDelegationStatus(serviceUrl, limit = 12) {
|
|
275
306
|
const ledger = await new DelegationLedger().load()
|
|
276
307
|
return ledger.values().filter((entry) => entry.service_url === serviceUrl && entry.status !== 'ignored'
|
|
277
|
-
&& (!entry.kind || ['card_task', 'legacy_pr_task'].includes(entry.kind))
|
|
308
|
+
&& (!entry.kind || ['card_task', 'legacy_pr_task', 'owner_approval'].includes(entry.kind))
|
|
309
|
+
&& (entry.kind !== 'owner_approval' || typeof entry.task_id === 'string'))
|
|
278
310
|
.sort((left, right) => (right.created_at || '').localeCompare(left.created_at || ''))
|
|
279
311
|
.slice(0, limit)
|
|
280
312
|
.map((entry) => ({
|
|
@@ -284,9 +316,9 @@ export async function recentDelegationStatus(serviceUrl, limit = 12) {
|
|
|
284
316
|
repository: entry.grant?.repository || null,
|
|
285
317
|
pr_number: entry.grant?.pr_number || null,
|
|
286
318
|
task_id: entry.task_id || null,
|
|
287
|
-
title: entry.card?.title || null,
|
|
288
|
-
outcome: entry.outcome || null,
|
|
289
|
-
failure_reason: statusErrorText(entry.failure_reason)
|
|
319
|
+
title: entry.card?.title || entry.proposal?.title || null,
|
|
320
|
+
outcome: entry.status === 'finishing' ? null : entry.outcome || null,
|
|
321
|
+
failure_reason: statusErrorText(entry.finish_error) || statusErrorText(entry.failure_reason)
|
|
290
322
|
|| (entry.outcome === 'failed' ? statusErrorText(entry.reply) : null),
|
|
291
323
|
delivery_error: statusErrorText(entry.delivery_error),
|
|
292
324
|
}))
|
|
@@ -994,7 +1026,19 @@ export async function runDshSubagentTask(ctx, executor, task, resources = [], se
|
|
|
994
1026
|
const boundedSignal = signal ? AbortSignal.any([signal, timeout]) : timeout
|
|
995
1027
|
let removeAbort = () => {}
|
|
996
1028
|
try {
|
|
997
|
-
const
|
|
1029
|
+
const createPromise = Promise.resolve().then(() => {
|
|
1030
|
+
boundedSignal.throwIfAborted()
|
|
1031
|
+
return ctx.sessionController.create({ cwd: workspace, agentPreset: TASK_PRESET })
|
|
1032
|
+
})
|
|
1033
|
+
let created
|
|
1034
|
+
try { created = await waitForSignal(createPromise, boundedSignal) }
|
|
1035
|
+
catch (error) {
|
|
1036
|
+
if (boundedSignal.aborted) {
|
|
1037
|
+
void createPromise.then((lateSession) => cancelSession(ctx, lateSession.sessionId))
|
|
1038
|
+
.catch((cleanupError) => logger.warn?.('ANT 迟到的任务会话清理失败:', cleanupError?.message))
|
|
1039
|
+
}
|
|
1040
|
+
throw error
|
|
1041
|
+
}
|
|
998
1042
|
sessionId = created.sessionId
|
|
999
1043
|
if (created.agentPreset !== TASK_PRESET || typeof sessionId !== 'string' || !sessionId) {
|
|
1000
1044
|
throw new Error('DSH 未启用 ANT 受限任务父会话。')
|
|
@@ -1253,6 +1297,11 @@ export function taskGate(initialLimit = 3) {
|
|
|
1253
1297
|
}
|
|
1254
1298
|
}
|
|
1255
1299
|
|
|
1300
|
+
// A card task can span several messages; the owner must see the text the executor will receive.
|
|
1301
|
+
export function ownerApprovalRequestText(messageText, cardTask) {
|
|
1302
|
+
return (cardTask?.authorization?.request_text || messageText).slice(0, 8_192)
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1256
1305
|
export function startDelegationHost(ctx, readConnection, { ledger = new DelegationLedger(), logger = console,
|
|
1257
1306
|
workspaceRoot = dshRoot(), readSettings = readLocalDelegationSettings } = {}) {
|
|
1258
1307
|
const controller = new AbortController()
|
|
@@ -1268,9 +1317,11 @@ export function startDelegationHost(ctx, readConnection, { ledger = new Delegati
|
|
|
1268
1317
|
const processing = new Set()
|
|
1269
1318
|
const processingTasks = new Set()
|
|
1270
1319
|
const legacyTaskControllers = new Map()
|
|
1320
|
+
const ownerApprovalTaskControllers = new Map()
|
|
1271
1321
|
const receiptPending = new Set()
|
|
1272
1322
|
const accepting = new Map()
|
|
1273
1323
|
const sessionIds = new Set()
|
|
1324
|
+
const pendingOwnerChallenges = new Map()
|
|
1274
1325
|
const gate = taskGate()
|
|
1275
1326
|
let refreshing = false
|
|
1276
1327
|
let refreshDone = Promise.resolve()
|
|
@@ -1286,7 +1337,8 @@ export function startDelegationHost(ctx, readConnection, { ledger = new Delegati
|
|
|
1286
1337
|
const event = turn.event
|
|
1287
1338
|
const title = typeof args?.title === 'string' ? args.title.trim().slice(0, 100) : ''
|
|
1288
1339
|
const taskPrompt = typeof args?.task_prompt === 'string' ? args.task_prompt.trim().slice(0, 4_000) : ''
|
|
1289
|
-
const
|
|
1340
|
+
const cardTask = args?.card_task || null
|
|
1341
|
+
const requestText = ownerApprovalRequestText(addressedText(event), cardTask)
|
|
1290
1342
|
if (!title || !taskPrompt || !requestText) throw new Error('本次审批请求缺少标题、执行说明或原始请求。')
|
|
1291
1343
|
if (turn.owner_approval_bound !== true) {
|
|
1292
1344
|
throw new Error('机器人主人尚未绑定审批私聊,请联系 B 在 ANT 协作设置中绑定。')
|
|
@@ -1303,14 +1355,16 @@ export function startDelegationHost(ctx, readConnection, { ledger = new Delegati
|
|
|
1303
1355
|
requester_id: event.actor.id, message_id: event.messageId,
|
|
1304
1356
|
conversation_kind: event.conversation.kind,
|
|
1305
1357
|
...(event.conversation.kind === 'group' ? { conversation_id: event.conversation.id } : {}),
|
|
1306
|
-
request_text: requestText, title, task_prompt: taskPrompt
|
|
1358
|
+
request_text: requestText, title, task_prompt: taskPrompt,
|
|
1359
|
+
...(cardTask ? { card_id: cardTask.card.id, parameters: cardTask.parameters } : {}) }
|
|
1307
1360
|
const requestHash = createHash('sha256').update(JSON.stringify(bound)).digest('hex')
|
|
1308
1361
|
const approval = await memberJson(turn.connection, '/api/me/im-delegations/owner-approvals', {
|
|
1309
1362
|
method: 'POST', body: JSON.stringify({ bot_id: event.botId,
|
|
1310
1363
|
bot_fingerprint: event.fingerprint, requester_id: event.actor.id,
|
|
1311
1364
|
message_id: event.messageId, conversation_kind: event.conversation.kind,
|
|
1312
1365
|
...(event.conversation.kind === 'group' ? { conversation_id: event.conversation.id } : {}),
|
|
1313
|
-
request_sha256: requestHash }
|
|
1366
|
+
request_sha256: requestHash, ...(cardTask ? { card_id: cardTask.card.id } : {}) }),
|
|
1367
|
+
signal: requestSignal,
|
|
1314
1368
|
})
|
|
1315
1369
|
if (!/^[A-F0-9]{12}$/u.test(approval.id || '') || !approval.target_id
|
|
1316
1370
|
|| !approval.owner_sender_id || !Number.isSafeInteger(approval.expires_at)) {
|
|
@@ -1323,13 +1377,17 @@ export function startDelegationHost(ctx, readConnection, { ledger = new Delegati
|
|
|
1323
1377
|
...(event.conversation.kind === 'group' ? { conversation_id: event.conversation.id } : {}),
|
|
1324
1378
|
service_url: turn.connection.serviceUrl, route: event.reply, scope: turn.scope,
|
|
1325
1379
|
target_id: approval.target_id, owner_sender_id: approval.owner_sender_id,
|
|
1326
|
-
created_at: event.at, expires_at: approval.expires_at, status: 'pending'
|
|
1380
|
+
created_at: event.at, expires_at: approval.expires_at, status: 'pending',
|
|
1381
|
+
...(cardTask ? { card_task: cardTask } : {}) })
|
|
1327
1382
|
const available = []
|
|
1328
|
-
if (
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1383
|
+
if (cardTask) available.push(cardTask.card.runner)
|
|
1384
|
+
else {
|
|
1385
|
+
if (ctx.sessionController && ctx.agents) available.push('dsh')
|
|
1386
|
+
try {
|
|
1387
|
+
const providers = (ctx.get?.('subagents') ?? ctx.subagents)?.list?.() || []
|
|
1388
|
+
for (const runner of ['codex', 'claude-code']) if (providers.includes(runner)) available.push(runner)
|
|
1389
|
+
} catch {}
|
|
1390
|
+
}
|
|
1333
1391
|
if (!available.length) {
|
|
1334
1392
|
await memberJson(turn.connection, `/api/me/im-delegations/owner-approvals/${approval.id}/cancel`,
|
|
1335
1393
|
{ method: 'POST', body: '{}', signal: requestSignal })
|
|
@@ -1346,7 +1404,9 @@ export function startDelegationHost(ctx, readConnection, { ledger = new Delegati
|
|
|
1346
1404
|
`A 的原始请求:${requestText}`,
|
|
1347
1405
|
`拟执行任务:${title}`,
|
|
1348
1406
|
`执行说明:${taskPrompt}`,
|
|
1349
|
-
|
|
1407
|
+
...(cardTask ? [`任务参数:${JSON.stringify(cardTask.parameters)}`,
|
|
1408
|
+
`任务卡执行器:${(cardTask.parallel_agents || cardTask.card.cross_review_always
|
|
1409
|
+
? cardTask.card.reviewers : available).join('、')}`] : [`可选执行器:${available.join('、')}`]),
|
|
1350
1410
|
'权限说明:DSH 仅使用团队模型;Codex/Claude Code 按本机 DSH Provider Profile 的文件、Git、网络权限运行,ANT 不会把这些权限隔离到本次任务。结果返回原会话。批准前不会启动。',
|
|
1351
1411
|
...available.map((runner) => `批准:批准 ${approval.id} ${runner}`),
|
|
1352
1412
|
`拒绝:拒绝 ${approval.id}`,
|
|
@@ -1384,7 +1444,7 @@ export function startDelegationHost(ctx, readConnection, { ledger = new Delegati
|
|
|
1384
1444
|
async function runApprovedDshTask(entry, connection, taskSignal) {
|
|
1385
1445
|
const model = await modelFor(ctx, memberJson, connection)
|
|
1386
1446
|
const { sessionId, cwd } = await restrictedSession(ctx, model, workspaceRoot,
|
|
1387
|
-
sessionIds, null, null, TASK_PRESET)
|
|
1447
|
+
sessionIds, null, null, TASK_PRESET, taskSignal)
|
|
1388
1448
|
try {
|
|
1389
1449
|
const text = [
|
|
1390
1450
|
'这是一项由机器人主人 B 明确批准、只执行一次的任务。只按 B 已确认的执行说明处理,不扩大任务范围。',
|
|
@@ -1402,14 +1462,19 @@ export function startDelegationHost(ctx, readConnection, { ledger = new Delegati
|
|
|
1402
1462
|
}
|
|
1403
1463
|
}
|
|
1404
1464
|
|
|
1405
|
-
async function executeOwnerApproval(entry, acceptedConnection
|
|
1465
|
+
async function executeOwnerApproval(entry, acceptedConnection) {
|
|
1406
1466
|
if (processing.has(entry.key)) return
|
|
1407
1467
|
processing.add(entry.key)
|
|
1468
|
+
const runningTask = { controller: new AbortController(), finished: false }
|
|
1469
|
+
const taskSignal = AbortSignal.any([controller.signal, runningTask.controller.signal])
|
|
1470
|
+
ownerApprovalTaskControllers.set(entry.key, runningTask)
|
|
1408
1471
|
let release = null
|
|
1409
1472
|
let outcome = 'failed'
|
|
1410
1473
|
let resultText = '任务未完成,请重新发起审批请求。'
|
|
1474
|
+
let interruptedForRestart = false
|
|
1411
1475
|
try {
|
|
1412
|
-
release = await gate.acquire(
|
|
1476
|
+
release = await gate.acquire(taskSignal)
|
|
1477
|
+
entry = await ledger.put({ ...entry, status: 'running' })
|
|
1413
1478
|
const connection = await readConnection()
|
|
1414
1479
|
if (!connection || connection.serviceUrl !== acceptedConnection.serviceUrl
|
|
1415
1480
|
|| connection.serviceUrl !== entry.service_url) throw new Error('ANT 团队连接已变化,任务没有启动。')
|
|
@@ -1420,28 +1485,43 @@ export function startDelegationHost(ctx, readConnection, { ledger = new Delegati
|
|
|
1420
1485
|
for (const runner of ['codex', 'claude-code']) if (providers.includes(runner)) available.push(runner)
|
|
1421
1486
|
} catch {}
|
|
1422
1487
|
if (!available.includes(entry.runner)) throw new Error(`这台 DSH 当前没有启用 ${entry.runner} 执行器。`)
|
|
1423
|
-
|
|
1488
|
+
taskSignal.throwIfAborted()
|
|
1424
1489
|
const task = { title: entry.proposal.title, task_prompt: entry.proposal.task_prompt,
|
|
1425
|
-
request_text: entry.request_text, owner_approval_id: entry.approval_id }
|
|
1490
|
+
request_text: entry.request_text, owner_approval_id: entry.approval_id, task_id: entry.task_id }
|
|
1426
1491
|
const result = entry.runner === 'dsh'
|
|
1427
|
-
? await runApprovedDshTask(entry, connection,
|
|
1492
|
+
? await runApprovedDshTask(entry, connection, taskSignal)
|
|
1428
1493
|
: await runDshSubagentTask(ctx, entry.runner, task, [], sessionIds,
|
|
1429
|
-
|
|
1430
|
-
|
|
1494
|
+
taskSignal, workspaceRoot, logger)
|
|
1495
|
+
taskSignal.throwIfAborted()
|
|
1496
|
+
runningTask.finished = true
|
|
1431
1497
|
outcome = 'completed'
|
|
1432
1498
|
resultText = `${entry.proposal.title}已完成:\n${truncateTaskOutput(result, MAX_REPLY_LENGTH - entry.proposal.title.length - 20)}`
|
|
1433
1499
|
} catch (error) {
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1500
|
+
runningTask.finished = true
|
|
1501
|
+
if (controller.signal.aborted && !runningTask.controller.signal.aborted) {
|
|
1502
|
+
interruptedForRestart = true
|
|
1503
|
+
} else {
|
|
1504
|
+
outcome = runningTask.controller.signal.aborted ? 'canceled' : 'failed'
|
|
1505
|
+
if (outcome === 'canceled') resultText = '主人审批的任务已取消,未自动重试。'
|
|
1506
|
+
else {
|
|
1507
|
+
entry = { ...entry, failure_reason: taskErrorText(error, '请检查本机执行器状态后重新发起。') }
|
|
1508
|
+
resultText = `主人审批的任务未完成:${entry.failure_reason}`
|
|
1509
|
+
}
|
|
1510
|
+
}
|
|
1437
1511
|
} finally {
|
|
1438
1512
|
release?.()
|
|
1439
1513
|
}
|
|
1514
|
+
if (interruptedForRestart) {
|
|
1515
|
+
ownerApprovalTaskControllers.delete(entry.key)
|
|
1516
|
+
processing.delete(entry.key)
|
|
1517
|
+
return
|
|
1518
|
+
}
|
|
1440
1519
|
try {
|
|
1520
|
+
entry = await ledger.put({ ...entry, status: 'finishing', outcome, reply: resultText })
|
|
1441
1521
|
const currentConnection = await readConnection()
|
|
1442
1522
|
if (!currentConnection || currentConnection.serviceUrl !== entry.service_url) {
|
|
1443
|
-
await ledger.put({ ...entry, status:
|
|
1444
|
-
|
|
1523
|
+
await ledger.put({ ...entry, status: 'finishing', outcome,
|
|
1524
|
+
finish_error: 'ANT 团队连接已变化,服务端终态和任务结果待核对。' })
|
|
1445
1525
|
return
|
|
1446
1526
|
}
|
|
1447
1527
|
const finished = await memberJson(currentConnection,
|
|
@@ -1449,34 +1529,63 @@ export function startDelegationHost(ctx, readConnection, { ledger = new Delegati
|
|
|
1449
1529
|
method: 'POST', body: JSON.stringify({ outcome }), signal: controller.signal,
|
|
1450
1530
|
})
|
|
1451
1531
|
if (finished?.finished !== true) {
|
|
1452
|
-
await ledger.put({ ...entry, status:
|
|
1453
|
-
|
|
1532
|
+
await ledger.put({ ...entry, status: 'finishing', outcome,
|
|
1533
|
+
finish_error: '服务端未确认审批任务终态;请检查主人绑定和私聊权限后重试。' })
|
|
1454
1534
|
return
|
|
1455
1535
|
}
|
|
1456
|
-
const final = await ledger.put({ ...entry, status: outcome, outcome, reply: resultText
|
|
1536
|
+
const final = await ledger.put({ ...entry, status: outcome, outcome, reply: resultText,
|
|
1537
|
+
finish_error: null })
|
|
1457
1538
|
await replyOwnerApprovalResult(final, resultText, currentConnection)
|
|
1458
1539
|
} catch (error) {
|
|
1459
1540
|
logger.warn?.('ANT 主人审批任务状态无法回写:', error?.message)
|
|
1460
|
-
await ledger.put({ ...entry, status: '
|
|
1461
|
-
|
|
1541
|
+
await ledger.put({ ...entry, status: 'finishing', outcome,
|
|
1542
|
+
finish_error: statusErrorText(error?.message) || '服务端未确认审批任务终态;结果待核对。' })
|
|
1462
1543
|
} finally {
|
|
1544
|
+
ownerApprovalTaskControllers.delete(entry.key)
|
|
1463
1545
|
processing.delete(entry.key)
|
|
1464
1546
|
}
|
|
1465
1547
|
}
|
|
1466
1548
|
|
|
1549
|
+
// The approval is spent once the card task is queued; the card pipeline owns the task from there.
|
|
1550
|
+
async function startApprovedCardTask(entry, connection) {
|
|
1551
|
+
const finished = await memberJson(connection,
|
|
1552
|
+
`/api/me/im-delegations/owner-approvals/${entry.approval_id}/finish`, {
|
|
1553
|
+
method: 'POST', body: JSON.stringify({ outcome: 'completed' }), signal: controller.signal,
|
|
1554
|
+
})
|
|
1555
|
+
if (finished?.finished !== true) throw new Error('服务端未确认审批状态。')
|
|
1556
|
+
await conversation.enqueueApprovedCardTask(entry)
|
|
1557
|
+
await ledger.put({ ...entry, status: 'done', outcome: 'completed' })
|
|
1558
|
+
}
|
|
1559
|
+
|
|
1467
1560
|
async function recoverOwnerApproval(entry, connection) {
|
|
1468
1561
|
try {
|
|
1469
1562
|
let state = await memberJson(connection,
|
|
1470
1563
|
`/api/me/im-delegations/owner-approvals/${entry.approval_id}`)
|
|
1564
|
+
if (entry.card_task && ['claimed', 'completed'].includes(state.status)) {
|
|
1565
|
+
if (state.status === 'claimed') return await startApprovedCardTask(entry, connection)
|
|
1566
|
+
await conversation.enqueueApprovedCardTask(entry)
|
|
1567
|
+
await ledger.put({ ...entry, status: 'done', outcome: 'completed' })
|
|
1568
|
+
return
|
|
1569
|
+
}
|
|
1471
1570
|
const wasClaimed = state.status === 'claimed'
|
|
1472
1571
|
if (state.status === 'claimed') {
|
|
1572
|
+
const finishOutcome = ['completed', 'failed', 'canceled'].includes(entry.outcome)
|
|
1573
|
+
? entry.outcome : 'failed'
|
|
1473
1574
|
const finished = await memberJson(connection,
|
|
1474
1575
|
`/api/me/im-delegations/owner-approvals/${entry.approval_id}/finish`, {
|
|
1475
|
-
method: 'POST', body: JSON.stringify({ outcome:
|
|
1576
|
+
method: 'POST', body: JSON.stringify({ outcome: finishOutcome }),
|
|
1476
1577
|
})
|
|
1477
|
-
if (finished?.finished) state = { status:
|
|
1478
|
-
else
|
|
1479
|
-
|
|
1578
|
+
if (finished?.finished) state = { status: finishOutcome, outcome: finishOutcome, runner: state.runner }
|
|
1579
|
+
else {
|
|
1580
|
+
state = await memberJson(connection,
|
|
1581
|
+
`/api/me/im-delegations/owner-approvals/${entry.approval_id}`)
|
|
1582
|
+
if (state.status === 'claimed') {
|
|
1583
|
+
await ledger.put({ ...entry, status: 'finishing',
|
|
1584
|
+
finish_error: entry.finish_error
|
|
1585
|
+
|| '服务端未确认审批任务终态;请检查主人绑定和私聊权限后重试。' })
|
|
1586
|
+
return
|
|
1587
|
+
}
|
|
1588
|
+
}
|
|
1480
1589
|
}
|
|
1481
1590
|
if (['pending', 'approved'].includes(state.status)) return
|
|
1482
1591
|
if (['reply_pending', 'done'].includes(entry.status)) return
|
|
@@ -1484,6 +1593,7 @@ export function startDelegationHost(ctx, readConnection, { ledger = new Delegati
|
|
|
1484
1593
|
if (state.status === 'expired') text = '主人审批已过期,任务没有启动。请重新发起请求。'
|
|
1485
1594
|
else if (state.status === 'rejected') text = '主人拒绝了这次任务请求。'
|
|
1486
1595
|
else if (state.status === 'canceled') text = '这次审批任务已取消,没有自动重试。'
|
|
1596
|
+
else if (state.status === 'failed' && entry.reply) text = entry.reply
|
|
1487
1597
|
else if (wasClaimed && state.status === 'failed') {
|
|
1488
1598
|
text = 'DSH 上次中断,任务执行结果未知;为避免重复操作,没有自动重跑。请检查本机后重新发起。'
|
|
1489
1599
|
} else if (state.status === 'completed' && entry.reply) text = entry.reply
|
|
@@ -1494,10 +1604,15 @@ export function startDelegationHost(ctx, readConnection, { ledger = new Delegati
|
|
|
1494
1604
|
else return
|
|
1495
1605
|
const outcome = state.outcome || (state.status === 'expired' ? 'expired' : state.status)
|
|
1496
1606
|
const current = await ledger.put({ ...entry, status: outcome, outcome,
|
|
1497
|
-
reply: text, failure_reason: state.status === 'completed' ? null : text })
|
|
1607
|
+
reply: text, failure_reason: state.status === 'completed' ? null : text, finish_error: null })
|
|
1498
1608
|
await replyOwnerApprovalResult(current, text, connection)
|
|
1499
1609
|
} catch (error) {
|
|
1500
1610
|
logger.warn?.('ANT 主人审批状态恢复失败:', error?.message)
|
|
1611
|
+
if (['claimed', 'running', 'finishing'].includes(entry.status)) {
|
|
1612
|
+
await ledger.put({ ...entry, status: 'finishing',
|
|
1613
|
+
finish_error: statusErrorText(error?.message) || '无法读取服务端审批状态;结果待核对。' })
|
|
1614
|
+
.catch(() => undefined)
|
|
1615
|
+
}
|
|
1501
1616
|
}
|
|
1502
1617
|
}
|
|
1503
1618
|
|
|
@@ -1708,17 +1823,36 @@ export function startDelegationHost(ctx, readConnection, { ledger = new Delegati
|
|
|
1708
1823
|
}
|
|
1709
1824
|
if (event.conversation?.kind !== 'dm') return { accepted: true }
|
|
1710
1825
|
const text = addressedText(event)
|
|
1711
|
-
const
|
|
1826
|
+
const ownerBindingCommand = /^绑定审批身份\s+([a-f0-9]{12})$/iu.exec(text)
|
|
1827
|
+
const bareOwnerBindingCode = /^([a-f0-9]{12})$/iu.exec(text)
|
|
1828
|
+
const ownerChallengeKey = event.fingerprint.toLowerCase()
|
|
1829
|
+
const pendingOwnerChallenge = pendingOwnerChallenges.get(ownerChallengeKey)
|
|
1830
|
+
const ownerBinding = ownerBindingCommand || (pendingOwnerChallenge && bareOwnerBindingCode)
|
|
1712
1831
|
if (ownerBinding) {
|
|
1832
|
+
const code = (ownerBindingCommand?.[1] || bareOwnerBindingCode[1]).toUpperCase()
|
|
1833
|
+
if (pendingOwnerChallenge && pendingOwnerChallenge.expires_at <= Date.now()) {
|
|
1834
|
+
pendingOwnerChallenges.delete(ownerChallengeKey)
|
|
1835
|
+
await ctx.dshIm.replyChecked(event.botId, event.reply,
|
|
1836
|
+
'审批私聊绑定未完成:绑定码已过期,请从 ANT“私聊入口”重新生成。',
|
|
1837
|
+
{ expectedFingerprint: event.fingerprint, signal: signal || controller.signal }).catch(() => undefined)
|
|
1838
|
+
return { accepted: true }
|
|
1839
|
+
}
|
|
1840
|
+
if (pendingOwnerChallenge && code !== pendingOwnerChallenge.code) {
|
|
1841
|
+
await ctx.dshIm.replyChecked(event.botId, event.reply,
|
|
1842
|
+
'审批私聊绑定未完成:这不是当前机器人生成的绑定码;请从 ANT“私聊入口”复制最新验证码。',
|
|
1843
|
+
{ expectedFingerprint: event.fingerprint, signal: signal || controller.signal }).catch(() => undefined)
|
|
1844
|
+
return { accepted: true }
|
|
1845
|
+
}
|
|
1713
1846
|
try {
|
|
1714
1847
|
const result = await memberJson(connection, '/api/me/im-delegations/owner-bind', {
|
|
1715
1848
|
method: 'POST', body: JSON.stringify({ bot_id: event.botId,
|
|
1716
1849
|
bot_fingerprint: event.fingerprint, sender_id: event.actor.id,
|
|
1717
|
-
code
|
|
1850
|
+
code }), signal,
|
|
1718
1851
|
})
|
|
1719
1852
|
if (!result?.bound || typeof result.target_id !== 'string') {
|
|
1720
1853
|
throw new Error('绑定码无效、已过期或尝试次数已用完。')
|
|
1721
1854
|
}
|
|
1855
|
+
pendingOwnerChallenges.delete(ownerChallengeKey)
|
|
1722
1856
|
await ensureOwnerApprovalTarget(ctx, event.botId, event.fingerprint,
|
|
1723
1857
|
event.actor.id, result.target_id)
|
|
1724
1858
|
ownerBoundByFingerprint.set(event.fingerprint, true)
|
|
@@ -1750,6 +1884,9 @@ export function startDelegationHost(ctx, readConnection, { ledger = new Delegati
|
|
|
1750
1884
|
const available = runner === 'dsh' ? Boolean(ctx.sessionController && ctx.agents)
|
|
1751
1885
|
: (() => { try { return (ctx.get?.('subagents') ?? ctx.subagents)?.list?.().includes(runner) === true } catch { return false } })()
|
|
1752
1886
|
if (!available) throw new Error(`这台 DSH 尚未启用 ${runner} 子 Agent 提供方;审批仍保留,可改选其他执行器。`)
|
|
1887
|
+
if (pending.card_task && runner !== pending.card_task.card.runner) {
|
|
1888
|
+
throw new Error(`这项任务卡固定由 ${pending.card_task.card.runner} 执行;审批仍保留。`)
|
|
1889
|
+
}
|
|
1753
1890
|
}
|
|
1754
1891
|
const result = await memberJson(connection,
|
|
1755
1892
|
`/api/me/im-delegations/owner-approvals/${approvalId}/decision`, {
|
|
@@ -1768,10 +1905,22 @@ export function startDelegationHost(ctx, readConnection, { ledger = new Delegati
|
|
|
1768
1905
|
method: 'POST', body: JSON.stringify({ request_sha256: pending.request_sha256, runner }), signal,
|
|
1769
1906
|
})
|
|
1770
1907
|
if (!claimed?.claimed) throw new Error('审批已被使用、已过期或权限状态已改变。')
|
|
1771
|
-
|
|
1908
|
+
if (pending.card_task) {
|
|
1909
|
+
await ledger.put({ ...pending, status: 'claimed', runner, decision_sender_id: event.actor.id,
|
|
1910
|
+
decided_at: event.at })
|
|
1911
|
+
await startApprovedCardTask(ledger.get(pending.key), connection)
|
|
1912
|
+
await ctx.dshIm.replyChecked(event.botId, event.reply,
|
|
1913
|
+
`已批准审批 ${approvalId},任务已进入队列;完成后结果返回原会话。`,
|
|
1914
|
+
{ expectedFingerprint: event.fingerprint, signal: signal || controller.signal })
|
|
1915
|
+
.catch((error) => logger.warn?.('ANT 主人审批回执发送失败:', error?.message))
|
|
1916
|
+
return { accepted: true }
|
|
1917
|
+
}
|
|
1918
|
+
const taskId = createHash('sha256').update(`${pending.key}\0${pending.approval_id}`)
|
|
1919
|
+
.digest('hex').slice(0, 10).toUpperCase()
|
|
1920
|
+
const approved = await ledger.put({ ...pending, task_id: taskId, status: 'claimed', runner,
|
|
1772
1921
|
decision_sender_id: event.actor.id, decided_at: event.at })
|
|
1773
|
-
reply = `已批准审批 ${approvalId},正在启动 ${runner}
|
|
1774
|
-
const work = executeOwnerApproval(approved, connection
|
|
1922
|
+
reply = `已批准审批 ${approvalId},任务编号 ${taskId},正在启动 ${runner}。可在 ANT 协作页查看状态或取消;完成后结果返回原会话。`
|
|
1923
|
+
const work = executeOwnerApproval(approved, connection)
|
|
1775
1924
|
processingTasks.add(work)
|
|
1776
1925
|
void work.catch((error) => logger.warn?.('ANT 主人审批任务收尾失败:', error?.message))
|
|
1777
1926
|
.finally(() => processingTasks.delete(work))
|
|
@@ -2059,10 +2208,32 @@ export function startDelegationHost(ctx, readConnection, { ledger = new Delegati
|
|
|
2059
2208
|
dmChatEnabledByFingerprint.set(fingerprint.toLowerCase(), enabled)
|
|
2060
2209
|
}
|
|
2061
2210
|
}
|
|
2062
|
-
stop.
|
|
2063
|
-
|
|
2211
|
+
stop.rememberOwnerChallenge = (fingerprint, code) => {
|
|
2212
|
+
if (typeof fingerprint !== 'string' || !/^[a-f0-9]{64}$/iu.test(fingerprint)
|
|
2213
|
+
|| typeof code !== 'string' || !/^[a-f0-9]{12}$/iu.test(code)) return false
|
|
2214
|
+
pendingOwnerChallenges.set(fingerprint.toLowerCase(), {
|
|
2215
|
+
code: code.toUpperCase(), expires_at: Date.now() + 5 * 60_000,
|
|
2216
|
+
})
|
|
2217
|
+
return true
|
|
2218
|
+
}
|
|
2219
|
+
stop.ownerChallengeStatus = (fingerprint) => {
|
|
2220
|
+
if (typeof fingerprint !== 'string' || !/^[a-f0-9]{64}$/iu.test(fingerprint)) return null
|
|
2221
|
+
const key = fingerprint.toLowerCase()
|
|
2222
|
+
const challenge = pendingOwnerChallenges.get(key)
|
|
2223
|
+
if (!challenge || challenge.expires_at <= Date.now()) {
|
|
2224
|
+
pendingOwnerChallenges.delete(key)
|
|
2225
|
+
return { pending: false }
|
|
2226
|
+
}
|
|
2227
|
+
return { pending: true }
|
|
2228
|
+
}
|
|
2229
|
+
stop.cancelOwnerChallenge = (fingerprint) => {
|
|
2230
|
+
if (typeof fingerprint !== 'string' || !/^[a-f0-9]{64}$/iu.test(fingerprint)) return false
|
|
2231
|
+
return pendingOwnerChallenges.delete(fingerprint.toLowerCase())
|
|
2232
|
+
}
|
|
2233
|
+
stop.recognizeRule = async (description, requestSignal = null) => {
|
|
2234
|
+
await waitForSignal(ready, requestSignal)
|
|
2064
2235
|
if (stopped || !ledgerReady) throw new Error('本机协作服务尚未就绪,请稍后重试。')
|
|
2065
|
-
return conversation.recognizeRule(description)
|
|
2236
|
+
return conversation.recognizeRule(description, requestSignal)
|
|
2066
2237
|
}
|
|
2067
2238
|
stop.cancelTask = async (taskId, serviceUrl) => {
|
|
2068
2239
|
await ready
|
|
@@ -2079,6 +2250,14 @@ export function startDelegationHost(ctx, readConnection, { ledger = new Delegati
|
|
|
2079
2250
|
}
|
|
2080
2251
|
return true
|
|
2081
2252
|
}
|
|
2253
|
+
const approval = ledger.values().find((entry) => entry.kind === 'owner_approval'
|
|
2254
|
+
&& entry.task_id === taskId && entry.service_url === serviceUrl)
|
|
2255
|
+
if (approval && ['claimed', 'running'].includes(approval.status)) {
|
|
2256
|
+
const running = ownerApprovalTaskControllers.get(approval.key)
|
|
2257
|
+
if (!running || running.finished) return false
|
|
2258
|
+
running.controller.abort()
|
|
2259
|
+
return true
|
|
2260
|
+
}
|
|
2082
2261
|
return conversation.cancelTask(taskId, serviceUrl)
|
|
2083
2262
|
}
|
|
2084
2263
|
return stop
|
package/index.js
CHANGED
|
@@ -6,6 +6,7 @@ import { readFile } from 'node:fs/promises'
|
|
|
6
6
|
import { installSkill, listSkills, uninstallSkill } from './skill-install.js'
|
|
7
7
|
import { readLocalDelegationSettings, recentDelegationStatus, startDelegationHost,
|
|
8
8
|
writeLocalDelegationSettings } from './delegation.js'
|
|
9
|
+
import { waitForSignal } from './conversation.js'
|
|
9
10
|
|
|
10
11
|
export const inject = []
|
|
11
12
|
|
|
@@ -320,7 +321,7 @@ async function fetchDelegationBots(request, ctx) {
|
|
|
320
321
|
try {
|
|
321
322
|
const details = await service.describeBot(bot.botId)
|
|
322
323
|
return {
|
|
323
|
-
bot_id: bot.botId, name: details.account?.name || bot.botId,
|
|
324
|
+
bot_id: bot.botId, channel: bot.channel, name: details.account?.name || bot.botId,
|
|
324
325
|
fingerprint: details.account?.fingerprint,
|
|
325
326
|
connected: details.connected === true,
|
|
326
327
|
available: details.capabilities?.includes('exclusive-text-consumer')
|
|
@@ -385,14 +386,33 @@ async function fetchDelegationGrants(request, ctx, credentials, settings) {
|
|
|
385
386
|
}
|
|
386
387
|
|
|
387
388
|
async function fetchDelegationGroupSettings(request, ctx, credentials, settings, kind, getHost) {
|
|
388
|
-
const methods = kind === 'groups' ? ['GET', 'PUT'] : kind === 'dm-chat' ? ['PUT']
|
|
389
|
-
: kind === 'challenge' ? ['POST'] : ['GET', 'POST', 'DELETE']
|
|
389
|
+
const methods = kind === 'groups' ? ['GET', 'PUT'] : kind === 'dm-chat' ? ['PUT'] : ['GET', 'POST', 'DELETE']
|
|
390
390
|
if (!methods.includes(request.method)) return jsonResponse(405, { message: '请求方式不支持。' })
|
|
391
|
+
if (kind === 'challenge' && request.method === 'DELETE') {
|
|
392
|
+
const fingerprint = new URL(request.url).searchParams.get('bot_fingerprint')
|
|
393
|
+
if (typeof fingerprint !== 'string' || !/^[a-f0-9]{64}$/iu.test(fingerprint)) {
|
|
394
|
+
return jsonResponse(400, { message: '请求格式无效。' })
|
|
395
|
+
}
|
|
396
|
+
const host = getHost?.()
|
|
397
|
+
if (!host?.cancelOwnerChallenge) {
|
|
398
|
+
return jsonResponse(503, { message: '本机协作收件尚未就绪,请稍后刷新后重新生成绑定码。' })
|
|
399
|
+
}
|
|
400
|
+
return jsonResponse(200, { cancelled: host.cancelOwnerChallenge(fingerprint) })
|
|
401
|
+
}
|
|
391
402
|
let connection
|
|
392
403
|
try { connection = await readMemberConnection(credentials, settings) } catch {
|
|
393
404
|
return jsonResponse(500, { message: '无法读取 ANT 连接。' })
|
|
394
405
|
}
|
|
395
406
|
if (!connection) return jsonResponse(401, { message: '请先连接 ANT 团队。' })
|
|
407
|
+
if (kind === 'challenge' && request.method === 'GET') {
|
|
408
|
+
const fingerprint = new URL(request.url).searchParams.get('bot_fingerprint')
|
|
409
|
+
if (typeof fingerprint !== 'string' || !/^[a-f0-9]{64}$/iu.test(fingerprint)) {
|
|
410
|
+
return jsonResponse(400, { message: '请求格式无效。' })
|
|
411
|
+
}
|
|
412
|
+
const status = getHost?.()?.ownerChallengeStatus?.(fingerprint)
|
|
413
|
+
if (!status) return jsonResponse(503, { message: '本机协作收件尚未就绪,请稍后刷新后重新生成绑定码。' })
|
|
414
|
+
return jsonResponse(200, status)
|
|
415
|
+
}
|
|
396
416
|
let body = null
|
|
397
417
|
if (request.method !== 'GET') {
|
|
398
418
|
try {
|
|
@@ -451,6 +471,16 @@ async function fetchDelegationGroupSettings(request, ctx, credentials, settings,
|
|
|
451
471
|
}
|
|
452
472
|
const text = await readLimitedBody(response)
|
|
453
473
|
if (text === null) return jsonResponse(502, { message: '团队服务响应过大。' })
|
|
474
|
+
if (kind === 'challenge' && request.method === 'POST' && response.ok) {
|
|
475
|
+
let challenge
|
|
476
|
+
try { challenge = JSON.parse(text) } catch {
|
|
477
|
+
return jsonResponse(502, { message: '团队服务返回的绑定码格式无效。' })
|
|
478
|
+
}
|
|
479
|
+
const host = getHost?.()
|
|
480
|
+
if (!host?.rememberOwnerChallenge?.(body.bot_fingerprint, challenge?.code)) {
|
|
481
|
+
return jsonResponse(503, { message: '本机协作收件尚未就绪,请稍后刷新后重新生成绑定码。' })
|
|
482
|
+
}
|
|
483
|
+
}
|
|
454
484
|
if (kind === 'groups' && request.method === 'GET' && response.ok) {
|
|
455
485
|
try {
|
|
456
486
|
const groups = JSON.parse(text)
|
|
@@ -566,9 +596,10 @@ async function fetchDelegationCards(request, ctx, credentials, settings) {
|
|
|
566
596
|
async function fetchDelegationRuleRecognition(request, credentials, settings, getHost) {
|
|
567
597
|
if (request.method !== 'POST') return jsonResponse(405, { message: '只支持 POST 请求。' })
|
|
568
598
|
try {
|
|
569
|
-
|
|
599
|
+
request.signal.throwIfAborted()
|
|
600
|
+
const connection = await waitForSignal(readMemberConnection(credentials, settings), request.signal)
|
|
570
601
|
if (!connection) return jsonResponse(401, { message: '请先连接 ANT 团队。' })
|
|
571
|
-
const raw = await request.text()
|
|
602
|
+
const raw = await waitForSignal(request.text(), request.signal)
|
|
572
603
|
if (raw.length > 8_192) return jsonResponse(413, { message: '规则描述过长。' })
|
|
573
604
|
let body
|
|
574
605
|
try { body = JSON.parse(raw) } catch { return jsonResponse(400, { message: '规则识别请求格式无效。' }) }
|
|
@@ -577,7 +608,7 @@ async function fetchDelegationRuleRecognition(request, credentials, settings, ge
|
|
|
577
608
|
}
|
|
578
609
|
const host = getHost?.()
|
|
579
610
|
if (!host?.recognizeRule) return jsonResponse(503, { message: '本机 DSH 协作会话尚未就绪,请稍后重试。' })
|
|
580
|
-
return jsonResponse(200, await host.recognizeRule(body.description))
|
|
611
|
+
return jsonResponse(200, await host.recognizeRule(body.description, request.signal))
|
|
581
612
|
} catch (error) {
|
|
582
613
|
return jsonResponse(502, { message: error?.message || '任务规则识别失败,请稍后重试。' })
|
|
583
614
|
}
|
|
@@ -1166,7 +1197,7 @@ export function apply(ctx) {
|
|
|
1166
1197
|
[DELEGATION_ENROLLED_BOTS_PATH, ['GET', 'POST', 'DELETE'], 'bots'],
|
|
1167
1198
|
[DELEGATION_GROUPS_PATH, ['GET', 'PUT'], 'groups'],
|
|
1168
1199
|
[DELEGATION_DM_CHAT_PATH, ['PUT'], 'dm-chat'],
|
|
1169
|
-
[DELEGATION_OWNER_CHALLENGE_PATH, ['POST'], 'challenge'],
|
|
1200
|
+
[DELEGATION_OWNER_CHALLENGE_PATH, ['GET', 'POST', 'DELETE'], 'challenge'],
|
|
1170
1201
|
]) {
|
|
1171
1202
|
connectionCtx.effect(() => connectionCtx.connection.fetch.register({
|
|
1172
1203
|
path, methods, requestBody: 'buffered',
|