@dcrays/dcgchat-test 0.4.16 → 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 +16 -22
- package/src/channel.ts +43 -6
- package/src/tool.ts +23 -0
- package/src/utils/gatewayMsgHanlder.ts +7 -3
- package/src/utils/global.ts +4 -3
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
|
/**
|
|
@@ -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 {
|
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
|
*/
|
|
@@ -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,7 +35,7 @@ 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') {
|
|
38
|
+
if (p?.action === 'finished' && p?.status === 'ok') {
|
|
35
39
|
finishedDcgchatCron(p?.jobId as string, p?.summary as string)
|
|
36
40
|
}
|
|
37
41
|
}
|
package/src/utils/global.ts
CHANGED
|
@@ -111,14 +111,15 @@ export function clearSentMediaKeys(messageId?: string) {
|
|
|
111
111
|
/** 每个 sessionKey 下多个定时任务 messageId,入队在尾、出队在头(FIFO) */
|
|
112
112
|
const cronMessageIdMap = new Map<string, string[]>()
|
|
113
113
|
|
|
114
|
-
export function setCronMessageId(sk: string, messageId: string) {
|
|
115
|
-
|
|
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
|
|
116
117
|
let q = cronMessageIdMap.get(sk)
|
|
117
118
|
if (!q) {
|
|
118
119
|
q = []
|
|
119
120
|
cronMessageIdMap.set(sk, q)
|
|
120
121
|
}
|
|
121
|
-
q.push(
|
|
122
|
+
q.push(mid)
|
|
122
123
|
}
|
|
123
124
|
|
|
124
125
|
/** 窥视队首 messageId(不移除),供发送中与 finished 配对使用 */
|