@dcrays/dcgchat 0.4.13 → 0.4.18
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 +31 -32
- package/src/channel.ts +56 -19
- package/src/cron.ts +15 -16
- package/src/cronToolCall.ts +5 -6
- package/src/tool.ts +64 -6
- package/src/utils/gatewayMsgHanlder.ts +8 -4
- package/src/utils/global.ts +23 -6
package/package.json
CHANGED
package/src/bot.ts
CHANGED
|
@@ -17,7 +17,7 @@ import { dcgLogger } from './utils/log.js'
|
|
|
17
17
|
import { channelInfo, systemCommand, interruptCommand, 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
|
-
import {
|
|
20
|
+
import { getDescendantSessionKeysForRequester, resetSubagentStateForRequesterSession, waitUntilSubagentsIdle } from './tool.js'
|
|
21
21
|
|
|
22
22
|
type MediaInfo = {
|
|
23
23
|
path: string
|
|
@@ -30,7 +30,7 @@ type TFileInfo = { name: string; url: string }
|
|
|
30
30
|
|
|
31
31
|
const mediaMaxBytes = 300 * 1024 * 1024
|
|
32
32
|
|
|
33
|
-
/** 当前会话最近一次 agent run 的 runId
|
|
33
|
+
/** 当前会话最近一次 agent run 的 runId(供观测;/stop 时对子会话与主会话使用不传 runId 的 chat.abort 以终止该键下全部活跃 run) */
|
|
34
34
|
const activeRunIdBySessionKey = new Map<string, string>()
|
|
35
35
|
|
|
36
36
|
/**
|
|
@@ -143,7 +143,6 @@ const typingCallbacks = createTypingCallbacks({
|
|
|
143
143
|
* 处理一条用户消息,调用 Agent 并返回回复
|
|
144
144
|
*/
|
|
145
145
|
export async function handleDcgchatMessage(msg: InboundMessage, accountId: string): Promise<void> {
|
|
146
|
-
let completeText = ''
|
|
147
146
|
const config = getOpenClawConfig()
|
|
148
147
|
if (!config) {
|
|
149
148
|
dcgLogger('no config available', 'error')
|
|
@@ -251,7 +250,8 @@ export async function handleDcgchatMessage(msg: InboundMessage, accountId: strin
|
|
|
251
250
|
|
|
252
251
|
const sentMediaKeys = new Set<string>()
|
|
253
252
|
const getMediaKey = (url: string) => url.split(/[\\/]/).pop() ?? url
|
|
254
|
-
|
|
253
|
+
/** 与 Feishu snapshot 模式一致:payload.text 为当前轮助手全文快照,据此算增量,避免工具前后快照变短或非单调时丢字 */
|
|
254
|
+
let lastStreamSnapshot = ''
|
|
255
255
|
|
|
256
256
|
if (msg.content.skills_scope.length > 0 && !msg.content?.agent_clone_code && !ignoreToolCommand.includes(text?.trim())) {
|
|
257
257
|
const workspaceDir = getWorkspaceDir()
|
|
@@ -326,38 +326,32 @@ export async function handleDcgchatMessage(msg: InboundMessage, accountId: strin
|
|
|
326
326
|
dcgLogger(`interrupt command: ${text}`)
|
|
327
327
|
const ctxForAbort = priorOutboundCtx.messageId?.trim() || priorOutboundCtx.sessionId?.trim() ? priorOutboundCtx : outboundCtx
|
|
328
328
|
sendFinal(ctxForAbort.messageId?.trim() ? ctxForAbort : { ...ctxForAbort, messageId: `${Date.now()}` }, 'abort')
|
|
329
|
-
sendText('会话已终止', outboundCtx)
|
|
330
329
|
sessionStreamSuppressed.add(dcgSessionKey)
|
|
331
|
-
|
|
330
|
+
// 网关侧彻底中止:子 agent 使用独立 sessionKey,仅 abort 主会话不会停子 run;自深到浅 abort 后代,再 abort 主会话;不传 runId 以终止该键下全部活跃 chat run
|
|
331
|
+
const descendantKeys = getDescendantSessionKeysForRequester(dcgSessionKey)
|
|
332
|
+
const abortSubKeys = [...descendantKeys].reverse()
|
|
333
|
+
if (abortSubKeys.length > 0) {
|
|
334
|
+
dcgLogger(`interrupt: chat.abort ${abortSubKeys.length} subagent session(s) (nested incl.)`)
|
|
335
|
+
}
|
|
336
|
+
for (const subKey of abortSubKeys) {
|
|
332
337
|
try {
|
|
333
|
-
await sendGatewayRpc({ method: 'chat.abort', params: { sessionKey } })
|
|
338
|
+
await sendGatewayRpc({ method: 'chat.abort', params: { sessionKey: subKey } })
|
|
334
339
|
} catch (e) {
|
|
335
|
-
dcgLogger(`chat.abort ${
|
|
340
|
+
dcgLogger(`chat.abort subagent ${subKey}: ${String(e)}`, 'error')
|
|
336
341
|
}
|
|
337
342
|
}
|
|
338
|
-
const keysToAbort = new Set<string>(getChildSessionKeysTrackedForRequester(dcgSessionKey))
|
|
339
343
|
try {
|
|
340
|
-
|
|
341
|
-
method: 'sessions.list',
|
|
342
|
-
params: { spawnedBy: dcgSessionKey, limit: 256 }
|
|
343
|
-
})
|
|
344
|
-
for (const s of listed?.sessions ?? []) {
|
|
345
|
-
const k = typeof s?.key === 'string' ? s.key.trim() : ''
|
|
346
|
-
if (k) keysToAbort.add(k)
|
|
347
|
-
}
|
|
344
|
+
await sendGatewayRpc({ method: 'chat.abort', params: { sessionKey: dcgSessionKey } })
|
|
348
345
|
} catch (e) {
|
|
349
|
-
dcgLogger(`
|
|
350
|
-
}
|
|
351
|
-
for (const sk of keysToAbort) {
|
|
352
|
-
await abortOneSession(sk)
|
|
346
|
+
dcgLogger(`chat.abort main ${dcgSessionKey}: ${String(e)}`, 'error')
|
|
353
347
|
}
|
|
354
|
-
|
|
348
|
+
activeRunIdBySessionKey.delete(dcgSessionKey)
|
|
355
349
|
streamChunkIdxBySessionKey.delete(dcgSessionKey)
|
|
350
|
+
clearSentMediaKeys(msg.content.message_id)
|
|
356
351
|
resetSubagentStateForRequesterSession(dcgSessionKey)
|
|
357
352
|
setMsgStatus(dcgSessionKey, 'finished')
|
|
358
|
-
clearSentMediaKeys(msg.content.message_id)
|
|
359
353
|
clearParamsMessage(dcgSessionKey)
|
|
360
|
-
|
|
354
|
+
sendText('会话已终止', outboundCtx)
|
|
361
355
|
sendFinal(outboundCtx, 'stop')
|
|
362
356
|
return
|
|
363
357
|
} else {
|
|
@@ -394,22 +388,27 @@ export async function handleDcgchatMessage(msg: InboundMessage, accountId: strin
|
|
|
394
388
|
onPartialReply: async (payload: ReplyPayload) => {
|
|
395
389
|
if (sessionStreamSuppressed.has(dcgSessionKey)) return
|
|
396
390
|
|
|
397
|
-
if (payload.text) {
|
|
398
|
-
completeText = payload.text
|
|
399
|
-
}
|
|
400
391
|
// --- Streaming text chunks ---
|
|
401
392
|
if (payload.text) {
|
|
402
|
-
const
|
|
403
|
-
|
|
404
|
-
|
|
393
|
+
const t = payload.text
|
|
394
|
+
let delta = ''
|
|
395
|
+
if (t.startsWith(lastStreamSnapshot)) {
|
|
396
|
+
delta = t.slice(lastStreamSnapshot.length)
|
|
397
|
+
lastStreamSnapshot = t
|
|
398
|
+
} else if (lastStreamSnapshot.startsWith(t)) {
|
|
399
|
+
// 快照缩短(模型修订等):不重复下发
|
|
400
|
+
} else {
|
|
401
|
+
// 与上一轮快照不衔接(常见于工具后快照从新的助手片段重新开始):整段下发
|
|
402
|
+
delta = t
|
|
403
|
+
lastStreamSnapshot = t
|
|
404
|
+
}
|
|
405
405
|
if (delta.trim()) {
|
|
406
406
|
const prev = streamChunkIdxBySessionKey.get(dcgSessionKey) ?? 0
|
|
407
407
|
streamChunkIdxBySessionKey.set(dcgSessionKey, prev + 1)
|
|
408
408
|
sendChunk(delta, outboundCtx, prev)
|
|
409
409
|
}
|
|
410
|
-
streamedTextLen = payload.text.length
|
|
411
410
|
} else {
|
|
412
|
-
dcgLogger(`onPartialReply no text: ${JSON.stringify(payload)}
|
|
411
|
+
dcgLogger(`onPartialReply no text (media/tool metadata): ${JSON.stringify(payload)}`)
|
|
413
412
|
}
|
|
414
413
|
// --- Media from payload ---
|
|
415
414
|
const mediaList = resolveReplyMediaList(payload)
|
package/src/channel.ts
CHANGED
|
@@ -14,6 +14,7 @@ import { isWsOpen, mergeDefaultParams, mergeSessionParams, sendFinal, wsSendRaw
|
|
|
14
14
|
import { dcgLogger, setLogger } from './utils/log.js'
|
|
15
15
|
import { getOutboundMsgParams, getParamsMessage } from './utils/params.js'
|
|
16
16
|
import { startDcgchatGatewaySocket } from './gateway/socket.js'
|
|
17
|
+
import { getCronJobsPath, readCronJob } from './cron.js'
|
|
17
18
|
|
|
18
19
|
function dcgchatChannelCfg(): DcgchatConfig {
|
|
19
20
|
return (getOpenClawConfig()?.channels?.["dcgchat"] as DcgchatConfig | undefined) ?? {}
|
|
@@ -61,6 +62,8 @@ export type DcgchatMediaSendOptions = {
|
|
|
61
62
|
sessionKey: string
|
|
62
63
|
mediaUrl?: string
|
|
63
64
|
text?: string
|
|
65
|
+
/** 定时任务等场景须与 `getCronMessageId` 一致,避免沿用 map 里上一条用户消息的 messageId */
|
|
66
|
+
messageId?: string
|
|
64
67
|
}
|
|
65
68
|
|
|
66
69
|
function normalizeSessionTarget(rawTo: string): string {
|
|
@@ -69,6 +72,29 @@ function normalizeSessionTarget(rawTo: string): string {
|
|
|
69
72
|
return getParamsMessage(cleaned)?.sessionKey?.trim() || cleaned
|
|
70
73
|
}
|
|
71
74
|
|
|
75
|
+
/**
|
|
76
|
+
* OpenClaw 定时任务 `sessionTarget: isolated` 时,出站 `to` 常为 `agent:<code>:cron:<jobId>[:run:…]`,
|
|
77
|
+
* 与 `paramsMessageMap` / `getCronMessageId` 使用的 jobs.json `sessionKey`(mobook 用户会话)不一致,导致发文件时 sessionId、messageId 错位或缺省。
|
|
78
|
+
*/
|
|
79
|
+
function extractCronJobIdFromIsolatedSessionKey(sessionKey: string): string | null {
|
|
80
|
+
const parts = sessionKey.split(':').filter((p) => p.length > 0)
|
|
81
|
+
const i = parts.findIndex((p) => p.toLowerCase() === 'cron')
|
|
82
|
+
if (i < 0 || i + 1 >= parts.length) return null
|
|
83
|
+
return parts[i + 1] ?? null
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function resolveIsolatedCronSessionToJobSessionKey(sessionKey: string): string {
|
|
87
|
+
const jobId = extractCronJobIdFromIsolatedSessionKey(sessionKey)
|
|
88
|
+
if (!jobId) return sessionKey
|
|
89
|
+
const job = readCronJob(getCronJobsPath(), jobId)
|
|
90
|
+
const sk = job && typeof job.sessionKey === 'string' ? job.sessionKey.trim() : ''
|
|
91
|
+
if (!sk) {
|
|
92
|
+
dcgLogger(`dcgchat: cron job ${jobId} has no sessionKey in jobs.json, keep outbound key=${sessionKey}`, 'error')
|
|
93
|
+
return sessionKey
|
|
94
|
+
}
|
|
95
|
+
return sk
|
|
96
|
+
}
|
|
97
|
+
|
|
72
98
|
/** 出站返回的 chatId:含 `dcg-cron:` 时保留原始 `to`,便于下游识别定时投递 */
|
|
73
99
|
function outboundChatId(rawTo: string | undefined, normalizedTo: string): string {
|
|
74
100
|
const raw = rawTo?.trim() ?? ''
|
|
@@ -76,8 +102,10 @@ function outboundChatId(rawTo: string | undefined, normalizedTo: string): string
|
|
|
76
102
|
}
|
|
77
103
|
|
|
78
104
|
export async function sendDcgchatMedia(opts: DcgchatMediaSendOptions): Promise<void> {
|
|
79
|
-
|
|
80
|
-
|
|
105
|
+
let sessionKey = normalizeSessionTarget(opts.sessionKey ?? '')
|
|
106
|
+
sessionKey = resolveIsolatedCronSessionToJobSessionKey(sessionKey)
|
|
107
|
+
const baseCtx = getOutboundMsgParams(sessionKey)
|
|
108
|
+
const msgCtx = opts.messageId?.trim() ? { ...baseCtx, messageId: opts.messageId.trim() } : baseCtx
|
|
81
109
|
if (!isWsOpen()) {
|
|
82
110
|
dcgLogger(`outbound media skipped -> ws not open: ${opts.mediaUrl ?? ''}`)
|
|
83
111
|
return
|
|
@@ -98,9 +126,13 @@ export async function sendDcgchatMedia(opts: DcgchatMediaSendOptions): Promise<v
|
|
|
98
126
|
try {
|
|
99
127
|
const botToken = msgCtx.botToken ?? getOpenClawConfig()?.channels?.["dcgchat"]?.botToken ?? ''
|
|
100
128
|
const url = opts.mediaUrl ? await ossUpload(opts.mediaUrl, botToken, 1) : ''
|
|
129
|
+
if (!msgCtx.sessionId) {
|
|
130
|
+
const { sessionId, agentId } = getInfoBySessionKey(sessionKey)
|
|
131
|
+
msgCtx.sessionId = sessionId
|
|
132
|
+
msgCtx.agentId = agentId
|
|
133
|
+
}
|
|
101
134
|
wsSendRaw(msgCtx, {
|
|
102
135
|
response: opts.text ?? '',
|
|
103
|
-
message_tags: { source: 'file' },
|
|
104
136
|
files: [{ url, name: fileName }]
|
|
105
137
|
})
|
|
106
138
|
dcgLogger(`dcgchat: sendMedia session=${sessionKey}, file=${fileName}`)
|
|
@@ -254,19 +286,19 @@ export const dcgchatPlugin: ChannelPlugin<ResolvedDcgchatAccount> = {
|
|
|
254
286
|
const isCron = ctx.to.indexOf('dcg-cron:') >= 0
|
|
255
287
|
const outboundCtx = getOutboundMsgParams(to)
|
|
256
288
|
const content: Record<string, unknown> = { response: ctx.text }
|
|
257
|
-
if (isCron) {
|
|
258
|
-
messageId = getCronMessageId(to) || `${Date.now()}`
|
|
259
|
-
const { sessionId, agentId } = getInfoBySessionKey(to)
|
|
260
|
-
content.is_finish = -1
|
|
261
|
-
content.message_tags = { source: 'cron' }
|
|
262
|
-
const merged = mergeDefaultParams({
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
})
|
|
268
|
-
wsSendRaw(merged, content)
|
|
269
|
-
|
|
289
|
+
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 {
|
|
270
302
|
if (outboundCtx?.sessionId) {
|
|
271
303
|
messageId = outboundCtx?.messageId || `${Date.now()}`
|
|
272
304
|
const newCtx = { ...outboundCtx, messageId }
|
|
@@ -283,8 +315,9 @@ export const dcgchatPlugin: ChannelPlugin<ResolvedDcgchatAccount> = {
|
|
|
283
315
|
}
|
|
284
316
|
},
|
|
285
317
|
sendMedia: async (ctx) => {
|
|
286
|
-
const
|
|
287
|
-
const
|
|
318
|
+
const normalizedFromTo = normalizeSessionTarget(ctx.to)
|
|
319
|
+
const isCron = ctx.to.indexOf('dcg-cron:') >= 0 || extractCronJobIdFromIsolatedSessionKey(normalizedFromTo) !== null
|
|
320
|
+
const to = resolveIsolatedCronSessionToJobSessionKey(normalizedFromTo)
|
|
288
321
|
const outboundCtx = getOutboundMsgParams(to)
|
|
289
322
|
const msgCtx = getParamsMessage(to) ?? outboundCtx
|
|
290
323
|
const cronMsgId = getCronMessageId(to)
|
|
@@ -301,7 +334,11 @@ export const dcgchatPlugin: ChannelPlugin<ResolvedDcgchatAccount> = {
|
|
|
301
334
|
}
|
|
302
335
|
|
|
303
336
|
dcgLogger(`channel sendMedia to ${ctx.to}`)
|
|
304
|
-
await sendDcgchatMedia({
|
|
337
|
+
await sendDcgchatMedia({
|
|
338
|
+
sessionKey: to || '',
|
|
339
|
+
mediaUrl: ctx.mediaUrl || '',
|
|
340
|
+
...(isCron ? { messageId } : {})
|
|
341
|
+
})
|
|
305
342
|
return {
|
|
306
343
|
channel: "dcgchat",
|
|
307
344
|
messageId,
|
package/src/cron.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import path from 'node:path'
|
|
2
2
|
import fs from 'node:fs'
|
|
3
3
|
import type { IMsgParams } from './types.js'
|
|
4
|
-
import { isWsOpen, mergeDefaultParams, sendEventMessage, sendFinal } from './transport.js'
|
|
4
|
+
import { isWsOpen, mergeDefaultParams, sendEventMessage, sendFinal, wsSendRaw } from './transport.js'
|
|
5
5
|
import { getCronMessageId, getWorkspaceDir, getWsConnection, removeCronMessageId, setCronMessageId } from './utils/global.js'
|
|
6
6
|
import { ossUpload } from './request/oss.js'
|
|
7
7
|
import { dcgLogger } from './utils/log.js'
|
|
@@ -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) => {
|
|
138
|
+
export const finishedDcgchatCron = async (jobId: string, summary: string) => {
|
|
139
139
|
const id = jobId?.trim()
|
|
140
140
|
if (!id) {
|
|
141
141
|
dcgLogger('finishedDcgchatCron: empty jobId', 'error')
|
|
@@ -147,24 +147,23 @@ export const finishedDcgchatCron = async (jobId: string) => {
|
|
|
147
147
|
dcgLogger(`finishedDcgchatCron: no sessionKey for job id=${id}`, 'error')
|
|
148
148
|
return
|
|
149
149
|
}
|
|
150
|
-
const
|
|
151
|
-
const messageId = getCronMessageId(sessionKey)
|
|
150
|
+
const messageId = getCronMessageId(sessionKey) || `${Date.now()}`
|
|
152
151
|
const sessionInfo = sessionKey.split(':')
|
|
153
152
|
const sessionId = sessionInfo.at(-1) ?? ''
|
|
154
153
|
const agentId = sessionInfo.at(-2) ?? ''
|
|
155
|
-
if (outboundCtx?.sessionId) {
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
})
|
|
154
|
+
// if (outboundCtx?.sessionId) {
|
|
155
|
+
|
|
156
|
+
const merged = mergeDefaultParams({
|
|
157
|
+
agentId: agentId,
|
|
158
|
+
sessionId: `${sessionId}`,
|
|
159
|
+
messageId: messageId || `${Date.now()}`,
|
|
160
|
+
real_mobook: !sessionId ? 1 : ''
|
|
161
|
+
})
|
|
162
|
+
wsSendRaw(merged, { response: summary, message_tags: { source: 'cron' }, is_finish: -1 })
|
|
163
|
+
setTimeout(() => {
|
|
166
164
|
sendFinal(merged, 'cron send')
|
|
167
|
-
}
|
|
165
|
+
}, 200)
|
|
166
|
+
// }
|
|
168
167
|
const ws = getWsConnection()
|
|
169
168
|
const baseContent = getParamsDefaults()
|
|
170
169
|
if (isWsOpen()) {
|
package/src/cronToolCall.ts
CHANGED
|
@@ -132,14 +132,13 @@ function injectBestEffort(params: Record<string, unknown>, sk: string): Record<s
|
|
|
132
132
|
return newParams
|
|
133
133
|
}
|
|
134
134
|
|
|
135
|
-
// job.delivery
|
|
135
|
+
// job.delivery
|
|
136
136
|
const job = newParams.job as Record<string, unknown> | undefined
|
|
137
137
|
if (job?.delivery && typeof job.delivery === 'object') {
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
del.channel = "dcgchat"
|
|
138
|
+
;(job.delivery as CronDelivery).bestEffort = true
|
|
139
|
+
;(newParams.delivery as CronDelivery).to = `dcg-cron:${sk}`
|
|
140
|
+
;(newParams.delivery as CronDelivery).accountId = agentId
|
|
141
|
+
;(newParams.delivery as CronDelivery).channel = "dcgchat"
|
|
143
142
|
newParams.sessionKey = sk
|
|
144
143
|
return newParams
|
|
145
144
|
}
|
package/src/tool.ts
CHANGED
|
@@ -51,8 +51,30 @@ const eventList = [
|
|
|
51
51
|
{ event: 'after_tool_call', message: '' }
|
|
52
52
|
]
|
|
53
53
|
|
|
54
|
+
/** 子 agent 的 sessionKey 往往未写入 params map,回落到主会话 outbound 参数避免 messageId 缺失 */
|
|
55
|
+
function resolveOutboundParamsForSession(sk: string) {
|
|
56
|
+
const k = sk.trim()
|
|
57
|
+
let params = getEffectiveMsgParams(k)
|
|
58
|
+
if (params.messageId?.trim() || params.sessionId?.trim()) return params
|
|
59
|
+
const parent = requesterByChildSessionKey.get(k)
|
|
60
|
+
if (parent) {
|
|
61
|
+
const parentParams = getEffectiveMsgParams(parent)
|
|
62
|
+
if (parentParams.messageId?.trim() || parentParams.sessionId?.trim()) return parentParams
|
|
63
|
+
}
|
|
64
|
+
return params
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** 主会话已 running 时,子会话上的工具/事件也应下发(否则子 key 无 running 状态会整段丢消息) */
|
|
68
|
+
function isSessionActiveForTool(sk: string): boolean {
|
|
69
|
+
const k = sk.trim()
|
|
70
|
+
if (!k) return false
|
|
71
|
+
if (getMsgStatus(k) === 'running') return true
|
|
72
|
+
const parent = requesterByChildSessionKey.get(k)
|
|
73
|
+
return parent ? getMsgStatus(parent) === 'running' : false
|
|
74
|
+
}
|
|
75
|
+
|
|
54
76
|
function sendToolCallMessage(sk: string, text: string, toolCallId: string, isCover: number) {
|
|
55
|
-
const params =
|
|
77
|
+
const params = resolveOutboundParamsForSession(sk)
|
|
56
78
|
const content = { is_finish: -1, tool_call_id: toolCallId, is_cover: isCover, thinking_content: text, response: '' }
|
|
57
79
|
wsSendRaw(params, content, false)
|
|
58
80
|
}
|
|
@@ -158,6 +180,29 @@ export function getChildSessionKeysTrackedForRequester(requesterSessionKey: stri
|
|
|
158
180
|
return out
|
|
159
181
|
}
|
|
160
182
|
|
|
183
|
+
/**
|
|
184
|
+
* 自根 requester 起 BFS 收集所有已跟踪后代子会话(含嵌套)。网关 abort 时宜自深到浅,调用方对结果 `.reverse()` 后再逐个 chat.abort。
|
|
185
|
+
*/
|
|
186
|
+
export function getDescendantSessionKeysForRequester(rootRequesterSessionKey: string): string[] {
|
|
187
|
+
const root = rootRequesterSessionKey.trim()
|
|
188
|
+
if (!root) return []
|
|
189
|
+
const ordered: string[] = []
|
|
190
|
+
const seen = new Set<string>()
|
|
191
|
+
let frontier = getChildSessionKeysTrackedForRequester(root)
|
|
192
|
+
while (frontier.length > 0) {
|
|
193
|
+
const next: string[] = []
|
|
194
|
+
for (const sk of frontier) {
|
|
195
|
+
const k = sk.trim()
|
|
196
|
+
if (!k || seen.has(k)) continue
|
|
197
|
+
seen.add(k)
|
|
198
|
+
ordered.push(k)
|
|
199
|
+
next.push(...getChildSessionKeysTrackedForRequester(k))
|
|
200
|
+
}
|
|
201
|
+
frontier = next
|
|
202
|
+
}
|
|
203
|
+
return ordered
|
|
204
|
+
}
|
|
205
|
+
|
|
161
206
|
/**
|
|
162
207
|
* 打断后清空本地子 agent 跟踪(runId、父子映射、子 runId→sessionKey),并唤醒 waitUntilSubagentsIdle,避免永久挂起。
|
|
163
208
|
*/
|
|
@@ -306,14 +351,27 @@ export function monitoringToolMessage(api: OpenClawPluginApi) {
|
|
|
306
351
|
trackSubagentLifecycle(item.event, event, args)
|
|
307
352
|
const sk = resolveHookSessionKey(item.event, args ?? {})
|
|
308
353
|
if (sk) {
|
|
309
|
-
|
|
310
|
-
if (status === 'running') {
|
|
354
|
+
if (isSessionActiveForTool(sk)) {
|
|
311
355
|
if (['after_tool_call', 'before_tool_call'].includes(item.event)) {
|
|
312
356
|
const { result: _result, ...rest } = event
|
|
313
357
|
dcgLogger(`工具调用结果: ~ event:${item.event} ~ params:${JSON.stringify(rest)}`)
|
|
314
358
|
|
|
315
359
|
if (item.event === 'before_tool_call') {
|
|
316
|
-
|
|
360
|
+
const hookResult = cronToolCall(rest, sk)
|
|
361
|
+
const text = JSON.stringify({
|
|
362
|
+
type: item.event,
|
|
363
|
+
specialIdentification: 'dcgchat_tool_call_special_identification',
|
|
364
|
+
callId: event.toolCallId || event.runId || Date.now().toString(),
|
|
365
|
+
...rest,
|
|
366
|
+
status: 'running'
|
|
367
|
+
})
|
|
368
|
+
sendToolCallMessage(
|
|
369
|
+
sk,
|
|
370
|
+
text,
|
|
371
|
+
event.toolCallId || event.runId || Date.now().toString(),
|
|
372
|
+
0
|
|
373
|
+
)
|
|
374
|
+
return hookResult
|
|
317
375
|
}
|
|
318
376
|
const text = JSON.stringify({
|
|
319
377
|
type: item.event,
|
|
@@ -329,7 +387,7 @@ export function monitoringToolMessage(api: OpenClawPluginApi) {
|
|
|
329
387
|
item.event === 'after_tool_call' ? 1 : 0
|
|
330
388
|
)
|
|
331
389
|
} else if (item.event) {
|
|
332
|
-
const msgCtx =
|
|
390
|
+
const msgCtx = resolveOutboundParamsForSession(sk)
|
|
333
391
|
if (item.event === 'llm_output') {
|
|
334
392
|
if (event.lastAssistant?.errorMessage === '1003-额度不足请充值') {
|
|
335
393
|
const message = '您的积分已消耗完,您可以通过充值积分来继续使用'
|
|
@@ -346,7 +404,7 @@ export function monitoringToolMessage(api: OpenClawPluginApi) {
|
|
|
346
404
|
params: item.message
|
|
347
405
|
})
|
|
348
406
|
sendToolCallMessage(sk, text, event.runId || Date.now().toString(), 0)
|
|
349
|
-
dcgLogger(`工具调用结果: ~ event:${item.event}
|
|
407
|
+
dcgLogger(`工具调用结果: ~ event:${item.event}`)
|
|
350
408
|
}
|
|
351
409
|
}
|
|
352
410
|
} else if (item.event !== 'before_tool_call') {
|
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
import type { GatewayEvent } from '../gateway/index.js'
|
|
2
2
|
import { finishedDcgchatCron, sendDcgchatCron } from '../cron.js'
|
|
3
3
|
import { dcgLogger } from './log.js'
|
|
4
|
-
import { getEffectiveMsgParams, getSessionKeyBySubAgentRunId } from './params.js'
|
|
5
|
-
import { sendChunk } from '../transport.js'
|
|
4
|
+
import { clearParamsMessage, getEffectiveMsgParams, getSessionKeyBySubAgentRunId } from './params.js'
|
|
5
|
+
import { sendChunk, sendFinal, sendText } from '../transport.js'
|
|
6
|
+
import { resetSubagentStateForRequesterSession } from '../tool.js'
|
|
7
|
+
import { setMsgStatus } from './global.js'
|
|
6
8
|
|
|
7
9
|
/**
|
|
8
10
|
* 处理网关 event 帧的副作用(agent 流式输出、cron 同步),并构造供上层分发的 GatewayEvent。
|
|
9
11
|
*/
|
|
10
12
|
export function handleGatewayEventMessage(msg: { event?: string; payload?: Record<string, unknown>; seq?: number }): GatewayEvent {
|
|
11
13
|
try {
|
|
14
|
+
// 子agent消息输出
|
|
12
15
|
if (msg.event === 'agent') {
|
|
13
16
|
const pl = msg.payload as { runId: string; data?: { delta?: unknown } }
|
|
14
17
|
const sessionKey = getSessionKeyBySubAgentRunId(pl.runId)
|
|
@@ -19,6 +22,7 @@ export function handleGatewayEventMessage(msg: { event?: string; payload?: Recor
|
|
|
19
22
|
}
|
|
20
23
|
}
|
|
21
24
|
}
|
|
25
|
+
// 定时任务
|
|
22
26
|
if (msg.event === 'cron') {
|
|
23
27
|
const p = msg.payload
|
|
24
28
|
dcgLogger(`[Gateway] 收到定时任务事件: ${JSON.stringify(p)}`)
|
|
@@ -31,8 +35,8 @@ export function handleGatewayEventMessage(msg: { event?: string; payload?: Recor
|
|
|
31
35
|
if (p?.action === 'removed') {
|
|
32
36
|
sendDcgchatCron(p?.jobId as string)
|
|
33
37
|
}
|
|
34
|
-
if (p?.action === 'finished') {
|
|
35
|
-
finishedDcgchatCron(p?.jobId as string)
|
|
38
|
+
if (p?.action === 'finished' && p?.status === 'ok') {
|
|
39
|
+
finishedDcgchatCron(p?.jobId as string, p?.summary as string)
|
|
36
40
|
}
|
|
37
41
|
}
|
|
38
42
|
} catch (error) {
|
package/src/utils/global.ts
CHANGED
|
@@ -108,18 +108,35 @@ export function clearSentMediaKeys(messageId?: string) {
|
|
|
108
108
|
}
|
|
109
109
|
}
|
|
110
110
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
111
|
+
/** 每个 sessionKey 下多个定时任务 messageId,入队在尾、出队在头(FIFO) */
|
|
112
|
+
const cronMessageIdMap = new Map<string, string[]>()
|
|
113
|
+
|
|
114
|
+
export function setCronMessageId(sk: string, messageId: string | number | null | undefined) {
|
|
115
|
+
const mid = messageId != null && messageId !== '' ? String(messageId).trim() : ''
|
|
116
|
+
if (!sk?.trim() || !mid) return
|
|
117
|
+
let q = cronMessageIdMap.get(sk)
|
|
118
|
+
if (!q) {
|
|
119
|
+
q = []
|
|
120
|
+
cronMessageIdMap.set(sk, q)
|
|
121
|
+
}
|
|
122
|
+
q.push(mid)
|
|
115
123
|
}
|
|
116
124
|
|
|
125
|
+
/** 窥视队首 messageId(不移除),供发送中与 finished 配对使用 */
|
|
117
126
|
export function getCronMessageId(sk: string): string {
|
|
118
|
-
|
|
127
|
+
if (!sk?.trim()) return ''
|
|
128
|
+
return cronMessageIdMap.get(sk)?.[0] ?? ''
|
|
119
129
|
}
|
|
120
130
|
|
|
131
|
+
/** 弹出队首一条,与一次 finished 对应;队列为空时移除 key */
|
|
121
132
|
export function removeCronMessageId(sk: string) {
|
|
122
|
-
|
|
133
|
+
if (!sk?.trim()) return
|
|
134
|
+
const q = cronMessageIdMap.get(sk)
|
|
135
|
+
if (!q?.length) return
|
|
136
|
+
q.shift()
|
|
137
|
+
if (q.length === 0) {
|
|
138
|
+
cronMessageIdMap.delete(sk)
|
|
139
|
+
}
|
|
123
140
|
}
|
|
124
141
|
|
|
125
142
|
export const getSessionKey = (content: any, accountId: string) => {
|