@dcrays/dcgchat-test 0.4.19 → 0.4.20
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 +50 -6
- package/src/channel.ts +7 -20
- package/src/cronToolCall.ts +40 -26
- package/src/utils/constant.ts +2 -2
package/package.json
CHANGED
package/src/bot.ts
CHANGED
|
@@ -14,7 +14,7 @@ import { resolveAccount, sendDcgchatMedia } from './channel.js'
|
|
|
14
14
|
import { generateSignUrl } from './request/api.js'
|
|
15
15
|
import { sendChunk, sendFinal, sendText as sendTextMsg, sendError, wsSendRaw, sendText } from './transport.js'
|
|
16
16
|
import { dcgLogger } from './utils/log.js'
|
|
17
|
-
import { channelInfo, systemCommand,
|
|
17
|
+
import { channelInfo, systemCommand, stopCommand, ENV, ignoreToolCommand } from './utils/constant.js'
|
|
18
18
|
import { sendGatewayRpc } from './gateway/socket.js'
|
|
19
19
|
import { clearParamsMessage, getEffectiveMsgParams, setParamsMessage } from './utils/params.js'
|
|
20
20
|
import { getDescendantSessionKeysForRequester, resetSubagentStateForRequesterSession, waitUntilSubagentsIdle } from './tool.js'
|
|
@@ -33,12 +33,21 @@ const mediaMaxBytes = 300 * 1024 * 1024
|
|
|
33
33
|
/** 当前会话最近一次 agent run 的 runId(供观测;/stop 时对子会话与主会话使用不传 runId 的 chat.abort 以终止该键下全部活跃 run) */
|
|
34
34
|
const activeRunIdBySessionKey = new Map<string, string>()
|
|
35
35
|
|
|
36
|
+
/** 本通道用 dispatchReplyFromConfig 在进程内跑 agent,网关 chat.abort 往往碰不到该 run;靠 AbortSignal 才能真正停工具与模型调用。 */
|
|
37
|
+
const dispatchAbortBySessionKey = new Map<string, AbortController>()
|
|
38
|
+
|
|
36
39
|
/**
|
|
37
40
|
* 用户在该 sessionKey 上触发打断后,旧 run 的流式/投递不再下发;与 sessionKey 一一对应,支持多会话。
|
|
38
41
|
* 清除时机:① 下一条非打断用户消息开始处理时;② 旧 run 收尾到 mobook 段时若仍抑制则跳过并发后删除。
|
|
39
42
|
*/
|
|
40
43
|
const sessionStreamSuppressed = new Set<string>()
|
|
41
44
|
|
|
45
|
+
/**
|
|
46
|
+
* 每次 /stop 递增。用于丢弃「已被后续打断」的那条入站处理在 dispatch 结束后的尾部逻辑
|
|
47
|
+
*(否则旧 handler 会删掉 sessionStreamSuppressed、再发 sendFinal(end)/recordInboundSession,导致旧回复复活、新消息异常)。
|
|
48
|
+
*/
|
|
49
|
+
const replyAbortEpochBySessionKey = new Map<string, number>()
|
|
50
|
+
|
|
42
51
|
/** 各 sessionKey 当前轮回复的流式分片序号(仅统计实际下发的文本 chunk);每轮非打断消息开始时置 0 */
|
|
43
52
|
const streamChunkIdxBySessionKey = new Map<string, number>()
|
|
44
53
|
|
|
@@ -167,6 +176,7 @@ export async function handleDcgchatMessage(msg: InboundMessage, accountId: strin
|
|
|
167
176
|
|
|
168
177
|
const effectiveAgentId = embeddedAgentId ?? route.agentId
|
|
169
178
|
const dcgSessionKey = getSessionKey(msg.content, account.accountId)
|
|
179
|
+
const replyAbortEpochAtStart = replyAbortEpochBySessionKey.get(dcgSessionKey) ?? 0
|
|
170
180
|
|
|
171
181
|
const mergedParams = {
|
|
172
182
|
userId: msg._userId,
|
|
@@ -297,10 +307,14 @@ export async function handleDcgchatMessage(msg: InboundMessage, accountId: strin
|
|
|
297
307
|
}
|
|
298
308
|
})
|
|
299
309
|
|
|
310
|
+
let dispatchAbort: AbortController | undefined
|
|
300
311
|
try {
|
|
301
|
-
if (!
|
|
312
|
+
if (!stopCommand.includes(text?.trim())) {
|
|
302
313
|
sessionStreamSuppressed.delete(dcgSessionKey)
|
|
303
314
|
streamChunkIdxBySessionKey.set(dcgSessionKey, 0)
|
|
315
|
+
dispatchAbortBySessionKey.get(dcgSessionKey)?.abort()
|
|
316
|
+
dispatchAbort = new AbortController()
|
|
317
|
+
dispatchAbortBySessionKey.set(dcgSessionKey, dispatchAbort)
|
|
304
318
|
}
|
|
305
319
|
|
|
306
320
|
if (systemCommand.includes(text?.trim())) {
|
|
@@ -315,6 +329,7 @@ export async function handleDcgchatMessage(msg: InboundMessage, accountId: strin
|
|
|
315
329
|
dispatcher,
|
|
316
330
|
replyOptions: {
|
|
317
331
|
...replyOptions,
|
|
332
|
+
abortSignal: dispatchAbort!.signal,
|
|
318
333
|
onModelSelected: prefixContext.onModelSelected,
|
|
319
334
|
onAgentRunStart: (runId) => {
|
|
320
335
|
activeRunIdBySessionKey.set(dcgSessionKey, runId)
|
|
@@ -322,12 +337,22 @@ export async function handleDcgchatMessage(msg: InboundMessage, accountId: strin
|
|
|
322
337
|
}
|
|
323
338
|
})
|
|
324
339
|
})
|
|
325
|
-
} else if (
|
|
340
|
+
} else if (stopCommand.includes(text?.trim())) {
|
|
326
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
|
+
}
|
|
327
348
|
const ctxForAbort = priorOutboundCtx.messageId?.trim() || priorOutboundCtx.sessionId?.trim() ? priorOutboundCtx : outboundCtx
|
|
328
349
|
sendFinal(ctxForAbort.messageId?.trim() ? ctxForAbort : { ...ctxForAbort, messageId: `${Date.now()}` }, 'abort')
|
|
329
350
|
sessionStreamSuppressed.add(dcgSessionKey)
|
|
330
|
-
|
|
351
|
+
replyAbortEpochBySessionKey.set(
|
|
352
|
+
dcgSessionKey,
|
|
353
|
+
(replyAbortEpochBySessionKey.get(dcgSessionKey) ?? 0) + 1
|
|
354
|
+
)
|
|
355
|
+
// 网关侧:子 agent 独立 sessionKey,自深到浅 abort 后再 abort 主会话;主会话有 runId 时带上以便网关侧若有登记可一并取消
|
|
331
356
|
const descendantKeys = getDescendantSessionKeysForRequester(dcgSessionKey)
|
|
332
357
|
const abortSubKeys = [...descendantKeys].reverse()
|
|
333
358
|
if (abortSubKeys.length > 0) {
|
|
@@ -341,7 +366,13 @@ export async function handleDcgchatMessage(msg: InboundMessage, accountId: strin
|
|
|
341
366
|
}
|
|
342
367
|
}
|
|
343
368
|
try {
|
|
344
|
-
|
|
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
|
+
})
|
|
345
376
|
} catch (e) {
|
|
346
377
|
dcgLogger(`chat.abort main ${dcgSessionKey}: ${String(e)}`, 'error')
|
|
347
378
|
}
|
|
@@ -381,6 +412,7 @@ export async function handleDcgchatMessage(msg: InboundMessage, accountId: strin
|
|
|
381
412
|
dispatcher,
|
|
382
413
|
replyOptions: {
|
|
383
414
|
...replyOptions,
|
|
415
|
+
abortSignal: dispatchAbort!.signal,
|
|
384
416
|
onModelSelected: prefixContext.onModelSelected,
|
|
385
417
|
onAgentRunStart: (runId) => {
|
|
386
418
|
activeRunIdBySessionKey.set(dcgSessionKey, runId)
|
|
@@ -425,9 +457,21 @@ export async function handleDcgchatMessage(msg: InboundMessage, accountId: strin
|
|
|
425
457
|
}
|
|
426
458
|
} catch (err: unknown) {
|
|
427
459
|
dcgLogger(` dispatchReplyFromConfig error: ${String(err)}`, 'error')
|
|
460
|
+
} finally {
|
|
461
|
+
if (dispatchAbort && dispatchAbortBySessionKey.get(dcgSessionKey) === dispatchAbort) {
|
|
462
|
+
dispatchAbortBySessionKey.delete(dcgSessionKey)
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
const replyAbortEpochNow = replyAbortEpochBySessionKey.get(dcgSessionKey) ?? 0
|
|
467
|
+
if (replyAbortEpochNow !== replyAbortEpochAtStart) {
|
|
468
|
+
dcgLogger(
|
|
469
|
+
`skip post-reply tail: sessionKey=${dcgSessionKey} (abort epoch ${replyAbortEpochAtStart}→${replyAbortEpochNow}, stale handler after interrupt)`
|
|
470
|
+
)
|
|
471
|
+
return
|
|
428
472
|
}
|
|
429
473
|
|
|
430
|
-
if (![...systemCommand, ...
|
|
474
|
+
if (![...systemCommand, ...stopCommand].includes(text?.trim())) {
|
|
431
475
|
if (sessionStreamSuppressed.has(dcgSessionKey)) {
|
|
432
476
|
sessionStreamSuppressed.delete(dcgSessionKey)
|
|
433
477
|
}
|
package/src/channel.ts
CHANGED
|
@@ -129,9 +129,8 @@ export async function sendDcgchatMedia(opts: DcgchatMediaSendOptions): Promise<v
|
|
|
129
129
|
dcgLogger(`dcgchat: sendMedia skipped (duplicate in session): ${mediaUrl} sessionId=${msgCtx.sessionId} sessionKey=${sessionKey}`)
|
|
130
130
|
return
|
|
131
131
|
}
|
|
132
|
-
|
|
133
132
|
const fileName = mediaUrl?.split(/[\\/]/).pop() || ''
|
|
134
|
-
|
|
133
|
+
const notMessageId = `${msgCtx?.messageId}`?.length !== 13 || !msgCtx?.messageId
|
|
135
134
|
try {
|
|
136
135
|
const botToken = msgCtx.botToken ?? getOpenClawConfig()?.channels?.["dcgchat-test"]?.botToken ?? ''
|
|
137
136
|
const url = opts.mediaUrl ? await ossUpload(opts.mediaUrl, botToken, 1) : ''
|
|
@@ -140,14 +139,16 @@ export async function sendDcgchatMedia(opts: DcgchatMediaSendOptions): Promise<v
|
|
|
140
139
|
}
|
|
141
140
|
wsSendRaw(msgCtx, {
|
|
142
141
|
response: opts.text ?? '',
|
|
143
|
-
is_finish:
|
|
142
|
+
is_finish: notMessageId ? -1 : 0,
|
|
143
|
+
message_tags: notMessageId ? { mark: 'empty_text_file' } : {},
|
|
144
144
|
files: [{ url, name: fileName }]
|
|
145
145
|
})
|
|
146
146
|
dcgLogger(`dcgchat: sendMedia session=${sessionKey}, file=${fileName}`)
|
|
147
147
|
} catch (error) {
|
|
148
148
|
wsSendRaw(msgCtx, {
|
|
149
149
|
response: opts.text ?? '',
|
|
150
|
-
|
|
150
|
+
message_tags: notMessageId ? { mark: 'empty_text_file' } : {},
|
|
151
|
+
is_finish: notMessageId ? -1 : 0,
|
|
151
152
|
files: [{ url: opts.mediaUrl ?? '', name: fileName }]
|
|
152
153
|
})
|
|
153
154
|
dcgLogger(`dcgchat: error sendMedia session=${sessionKey}: ${String(error)}`, 'error')
|
|
@@ -295,24 +296,10 @@ export const dcgchatPlugin: ChannelPlugin<ResolvedDcgchatAccount> = {
|
|
|
295
296
|
const outboundCtx = getOutboundMsgParams(to)
|
|
296
297
|
const content: Record<string, unknown> = { response: ctx.text }
|
|
297
298
|
if (!isCron) {
|
|
298
|
-
// messageId = getCronMessageId(to) || `${Date.now()}`
|
|
299
|
-
// const { sessionId, agentId } = getInfoBySessionKey(to)
|
|
300
|
-
// content.is_finish = -1
|
|
301
|
-
// content.message_tags = { source: 'cron' }
|
|
302
|
-
// const merged = mergeDefaultParams({
|
|
303
|
-
// agentId: agentId,
|
|
304
|
-
// sessionId: `${sessionId}`,
|
|
305
|
-
// messageId: messageId,
|
|
306
|
-
// real_mobook: !sessionId ? 1 : ''
|
|
307
|
-
// })
|
|
308
|
-
// wsSendRaw(merged, content)
|
|
309
|
-
// } else {
|
|
310
299
|
if (outboundCtx?.sessionId) {
|
|
311
300
|
messageId = outboundCtx?.messageId || `${Date.now()}`
|
|
312
301
|
const newCtx = { ...outboundCtx, messageId }
|
|
313
302
|
wsSendRaw(newCtx, content)
|
|
314
|
-
} else {
|
|
315
|
-
dcgLogger(`channel sendText to ${ctx.to} -> sessionId not found`, 'error')
|
|
316
303
|
}
|
|
317
304
|
}
|
|
318
305
|
}
|
|
@@ -331,8 +318,8 @@ export const dcgchatPlugin: ChannelPlugin<ResolvedDcgchatAccount> = {
|
|
|
331
318
|
const cronMsgId = getCronMessageId(to)
|
|
332
319
|
const fallbackMessageId = `${Date.now()}`
|
|
333
320
|
const messageId = cronMsgId || (isCron ? fallbackMessageId : msgCtx?.messageId || fallbackMessageId)
|
|
334
|
-
|
|
335
|
-
if (!
|
|
321
|
+
const { sessionId } = getInfoBySessionKey(to)
|
|
322
|
+
if (!sessionId) {
|
|
336
323
|
dcgLogger(`channel sendMedia to ${ctx.to} -> sessionId not found`, 'error')
|
|
337
324
|
return {
|
|
338
325
|
channel: "dcgchat-test",
|
package/src/cronToolCall.ts
CHANGED
|
@@ -117,34 +117,44 @@ function needsBestEffort(delivery: CronDelivery): boolean {
|
|
|
117
117
|
}
|
|
118
118
|
|
|
119
119
|
/**
|
|
120
|
-
* 深拷贝 params
|
|
120
|
+
* 深拷贝 params,在 delivery 上写入 dcg 路由(to / accountId / sessionKey)。
|
|
121
|
+
* bestEffort + 默认 channel 仅在 needsBestEffort 为真时写入(announce 且无 channel)。
|
|
122
|
+
*
|
|
123
|
+
* 说明:原先仅在 needsBestEffort 为真时才改 delivery,若 jobs 里已有 channel 会整段跳过,
|
|
124
|
+
* 导致 `delivery.to` 永远不会被本钩子写入;运行时若缺 to 就会一直 not-delivered。
|
|
121
125
|
*/
|
|
122
|
-
function
|
|
126
|
+
function patchCronDeliveryInParams(
|
|
127
|
+
params: Record<string, unknown>,
|
|
128
|
+
sk: string,
|
|
129
|
+
deliverySnapshot: CronDelivery
|
|
130
|
+
): Record<string, unknown> | null {
|
|
123
131
|
const newParams = JSON.parse(JSON.stringify(params)) as Record<string, unknown>
|
|
124
|
-
const { agentId
|
|
125
|
-
|
|
132
|
+
const { agentId } = formatterSessionKey(sk)
|
|
133
|
+
const announceNoChannel = needsBestEffort(deliverySnapshot)
|
|
134
|
+
|
|
135
|
+
const apply = (d: CronDelivery) => {
|
|
136
|
+
d.to = `dcg-cron:${sk}`
|
|
137
|
+
if (agentId) d.accountId = agentId
|
|
138
|
+
if (announceNoChannel) {
|
|
139
|
+
d.bestEffort = true
|
|
140
|
+
d.channel = "dcgchat-test"
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
126
144
|
if (newParams.delivery && typeof newParams.delivery === 'object') {
|
|
127
|
-
|
|
128
|
-
;(newParams.delivery as CronDelivery).to = `dcg-cron:${sk}`
|
|
129
|
-
;(newParams.delivery as CronDelivery).accountId = agentId
|
|
130
|
-
;(newParams.delivery as CronDelivery).channel = "dcgchat-test"
|
|
145
|
+
apply(newParams.delivery as CronDelivery)
|
|
131
146
|
newParams.sessionKey = sk
|
|
132
147
|
return newParams
|
|
133
148
|
}
|
|
134
149
|
|
|
135
|
-
// job.delivery
|
|
136
150
|
const job = newParams.job as Record<string, unknown> | undefined
|
|
137
151
|
if (job?.delivery && typeof job.delivery === 'object') {
|
|
138
|
-
|
|
139
|
-
jd.bestEffort = true
|
|
140
|
-
jd.to = `dcg-cron:${sk}`
|
|
141
|
-
jd.accountId = agentId
|
|
142
|
-
jd.channel = "dcgchat-test"
|
|
152
|
+
apply(job.delivery as CronDelivery)
|
|
143
153
|
newParams.sessionKey = sk
|
|
144
154
|
return newParams
|
|
145
155
|
}
|
|
146
156
|
|
|
147
|
-
return
|
|
157
|
+
return null
|
|
148
158
|
}
|
|
149
159
|
|
|
150
160
|
export function cronToolCall(event: { toolName: any; params: any; toolCallId: any }, sk: string) {
|
|
@@ -157,20 +167,24 @@ export function cronToolCall(event: { toolName: any; params: any; toolCallId: an
|
|
|
157
167
|
dcgLogger(`[${LOG_TAG}] cron call (${toolCallId}) has no delivery config, skip.`)
|
|
158
168
|
return undefined
|
|
159
169
|
}
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
`(mode=${String(delivery.mode)}, channel=${String(delivery.channel)}, bestEffort=${String(delivery.bestEffort)}), skip.`
|
|
164
|
-
)
|
|
170
|
+
const newParams = patchCronDeliveryInParams(params, sk, delivery)
|
|
171
|
+
if (!newParams) {
|
|
172
|
+
dcgLogger(`[${LOG_TAG}] cron call (${toolCallId}) could not locate delivery on params clone, skip.`)
|
|
165
173
|
return undefined
|
|
166
174
|
}
|
|
167
175
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
176
|
+
const patched = newParams.delivery ?? (newParams.job as Record<string, unknown>)?.delivery
|
|
177
|
+
if (needsBestEffort(delivery)) {
|
|
178
|
+
dcgLogger(
|
|
179
|
+
`[${LOG_TAG}] cron call (${toolCallId}) patched delivery (announce, no channel): bestEffort=true, ` +
|
|
180
|
+
`delivery=${JSON.stringify(patched)}`
|
|
181
|
+
)
|
|
182
|
+
} else {
|
|
183
|
+
dcgLogger(
|
|
184
|
+
`[${LOG_TAG}] cron call (${toolCallId}) patched delivery.to / accountId (sessionKey=${sk}), ` +
|
|
185
|
+
`delivery=${JSON.stringify(patched)}`
|
|
186
|
+
)
|
|
187
|
+
}
|
|
174
188
|
|
|
175
189
|
return { params: newParams }
|
|
176
190
|
} else if (toolName === 'exec') {
|
package/src/utils/constant.ts
CHANGED
|
@@ -2,6 +2,6 @@ export const ENV: 'production' | 'test' | 'develop' = 'test'
|
|
|
2
2
|
|
|
3
3
|
|
|
4
4
|
export const systemCommand = ['/new', '/status']
|
|
5
|
-
export const
|
|
5
|
+
export const stopCommand = ['/stop']
|
|
6
6
|
|
|
7
|
-
export const ignoreToolCommand = ['/search', '/abort', '/queue interrupt', ...systemCommand, ...
|
|
7
|
+
export const ignoreToolCommand = ['/search', '/abort', '/queue interrupt', ...systemCommand, ...stopCommand]
|