@dcrays/dcgchat-test 0.4.18 → 0.4.19

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.19",
4
4
  "type": "module",
5
5
  "description": "OpenClaw channel plugin for 书灵墨宝 (WebSocket)",
6
6
  "main": "index.ts",
package/src/bot.ts CHANGED
@@ -174,7 +174,7 @@ export async function handleDcgchatMessage(msg: InboundMessage, accountId: strin
174
174
  sessionId: conversationId,
175
175
  messageId: msg.content.message_id,
176
176
  domainId: msg.content.domain_id,
177
- appId: msg.content.app_id,
177
+ appId: config.channels?.["dcgchat-test"]?.appId || 100,
178
178
  botId: msg.content.bot_id ?? '',
179
179
  agentId: msg.content.agent_id ?? '',
180
180
  sessionKey: dcgSessionKey,
package/src/channel.ts CHANGED
@@ -107,18 +107,27 @@ 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
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)
119
122
  }
120
- if (mediaUrl && dedupeId) {
121
- addSentMediaKey(dedupeId, mediaUrl)
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
122
131
  }
123
132
 
124
133
  const fileName = mediaUrl?.split(/[\\/]/).pop() || ''
@@ -126,20 +135,19 @@ export async function sendDcgchatMedia(opts: DcgchatMediaSendOptions): Promise<v
126
135
  try {
127
136
  const botToken = msgCtx.botToken ?? getOpenClawConfig()?.channels?.["dcgchat-test"]?.botToken ?? ''
128
137
  const url = opts.mediaUrl ? await ossUpload(opts.mediaUrl, botToken, 1) : ''
129
- if (!msgCtx.sessionId) {
130
- const { sessionId, agentId } = getInfoBySessionKey(sessionKey)
131
- msgCtx.sessionId = sessionId
138
+ if (!msgCtx.agentId) {
132
139
  msgCtx.agentId = agentId
133
140
  }
134
141
  wsSendRaw(msgCtx, {
135
142
  response: opts.text ?? '',
143
+ is_finish: `${msgCtx?.messageId}`?.length === 13 ? -1 : 0,
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
+ is_finish: `${msgCtx?.messageId}`?.length === 13 ? -1 : 0,
143
151
  files: [{ url: opts.mediaUrl ?? '', name: fileName }]
144
152
  })
145
153
  dcgLogger(`dcgchat: error sendMedia session=${sessionKey}: ${String(error)}`, 'error')
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)
@@ -135,10 +135,11 @@ function injectBestEffort(params: Record<string, unknown>, sk: string): Record<s
135
135
  // job.delivery
136
136
  const job = newParams.job as Record<string, unknown> | undefined
137
137
  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"
138
+ const jd = job.delivery as CronDelivery
139
+ jd.bestEffort = true
140
+ jd.to = `dcg-cron:${sk}`
141
+ jd.accountId = agentId
142
+ jd.channel = "dcgchat-test"
142
143
  newParams.sessionKey = sk
143
144
  return newParams
144
145
  }
@@ -154,14 +155,14 @@ export function cronToolCall(event: { toolName: any; params: any; toolCallId: an
154
155
  const delivery = extractDelivery(params)
155
156
  if (!delivery) {
156
157
  dcgLogger(`[${LOG_TAG}] cron call (${toolCallId}) has no delivery config, skip.`)
157
- return params
158
+ return undefined
158
159
  }
159
160
  if (!needsBestEffort(delivery)) {
160
161
  dcgLogger(
161
162
  `[${LOG_TAG}] cron call (${toolCallId}) delivery does not need bestEffort ` +
162
163
  `(mode=${String(delivery.mode)}, channel=${String(delivery.channel)}, bestEffort=${String(delivery.bestEffort)}), skip.`
163
164
  )
164
- return params
165
+ return undefined
165
166
  }
166
167
 
167
168
  // ★ 核心:注入 bestEffort: true
@@ -179,9 +180,9 @@ export function cronToolCall(event: { toolName: any; params: any; toolCallId: an
179
180
  params.command.replace('--json', '') + ` --session-key ${sk} --channel ${"dcgchat-test"} --to dcg-cron:${sk} --json`
180
181
  return { params: newParams }
181
182
  } else {
182
- return params
183
+ return undefined
183
184
  }
184
185
  }
185
186
 
186
- return params
187
+ return undefined
187
188
  }
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
 
@@ -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
  }