@dcrays/dcgchat 0.2.32 → 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 +1 -0
- package/package.json +3 -3
- package/src/bot.ts +175 -80
- package/src/channel.ts +52 -31
- 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 +66 -46
- 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 +142 -49
- package/src/types.ts +13 -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 +21 -5
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",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dcrays/dcgchat",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.18",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "OpenClaw channel plugin for 书灵墨宝 (WebSocket)",
|
|
6
6
|
"main": "index.ts",
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
],
|
|
13
13
|
"keywords": [
|
|
14
14
|
"openclaw",
|
|
15
|
-
"dcgchat",
|
|
15
|
+
"dcgchat-test",
|
|
16
16
|
"websocket",
|
|
17
17
|
"ai"
|
|
18
18
|
],
|
|
@@ -42,4 +42,4 @@
|
|
|
42
42
|
"defaultChoice": "npm"
|
|
43
43
|
}
|
|
44
44
|
}
|
|
45
|
-
}
|
|
45
|
+
}
|
package/src/bot.ts
CHANGED
|
@@ -3,20 +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 os from 'node:os'
|
|
6
7
|
import {
|
|
7
8
|
clearSentMediaKeys,
|
|
8
9
|
getDcgchatRuntime,
|
|
9
10
|
getOpenClawConfig,
|
|
11
|
+
getSessionKey,
|
|
10
12
|
getWorkspaceDir,
|
|
11
|
-
getWsConnection,
|
|
12
13
|
setMsgStatus
|
|
13
14
|
} from './utils/global.js'
|
|
14
15
|
import { resolveAccount, sendDcgchatMedia } from './channel.js'
|
|
15
16
|
import { generateSignUrl } from './request/api.js'
|
|
16
17
|
import { extractMobookFiles } from './utils/searchFile.js'
|
|
17
|
-
import {
|
|
18
|
+
import { sendChunk, sendFinal, sendText as sendTextMsg, sendError, wsSendRaw, sendText } from './transport.js'
|
|
18
19
|
import { dcgLogger } from './utils/log.js'
|
|
19
|
-
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'
|
|
20
23
|
|
|
21
24
|
type MediaInfo = {
|
|
22
25
|
path: string
|
|
@@ -29,17 +32,20 @@ type TFileInfo = { name: string; url: string }
|
|
|
29
32
|
|
|
30
33
|
const mediaMaxBytes = 300 * 1024 * 1024
|
|
31
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
|
+
|
|
32
47
|
/** Active LLM generation abort controllers, keyed by conversationId */
|
|
33
|
-
const activeGenerations = new Map<string, AbortController>()
|
|
34
|
-
|
|
35
|
-
/** Abort an in-progress LLM generation for a given conversationId */
|
|
36
|
-
export function abortMobookappGeneration(conversationId: string): void {
|
|
37
|
-
const ctrl = activeGenerations.get(conversationId)
|
|
38
|
-
if (ctrl) {
|
|
39
|
-
ctrl.abort()
|
|
40
|
-
activeGenerations.delete(conversationId)
|
|
41
|
-
}
|
|
42
|
-
}
|
|
48
|
+
// const activeGenerations = new Map<string, AbortController>()
|
|
43
49
|
|
|
44
50
|
/**
|
|
45
51
|
* Extract agentId from conversation_id formatted as "agentId::suffix".
|
|
@@ -131,7 +137,7 @@ function resolveReplyMediaList(payload: ReplyPayload): string[] {
|
|
|
131
137
|
* 处理一条用户消息,调用 Agent 并返回回复
|
|
132
138
|
*/
|
|
133
139
|
export async function handleDcgchatMessage(msg: InboundMessage, accountId: string): Promise<void> {
|
|
134
|
-
|
|
140
|
+
let finalSent = false
|
|
135
141
|
|
|
136
142
|
let completeText = ''
|
|
137
143
|
const config = getOpenClawConfig()
|
|
@@ -141,43 +147,64 @@ export async function handleDcgchatMessage(msg: InboundMessage, accountId: strin
|
|
|
141
147
|
}
|
|
142
148
|
const account = resolveAccount(config, accountId)
|
|
143
149
|
const userId = msg._userId.toString()
|
|
144
|
-
const text = msg.content.text?.trim()
|
|
145
150
|
|
|
146
|
-
|
|
147
|
-
sendTextMsg(msgCtx, '你需要我帮你做什么呢?')
|
|
148
|
-
sendFinal(msgCtx)
|
|
149
|
-
return
|
|
150
|
-
}
|
|
151
|
+
const core = getDcgchatRuntime()
|
|
151
152
|
|
|
152
|
-
|
|
153
|
-
|
|
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()
|
|
154
156
|
|
|
155
|
-
|
|
157
|
+
const route = core.channel.routing.resolveAgentRoute({
|
|
158
|
+
cfg: config,
|
|
159
|
+
channel: "dcgchat",
|
|
160
|
+
accountId: account.accountId,
|
|
161
|
+
peer: { kind: 'direct', id: conversationId }
|
|
162
|
+
})
|
|
156
163
|
|
|
157
|
-
|
|
158
|
-
cfg: config,
|
|
159
|
-
channel: "dcgchat",
|
|
160
|
-
accountId: account.accountId,
|
|
161
|
-
peer: { kind: 'direct', id: conversationId }
|
|
162
|
-
})
|
|
164
|
+
const embeddedAgentId = extractAgentIdFromConversationId(conversationId)
|
|
163
165
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
166
|
+
const effectiveAgentId = embeddedAgentId ?? route.agentId
|
|
167
|
+
const effectiveSessionKey = getSessionKey(msg.content, account.accountId)
|
|
168
|
+
|
|
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)
|
|
185
|
+
|
|
186
|
+
const safeSendFinal = (tag: string) => {
|
|
187
|
+
if (finalSent) return
|
|
188
|
+
finalSent = true
|
|
189
|
+
sendFinal(outboundCtx, tag)
|
|
190
|
+
setMsgStatus(effectiveSessionKey, 'finished')
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const text = msg.content.text?.trim()
|
|
170
194
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
195
|
+
if (!text) {
|
|
196
|
+
sendTextMsg('你需要我帮你做什么呢?', outboundCtx)
|
|
197
|
+
safeSendFinal('not text')
|
|
198
|
+
return
|
|
199
|
+
}
|
|
174
200
|
|
|
201
|
+
try {
|
|
175
202
|
// Abort any existing generation for this conversation, then start a new one
|
|
176
|
-
const existingCtrl = activeGenerations.get(conversationId)
|
|
177
|
-
if (existingCtrl) existingCtrl.abort()
|
|
178
|
-
const genCtrl = new AbortController()
|
|
179
|
-
const genSignal = genCtrl.signal
|
|
180
|
-
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)
|
|
181
208
|
|
|
182
209
|
// 处理用户上传的文件
|
|
183
210
|
const files = msg.content.files ?? []
|
|
@@ -202,7 +229,7 @@ export async function handleDcgchatMessage(msg: InboundMessage, accountId: strin
|
|
|
202
229
|
RawBody: text,
|
|
203
230
|
CommandBody: text,
|
|
204
231
|
From: userId,
|
|
205
|
-
To:
|
|
232
|
+
To: effectiveSessionKey,
|
|
206
233
|
SessionKey: effectiveSessionKey,
|
|
207
234
|
AccountId: route.accountId,
|
|
208
235
|
ChatType: 'direct',
|
|
@@ -215,7 +242,7 @@ export async function handleDcgchatMessage(msg: InboundMessage, accountId: strin
|
|
|
215
242
|
WasMentioned: true,
|
|
216
243
|
CommandAuthorized: true,
|
|
217
244
|
OriginatingChannel: "dcgchat",
|
|
218
|
-
OriginatingTo:
|
|
245
|
+
OriginatingTo: effectiveSessionKey,
|
|
219
246
|
...mediaPayload
|
|
220
247
|
})
|
|
221
248
|
|
|
@@ -236,24 +263,35 @@ export async function handleDcgchatMessage(msg: InboundMessage, accountId: strin
|
|
|
236
263
|
humanDelay: core.channel.reply.resolveHumanDelayConfig(config, route.agentId),
|
|
237
264
|
onReplyStart: async () => {},
|
|
238
265
|
deliver: async (payload: ReplyPayload, info) => {
|
|
266
|
+
if (sessionStreamSuppressed.has(effectiveSessionKey)) return
|
|
239
267
|
const mediaList = resolveReplyMediaList(payload)
|
|
240
268
|
for (const mediaUrl of mediaList) {
|
|
241
269
|
const key = getMediaKey(mediaUrl)
|
|
242
270
|
if (sentMediaKeys.has(key)) continue
|
|
243
271
|
sentMediaKeys.add(key)
|
|
244
|
-
await sendDcgchatMedia({
|
|
272
|
+
await sendDcgchatMedia({ sessionKey: effectiveSessionKey, mediaUrl, text: '' })
|
|
245
273
|
}
|
|
246
274
|
},
|
|
247
275
|
onError: (err: unknown, info: { kind: string }) => {
|
|
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
|
+
}
|
|
248
284
|
dcgLogger(`${info.kind} reply failed: ${String(err)}`, 'error')
|
|
249
285
|
},
|
|
250
|
-
onIdle: () => {
|
|
251
|
-
sendFinal(msgCtx)
|
|
252
|
-
}
|
|
286
|
+
onIdle: () => {}
|
|
253
287
|
})
|
|
254
288
|
|
|
255
|
-
let wasAborted = false
|
|
256
289
|
try {
|
|
290
|
+
if (!interruptCommand.includes(text?.trim())) {
|
|
291
|
+
sessionStreamSuppressed.delete(effectiveSessionKey)
|
|
292
|
+
streamChunkIdxBySessionKey.set(effectiveSessionKey, 0)
|
|
293
|
+
}
|
|
294
|
+
|
|
257
295
|
if (systemCommand.includes(text?.trim())) {
|
|
258
296
|
dcgLogger(`dispatching /new`)
|
|
259
297
|
await core.channel.reply.dispatchReplyFromConfig({
|
|
@@ -262,25 +300,63 @@ export async function handleDcgchatMessage(msg: InboundMessage, accountId: strin
|
|
|
262
300
|
dispatcher,
|
|
263
301
|
replyOptions: {
|
|
264
302
|
...replyOptions,
|
|
265
|
-
onModelSelected: prefixContext.onModelSelected
|
|
303
|
+
onModelSelected: prefixContext.onModelSelected,
|
|
304
|
+
onAgentRunStart: (runId) => {
|
|
305
|
+
activeRunIdBySessionKey.set(effectiveSessionKey, runId)
|
|
306
|
+
}
|
|
266
307
|
}
|
|
267
308
|
})
|
|
268
309
|
} else if (interruptCommand.includes(text?.trim())) {
|
|
269
310
|
dcgLogger(`interrupt command: ${text}`)
|
|
270
|
-
|
|
271
|
-
|
|
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')
|
|
272
326
|
return
|
|
273
327
|
} else {
|
|
274
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
|
+
}
|
|
275
346
|
await core.channel.reply.dispatchReplyFromConfig({
|
|
276
347
|
ctx: ctxPayload,
|
|
277
348
|
cfg: config,
|
|
278
349
|
dispatcher,
|
|
279
350
|
replyOptions: {
|
|
280
351
|
...replyOptions,
|
|
281
|
-
abortSignal: genSignal,
|
|
352
|
+
// abortSignal: genSignal,
|
|
282
353
|
onModelSelected: prefixContext.onModelSelected,
|
|
354
|
+
onAgentRunStart: (runId) => {
|
|
355
|
+
activeRunIdBySessionKey.set(effectiveSessionKey, runId)
|
|
356
|
+
},
|
|
283
357
|
onPartialReply: async (payload: ReplyPayload) => {
|
|
358
|
+
if (sessionStreamSuppressed.has(effectiveSessionKey)) return
|
|
359
|
+
|
|
284
360
|
// Accumulate full text
|
|
285
361
|
if (payload.text) {
|
|
286
362
|
completeText = payload.text
|
|
@@ -291,8 +367,10 @@ export async function handleDcgchatMessage(msg: InboundMessage, accountId: strin
|
|
|
291
367
|
? payload.text.slice(streamedTextLen)
|
|
292
368
|
: payload.text
|
|
293
369
|
if (delta.trim()) {
|
|
294
|
-
|
|
295
|
-
|
|
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)}`)
|
|
296
374
|
}
|
|
297
375
|
streamedTextLen = payload.text.length
|
|
298
376
|
}
|
|
@@ -302,46 +380,61 @@ export async function handleDcgchatMessage(msg: InboundMessage, accountId: strin
|
|
|
302
380
|
const key = getMediaKey(mediaUrl)
|
|
303
381
|
if (sentMediaKeys.has(key)) continue
|
|
304
382
|
sentMediaKeys.add(key)
|
|
305
|
-
await sendDcgchatMedia({
|
|
383
|
+
await sendDcgchatMedia({ sessionKey: effectiveSessionKey, mediaUrl, text: '' })
|
|
306
384
|
}
|
|
307
385
|
}
|
|
308
386
|
}
|
|
309
387
|
})
|
|
310
388
|
}
|
|
311
389
|
} catch (err: unknown) {
|
|
312
|
-
if (genSignal.aborted) {
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
} else if (err instanceof Error && err.name === 'AbortError') {
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
} else {
|
|
319
|
-
|
|
320
|
-
}
|
|
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
|
+
// }
|
|
321
399
|
} finally {
|
|
322
|
-
if (activeGenerations.get(conversationId) === genCtrl) {
|
|
323
|
-
|
|
324
|
-
}
|
|
400
|
+
// if (activeGenerations.get(conversationId) === genCtrl) {
|
|
401
|
+
// activeGenerations.delete(conversationId)
|
|
402
|
+
// }
|
|
325
403
|
}
|
|
326
404
|
try {
|
|
327
405
|
markRunComplete()
|
|
406
|
+
markDispatchIdle()
|
|
328
407
|
} catch (err) {
|
|
329
|
-
dcgLogger(` markRunComplete error: ${String(err)}`, 'error')
|
|
408
|
+
dcgLogger(` markRunComplete||markRunComplete error: ${String(err)}`, 'error')
|
|
330
409
|
}
|
|
331
|
-
markDispatchIdle()
|
|
332
410
|
if (![...systemCommand, ...interruptCommand].includes(text?.trim())) {
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
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(/^\//, '')))
|
|
418
|
+
const underMobook = file.replace(/^\/mobook\//i, '').replace(/^mobook[\\/]/i, '')
|
|
419
|
+
if (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')
|
|
432
|
+
}
|
|
338
433
|
}
|
|
339
|
-
await sendDcgchatMedia({ msgCtx, mediaUrl: resolved, text: '' })
|
|
340
434
|
}
|
|
341
435
|
}
|
|
342
|
-
|
|
436
|
+
safeSendFinal('end')
|
|
343
437
|
clearSentMediaKeys(msg.content.message_id)
|
|
344
|
-
setMsgStatus('finished')
|
|
345
438
|
|
|
346
439
|
// Record session metadata
|
|
347
440
|
const storePath = core.channel.session.resolveStorePath(config.session?.store)
|
|
@@ -359,6 +452,8 @@ export async function handleDcgchatMessage(msg: InboundMessage, accountId: strin
|
|
|
359
452
|
})
|
|
360
453
|
} catch (err) {
|
|
361
454
|
dcgLogger(` handle message failed: ${String(err)}`, 'error')
|
|
362
|
-
sendError(
|
|
455
|
+
sendError(err instanceof Error ? err.message : String(err), outboundCtx)
|
|
456
|
+
} finally {
|
|
457
|
+
safeSendFinal('finally')
|
|
363
458
|
}
|
|
364
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,
|
|
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 ?? '',
|
|
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
|
-
|
|
159
|
-
|
|
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:
|
|
183
|
+
chatId: ctx.to
|
|
165
184
|
}
|
|
166
185
|
},
|
|
167
186
|
sendMedia: async (ctx) => {
|
|
168
|
-
const msgCtx =
|
|
169
|
-
|
|
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
|
|
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,
|