@dcrays/dcgchat-test 0.4.18 → 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 +51 -7
- package/src/channel.ts +24 -29
- package/src/cron.ts +6 -2
- package/src/cronToolCall.ts +43 -28
- package/src/tool.ts +14 -1
- package/src/transport.ts +2 -1
- package/src/utils/constant.ts +2 -2
- package/src/utils/gatewayMsgHanlder.ts +6 -1
- package/src/utils/global.ts +8 -8
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,
|
|
@@ -174,7 +184,7 @@ export async function handleDcgchatMessage(msg: InboundMessage, accountId: strin
|
|
|
174
184
|
sessionId: conversationId,
|
|
175
185
|
messageId: msg.content.message_id,
|
|
176
186
|
domainId: msg.content.domain_id,
|
|
177
|
-
appId:
|
|
187
|
+
appId: config.channels?.["dcgchat-test"]?.appId || 100,
|
|
178
188
|
botId: msg.content.bot_id ?? '',
|
|
179
189
|
agentId: msg.content.agent_id ?? '',
|
|
180
190
|
sessionKey: dcgSessionKey,
|
|
@@ -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
|
@@ -107,39 +107,48 @@ export async function sendDcgchatMedia(opts: DcgchatMediaSendOptions): Promise<v
|
|
|
107
107
|
const baseCtx = getOutboundMsgParams(sessionKey)
|
|
108
108
|
const msgCtx = opts.messageId?.trim() ? { ...baseCtx, messageId: opts.messageId.trim() } : baseCtx
|
|
109
109
|
if (!isWsOpen()) {
|
|
110
|
-
dcgLogger(`outbound media skipped -> ws not open: ${opts.mediaUrl ?? ''}`)
|
|
110
|
+
dcgLogger(`outbound media skipped -> ws not isWsOpen failed open: ${opts.mediaUrl ?? ''}`)
|
|
111
111
|
return
|
|
112
112
|
}
|
|
113
113
|
|
|
114
114
|
const mediaUrl = opts.mediaUrl
|
|
115
|
-
|
|
116
|
-
if (mediaUrl &&
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
addSentMediaKey(
|
|
115
|
+
|
|
116
|
+
if (mediaUrl && msgCtx.sessionId) {
|
|
117
|
+
if (hasSentMediaKey(msgCtx.sessionId, mediaUrl)) {
|
|
118
|
+
dcgLogger(`dcgchat: sendMedia skipped (hasSentMediaKey): ${mediaUrl} sessionId=${msgCtx.sessionId} sessionKey=${sessionKey}`)
|
|
119
|
+
return
|
|
120
|
+
}
|
|
121
|
+
addSentMediaKey(msgCtx.sessionId, mediaUrl)
|
|
122
122
|
}
|
|
123
123
|
|
|
124
|
+
const { sessionId, agentId } = getInfoBySessionKey(sessionKey)
|
|
125
|
+
if (!msgCtx.sessionId) {
|
|
126
|
+
msgCtx.sessionId = sessionId
|
|
127
|
+
}
|
|
128
|
+
if (!mediaUrl || !msgCtx.sessionId) {
|
|
129
|
+
dcgLogger(`dcgchat: sendMedia skipped (duplicate in session): ${mediaUrl} sessionId=${msgCtx.sessionId} sessionKey=${sessionKey}`)
|
|
130
|
+
return
|
|
131
|
+
}
|
|
124
132
|
const fileName = mediaUrl?.split(/[\\/]/).pop() || ''
|
|
125
|
-
|
|
133
|
+
const notMessageId = `${msgCtx?.messageId}`?.length !== 13 || !msgCtx?.messageId
|
|
126
134
|
try {
|
|
127
135
|
const botToken = msgCtx.botToken ?? getOpenClawConfig()?.channels?.["dcgchat-test"]?.botToken ?? ''
|
|
128
136
|
const url = opts.mediaUrl ? await ossUpload(opts.mediaUrl, botToken, 1) : ''
|
|
129
|
-
if (!msgCtx.
|
|
130
|
-
const { sessionId, agentId } = getInfoBySessionKey(sessionKey)
|
|
131
|
-
msgCtx.sessionId = sessionId
|
|
137
|
+
if (!msgCtx.agentId) {
|
|
132
138
|
msgCtx.agentId = agentId
|
|
133
139
|
}
|
|
134
140
|
wsSendRaw(msgCtx, {
|
|
135
141
|
response: opts.text ?? '',
|
|
142
|
+
is_finish: notMessageId ? -1 : 0,
|
|
143
|
+
message_tags: notMessageId ? { mark: 'empty_text_file' } : {},
|
|
136
144
|
files: [{ url, name: fileName }]
|
|
137
145
|
})
|
|
138
146
|
dcgLogger(`dcgchat: sendMedia session=${sessionKey}, file=${fileName}`)
|
|
139
147
|
} catch (error) {
|
|
140
148
|
wsSendRaw(msgCtx, {
|
|
141
149
|
response: opts.text ?? '',
|
|
142
|
-
message_tags: {
|
|
150
|
+
message_tags: notMessageId ? { mark: 'empty_text_file' } : {},
|
|
151
|
+
is_finish: notMessageId ? -1 : 0,
|
|
143
152
|
files: [{ url: opts.mediaUrl ?? '', name: fileName }]
|
|
144
153
|
})
|
|
145
154
|
dcgLogger(`dcgchat: error sendMedia session=${sessionKey}: ${String(error)}`, 'error')
|
|
@@ -287,24 +296,10 @@ export const dcgchatPlugin: ChannelPlugin<ResolvedDcgchatAccount> = {
|
|
|
287
296
|
const outboundCtx = getOutboundMsgParams(to)
|
|
288
297
|
const content: Record<string, unknown> = { response: ctx.text }
|
|
289
298
|
if (!isCron) {
|
|
290
|
-
// messageId = getCronMessageId(to) || `${Date.now()}`
|
|
291
|
-
// const { sessionId, agentId } = getInfoBySessionKey(to)
|
|
292
|
-
// content.is_finish = -1
|
|
293
|
-
// content.message_tags = { source: 'cron' }
|
|
294
|
-
// const merged = mergeDefaultParams({
|
|
295
|
-
// agentId: agentId,
|
|
296
|
-
// sessionId: `${sessionId}`,
|
|
297
|
-
// messageId: messageId,
|
|
298
|
-
// real_mobook: !sessionId ? 1 : ''
|
|
299
|
-
// })
|
|
300
|
-
// wsSendRaw(merged, content)
|
|
301
|
-
// } else {
|
|
302
299
|
if (outboundCtx?.sessionId) {
|
|
303
300
|
messageId = outboundCtx?.messageId || `${Date.now()}`
|
|
304
301
|
const newCtx = { ...outboundCtx, messageId }
|
|
305
302
|
wsSendRaw(newCtx, content)
|
|
306
|
-
} else {
|
|
307
|
-
dcgLogger(`channel sendText to ${ctx.to} -> sessionId not found`, 'error')
|
|
308
303
|
}
|
|
309
304
|
}
|
|
310
305
|
}
|
|
@@ -323,8 +318,8 @@ export const dcgchatPlugin: ChannelPlugin<ResolvedDcgchatAccount> = {
|
|
|
323
318
|
const cronMsgId = getCronMessageId(to)
|
|
324
319
|
const fallbackMessageId = `${Date.now()}`
|
|
325
320
|
const messageId = cronMsgId || (isCron ? fallbackMessageId : msgCtx?.messageId || fallbackMessageId)
|
|
326
|
-
|
|
327
|
-
if (!
|
|
321
|
+
const { sessionId } = getInfoBySessionKey(to)
|
|
322
|
+
if (!sessionId) {
|
|
328
323
|
dcgLogger(`channel sendMedia to ${ctx.to} -> sessionId not found`, 'error')
|
|
329
324
|
return {
|
|
330
325
|
channel: "dcgchat-test",
|
package/src/cron.ts
CHANGED
|
@@ -135,7 +135,7 @@ export const onRunCronJob = async (jobId: string, messageId: string) => {
|
|
|
135
135
|
)
|
|
136
136
|
sendMessageToGateway(JSON.stringify({ method: 'cron.run', params: { id: jobId, mode: 'force' } }))
|
|
137
137
|
}
|
|
138
|
-
export const finishedDcgchatCron = async (jobId: string, summary: string) => {
|
|
138
|
+
export const finishedDcgchatCron = async (jobId: string, summary: string, hasFileOutput?: boolean) => {
|
|
139
139
|
const id = jobId?.trim()
|
|
140
140
|
if (!id) {
|
|
141
141
|
dcgLogger('finishedDcgchatCron: empty jobId', 'error')
|
|
@@ -159,7 +159,11 @@ export const finishedDcgchatCron = async (jobId: string, summary: string) => {
|
|
|
159
159
|
messageId: messageId || `${Date.now()}`,
|
|
160
160
|
real_mobook: !sessionId ? 1 : ''
|
|
161
161
|
})
|
|
162
|
-
|
|
162
|
+
const message_tags = { source: 'cron' } as Record<string, string | boolean>
|
|
163
|
+
if (hasFileOutput) {
|
|
164
|
+
message_tags.hasFile = true
|
|
165
|
+
}
|
|
166
|
+
wsSendRaw(merged, { response: summary, message_tags, is_finish: -1 })
|
|
163
167
|
setTimeout(() => {
|
|
164
168
|
sendFinal(merged, 'cron send')
|
|
165
169
|
}, 200)
|
package/src/cronToolCall.ts
CHANGED
|
@@ -117,33 +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
|
-
;(newParams.delivery as CronDelivery).to = `dcg-cron:${sk}`
|
|
140
|
-
;(newParams.delivery as CronDelivery).accountId = agentId
|
|
141
|
-
;(newParams.delivery as CronDelivery).channel = "dcgchat-test"
|
|
152
|
+
apply(job.delivery as CronDelivery)
|
|
142
153
|
newParams.sessionKey = sk
|
|
143
154
|
return newParams
|
|
144
155
|
}
|
|
145
156
|
|
|
146
|
-
return
|
|
157
|
+
return null
|
|
147
158
|
}
|
|
148
159
|
|
|
149
160
|
export function cronToolCall(event: { toolName: any; params: any; toolCallId: any }, sk: string) {
|
|
@@ -154,23 +165,27 @@ export function cronToolCall(event: { toolName: any; params: any; toolCallId: an
|
|
|
154
165
|
const delivery = extractDelivery(params)
|
|
155
166
|
if (!delivery) {
|
|
156
167
|
dcgLogger(`[${LOG_TAG}] cron call (${toolCallId}) has no delivery config, skip.`)
|
|
157
|
-
return
|
|
168
|
+
return undefined
|
|
169
|
+
}
|
|
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.`)
|
|
173
|
+
return undefined
|
|
158
174
|
}
|
|
159
|
-
|
|
175
|
+
|
|
176
|
+
const patched = newParams.delivery ?? (newParams.job as Record<string, unknown>)?.delivery
|
|
177
|
+
if (needsBestEffort(delivery)) {
|
|
160
178
|
dcgLogger(
|
|
161
|
-
`[${LOG_TAG}] cron call (${toolCallId}) delivery
|
|
162
|
-
`
|
|
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)}`
|
|
163
186
|
)
|
|
164
|
-
return params
|
|
165
187
|
}
|
|
166
188
|
|
|
167
|
-
// ★ 核心:注入 bestEffort: true
|
|
168
|
-
const newParams = injectBestEffort(params, sk)
|
|
169
|
-
dcgLogger(
|
|
170
|
-
`[${LOG_TAG}] cron call (${toolCallId}) injected bestEffort=true ` +
|
|
171
|
-
`(mode=announce, no channel). delivery=${JSON.stringify(newParams.delivery || (newParams.job as Record<string, unknown>)?.delivery)}`
|
|
172
|
-
)
|
|
173
|
-
|
|
174
189
|
return { params: newParams }
|
|
175
190
|
} else if (toolName === 'exec') {
|
|
176
191
|
if (params.command.indexOf('cron create') > -1 || params.command.indexOf('cron add') > -1) {
|
|
@@ -179,9 +194,9 @@ export function cronToolCall(event: { toolName: any; params: any; toolCallId: an
|
|
|
179
194
|
params.command.replace('--json', '') + ` --session-key ${sk} --channel ${"dcgchat-test"} --to dcg-cron:${sk} --json`
|
|
180
195
|
return { params: newParams }
|
|
181
196
|
} else {
|
|
182
|
-
return
|
|
197
|
+
return undefined
|
|
183
198
|
}
|
|
184
199
|
}
|
|
185
200
|
|
|
186
|
-
return
|
|
201
|
+
return undefined
|
|
187
202
|
}
|
package/src/tool.ts
CHANGED
|
@@ -325,6 +325,16 @@ function resolveHookSessionKey(
|
|
|
325
325
|
return (args?.sessionKey || '').trim()
|
|
326
326
|
}
|
|
327
327
|
|
|
328
|
+
/** 定时触发时会话往往非 running,但仍需跑 before_tool_call 以注入 sessionKey / delivery(见 cronToolCall) */
|
|
329
|
+
function shouldRunBeforeToolCallWithoutRunningSession(event: { toolName?: string; params?: { command?: string } }): boolean {
|
|
330
|
+
if (event?.toolName === 'cron') return true
|
|
331
|
+
const cmd = event?.params?.command
|
|
332
|
+
if (event?.toolName === 'exec' && typeof cmd === 'string') {
|
|
333
|
+
return cmd.includes('cron create') || cmd.includes('cron add')
|
|
334
|
+
}
|
|
335
|
+
return false
|
|
336
|
+
}
|
|
337
|
+
|
|
328
338
|
function trackSubagentLifecycle(eventName: string, event: any, args: any): void {
|
|
329
339
|
if (eventName === 'subagent_spawned') {
|
|
330
340
|
const runId = typeof event?.runId === 'string' ? event.runId : ''
|
|
@@ -351,7 +361,10 @@ export function monitoringToolMessage(api: OpenClawPluginApi) {
|
|
|
351
361
|
trackSubagentLifecycle(item.event, event, args)
|
|
352
362
|
const sk = resolveHookSessionKey(item.event, args ?? {})
|
|
353
363
|
if (sk) {
|
|
354
|
-
|
|
364
|
+
const toolHooksOk =
|
|
365
|
+
isSessionActiveForTool(sk) ||
|
|
366
|
+
(item.event === 'before_tool_call' && shouldRunBeforeToolCallWithoutRunningSession(event))
|
|
367
|
+
if (toolHooksOk) {
|
|
355
368
|
if (['after_tool_call', 'before_tool_call'].includes(item.event)) {
|
|
356
369
|
const { result: _result, ...rest } = event
|
|
357
370
|
dcgLogger(`工具调用结果: ~ event:${item.event} ~ params:${JSON.stringify(rest)}`)
|
package/src/transport.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { getWsConnection } from './utils/global.js'
|
|
1
|
+
import { clearSentMediaKeys, getWsConnection } from './utils/global.js'
|
|
2
2
|
import { dcgLogger } from './utils/log.js'
|
|
3
3
|
import type { IMsgParams } from './types.js'
|
|
4
4
|
import { getEffectiveMsgParams, getParamsDefaults } from './utils/params.js'
|
|
@@ -170,6 +170,7 @@ export function sendChunk(text: string, ctx: IMsgParams, chunkIdx: number): bool
|
|
|
170
170
|
|
|
171
171
|
export function sendFinal(ctx: IMsgParams, tag: string): boolean {
|
|
172
172
|
dcgLogger(` message handling complete state: to=${ctx.sessionId} final tag:${tag}`)
|
|
173
|
+
clearSentMediaKeys(ctx.sessionId)
|
|
173
174
|
return wsSend(ctx, { response: '', state: 'final' })
|
|
174
175
|
}
|
|
175
176
|
|
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]
|
|
@@ -36,7 +36,12 @@ export function handleGatewayEventMessage(msg: { event?: string; payload?: Recor
|
|
|
36
36
|
sendDcgchatCron(p?.jobId as string)
|
|
37
37
|
}
|
|
38
38
|
if (p?.action === 'finished' && p?.status === 'ok') {
|
|
39
|
-
|
|
39
|
+
const hasFileOutput = p.delivered === true && p.deliveryStatus === 'delivered'
|
|
40
|
+
let summary = p?.summary as string
|
|
41
|
+
if (summary.indexOf('HEARTBEAT_OK') >= 0 && summary !== 'HEARTBEAT_OK') {
|
|
42
|
+
summary = summary.replace('HEARTBEAT_OK', '')
|
|
43
|
+
}
|
|
44
|
+
finishedDcgchatCron(p?.jobId as string, summary, hasFileOutput)
|
|
40
45
|
}
|
|
41
46
|
}
|
|
42
47
|
} catch (error) {
|
package/src/utils/global.ts
CHANGED
|
@@ -82,11 +82,11 @@ const getMediaKey = (url: string) => url.split(/[\\/]/).pop() ?? url
|
|
|
82
82
|
/** 已发送媒体去重:外层 messageId → 内层该会话下已发送的媒体 key(文件名) */
|
|
83
83
|
const sentMediaKeysBySession = new Map<string, Set<string>>()
|
|
84
84
|
|
|
85
|
-
function getSessionMediaSet(
|
|
86
|
-
let set = sentMediaKeysBySession.get(
|
|
85
|
+
function getSessionMediaSet(sessionId: string): Set<string> {
|
|
86
|
+
let set = sentMediaKeysBySession.get(sessionId)
|
|
87
87
|
if (!set) {
|
|
88
88
|
set = new Set<string>()
|
|
89
|
-
sentMediaKeysBySession.set(
|
|
89
|
+
sentMediaKeysBySession.set(sessionId, set)
|
|
90
90
|
}
|
|
91
91
|
return set
|
|
92
92
|
}
|
|
@@ -95,14 +95,14 @@ export function addSentMediaKey(messageId: string, url: string) {
|
|
|
95
95
|
getSessionMediaSet(messageId).add(getMediaKey(url))
|
|
96
96
|
}
|
|
97
97
|
|
|
98
|
-
export function hasSentMediaKey(
|
|
99
|
-
return sentMediaKeysBySession.get(
|
|
98
|
+
export function hasSentMediaKey(sessionId: string, url: string): boolean {
|
|
99
|
+
return sentMediaKeysBySession.get(sessionId)?.has(getMediaKey(url)) ?? false
|
|
100
100
|
}
|
|
101
101
|
|
|
102
102
|
/** 不传 messageId 时清空全部会话;传入则只清空该会话 */
|
|
103
|
-
export function clearSentMediaKeys(
|
|
104
|
-
if (
|
|
105
|
-
sentMediaKeysBySession.delete(
|
|
103
|
+
export function clearSentMediaKeys(sessionId?: string) {
|
|
104
|
+
if (sessionId) {
|
|
105
|
+
sentMediaKeysBySession.delete(sessionId)
|
|
106
106
|
} else {
|
|
107
107
|
sentMediaKeysBySession.clear()
|
|
108
108
|
}
|