@dcrays/dcgchat 0.2.34 → 0.3.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/index.ts +1 -1
- package/package.json +1 -1
- package/src/bot.ts +175 -94
- package/src/channel.ts +50 -33
- package/src/cron.ts +174 -0
- package/src/cronToolCall.ts +187 -0
- package/src/gateway/index.ts +466 -0
- package/src/gateway/security.ts +95 -0
- package/src/gateway/socket.ts +283 -0
- package/src/monitor.ts +48 -44
- package/src/request/api.ts +4 -18
- package/src/request/oss.ts +5 -4
- package/src/request/request.ts +2 -2
- package/src/skill.ts +2 -0
- package/src/tool.ts +72 -69
- package/src/transport.ts +143 -49
- package/src/types.ts +14 -8
- package/src/utils/constant.ts +2 -2
- package/src/utils/global.ts +41 -13
- package/src/utils/params.ts +67 -0
- package/src/utils/searchFile.ts +18 -4
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
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
|
|
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 {
|
|
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
|
-
|
|
148
|
-
sendTextMsg(msgCtx, '你需要我帮你做什么呢?')
|
|
149
|
-
safeSendFinal()
|
|
150
|
-
return
|
|
151
|
-
}
|
|
151
|
+
const core = getDcgchatRuntime()
|
|
152
152
|
|
|
153
|
-
|
|
154
|
-
|
|
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
|
-
|
|
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
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
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
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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:
|
|
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:
|
|
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({
|
|
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
|
-
|
|
273
|
-
|
|
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
|
-
|
|
297
|
-
|
|
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({
|
|
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
|
-
|
|
316
|
-
|
|
317
|
-
} else if (err instanceof Error && err.name === 'AbortError') {
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
} else {
|
|
321
|
-
|
|
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
|
-
|
|
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
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
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
|
-
|
|
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(
|
|
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,44 +2,51 @@ 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,
|
|
6
|
-
import {
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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(
|
|
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
|
|
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 ?? '',
|
|
42
|
+
message_tags: { source: 'file' },
|
|
37
43
|
files: [{ url, name: fileName }]
|
|
38
44
|
})
|
|
39
45
|
dcgLogger(`dcgchat: sendMedia to user ${msgCtx.userId}, file=${fileName}`)
|
|
40
46
|
} catch (error) {
|
|
41
47
|
wsSendRaw(msgCtx, {
|
|
42
48
|
response: opts.text ?? '',
|
|
49
|
+
message_tags: { source: 'file' },
|
|
43
50
|
files: [{ url: opts.mediaUrl ?? '', name: fileName }]
|
|
44
51
|
})
|
|
45
52
|
dcgLogger(`dcgchat: error sendMedia to user ${msgCtx.userId}: ${String(error)}`, 'error')
|
|
@@ -61,22 +68,6 @@ export function resolveAccount(cfg: OpenClawConfig, accountId?: string | null):
|
|
|
61
68
|
}
|
|
62
69
|
}
|
|
63
70
|
|
|
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
71
|
export const dcgchatPlugin: ChannelPlugin<ResolvedDcgchatAccount> = {
|
|
81
72
|
id: "dcgchat",
|
|
82
73
|
meta: {
|
|
@@ -153,24 +144,49 @@ export const dcgchatPlugin: ChannelPlugin<ResolvedDcgchatAccount> = {
|
|
|
153
144
|
deliveryMode: 'direct',
|
|
154
145
|
textChunkLimit: 4000,
|
|
155
146
|
sendText: async (ctx) => {
|
|
156
|
-
const
|
|
147
|
+
const isCron = ctx.to.indexOf('dcg-cron:') >= 0
|
|
148
|
+
const to = ctx.to.replace('dcg-cron:', '')
|
|
149
|
+
dcgLogger(`channel sendText to ${ctx.to} `)
|
|
150
|
+
const outboundCtx = getEffectiveMsgParams(to)
|
|
151
|
+
const cronMsgId = getCronMessageId(to)
|
|
152
|
+
const messageId = !!cronMsgId ? cronMsgId : isCron ? `${Date.now()}` : outboundCtx?.messageId
|
|
157
153
|
if (isWsOpen()) {
|
|
158
|
-
|
|
159
|
-
|
|
154
|
+
if (outboundCtx?.sessionId) {
|
|
155
|
+
const newCtx = { ...outboundCtx, messageId }
|
|
156
|
+
wsSendRaw(newCtx, { response: ctx.text, is_finish: -1, message_tags: { source: 'channel' } })
|
|
157
|
+
} else {
|
|
158
|
+
const sessionInfo = to.split(':')
|
|
159
|
+
const sessionId = sessionInfo.at(-1) ?? ''
|
|
160
|
+
const agentId = sessionInfo.at(-2) ?? ''
|
|
161
|
+
const merged = mergeDefaultParams({
|
|
162
|
+
agentId: agentId,
|
|
163
|
+
sessionId: `${sessionId}`,
|
|
164
|
+
messageId: messageId,
|
|
165
|
+
is_finish: -1,
|
|
166
|
+
message_tags: { source: 'channel' },
|
|
167
|
+
real_mobook: !sessionId ? 1 : ''
|
|
168
|
+
})
|
|
169
|
+
wsSendRaw(merged, { response: ctx.text })
|
|
170
|
+
}
|
|
160
171
|
}
|
|
161
172
|
return {
|
|
162
173
|
channel: "dcgchat",
|
|
163
|
-
messageId:
|
|
164
|
-
chatId:
|
|
174
|
+
messageId: `${messageId}`,
|
|
175
|
+
chatId: to
|
|
165
176
|
}
|
|
166
177
|
},
|
|
167
178
|
sendMedia: async (ctx) => {
|
|
168
|
-
const
|
|
169
|
-
|
|
179
|
+
const to = ctx.to.replace('dcg-cron:', '')
|
|
180
|
+
const msgCtx = getEffectiveMsgParams(to)
|
|
181
|
+
const cronMsgId = getCronMessageId(to)
|
|
182
|
+
const isCron = ctx.to.indexOf('dcg-cron:') >= 0
|
|
183
|
+
const messageId = !!cronMsgId ? cronMsgId : isCron ? `${Date.now()}` : msgCtx?.messageId
|
|
184
|
+
dcgLogger(`channel sendMedia to ${ctx.to}`)
|
|
185
|
+
await sendDcgchatMedia({ sessionKey: to ?? '', mediaUrl: ctx.mediaUrl ?? '' })
|
|
170
186
|
return {
|
|
171
187
|
channel: "dcgchat",
|
|
172
|
-
messageId:
|
|
173
|
-
chatId: msgCtx.userId
|
|
188
|
+
messageId: `${messageId}`,
|
|
189
|
+
chatId: msgCtx.userId?.toString()
|
|
174
190
|
}
|
|
175
191
|
}
|
|
176
192
|
},
|
|
@@ -183,6 +199,7 @@ export const dcgchatPlugin: ChannelPlugin<ResolvedDcgchatAccount> = {
|
|
|
183
199
|
dcgLogger(`dcgchat[${account.accountId}]: wsUrl not configured, skipping`, 'error')
|
|
184
200
|
return
|
|
185
201
|
}
|
|
202
|
+
startDcgchatGatewaySocket()
|
|
186
203
|
return monitorDcgchatProvider({
|
|
187
204
|
config: ctx.cfg,
|
|
188
205
|
runtime: ctx.runtime,
|