@dcrays/dcgchat-test 0.4.17 → 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 +18 -5
- package/src/channel.ts +43 -6
- package/src/tool.ts +23 -0
- package/src/utils/gatewayMsgHanlder.ts +1 -1
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
|
/**
|
|
@@ -327,12 +327,25 @@ export async function handleDcgchatMessage(msg: InboundMessage, accountId: strin
|
|
|
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
329
|
sessionStreamSuppressed.add(dcgSessionKey)
|
|
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) {
|
|
337
|
+
try {
|
|
338
|
+
await sendGatewayRpc({ method: 'chat.abort', params: { sessionKey: subKey } })
|
|
339
|
+
} catch (e) {
|
|
340
|
+
dcgLogger(`chat.abort subagent ${subKey}: ${String(e)}`, 'error')
|
|
341
|
+
}
|
|
342
|
+
}
|
|
330
343
|
try {
|
|
331
|
-
|
|
332
|
-
await sendGatewayRpc({ method: 'chat.abort', params: { sessionKey: dcgSessionKey, runId } })
|
|
344
|
+
await sendGatewayRpc({ method: 'chat.abort', params: { sessionKey: dcgSessionKey } })
|
|
333
345
|
} catch (e) {
|
|
334
|
-
dcgLogger(`chat.abort ${dcgSessionKey}: ${String(e)}`, 'error')
|
|
346
|
+
dcgLogger(`chat.abort main ${dcgSessionKey}: ${String(e)}`, 'error')
|
|
335
347
|
}
|
|
348
|
+
activeRunIdBySessionKey.delete(dcgSessionKey)
|
|
336
349
|
streamChunkIdxBySessionKey.delete(dcgSessionKey)
|
|
337
350
|
clearSentMediaKeys(msg.content.message_id)
|
|
338
351
|
resetSubagentStateForRequesterSession(dcgSessionKey)
|
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-test"] 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-test"]?.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}`)
|
|
@@ -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-test",
|
|
307
344
|
messageId,
|
package/src/tool.ts
CHANGED
|
@@ -180,6 +180,29 @@ export function getChildSessionKeysTrackedForRequester(requesterSessionKey: stri
|
|
|
180
180
|
return out
|
|
181
181
|
}
|
|
182
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
|
+
|
|
183
206
|
/**
|
|
184
207
|
* 打断后清空本地子 agent 跟踪(runId、父子映射、子 runId→sessionKey),并唤醒 waitUntilSubagentsIdle,避免永久挂起。
|
|
185
208
|
*/
|
|
@@ -35,7 +35,7 @@ export function handleGatewayEventMessage(msg: { event?: string; payload?: Recor
|
|
|
35
35
|
if (p?.action === 'removed') {
|
|
36
36
|
sendDcgchatCron(p?.jobId as string)
|
|
37
37
|
}
|
|
38
|
-
if (p?.action === 'finished') {
|
|
38
|
+
if (p?.action === 'finished' && p?.status === 'ok') {
|
|
39
39
|
finishedDcgchatCron(p?.jobId as string, p?.summary as string)
|
|
40
40
|
}
|
|
41
41
|
}
|