@dcrays/dcgchat 0.2.34 → 0.3.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/index.ts CHANGED
@@ -4,6 +4,7 @@ import { dcgchatPlugin } from './src/channel.js'
4
4
  import { setDcgchatRuntime, setWorkspaceDir } from './src/utils/global.js'
5
5
  import { monitoringToolMessage } from './src/tool.js'
6
6
  import { setOpenClawConfig } from './src/utils/global.js'
7
+ import { startDcgchatGatewaySocket } from './src/gateway/socket.js'
7
8
 
8
9
  const plugin = {
9
10
  id: "dcgchat",
@@ -12,7 +13,6 @@ const plugin = {
12
13
  configSchema: emptyPluginConfigSchema(),
13
14
  register(api: OpenClawPluginApi) {
14
15
  setDcgchatRuntime(api.runtime)
15
-
16
16
  monitoringToolMessage(api)
17
17
  setOpenClawConfig(api.config)
18
18
  api.registerChannel({ plugin: dcgchatPlugin })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dcrays/dcgchat",
3
- "version": "0.2.34",
3
+ "version": "0.3.18",
4
4
  "type": "module",
5
5
  "description": "OpenClaw channel plugin for 书灵墨宝 (WebSocket)",
6
6
  "main": "index.ts",
package/src/bot.ts CHANGED
@@ -3,13 +3,23 @@ import path from 'node:path'
3
3
  import type { ReplyPayload } from 'openclaw/plugin-sdk'
4
4
  import { createReplyPrefixContext } from 'openclaw/plugin-sdk'
5
5
  import type { InboundMessage } from './types.js'
6
- import { clearSentMediaKeys, getDcgchatRuntime, getOpenClawConfig, getWorkspaceDir, setMsgStatus } from './utils/global.js'
6
+ import os from 'node:os'
7
+ import {
8
+ clearSentMediaKeys,
9
+ getDcgchatRuntime,
10
+ getOpenClawConfig,
11
+ getSessionKey,
12
+ getWorkspaceDir,
13
+ setMsgStatus
14
+ } from './utils/global.js'
7
15
  import { resolveAccount, sendDcgchatMedia } from './channel.js'
8
16
  import { generateSignUrl } from './request/api.js'
9
17
  import { extractMobookFiles } from './utils/searchFile.js'
10
- import { createMsgContext, sendChunk, sendFinal, sendText as sendTextMsg, sendError, sendText } from './transport.js'
18
+ import { sendChunk, sendFinal, sendText as sendTextMsg, sendError, wsSendRaw, sendText } from './transport.js'
11
19
  import { dcgLogger } from './utils/log.js'
12
- import { channelInfo, systemCommand, interruptCommand, ENV } from './utils/constant.js'
20
+ import { channelInfo, systemCommand, interruptCommand, ENV, ignoreToolCommand } from './utils/constant.js'
21
+ import { sendMessageToGateway } from './gateway/socket.js'
22
+ import { getEffectiveMsgParams, setParamsMessage } from './utils/params.js'
13
23
 
14
24
  type MediaInfo = {
15
25
  path: string
@@ -22,17 +32,20 @@ type TFileInfo = { name: string; url: string }
22
32
 
23
33
  const mediaMaxBytes = 300 * 1024 * 1024
24
34
 
35
+ /** 当前会话最近一次 agent run 的 runId(供 chat.abort 精确打断;无则仅传 sessionKey 仍会中止该会话全部活动运行) */
36
+ const activeRunIdBySessionKey = new Map<string, string>()
37
+
38
+ /**
39
+ * 用户在该 sessionKey 上触发打断后,旧 run 的流式/投递不再下发;与 sessionKey 一一对应,支持多会话。
40
+ * 清除时机:① 下一条非打断用户消息开始处理时;② 旧 run 收尾到 mobook 段时若仍抑制则跳过并发后删除。
41
+ */
42
+ const sessionStreamSuppressed = new Set<string>()
43
+
44
+ /** 各 sessionKey 当前轮回复的流式分片序号(仅统计实际下发的文本 chunk);每轮非打断消息开始时置 0 */
45
+ const streamChunkIdxBySessionKey = new Map<string, number>()
46
+
25
47
  /** Active LLM generation abort controllers, keyed by conversationId */
26
- const activeGenerations = new Map<string, AbortController>()
27
-
28
- /** Abort an in-progress LLM generation for a given conversationId */
29
- export function abortMobookappGeneration(conversationId: string): void {
30
- const ctrl = activeGenerations.get(conversationId)
31
- if (ctrl) {
32
- ctrl.abort()
33
- activeGenerations.delete(conversationId)
34
- }
35
- }
48
+ // const activeGenerations = new Map<string, AbortController>()
36
49
 
37
50
  /**
38
51
  * Extract agentId from conversation_id formatted as "agentId::suffix".
@@ -124,16 +137,8 @@ function resolveReplyMediaList(payload: ReplyPayload): string[] {
124
137
  * 处理一条用户消息,调用 Agent 并返回回复
125
138
  */
126
139
  export async function handleDcgchatMessage(msg: InboundMessage, accountId: string): Promise<void> {
127
- const msgCtx = createMsgContext(msg)
128
-
129
140
  let finalSent = false
130
141
 
131
- const safeSendFinal = () => {
132
- if (finalSent) return
133
- finalSent = true
134
- sendFinal(msgCtx)
135
- }
136
-
137
142
  let completeText = ''
138
143
  const config = getOpenClawConfig()
139
144
  if (!config) {
@@ -142,43 +147,64 @@ export async function handleDcgchatMessage(msg: InboundMessage, accountId: strin
142
147
  }
143
148
  const account = resolveAccount(config, accountId)
144
149
  const userId = msg._userId.toString()
145
- const text = msg.content.text?.trim()
146
150
 
147
- if (!text) {
148
- sendTextMsg(msgCtx, '你需要我帮你做什么呢?')
149
- safeSendFinal()
150
- return
151
- }
151
+ const core = getDcgchatRuntime()
152
152
 
153
- try {
154
- const core = getDcgchatRuntime()
153
+ const conversationId = msg.content.session_id?.trim()
154
+ const agentId = msg.content.agent_id?.trim()
155
+ const real_mobook = msg.content.real_mobook?.toString().trim()
155
156
 
156
- const conversationId = msg.content.session_id?.trim()
157
+ const route = core.channel.routing.resolveAgentRoute({
158
+ cfg: config,
159
+ channel: "dcgchat",
160
+ accountId: account.accountId,
161
+ peer: { kind: 'direct', id: conversationId }
162
+ })
157
163
 
158
- const route = core.channel.routing.resolveAgentRoute({
159
- cfg: config,
160
- channel: "dcgchat",
161
- accountId: account.accountId,
162
- peer: { kind: 'direct', id: conversationId }
163
- })
164
+ const embeddedAgentId = extractAgentIdFromConversationId(conversationId)
165
+
166
+ const effectiveAgentId = embeddedAgentId ?? route.agentId
167
+ const effectiveSessionKey = getSessionKey(msg.content, account.accountId)
164
168
 
165
- // If conversation_id encodes an agentId prefix ("agentId::suffix"), override the route.
166
- const embeddedAgentId = extractAgentIdFromConversationId(conversationId)
167
- const effectiveAgentId = embeddedAgentId ?? route.agentId
168
- const effectiveSessionKey = embeddedAgentId
169
- ? `agent:${embeddedAgentId}:mobook:direct:${conversationId}`.toLowerCase()
170
- : route.sessionKey
169
+ setParamsMessage(effectiveSessionKey, {
170
+ userId: msg._userId,
171
+ botToken: msg.content.bot_token,
172
+ sessionId: conversationId,
173
+ messageId: msg.content.message_id,
174
+ domainId: msg.content.domain_id,
175
+ appId: msg.content.app_id,
176
+ botId: msg.content.bot_id ?? '',
177
+ agentId: msg.content.agent_id ?? '',
178
+ sessionKey: effectiveSessionKey,
179
+ real_mobook
180
+ })
181
+ const outboundCtx = getEffectiveMsgParams(effectiveSessionKey)
182
+ const agentEntry =
183
+ effectiveAgentId && effectiveAgentId !== 'main' ? config.agents?.list?.find((a) => a.id === effectiveAgentId) : undefined
184
+ const agentDisplayName = agentEntry?.name ?? (effectiveAgentId && effectiveAgentId !== 'main' ? effectiveAgentId : undefined)
171
185
 
172
- const agentEntry =
173
- effectiveAgentId && effectiveAgentId !== 'main' ? config.agents?.list?.find((a) => a.id === effectiveAgentId) : undefined
174
- const agentDisplayName = agentEntry?.name ?? (effectiveAgentId && effectiveAgentId !== 'main' ? effectiveAgentId : undefined)
186
+ const safeSendFinal = (tag: string) => {
187
+ if (finalSent) return
188
+ finalSent = true
189
+ sendFinal(outboundCtx, tag)
190
+ setMsgStatus(effectiveSessionKey, 'finished')
191
+ }
175
192
 
193
+ const text = msg.content.text?.trim()
194
+
195
+ if (!text) {
196
+ sendTextMsg('你需要我帮你做什么呢?', outboundCtx)
197
+ safeSendFinal('not text')
198
+ return
199
+ }
200
+
201
+ try {
176
202
  // Abort any existing generation for this conversation, then start a new one
177
- const existingCtrl = activeGenerations.get(conversationId)
178
- if (existingCtrl) existingCtrl.abort()
179
- const genCtrl = new AbortController()
180
- const genSignal = genCtrl.signal
181
- activeGenerations.set(conversationId, genCtrl)
203
+ // const existingCtrl = activeGenerations.get(conversationId)
204
+ // if (existingCtrl) existingCtrl.abort()
205
+ // const genCtrl = new AbortController()
206
+ // const genSignal = genCtrl.signal
207
+ // activeGenerations.set(conversationId, genCtrl)
182
208
 
183
209
  // 处理用户上传的文件
184
210
  const files = msg.content.files ?? []
@@ -203,7 +229,7 @@ export async function handleDcgchatMessage(msg: InboundMessage, accountId: strin
203
229
  RawBody: text,
204
230
  CommandBody: text,
205
231
  From: userId,
206
- To: conversationId,
232
+ To: effectiveSessionKey,
207
233
  SessionKey: effectiveSessionKey,
208
234
  AccountId: route.accountId,
209
235
  ChatType: 'direct',
@@ -216,7 +242,7 @@ export async function handleDcgchatMessage(msg: InboundMessage, accountId: strin
216
242
  WasMentioned: true,
217
243
  CommandAuthorized: true,
218
244
  OriginatingChannel: "dcgchat",
219
- OriginatingTo: `user:${userId}`,
245
+ OriginatingTo: effectiveSessionKey,
220
246
  ...mediaPayload
221
247
  })
222
248
 
@@ -237,25 +263,35 @@ export async function handleDcgchatMessage(msg: InboundMessage, accountId: strin
237
263
  humanDelay: core.channel.reply.resolveHumanDelayConfig(config, route.agentId),
238
264
  onReplyStart: async () => {},
239
265
  deliver: async (payload: ReplyPayload, info) => {
266
+ if (sessionStreamSuppressed.has(effectiveSessionKey)) return
240
267
  const mediaList = resolveReplyMediaList(payload)
241
268
  for (const mediaUrl of mediaList) {
242
269
  const key = getMediaKey(mediaUrl)
243
270
  if (sentMediaKeys.has(key)) continue
244
271
  sentMediaKeys.add(key)
245
- await sendDcgchatMedia({ msgCtx, mediaUrl, text: '' })
272
+ await sendDcgchatMedia({ sessionKey: effectiveSessionKey, mediaUrl, text: '' })
246
273
  }
247
274
  },
248
275
  onError: (err: unknown, info: { kind: string }) => {
249
- safeSendFinal()
276
+ safeSendFinal('error')
277
+ dcgLogger(`${info.kind} reply failed: ${String(err)}`, 'error')
278
+ activeRunIdBySessionKey.delete(effectiveSessionKey)
279
+ streamChunkIdxBySessionKey.delete(effectiveSessionKey)
280
+ if (sessionStreamSuppressed.has(effectiveSessionKey)) {
281
+ dcgLogger(`${info.kind} reply failed (stream suppressed): ${String(err)}`, 'error')
282
+ return
283
+ }
250
284
  dcgLogger(`${info.kind} reply failed: ${String(err)}`, 'error')
251
285
  },
252
- onIdle: () => {
253
- safeSendFinal()
254
- }
286
+ onIdle: () => {}
255
287
  })
256
288
 
257
- let wasAborted = false
258
289
  try {
290
+ if (!interruptCommand.includes(text?.trim())) {
291
+ sessionStreamSuppressed.delete(effectiveSessionKey)
292
+ streamChunkIdxBySessionKey.set(effectiveSessionKey, 0)
293
+ }
294
+
259
295
  if (systemCommand.includes(text?.trim())) {
260
296
  dcgLogger(`dispatching /new`)
261
297
  await core.channel.reply.dispatchReplyFromConfig({
@@ -264,25 +300,63 @@ export async function handleDcgchatMessage(msg: InboundMessage, accountId: strin
264
300
  dispatcher,
265
301
  replyOptions: {
266
302
  ...replyOptions,
267
- onModelSelected: prefixContext.onModelSelected
303
+ onModelSelected: prefixContext.onModelSelected,
304
+ onAgentRunStart: (runId) => {
305
+ activeRunIdBySessionKey.set(effectiveSessionKey, runId)
306
+ }
268
307
  }
269
308
  })
270
309
  } else if (interruptCommand.includes(text?.trim())) {
271
310
  dcgLogger(`interrupt command: ${text}`)
272
- abortMobookappGeneration(conversationId)
273
- safeSendFinal()
311
+ safeSendFinal('abort')
312
+ sendText('会话已终止', outboundCtx)
313
+ sessionStreamSuppressed.add(effectiveSessionKey)
314
+ const runId = activeRunIdBySessionKey.get(effectiveSessionKey)
315
+ sendMessageToGateway(
316
+ JSON.stringify({
317
+ method: 'chat.abort',
318
+ params: {
319
+ sessionKey: effectiveSessionKey,
320
+ ...(runId ? { runId } : {})
321
+ }
322
+ })
323
+ )
324
+ if (runId) activeRunIdBySessionKey.delete(effectiveSessionKey)
325
+ safeSendFinal('stop')
274
326
  return
275
327
  } else {
276
328
  dcgLogger(`dispatching to agent (session=${route.sessionKey})`)
329
+ const params = getEffectiveMsgParams(effectiveSessionKey)
330
+ if (!ignoreToolCommand.includes(text?.trim())) {
331
+ // message_received 没有 sessionKey 前置到bot中执行
332
+ wsSendRaw(params, {
333
+ is_finish: -1,
334
+ tool_call_id: Date.now().toString(),
335
+ is_cover: 0,
336
+ thinking_content: JSON.stringify({
337
+ type: 'message_received',
338
+ specialIdentification: 'dcgchat_tool_call_special_identification',
339
+ toolName: '',
340
+ callId: Date.now().toString(),
341
+ params: ''
342
+ }),
343
+ response: ''
344
+ })
345
+ }
277
346
  await core.channel.reply.dispatchReplyFromConfig({
278
347
  ctx: ctxPayload,
279
348
  cfg: config,
280
349
  dispatcher,
281
350
  replyOptions: {
282
351
  ...replyOptions,
283
- abortSignal: genSignal,
352
+ // abortSignal: genSignal,
284
353
  onModelSelected: prefixContext.onModelSelected,
354
+ onAgentRunStart: (runId) => {
355
+ activeRunIdBySessionKey.set(effectiveSessionKey, runId)
356
+ },
285
357
  onPartialReply: async (payload: ReplyPayload) => {
358
+ if (sessionStreamSuppressed.has(effectiveSessionKey)) return
359
+
286
360
  // Accumulate full text
287
361
  if (payload.text) {
288
362
  completeText = payload.text
@@ -293,8 +367,10 @@ export async function handleDcgchatMessage(msg: InboundMessage, accountId: strin
293
367
  ? payload.text.slice(streamedTextLen)
294
368
  : payload.text
295
369
  if (delta.trim()) {
296
- sendChunk(msgCtx, delta)
297
- dcgLogger(`[stream]: chunk ${delta.length} chars to user ${msg._userId} ${delta.slice(0, 100)}`)
370
+ const prev = streamChunkIdxBySessionKey.get(effectiveSessionKey) ?? 0
371
+ streamChunkIdxBySessionKey.set(effectiveSessionKey, prev + 1)
372
+ sendChunk(delta, outboundCtx, prev)
373
+ dcgLogger(`[stream]: chunkIdx=${prev} len=${delta.length} user=${msg._userId} ${delta.slice(0, 100)}`)
298
374
  }
299
375
  streamedTextLen = payload.text.length
300
376
  }
@@ -304,26 +380,26 @@ export async function handleDcgchatMessage(msg: InboundMessage, accountId: strin
304
380
  const key = getMediaKey(mediaUrl)
305
381
  if (sentMediaKeys.has(key)) continue
306
382
  sentMediaKeys.add(key)
307
- await sendDcgchatMedia({ msgCtx, mediaUrl, text: '' })
383
+ await sendDcgchatMedia({ sessionKey: effectiveSessionKey, mediaUrl, text: '' })
308
384
  }
309
385
  }
310
386
  }
311
387
  })
312
388
  }
313
389
  } catch (err: unknown) {
314
- if (genSignal.aborted) {
315
- wasAborted = true
316
- dcgLogger(` generation aborted for conversationId=${conversationId}`)
317
- } else if (err instanceof Error && err.name === 'AbortError') {
318
- wasAborted = true
319
- dcgLogger(` generation aborted for conversationId=${conversationId}`)
320
- } else {
321
- dcgLogger(` dispatchReplyFromConfig error: ${String(err)}`, 'error')
322
- }
390
+ // if (genSignal.aborted) {
391
+ // wasAborted = true
392
+ // dcgLogger(` generation aborted for conversationId=${conversationId}`)
393
+ // } else if (err instanceof Error && err.name === 'AbortError') {
394
+ // wasAborted = true
395
+ // dcgLogger(` generation aborted for conversationId=${conversationId}`)
396
+ // } else {
397
+ // dcgLogger(` dispatchReplyFromConfig error: ${String(err)}`, 'error')
398
+ // }
323
399
  } finally {
324
- if (activeGenerations.get(conversationId) === genCtrl) {
325
- activeGenerations.delete(conversationId)
326
- }
400
+ // if (activeGenerations.get(conversationId) === genCtrl) {
401
+ // activeGenerations.delete(conversationId)
402
+ // }
327
403
  }
328
404
  try {
329
405
  markRunComplete()
@@ -332,28 +408,33 @@ export async function handleDcgchatMessage(msg: InboundMessage, accountId: strin
332
408
  dcgLogger(` markRunComplete||markRunComplete error: ${String(err)}`, 'error')
333
409
  }
334
410
  if (![...systemCommand, ...interruptCommand].includes(text?.trim())) {
335
- for (const file of extractMobookFiles(completeText)) {
336
- const candidates: string[] = [file]
337
- candidates.push(path.join(getWorkspaceDir(), file))
338
- candidates.push(path.join(getWorkspaceDir(), file.replace(/^\//, '')))
339
- if (process.platform === 'win32') {
411
+ if (sessionStreamSuppressed.has(effectiveSessionKey)) {
412
+ sessionStreamSuppressed.delete(effectiveSessionKey)
413
+ } else {
414
+ for (const file of extractMobookFiles(completeText)) {
415
+ const candidates: string[] = [file]
416
+ candidates.push(path.join(getWorkspaceDir(), file))
417
+ candidates.push(path.join(getWorkspaceDir(), file.replace(/^\//, '')))
340
418
  const underMobook = file.replace(/^\/mobook\//i, '').replace(/^mobook[\\/]/i, '')
341
419
  if (underMobook) {
342
- candidates.push(path.join('C:\\', 'mobook', underMobook))
420
+ if (process.platform === 'win32') {
421
+ candidates.push(path.join('C:\\', 'mobook', underMobook))
422
+ } else if (process.platform === 'darwin') {
423
+ candidates.push(path.join(os.homedir(), 'mobook', underMobook))
424
+ }
425
+ }
426
+ const resolved = candidates.find((p) => fs.existsSync(p))
427
+ if (!resolved) continue
428
+ try {
429
+ await sendDcgchatMedia({ sessionKey: effectiveSessionKey, mediaUrl: resolved, text: '' })
430
+ } catch (err) {
431
+ dcgLogger(` sendDcgchatMedia error: ${String(err)}`, 'error')
343
432
  }
344
- }
345
- const resolved = candidates.find((p) => fs.existsSync(p))
346
- if (!resolved) continue
347
- try {
348
- await sendDcgchatMedia({ msgCtx, mediaUrl: resolved, text: '' })
349
- } catch (err) {
350
- dcgLogger(` sendDcgchatMedia error: ${String(err)}`, 'error')
351
433
  }
352
434
  }
353
435
  }
354
- safeSendFinal()
436
+ safeSendFinal('end')
355
437
  clearSentMediaKeys(msg.content.message_id)
356
- setMsgStatus('finished')
357
438
 
358
439
  // Record session metadata
359
440
  const storePath = core.channel.session.resolveStorePath(config.session?.store)
@@ -371,8 +452,8 @@ export async function handleDcgchatMessage(msg: InboundMessage, accountId: strin
371
452
  })
372
453
  } catch (err) {
373
454
  dcgLogger(` handle message failed: ${String(err)}`, 'error')
374
- sendError(msgCtx, err instanceof Error ? err.message : String(err))
455
+ sendError(err instanceof Error ? err.message : String(err), outboundCtx)
375
456
  } finally {
376
- safeSendFinal()
457
+ safeSendFinal('finally')
377
458
  }
378
459
  }
package/src/channel.ts CHANGED
@@ -2,36 +2,41 @@ import type { ChannelPlugin, OpenClawConfig } from 'openclaw/plugin-sdk'
2
2
  import { DEFAULT_ACCOUNT_ID } from 'openclaw/plugin-sdk'
3
3
  import type { ResolvedDcgchatAccount, DcgchatConfig } from './types.js'
4
4
  import { ossUpload } from './request/oss.js'
5
- import { addSentMediaKey, getMsgParams, hasSentMediaKey } from './utils/global.js'
6
- import { type DcgchatMsgContext, isWsOpen, sendFinal, wsSendRaw } from './transport.js'
5
+ import { addSentMediaKey, getCronMessageId, getOpenClawConfig, hasSentMediaKey } from './utils/global.js'
6
+ import { isWsOpen, mergeDefaultParams, mergeSessionParams, sendFinal, wsSendRaw } from './transport.js'
7
7
  import { dcgLogger, setLogger } from './utils/log.js'
8
+ import { getEffectiveMsgParams, getCurrentSessionKey } from './utils/params.js'
9
+ import { startDcgchatGatewaySocket } from './gateway/socket.js'
8
10
 
9
11
  export type DcgchatMediaSendOptions = {
10
- msgCtx: DcgchatMsgContext
12
+ /** 与 setParamsMessage / map 一致,用于 getEffectiveMsgParams */
13
+ sessionKey: string
11
14
  mediaUrl?: string
12
15
  text?: string
13
16
  }
14
17
 
15
18
  export async function sendDcgchatMedia(opts: DcgchatMediaSendOptions): Promise<void> {
16
- const { msgCtx } = opts
19
+ const msgCtx = getEffectiveMsgParams(opts.sessionKey)
17
20
  if (!isWsOpen()) {
18
21
  dcgLogger(`outbound media skipped -> ws not open: ${opts.mediaUrl ?? ''}`)
19
22
  return
20
23
  }
21
24
 
22
25
  const mediaUrl = opts.mediaUrl
23
- if (mediaUrl && hasSentMediaKey(msgCtx.messageId, mediaUrl)) {
26
+ const dedupeId = msgCtx.messageId
27
+ if (mediaUrl && dedupeId && hasSentMediaKey(dedupeId, mediaUrl)) {
24
28
  dcgLogger(`dcgchat: sendMedia skipped (duplicate in session): ${mediaUrl}`)
25
29
  return
26
30
  }
27
- if (mediaUrl) {
28
- addSentMediaKey(msgCtx.messageId, mediaUrl)
31
+ if (mediaUrl && dedupeId) {
32
+ addSentMediaKey(dedupeId, mediaUrl)
29
33
  }
30
34
 
31
35
  const fileName = mediaUrl?.split(/[\\/]/).pop() || ''
32
36
 
33
37
  try {
34
- const url = opts.mediaUrl ? await ossUpload(opts.mediaUrl, msgCtx.botToken) : ''
38
+ const botToken = msgCtx.botToken ?? getOpenClawConfig()?.channels?.["dcgchat"]?.botToken ?? ''
39
+ const url = opts.mediaUrl ? await ossUpload(opts.mediaUrl, botToken, 1) : ''
35
40
  wsSendRaw(msgCtx, {
36
41
  response: opts.text ?? '',
37
42
  files: [{ url, name: fileName }]
@@ -61,22 +66,6 @@ export function resolveAccount(cfg: OpenClawConfig, accountId?: string | null):
61
66
  }
62
67
  }
63
68
 
64
- /** Build a DcgchatMsgContext for the outbound pipeline (uses global msgParams). */
65
- function createOutboundMsgContext(cfg: OpenClawConfig, accountId?: string | null): DcgchatMsgContext {
66
- const params = getMsgParams()
67
- const { botToken } = resolveAccount(cfg, accountId)
68
- return {
69
- userId: params.userId,
70
- botToken,
71
- domainId: params.domainId,
72
- appId: params.appId,
73
- botId: params.botId,
74
- agentId: params.agentId,
75
- sessionId: params.sessionId,
76
- messageId: params.messageId
77
- }
78
- }
79
-
80
69
  export const dcgchatPlugin: ChannelPlugin<ResolvedDcgchatAccount> = {
81
70
  id: "dcgchat",
82
71
  meta: {
@@ -153,24 +142,55 @@ export const dcgchatPlugin: ChannelPlugin<ResolvedDcgchatAccount> = {
153
142
  deliveryMode: 'direct',
154
143
  textChunkLimit: 4000,
155
144
  sendText: async (ctx) => {
156
- const msgCtx = createOutboundMsgContext(ctx.cfg, ctx.accountId)
157
145
  if (isWsOpen()) {
158
- wsSendRaw(msgCtx, { response: ctx.text })
159
- dcgLogger(`channel sendText to ${msgCtx.userId} ${ctx.text?.slice(0, 50)}`)
146
+ // if (ctx.to.indexOf('cron:') >= 0) {
147
+ // const sessionInfo = ctx.to.split(':')[1]
148
+ // const sessionId = sessionInfo.split('-')[0]
149
+ // const agentId = sessionInfo.split('-')[1]
150
+ // const merged = mergeDefaultParams({
151
+ // agentId: agentId,
152
+ // sessionId: sessionId,
153
+ // messageId: `${Date.now()}`,
154
+ // is_finish: -1
155
+ // })
156
+ // wsSendRaw(merged, { response: ctx.text })
157
+ // } else {
158
+ // }
159
+ const outboundCtx = getEffectiveMsgParams(ctx.to)
160
+ const messageId = getCronMessageId(ctx.to)
161
+ if (outboundCtx?.sessionId) {
162
+ const newCtx = messageId ? { ...outboundCtx, messageId } : outboundCtx
163
+ wsSendRaw(newCtx, { response: ctx.text, is_finish: -1, message_tags: { source: 'channel' } })
164
+ dcgLogger(`channel sendText to ${ctx.to} ${ctx.text?.slice(0, 50)}`)
165
+ } else {
166
+ const sessionInfo = ctx.to.split(':')
167
+ const sessionId = sessionInfo.at(-1) ?? ''
168
+ const agentId = sessionInfo.at(-2) ?? ''
169
+ const merged = mergeDefaultParams({
170
+ agentId: agentId,
171
+ sessionId: `${sessionId}`,
172
+ messageId: messageId,
173
+ is_finish: -1,
174
+ real_mobook: !sessionId ? 1 : ''
175
+ })
176
+ dcgLogger(`channel sendText to ${ctx.to} ${ctx.text?.slice(0, 50)}`)
177
+ wsSendRaw(merged, { response: ctx.text })
178
+ }
160
179
  }
161
180
  return {
162
181
  channel: "dcgchat",
163
182
  messageId: `dcg-${Date.now()}`,
164
- chatId: msgCtx.userId.toString()
183
+ chatId: ctx.to
165
184
  }
166
185
  },
167
186
  sendMedia: async (ctx) => {
168
- const msgCtx = createOutboundMsgContext(ctx.cfg, ctx.accountId)
169
- await sendDcgchatMedia({ msgCtx, mediaUrl: ctx.mediaUrl })
187
+ const msgCtx = getEffectiveMsgParams(ctx.to)
188
+ dcgLogger(`channel sendMedia to ${ctx.to} ${ctx.mediaUrl?.slice(0, 50)}`)
189
+ await sendDcgchatMedia({ sessionKey: ctx.to ?? '', mediaUrl: ctx.mediaUrl ?? '' })
170
190
  return {
171
191
  channel: "dcgchat",
172
192
  messageId: `dcg-${Date.now()}`,
173
- chatId: msgCtx.userId.toString()
193
+ chatId: msgCtx.userId?.toString()
174
194
  }
175
195
  }
176
196
  },
@@ -183,6 +203,7 @@ export const dcgchatPlugin: ChannelPlugin<ResolvedDcgchatAccount> = {
183
203
  dcgLogger(`dcgchat[${account.accountId}]: wsUrl not configured, skipping`, 'error')
184
204
  return
185
205
  }
206
+ startDcgchatGatewaySocket()
186
207
  return monitorDcgchatProvider({
187
208
  config: ctx.cfg,
188
209
  runtime: ctx.runtime,