@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dcrays/dcgchat-test",
3
- "version": "0.4.17",
3
+ "version": "0.4.18",
4
4
  "type": "module",
5
5
  "description": "OpenClaw channel plugin for 书灵墨宝 (WebSocket)",
6
6
  "main": "index.ts",
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 { getChildSessionKeysTrackedForRequester, resetSubagentStateForRequesterSession, waitUntilSubagentsIdle } from './tool.js'
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(供 chat.abort 精确打断;无则仅传 sessionKey 仍会中止该会话全部活动运行) */
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
- const runId = activeRunIdBySessionKey.get(dcgSessionKey)
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
- const sessionKey = normalizeSessionTarget(opts.sessionKey ?? '')
80
- const msgCtx = getOutboundMsgParams(sessionKey)
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 isCron = ctx.to.indexOf('dcg-cron:') >= 0
287
- const to = normalizeSessionTarget(ctx.to)
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({ sessionKey: to || '', mediaUrl: ctx.mediaUrl || '' })
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
  }