@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dcrays/dcgchat-test",
3
- "version": "0.4.18",
3
+ "version": "0.4.20",
4
4
  "type": "module",
5
5
  "description": "OpenClaw channel plugin for 书灵墨宝 (WebSocket)",
6
6
  "main": "index.ts",
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, interruptCommand, ENV, ignoreToolCommand } from './utils/constant.js'
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: msg.content.app_id,
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 (!interruptCommand.includes(text?.trim())) {
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 (interruptCommand.includes(text?.trim())) {
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
- // 网关侧彻底中止:子 agent 使用独立 sessionKey,仅 abort 主会话不会停子 run;自深到浅 abort 后代,再 abort 主会话;不传 runId 以终止该键下全部活跃 chat run
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
- await sendGatewayRpc({ method: 'chat.abort', params: { sessionKey: dcgSessionKey } })
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, ...interruptCommand].includes(text?.trim())) {
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
- const dedupeId = msgCtx.messageId
116
- if (mediaUrl && dedupeId && hasSentMediaKey(dedupeId, mediaUrl)) {
117
- dcgLogger(`dcgchat: sendMedia skipped (duplicate in session): ${mediaUrl}`)
118
- return
119
- }
120
- if (mediaUrl && dedupeId) {
121
- addSentMediaKey(dedupeId, mediaUrl)
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.sessionId) {
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: { source: 'file' },
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 (!outboundCtx?.sessionId) {
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
- wsSendRaw(merged, { response: summary, message_tags: { source: 'cron' }, is_finish: -1 })
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)
@@ -117,33 +117,44 @@ function needsBestEffort(delivery: CronDelivery): boolean {
117
117
  }
118
118
 
119
119
  /**
120
- * 深拷贝 params 并注入 bestEffort: true
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 injectBestEffort(params: Record<string, unknown>, sk: string): Record<string, unknown> {
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, sessionId } = formatterSessionKey(sk)
125
- // 顶层 delivery
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
- ;(newParams.delivery as CronDelivery).bestEffort = true
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
- ;(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-test"
152
+ apply(job.delivery as CronDelivery)
142
153
  newParams.sessionKey = sk
143
154
  return newParams
144
155
  }
145
156
 
146
- return newParams
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 params
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
- if (!needsBestEffort(delivery)) {
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 does not need bestEffort ` +
162
- `(mode=${String(delivery.mode)}, channel=${String(delivery.channel)}, bestEffort=${String(delivery.bestEffort)}), skip.`
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 params
197
+ return undefined
183
198
  }
184
199
  }
185
200
 
186
- return params
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
- if (isSessionActiveForTool(sk)) {
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
 
@@ -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 interruptCommand = ['/stop']
5
+ export const stopCommand = ['/stop']
6
6
 
7
- export const ignoreToolCommand = ['/search', '/abort', '/queue interrupt', ...systemCommand, ...interruptCommand]
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
- finishedDcgchatCron(p?.jobId as string, p?.summary as string)
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) {
@@ -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(messageId: string): Set<string> {
86
- let set = sentMediaKeysBySession.get(messageId)
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(messageId, 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(messageId: string, url: string): boolean {
99
- return sentMediaKeysBySession.get(messageId)?.has(getMediaKey(url)) ?? false
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(messageId?: string) {
104
- if (messageId) {
105
- sentMediaKeysBySession.delete(messageId)
103
+ export function clearSentMediaKeys(sessionId?: string) {
104
+ if (sessionId) {
105
+ sentMediaKeysBySession.delete(sessionId)
106
106
  } else {
107
107
  sentMediaKeysBySession.clear()
108
108
  }