@dcrays/dcgchat-test 0.4.20 → 0.4.21
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/package.json +1 -1
- package/src/bot.ts +69 -78
- package/src/channel.ts +11 -0
- package/src/sessionTermination.ts +154 -0
- package/src/tool.ts +1 -1
package/package.json
CHANGED
package/src/bot.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto'
|
|
1
2
|
import path from 'node:path'
|
|
2
3
|
import type { ReplyPayload } from 'openclaw/plugin-sdk'
|
|
3
4
|
import { createReplyPrefixContext, createTypingCallbacks } from 'openclaw/plugin-sdk'
|
|
@@ -15,9 +16,19 @@ import { generateSignUrl } from './request/api.js'
|
|
|
15
16
|
import { sendChunk, sendFinal, sendText as sendTextMsg, sendError, wsSendRaw, sendText } from './transport.js'
|
|
16
17
|
import { dcgLogger } from './utils/log.js'
|
|
17
18
|
import { channelInfo, systemCommand, stopCommand, ENV, ignoreToolCommand } from './utils/constant.js'
|
|
18
|
-
import { sendGatewayRpc } from './gateway/socket.js'
|
|
19
19
|
import { clearParamsMessage, getEffectiveMsgParams, setParamsMessage } from './utils/params.js'
|
|
20
|
-
import {
|
|
20
|
+
import { waitUntilSubagentsIdle } from './tool.js'
|
|
21
|
+
import {
|
|
22
|
+
beginSupersedingUserTurn,
|
|
23
|
+
clearActiveRunIdForSession,
|
|
24
|
+
clearSessionStreamSuppression,
|
|
25
|
+
interruptLocalDispatchAndGateway,
|
|
26
|
+
isSessionStreamSuppressed,
|
|
27
|
+
preemptInboundQueueForStop,
|
|
28
|
+
releaseDispatchAbortIfCurrent,
|
|
29
|
+
runInboundTurnSequenced,
|
|
30
|
+
setActiveRunIdForSession
|
|
31
|
+
} from './sessionTermination.js'
|
|
21
32
|
|
|
22
33
|
type MediaInfo = {
|
|
23
34
|
path: string
|
|
@@ -30,23 +41,18 @@ type TFileInfo = { name: string; url: string }
|
|
|
30
41
|
|
|
31
42
|
const mediaMaxBytes = 300 * 1024 * 1024
|
|
32
43
|
|
|
33
|
-
/** 当前会话最近一次 agent run 的 runId(供观测;/stop 时对子会话与主会话使用不传 runId 的 chat.abort 以终止该键下全部活跃 run) */
|
|
34
|
-
const activeRunIdBySessionKey = new Map<string, string>()
|
|
35
|
-
|
|
36
|
-
/** 本通道用 dispatchReplyFromConfig 在进程内跑 agent,网关 chat.abort 往往碰不到该 run;靠 AbortSignal 才能真正停工具与模型调用。 */
|
|
37
|
-
const dispatchAbortBySessionKey = new Map<string, AbortController>()
|
|
38
|
-
|
|
39
44
|
/**
|
|
40
|
-
*
|
|
41
|
-
*
|
|
45
|
+
* 每条入站消息(含 /stop)递增。用于丢弃「已被后续入站抢占」的处理在 dispatch 结束后的尾部逻辑。
|
|
46
|
+
* 仅 /stop 时的 epoch 无法覆盖「新用户消息抢占上一轮」的并发 WS 场景,会导致旧 handler 仍执行 sendFinal(end)、
|
|
47
|
+
* 而新消息被误判为 stale;且上一轮若未对网关 chat.abort,服务端 run 会继续,表现为「新消息秒结束、旧回复复活」。
|
|
42
48
|
*/
|
|
43
|
-
const
|
|
49
|
+
const inboundGenerationBySessionKey = new Map<string, number>()
|
|
44
50
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
51
|
+
function bumpInboundGeneration(sessionKey: string): number {
|
|
52
|
+
const n = (inboundGenerationBySessionKey.get(sessionKey) ?? 0) + 1
|
|
53
|
+
inboundGenerationBySessionKey.set(sessionKey, n)
|
|
54
|
+
return n
|
|
55
|
+
}
|
|
50
56
|
|
|
51
57
|
/** 各 sessionKey 当前轮回复的流式分片序号(仅统计实际下发的文本 chunk);每轮非打断消息开始时置 0 */
|
|
52
58
|
const streamChunkIdxBySessionKey = new Map<string, number>()
|
|
@@ -149,7 +155,7 @@ const typingCallbacks = createTypingCallbacks({
|
|
|
149
155
|
})
|
|
150
156
|
|
|
151
157
|
/**
|
|
152
|
-
* 处理一条用户消息,调用 Agent
|
|
158
|
+
* 处理一条用户消息,调用 Agent 并返回回复(同会话串行见 sessionTermination.runInboundTurnSequenced)。
|
|
153
159
|
*/
|
|
154
160
|
export async function handleDcgchatMessage(msg: InboundMessage, accountId: string): Promise<void> {
|
|
155
161
|
const config = getOpenClawConfig()
|
|
@@ -158,6 +164,20 @@ export async function handleDcgchatMessage(msg: InboundMessage, accountId: strin
|
|
|
158
164
|
return
|
|
159
165
|
}
|
|
160
166
|
const account = resolveAccount(config, accountId)
|
|
167
|
+
const queueSessionKey = getSessionKey(msg.content, account.accountId)
|
|
168
|
+
const queueText = (msg.content.text ?? '').trim()
|
|
169
|
+
if (stopCommand.includes(queueText)) {
|
|
170
|
+
preemptInboundQueueForStop(queueSessionKey)
|
|
171
|
+
}
|
|
172
|
+
await runInboundTurnSequenced(queueSessionKey, () => handleDcgchatMessageInboundTurn(msg, accountId))
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
async function handleDcgchatMessageInboundTurn(msg: InboundMessage, accountId: string): Promise<void> {
|
|
176
|
+
const config = getOpenClawConfig()
|
|
177
|
+
if (!config) {
|
|
178
|
+
return
|
|
179
|
+
}
|
|
180
|
+
const account = resolveAccount(config, accountId)
|
|
161
181
|
const userId = msg._userId.toString()
|
|
162
182
|
|
|
163
183
|
const core = getDcgchatRuntime()
|
|
@@ -176,7 +196,7 @@ export async function handleDcgchatMessage(msg: InboundMessage, accountId: strin
|
|
|
176
196
|
|
|
177
197
|
const effectiveAgentId = embeddedAgentId ?? route.agentId
|
|
178
198
|
const dcgSessionKey = getSessionKey(msg.content, account.accountId)
|
|
179
|
-
const
|
|
199
|
+
const inboundGenAtStart = bumpInboundGeneration(dcgSessionKey)
|
|
180
200
|
|
|
181
201
|
const mergedParams = {
|
|
182
202
|
userId: msg._userId,
|
|
@@ -204,6 +224,7 @@ export async function handleDcgchatMessage(msg: InboundMessage, accountId: strin
|
|
|
204
224
|
if (!text) {
|
|
205
225
|
sendTextMsg('你需要我帮你做什么呢?', outboundCtx)
|
|
206
226
|
sendFinal(outboundCtx, 'not text')
|
|
227
|
+
setMsgStatus(dcgSessionKey, 'finished')
|
|
207
228
|
return
|
|
208
229
|
}
|
|
209
230
|
|
|
@@ -284,7 +305,7 @@ export async function handleDcgchatMessage(msg: InboundMessage, accountId: strin
|
|
|
284
305
|
humanDelay: core.channel.reply.resolveHumanDelayConfig(config, route.agentId),
|
|
285
306
|
onReplyStart: async () => {},
|
|
286
307
|
deliver: async (payload: ReplyPayload, info) => {
|
|
287
|
-
if (
|
|
308
|
+
if (isSessionStreamSuppressed(dcgSessionKey)) return
|
|
288
309
|
const mediaList = resolveReplyMediaList(payload)
|
|
289
310
|
for (const mediaUrl of mediaList) {
|
|
290
311
|
const key = getMediaKey(mediaUrl)
|
|
@@ -297,9 +318,9 @@ export async function handleDcgchatMessage(msg: InboundMessage, accountId: strin
|
|
|
297
318
|
onError: (err: unknown, info: { kind: string }) => {
|
|
298
319
|
setMsgStatus(dcgSessionKey, 'finished')
|
|
299
320
|
sendFinal(outboundCtx, 'error')
|
|
300
|
-
|
|
321
|
+
clearActiveRunIdForSession(dcgSessionKey)
|
|
301
322
|
streamChunkIdxBySessionKey.delete(dcgSessionKey)
|
|
302
|
-
const suppressed =
|
|
323
|
+
const suppressed = isSessionStreamSuppressed(dcgSessionKey)
|
|
303
324
|
dcgLogger(`${info.kind} reply failed${suppressed ? ' (stream suppressed)' : ''}: ${String(err)}`, 'error')
|
|
304
325
|
},
|
|
305
326
|
onIdle: () => {
|
|
@@ -310,11 +331,8 @@ export async function handleDcgchatMessage(msg: InboundMessage, accountId: strin
|
|
|
310
331
|
let dispatchAbort: AbortController | undefined
|
|
311
332
|
try {
|
|
312
333
|
if (!stopCommand.includes(text?.trim())) {
|
|
313
|
-
sessionStreamSuppressed.delete(dcgSessionKey)
|
|
314
334
|
streamChunkIdxBySessionKey.set(dcgSessionKey, 0)
|
|
315
|
-
|
|
316
|
-
dispatchAbort = new AbortController()
|
|
317
|
-
dispatchAbortBySessionKey.set(dcgSessionKey, dispatchAbort)
|
|
335
|
+
dispatchAbort = await beginSupersedingUserTurn(dcgSessionKey)
|
|
318
336
|
}
|
|
319
337
|
|
|
320
338
|
if (systemCommand.includes(text?.trim())) {
|
|
@@ -332,54 +350,16 @@ export async function handleDcgchatMessage(msg: InboundMessage, accountId: strin
|
|
|
332
350
|
abortSignal: dispatchAbort!.signal,
|
|
333
351
|
onModelSelected: prefixContext.onModelSelected,
|
|
334
352
|
onAgentRunStart: (runId) => {
|
|
335
|
-
|
|
353
|
+
setActiveRunIdForSession(dcgSessionKey, runId)
|
|
336
354
|
}
|
|
337
355
|
}
|
|
338
356
|
})
|
|
339
357
|
})
|
|
340
358
|
} else if (stopCommand.includes(text?.trim())) {
|
|
341
|
-
dcgLogger(`interrupt command: ${text}`)
|
|
342
|
-
const inFlightAbort = dispatchAbortBySessionKey.get(dcgSessionKey)
|
|
343
|
-
if (inFlightAbort) {
|
|
344
|
-
dcgLogger(`interrupt: AbortController.abort() in-process run sessionKey=${dcgSessionKey}`)
|
|
345
|
-
inFlightAbort.abort()
|
|
346
|
-
dispatchAbortBySessionKey.delete(dcgSessionKey)
|
|
347
|
-
}
|
|
348
359
|
const ctxForAbort = priorOutboundCtx.messageId?.trim() || priorOutboundCtx.sessionId?.trim() ? priorOutboundCtx : outboundCtx
|
|
349
|
-
|
|
350
|
-
sessionStreamSuppressed.add(dcgSessionKey)
|
|
351
|
-
replyAbortEpochBySessionKey.set(
|
|
352
|
-
dcgSessionKey,
|
|
353
|
-
(replyAbortEpochBySessionKey.get(dcgSessionKey) ?? 0) + 1
|
|
354
|
-
)
|
|
355
|
-
// 网关侧:子 agent 独立 sessionKey,自深到浅 abort 后再 abort 主会话;主会话有 runId 时带上以便网关侧若有登记可一并取消
|
|
356
|
-
const descendantKeys = getDescendantSessionKeysForRequester(dcgSessionKey)
|
|
357
|
-
const abortSubKeys = [...descendantKeys].reverse()
|
|
358
|
-
if (abortSubKeys.length > 0) {
|
|
359
|
-
dcgLogger(`interrupt: chat.abort ${abortSubKeys.length} subagent session(s) (nested incl.)`)
|
|
360
|
-
}
|
|
361
|
-
for (const subKey of abortSubKeys) {
|
|
362
|
-
try {
|
|
363
|
-
await sendGatewayRpc({ method: 'chat.abort', params: { sessionKey: subKey } })
|
|
364
|
-
} catch (e) {
|
|
365
|
-
dcgLogger(`chat.abort subagent ${subKey}: ${String(e)}`, 'error')
|
|
366
|
-
}
|
|
367
|
-
}
|
|
368
|
-
try {
|
|
369
|
-
const mainRunId = activeRunIdBySessionKey.get(dcgSessionKey)
|
|
370
|
-
await sendGatewayRpc({
|
|
371
|
-
method: 'chat.abort',
|
|
372
|
-
params: mainRunId
|
|
373
|
-
? { sessionKey: dcgSessionKey, runId: mainRunId }
|
|
374
|
-
: { sessionKey: dcgSessionKey }
|
|
375
|
-
})
|
|
376
|
-
} catch (e) {
|
|
377
|
-
dcgLogger(`chat.abort main ${dcgSessionKey}: ${String(e)}`, 'error')
|
|
378
|
-
}
|
|
379
|
-
activeRunIdBySessionKey.delete(dcgSessionKey)
|
|
360
|
+
await interruptLocalDispatchAndGateway(dcgSessionKey, ctxForAbort)
|
|
380
361
|
streamChunkIdxBySessionKey.delete(dcgSessionKey)
|
|
381
362
|
clearSentMediaKeys(msg.content.message_id)
|
|
382
|
-
resetSubagentStateForRequesterSession(dcgSessionKey)
|
|
383
363
|
setMsgStatus(dcgSessionKey, 'finished')
|
|
384
364
|
clearParamsMessage(dcgSessionKey)
|
|
385
365
|
sendText('会话已终止', outboundCtx)
|
|
@@ -415,10 +395,10 @@ export async function handleDcgchatMessage(msg: InboundMessage, accountId: strin
|
|
|
415
395
|
abortSignal: dispatchAbort!.signal,
|
|
416
396
|
onModelSelected: prefixContext.onModelSelected,
|
|
417
397
|
onAgentRunStart: (runId) => {
|
|
418
|
-
|
|
398
|
+
setActiveRunIdForSession(dcgSessionKey, runId)
|
|
419
399
|
},
|
|
420
400
|
onPartialReply: async (payload: ReplyPayload) => {
|
|
421
|
-
if (
|
|
401
|
+
if (isSessionStreamSuppressed(dcgSessionKey)) return
|
|
422
402
|
|
|
423
403
|
// --- Streaming text chunks ---
|
|
424
404
|
if (payload.text) {
|
|
@@ -456,29 +436,37 @@ export async function handleDcgchatMessage(msg: InboundMessage, accountId: strin
|
|
|
456
436
|
})
|
|
457
437
|
}
|
|
458
438
|
} catch (err: unknown) {
|
|
459
|
-
dcgLogger(`
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
dispatchAbortBySessionKey.delete(dcgSessionKey)
|
|
439
|
+
dcgLogger(`handleDcgchatMessage error: ${String(err)}`, 'error')
|
|
440
|
+
if ((inboundGenerationBySessionKey.get(dcgSessionKey) ?? 0) === inboundGenAtStart) {
|
|
441
|
+
setMsgStatus(dcgSessionKey, 'finished')
|
|
463
442
|
}
|
|
443
|
+
} finally {
|
|
444
|
+
releaseDispatchAbortIfCurrent(dcgSessionKey, dispatchAbort)
|
|
464
445
|
}
|
|
465
446
|
|
|
466
|
-
const
|
|
467
|
-
if (
|
|
468
|
-
dcgLogger(
|
|
469
|
-
`skip post-reply tail: sessionKey=${dcgSessionKey} (abort epoch ${replyAbortEpochAtStart}→${replyAbortEpochNow}, stale handler after interrupt)`
|
|
470
|
-
)
|
|
447
|
+
const inboundGenNow = inboundGenerationBySessionKey.get(dcgSessionKey) ?? 0
|
|
448
|
+
if (inboundGenNow !== inboundGenAtStart) {
|
|
449
|
+
dcgLogger(`skip post-reply tail: sessionKey=${dcgSessionKey} (stale handler: inbound gen ${inboundGenAtStart}→${inboundGenNow})`)
|
|
471
450
|
return
|
|
472
451
|
}
|
|
473
452
|
|
|
474
453
|
if (![...systemCommand, ...stopCommand].includes(text?.trim())) {
|
|
475
|
-
if (
|
|
476
|
-
|
|
454
|
+
if (isSessionStreamSuppressed(dcgSessionKey)) {
|
|
455
|
+
clearSessionStreamSuppression(dcgSessionKey)
|
|
477
456
|
}
|
|
478
457
|
}
|
|
479
458
|
clearSentMediaKeys(msg.content.message_id)
|
|
480
459
|
const storePath = core.channel.session.resolveStorePath(config.session?.store)
|
|
481
460
|
await waitUntilSubagentsIdle(dcgSessionKey, { timeoutMs: 600_000 })
|
|
461
|
+
// 等待子 agent 期间可能已有新入站消息(generation 已变),不能再 end/finished,否则会误结束新轮或放行错挂的正文。
|
|
462
|
+
if ((inboundGenerationBySessionKey.get(dcgSessionKey) ?? 0) !== inboundGenAtStart) {
|
|
463
|
+
dcgLogger(
|
|
464
|
+
`skip post-wait tail: sessionKey=${dcgSessionKey} (stale handler after subagent wait: inbound gen ${inboundGenAtStart}→${inboundGenerationBySessionKey.get(dcgSessionKey)})`
|
|
465
|
+
)
|
|
466
|
+
return
|
|
467
|
+
}
|
|
468
|
+
clearActiveRunIdForSession(dcgSessionKey)
|
|
469
|
+
setMsgStatus(dcgSessionKey, 'finished')
|
|
482
470
|
sendFinal(outboundCtx, 'end')
|
|
483
471
|
dcgLogger(`record session route: updateLastRoute.to=${dcgSessionKey}, accountId=${route.accountId}`)
|
|
484
472
|
core.channel.session
|
|
@@ -501,6 +489,9 @@ export async function handleDcgchatMessage(msg: InboundMessage, accountId: strin
|
|
|
501
489
|
})
|
|
502
490
|
} catch (err) {
|
|
503
491
|
dcgLogger(` handle message failed: ${String(err)}`, 'error')
|
|
492
|
+
if ((inboundGenerationBySessionKey.get(dcgSessionKey) ?? 0) === inboundGenAtStart) {
|
|
493
|
+
setMsgStatus(dcgSessionKey, 'finished')
|
|
494
|
+
}
|
|
504
495
|
sendError(err instanceof Error ? err.message : String(err), outboundCtx)
|
|
505
496
|
}
|
|
506
497
|
}
|
package/src/channel.ts
CHANGED
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
import { isWsOpen, mergeDefaultParams, mergeSessionParams, sendFinal, wsSendRaw } from './transport.js'
|
|
14
14
|
import { dcgLogger, setLogger } from './utils/log.js'
|
|
15
15
|
import { getOutboundMsgParams, getParamsMessage } from './utils/params.js'
|
|
16
|
+
import { isSessionActiveForTool } from './tool.js'
|
|
16
17
|
import { startDcgchatGatewaySocket } from './gateway/socket.js'
|
|
17
18
|
import { getCronJobsPath, readCronJob } from './cron.js'
|
|
18
19
|
|
|
@@ -297,6 +298,16 @@ export const dcgchatPlugin: ChannelPlugin<ResolvedDcgchatAccount> = {
|
|
|
297
298
|
const content: Record<string, unknown> = { response: ctx.text }
|
|
298
299
|
if (!isCron) {
|
|
299
300
|
if (outboundCtx?.sessionId) {
|
|
301
|
+
// 入站 handler 已将本轮标为 running;若已 sendFinal(end) 则应为 finished。
|
|
302
|
+
// 否则网关/Core 晚到的正文会仍用「当前 params map」里的 messageId,错挂到后一条用户消息上。
|
|
303
|
+
if (!isSessionActiveForTool(to)) {
|
|
304
|
+
dcgLogger(`channel sendText dropped (session not active): to=${to}`)
|
|
305
|
+
return {
|
|
306
|
+
channel: "dcgchat-test",
|
|
307
|
+
messageId: '',
|
|
308
|
+
chatId: outboundChatId(ctx.to, to)
|
|
309
|
+
}
|
|
310
|
+
}
|
|
300
311
|
messageId = outboundCtx?.messageId || `${Date.now()}`
|
|
301
312
|
const newCtx = { ...outboundCtx, messageId }
|
|
302
313
|
wsSendRaw(newCtx, content)
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 会话终止 / 抢占 / 网关 abort 的集中实现,便于单独调整策略与观测。
|
|
3
|
+
* 与 bot 的 generation、流式分片序号、入站业务上下文分离,仅负责:本地 AbortController、流抑制标记、
|
|
4
|
+
* 入站串行队尾、activeRunId、chat.abort 子会话→主会话。
|
|
5
|
+
*/
|
|
6
|
+
import { sendGatewayRpc } from './gateway/socket.js'
|
|
7
|
+
import { sendFinal } from './transport.js'
|
|
8
|
+
import type { IMsgParams } from './types.js'
|
|
9
|
+
import { dcgLogger } from './utils/log.js'
|
|
10
|
+
import { getDescendantSessionKeysForRequester, resetSubagentStateForRequesterSession } from './tool.js'
|
|
11
|
+
|
|
12
|
+
// --- 状态(仅本模块内修改,供 bot 通过下方 API 使用)---
|
|
13
|
+
|
|
14
|
+
/** 当前会话最近一次 agent run 的 runId(网关 chat.abort 主会话时携带) */
|
|
15
|
+
const activeRunIdBySessionKey = new Map<string, string>()
|
|
16
|
+
|
|
17
|
+
/** dispatchReplyFromConfig 使用的 AbortSignal,用于真正掐断工具与模型 */
|
|
18
|
+
const dispatchAbortBySessionKey = new Map<string, AbortController>()
|
|
19
|
+
|
|
20
|
+
/** 打断后抑制 deliver / onPartialReply 继续下发 */
|
|
21
|
+
const sessionStreamSuppressed = new Set<string>()
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* 同 sessionKey 入站串行队尾;与 Core session lane 对齐,避免并发 dispatch 过早返回。
|
|
25
|
+
*/
|
|
26
|
+
const inboundTurnTailBySessionKey = new Map<string, Promise<void>>()
|
|
27
|
+
|
|
28
|
+
// --- activeRunId(供 bot 在 onAgentRunStart / 错误收尾时同步)---
|
|
29
|
+
|
|
30
|
+
export function setActiveRunIdForSession(sessionKey: string, runId: string): void {
|
|
31
|
+
activeRunIdBySessionKey.set(sessionKey, runId)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function clearActiveRunIdForSession(sessionKey: string): void {
|
|
35
|
+
activeRunIdBySessionKey.delete(sessionKey)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// --- 流抑制 ---
|
|
39
|
+
|
|
40
|
+
export function isSessionStreamSuppressed(sessionKey: string): boolean {
|
|
41
|
+
return sessionStreamSuppressed.has(sessionKey)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function clearSessionStreamSuppression(sessionKey: string): void {
|
|
45
|
+
sessionStreamSuppressed.delete(sessionKey)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function markSessionStreamSuppressed(sessionKey: string): void {
|
|
49
|
+
sessionStreamSuppressed.add(sessionKey)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// --- 入站串行队列 ---
|
|
53
|
+
|
|
54
|
+
/** /stop 入队前:掐断当前 in-process,并重置队尾,使本条 stop 不必等待已被 abort 的长 turn */
|
|
55
|
+
export function preemptInboundQueueForStop(sessionKey: string): void {
|
|
56
|
+
const c = dispatchAbortBySessionKey.get(sessionKey)
|
|
57
|
+
if (c) {
|
|
58
|
+
c.abort()
|
|
59
|
+
dispatchAbortBySessionKey.delete(sessionKey)
|
|
60
|
+
}
|
|
61
|
+
inboundTurnTailBySessionKey.set(sessionKey, Promise.resolve())
|
|
62
|
+
dcgLogger(`inbound queue: reset tail for /stop sessionKey=${sessionKey}`)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** 将本轮入站处理挂到 sessionKey 队尾,保证同会话顺序执行 */
|
|
66
|
+
export async function runInboundTurnSequenced(sessionKey: string, run: () => Promise<void>): Promise<void> {
|
|
67
|
+
const prev = inboundTurnTailBySessionKey.get(sessionKey) ?? Promise.resolve()
|
|
68
|
+
const next = prev.catch(() => {}).then(run)
|
|
69
|
+
inboundTurnTailBySessionKey.set(sessionKey, next.catch(() => {}))
|
|
70
|
+
await next
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// --- 网关 abort ---
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* 终止网关上仍可能活跃的 run(子会话自深到浅,再主会话)。
|
|
77
|
+
* supersede:仅当有子会话或 mainRunId 时发 RPC;interrupt:主会话始终 chat.abort。
|
|
78
|
+
*/
|
|
79
|
+
export async function abortGatewayRunsForSession(sessionKey: string, reason: 'interrupt' | 'supersede'): Promise<void> {
|
|
80
|
+
const prefix = reason === 'interrupt' ? 'interrupt' : 'supersede'
|
|
81
|
+
const descendantKeys = getDescendantSessionKeysForRequester(sessionKey)
|
|
82
|
+
const abortSubKeys = [...descendantKeys].reverse()
|
|
83
|
+
const mainRunId = activeRunIdBySessionKey.get(sessionKey)
|
|
84
|
+
|
|
85
|
+
if (reason === 'supersede' && abortSubKeys.length === 0 && !mainRunId) {
|
|
86
|
+
return
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (abortSubKeys.length > 0) {
|
|
90
|
+
dcgLogger(`${prefix}: chat.abort ${abortSubKeys.length} subagent session(s) (nested incl.)`)
|
|
91
|
+
}
|
|
92
|
+
for (const subKey of abortSubKeys) {
|
|
93
|
+
try {
|
|
94
|
+
await sendGatewayRpc({ method: 'chat.abort', params: { sessionKey: subKey } })
|
|
95
|
+
} catch (e) {
|
|
96
|
+
dcgLogger(`${prefix}: chat.abort subagent ${subKey}: ${String(e)}`, 'error')
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const shouldMainAbort = reason === 'interrupt' || Boolean(mainRunId)
|
|
101
|
+
if (shouldMainAbort) {
|
|
102
|
+
try {
|
|
103
|
+
await sendGatewayRpc({
|
|
104
|
+
method: 'chat.abort',
|
|
105
|
+
params: mainRunId ? { sessionKey, runId: mainRunId } : { sessionKey }
|
|
106
|
+
})
|
|
107
|
+
} catch (e) {
|
|
108
|
+
dcgLogger(`${prefix}: chat.abort main ${sessionKey}: ${String(e)}`, 'error')
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
activeRunIdBySessionKey.delete(sessionKey)
|
|
113
|
+
resetSubagentStateForRequesterSession(sessionKey)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// --- 本地 dispatch AbortController ---
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* 新一轮非 /stop 用户消息:清除流抑制、abort 上一轮 controller、网关 supersede,并安装新的 AbortController。
|
|
120
|
+
* 调用方须在之前或之后自行 `streamChunkIdxBySessionKey.set(sessionKey, 0)`。
|
|
121
|
+
*/
|
|
122
|
+
export async function beginSupersedingUserTurn(sessionKey: string): Promise<AbortController> {
|
|
123
|
+
sessionStreamSuppressed.delete(sessionKey)
|
|
124
|
+
dispatchAbortBySessionKey.get(sessionKey)?.abort()
|
|
125
|
+
await abortGatewayRunsForSession(sessionKey, 'supersede')
|
|
126
|
+
const ac = new AbortController()
|
|
127
|
+
dispatchAbortBySessionKey.set(sessionKey, ac)
|
|
128
|
+
return ac
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** try/finally 中:仅当仍是当前 controller 时从 map 移除 */
|
|
132
|
+
export function releaseDispatchAbortIfCurrent(sessionKey: string, controller: AbortController | undefined): void {
|
|
133
|
+
if (controller && dispatchAbortBySessionKey.get(sessionKey) === controller) {
|
|
134
|
+
dispatchAbortBySessionKey.delete(sessionKey)
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* 用户发送 /stop:本地 abort、对「上一轮对话」发 abort final、标记流抑制、网关 interrupt。
|
|
140
|
+
* 之后的 clearSentMedia、params、UI「已终止」等由 bot 继续处理。
|
|
141
|
+
*/
|
|
142
|
+
export async function interruptLocalDispatchAndGateway(sessionKey: string, ctxForAbort: IMsgParams): Promise<void> {
|
|
143
|
+
dcgLogger(`interrupt command: sessionKey=${sessionKey}`)
|
|
144
|
+
const inFlight = dispatchAbortBySessionKey.get(sessionKey)
|
|
145
|
+
if (inFlight) {
|
|
146
|
+
dcgLogger(`interrupt: AbortController.abort() in-process run sessionKey=${sessionKey}`)
|
|
147
|
+
inFlight.abort()
|
|
148
|
+
dispatchAbortBySessionKey.delete(sessionKey)
|
|
149
|
+
}
|
|
150
|
+
const finalCtx = ctxForAbort.messageId?.trim() ? ctxForAbort : { ...ctxForAbort, messageId: `${Date.now()}` }
|
|
151
|
+
sendFinal(finalCtx, 'abort')
|
|
152
|
+
markSessionStreamSuppressed(sessionKey)
|
|
153
|
+
await abortGatewayRunsForSession(sessionKey, 'interrupt')
|
|
154
|
+
}
|
package/src/tool.ts
CHANGED
|
@@ -65,7 +65,7 @@ function resolveOutboundParamsForSession(sk: string) {
|
|
|
65
65
|
}
|
|
66
66
|
|
|
67
67
|
/** 主会话已 running 时,子会话上的工具/事件也应下发(否则子 key 无 running 状态会整段丢消息) */
|
|
68
|
-
function isSessionActiveForTool(sk: string): boolean {
|
|
68
|
+
export function isSessionActiveForTool(sk: string): boolean {
|
|
69
69
|
const k = sk.trim()
|
|
70
70
|
if (!k) return false
|
|
71
71
|
if (getMsgStatus(k) === 'running') return true
|