@dcrays/dcgchat-test 0.4.14 → 0.4.16

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.14",
3
+ "version": "0.4.16",
4
4
  "type": "module",
5
5
  "description": "OpenClaw channel plugin for 书灵墨宝 (WebSocket)",
6
6
  "main": "index.ts",
package/src/bot.ts CHANGED
@@ -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
- let streamedTextLen = 0
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()
@@ -394,22 +394,27 @@ export async function handleDcgchatMessage(msg: InboundMessage, accountId: strin
394
394
  onPartialReply: async (payload: ReplyPayload) => {
395
395
  if (sessionStreamSuppressed.has(dcgSessionKey)) return
396
396
 
397
- if (payload.text) {
398
- completeText = payload.text
399
- }
400
397
  // --- Streaming text chunks ---
401
398
  if (payload.text) {
402
- const delta = payload.text.startsWith(completeText.slice(0, streamedTextLen))
403
- ? payload.text.slice(streamedTextLen)
404
- : payload.text
399
+ const t = payload.text
400
+ let delta = ''
401
+ if (t.startsWith(lastStreamSnapshot)) {
402
+ delta = t.slice(lastStreamSnapshot.length)
403
+ lastStreamSnapshot = t
404
+ } else if (lastStreamSnapshot.startsWith(t)) {
405
+ // 快照缩短(模型修订等):不重复下发
406
+ } else {
407
+ // 与上一轮快照不衔接(常见于工具后快照从新的助手片段重新开始):整段下发
408
+ delta = t
409
+ lastStreamSnapshot = t
410
+ }
405
411
  if (delta.trim()) {
406
412
  const prev = streamChunkIdxBySessionKey.get(dcgSessionKey) ?? 0
407
413
  streamChunkIdxBySessionKey.set(dcgSessionKey, prev + 1)
408
414
  sendChunk(delta, outboundCtx, prev)
409
415
  }
410
- streamedTextLen = payload.text.length
411
416
  } else {
412
- dcgLogger(`onPartialReply no text: ${JSON.stringify(payload)}`, 'error')
417
+ dcgLogger(`onPartialReply no text (media/tool metadata): ${JSON.stringify(payload)}`)
413
418
  }
414
419
  // --- Media from payload ---
415
420
  const mediaList = resolveReplyMediaList(payload)
@@ -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(须改 job 内对象,勿写 newParams.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
- const del = job.delivery as CronDelivery
139
- del.bestEffort = true
140
- del.to = `dcg-cron:${sk}`
141
- del.accountId = agentId
142
- del.channel = "dcgchat-test"
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"
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 = getEffectiveMsgParams(sk)
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
  }
@@ -306,14 +328,27 @@ export function monitoringToolMessage(api: OpenClawPluginApi) {
306
328
  trackSubagentLifecycle(item.event, event, args)
307
329
  const sk = resolveHookSessionKey(item.event, args ?? {})
308
330
  if (sk) {
309
- const status = getMsgStatus(sk)
310
- if (status === 'running') {
331
+ if (isSessionActiveForTool(sk)) {
311
332
  if (['after_tool_call', 'before_tool_call'].includes(item.event)) {
312
333
  const { result: _result, ...rest } = event
313
334
  dcgLogger(`工具调用结果: ~ event:${item.event} ~ params:${JSON.stringify(rest)}`)
314
335
 
315
336
  if (item.event === 'before_tool_call') {
316
- return cronToolCall(rest, sk)
337
+ const hookResult = cronToolCall(rest, sk)
338
+ const text = JSON.stringify({
339
+ type: item.event,
340
+ specialIdentification: 'dcgchat_tool_call_special_identification',
341
+ callId: event.toolCallId || event.runId || Date.now().toString(),
342
+ ...rest,
343
+ status: 'running'
344
+ })
345
+ sendToolCallMessage(
346
+ sk,
347
+ text,
348
+ event.toolCallId || event.runId || Date.now().toString(),
349
+ 0
350
+ )
351
+ return hookResult
317
352
  }
318
353
  const text = JSON.stringify({
319
354
  type: item.event,
@@ -329,7 +364,7 @@ export function monitoringToolMessage(api: OpenClawPluginApi) {
329
364
  item.event === 'after_tool_call' ? 1 : 0
330
365
  )
331
366
  } else if (item.event) {
332
- const msgCtx = getEffectiveMsgParams(sk)
367
+ const msgCtx = resolveOutboundParamsForSession(sk)
333
368
  if (item.event === 'llm_output') {
334
369
  if (event.lastAssistant?.errorMessage === '1003-额度不足请充值') {
335
370
  const message = '您的积分已消耗完,您可以通过充值积分来继续使用'
@@ -346,7 +381,7 @@ export function monitoringToolMessage(api: OpenClawPluginApi) {
346
381
  params: item.message
347
382
  })
348
383
  sendToolCallMessage(sk, text, event.runId || Date.now().toString(), 0)
349
- dcgLogger(`工具调用结果: ~ event:${item.event} ${status}`)
384
+ dcgLogger(`工具调用结果: ~ event:${item.event}`)
350
385
  }
351
386
  }
352
387
  } else if (item.event !== 'before_tool_call') {
@@ -108,18 +108,34 @@ export function clearSentMediaKeys(messageId?: string) {
108
108
  }
109
109
  }
110
110
 
111
- const cronMessageIdMap = new Map<string, string>()
111
+ /** 每个 sessionKey 下多个定时任务 messageId,入队在尾、出队在头(FIFO) */
112
+ const cronMessageIdMap = new Map<string, string[]>()
112
113
 
113
114
  export function setCronMessageId(sk: string, messageId: string) {
114
- cronMessageIdMap.set(sk, messageId)
115
+ if (!sk?.trim() || !messageId?.trim()) return
116
+ let q = cronMessageIdMap.get(sk)
117
+ if (!q) {
118
+ q = []
119
+ cronMessageIdMap.set(sk, q)
120
+ }
121
+ q.push(messageId)
115
122
  }
116
123
 
124
+ /** 窥视队首 messageId(不移除),供发送中与 finished 配对使用 */
117
125
  export function getCronMessageId(sk: string): string {
118
- return cronMessageIdMap.get(sk) ?? ''
126
+ if (!sk?.trim()) return ''
127
+ return cronMessageIdMap.get(sk)?.[0] ?? ''
119
128
  }
120
129
 
130
+ /** 弹出队首一条,与一次 finished 对应;队列为空时移除 key */
121
131
  export function removeCronMessageId(sk: string) {
122
- cronMessageIdMap.delete(sk)
132
+ if (!sk?.trim()) return
133
+ const q = cronMessageIdMap.get(sk)
134
+ if (!q?.length) return
135
+ q.shift()
136
+ if (q.length === 0) {
137
+ cronMessageIdMap.delete(sk)
138
+ }
123
139
  }
124
140
 
125
141
  export const getSessionKey = (content: any, accountId: string) => {