@dcrays/dcgchat-test 0.3.2 → 0.3.4
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 +1 -1
- package/src/bot.ts +99 -28
- package/src/channel.ts +19 -9
- package/src/cron.ts +50 -30
- package/src/cronToolCall.ts +185 -0
- package/src/gateway/index.ts +6 -8
- package/src/gateway/socket.ts +2 -4
- package/src/monitor.ts +11 -8
- package/src/tool.ts +70 -63
- package/src/transport.ts +2 -2
- package/src/types.ts +1 -0
- package/src/utils/constant.ts +2 -2
- package/src/utils/global.ts +42 -5
- package/src/utils/params.ts +3 -1
package/package.json
CHANGED
package/src/bot.ts
CHANGED
|
@@ -3,13 +3,20 @@ 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 {
|
|
7
|
+
clearSentMediaKeys,
|
|
8
|
+
getDcgchatRuntime,
|
|
9
|
+
getOpenClawConfig,
|
|
10
|
+
getSessionKey,
|
|
11
|
+
getWorkspaceDir,
|
|
12
|
+
setMsgStatus
|
|
13
|
+
} from './utils/global.js'
|
|
7
14
|
import { resolveAccount, sendDcgchatMedia } from './channel.js'
|
|
8
15
|
import { generateSignUrl } from './request/api.js'
|
|
9
16
|
import { extractMobookFiles } from './utils/searchFile.js'
|
|
10
|
-
import { sendChunk, sendFinal, sendText as sendTextMsg, sendError } from './transport.js'
|
|
17
|
+
import { sendChunk, sendFinal, sendText as sendTextMsg, sendError, wsSendRaw, sendText } from './transport.js'
|
|
11
18
|
import { dcgLogger } from './utils/log.js'
|
|
12
|
-
import { channelInfo, systemCommand, interruptCommand, ENV } from './utils/constant.js'
|
|
19
|
+
import { channelInfo, systemCommand, interruptCommand, ENV, ignoreToolCommand } from './utils/constant.js'
|
|
13
20
|
import { sendMessageToGateway } from './gateway/socket.js'
|
|
14
21
|
import { getEffectiveMsgParams, setParamsMessage } from './utils/params.js'
|
|
15
22
|
|
|
@@ -24,6 +31,18 @@ type TFileInfo = { name: string; url: string }
|
|
|
24
31
|
|
|
25
32
|
const mediaMaxBytes = 300 * 1024 * 1024
|
|
26
33
|
|
|
34
|
+
/** 当前会话最近一次 agent run 的 runId(供 chat.abort 精确打断;无则仅传 sessionKey 仍会中止该会话全部活动运行) */
|
|
35
|
+
const activeRunIdBySessionKey = new Map<string, string>()
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* 用户在该 sessionKey 上触发打断后,旧 run 的流式/投递不再下发;与 sessionKey 一一对应,支持多会话。
|
|
39
|
+
* 清除时机:① 下一条非打断用户消息开始处理时;② 旧 run 收尾到 mobook 段时若仍抑制则跳过并发后删除。
|
|
40
|
+
*/
|
|
41
|
+
const sessionStreamSuppressed = new Set<string>()
|
|
42
|
+
|
|
43
|
+
/** 各 sessionKey 当前轮回复的流式分片序号(仅统计实际下发的文本 chunk);每轮非打断消息开始时置 0 */
|
|
44
|
+
const streamChunkIdxBySessionKey = new Map<string, number>()
|
|
45
|
+
|
|
27
46
|
/** Active LLM generation abort controllers, keyed by conversationId */
|
|
28
47
|
// const activeGenerations = new Map<string, AbortController>()
|
|
29
48
|
|
|
@@ -144,8 +163,7 @@ export async function handleDcgchatMessage(msg: InboundMessage, accountId: strin
|
|
|
144
163
|
const embeddedAgentId = extractAgentIdFromConversationId(conversationId)
|
|
145
164
|
|
|
146
165
|
const effectiveAgentId = embeddedAgentId ?? route.agentId
|
|
147
|
-
const effectiveSessionKey =
|
|
148
|
-
real_mobook === '1' ? route.sessionKey : `agent:main:mobook:direct:${agentId}:${conversationId}`.toLowerCase()
|
|
166
|
+
const effectiveSessionKey = getSessionKey(msg.content, account.accountId)
|
|
149
167
|
|
|
150
168
|
setParamsMessage(effectiveSessionKey, {
|
|
151
169
|
userId: msg._userId,
|
|
@@ -168,6 +186,7 @@ export async function handleDcgchatMessage(msg: InboundMessage, accountId: strin
|
|
|
168
186
|
if (finalSent) return
|
|
169
187
|
finalSent = true
|
|
170
188
|
sendFinal(outboundCtx)
|
|
189
|
+
setMsgStatus(effectiveSessionKey, 'finished')
|
|
171
190
|
}
|
|
172
191
|
|
|
173
192
|
const text = msg.content.text?.trim()
|
|
@@ -209,7 +228,7 @@ export async function handleDcgchatMessage(msg: InboundMessage, accountId: strin
|
|
|
209
228
|
RawBody: text,
|
|
210
229
|
CommandBody: text,
|
|
211
230
|
From: userId,
|
|
212
|
-
To:
|
|
231
|
+
To: effectiveSessionKey,
|
|
213
232
|
SessionKey: effectiveSessionKey,
|
|
214
233
|
AccountId: route.accountId,
|
|
215
234
|
ChatType: 'direct',
|
|
@@ -222,7 +241,7 @@ export async function handleDcgchatMessage(msg: InboundMessage, accountId: strin
|
|
|
222
241
|
WasMentioned: true,
|
|
223
242
|
CommandAuthorized: true,
|
|
224
243
|
OriginatingChannel: "dcgchat-test",
|
|
225
|
-
OriginatingTo:
|
|
244
|
+
OriginatingTo: effectiveSessionKey,
|
|
226
245
|
...mediaPayload
|
|
227
246
|
})
|
|
228
247
|
|
|
@@ -243,6 +262,7 @@ export async function handleDcgchatMessage(msg: InboundMessage, accountId: strin
|
|
|
243
262
|
humanDelay: core.channel.reply.resolveHumanDelayConfig(config, route.agentId),
|
|
244
263
|
onReplyStart: async () => {},
|
|
245
264
|
deliver: async (payload: ReplyPayload, info) => {
|
|
265
|
+
if (sessionStreamSuppressed.has(effectiveSessionKey)) return
|
|
246
266
|
const mediaList = resolveReplyMediaList(payload)
|
|
247
267
|
for (const mediaUrl of mediaList) {
|
|
248
268
|
const key = getMediaKey(mediaUrl)
|
|
@@ -252,15 +272,29 @@ export async function handleDcgchatMessage(msg: InboundMessage, accountId: strin
|
|
|
252
272
|
}
|
|
253
273
|
},
|
|
254
274
|
onError: (err: unknown, info: { kind: string }) => {
|
|
275
|
+
activeRunIdBySessionKey.delete(effectiveSessionKey)
|
|
276
|
+
streamChunkIdxBySessionKey.delete(effectiveSessionKey)
|
|
277
|
+
if (sessionStreamSuppressed.has(effectiveSessionKey)) {
|
|
278
|
+
dcgLogger(`${info.kind} reply failed (stream suppressed): ${String(err)}`, 'error')
|
|
279
|
+
return
|
|
280
|
+
}
|
|
255
281
|
safeSendFinal()
|
|
256
282
|
dcgLogger(`${info.kind} reply failed: ${String(err)}`, 'error')
|
|
257
283
|
},
|
|
258
284
|
onIdle: () => {
|
|
285
|
+
activeRunIdBySessionKey.delete(effectiveSessionKey)
|
|
286
|
+
streamChunkIdxBySessionKey.delete(effectiveSessionKey)
|
|
287
|
+
if (sessionStreamSuppressed.has(effectiveSessionKey)) return
|
|
259
288
|
safeSendFinal()
|
|
260
289
|
}
|
|
261
290
|
})
|
|
262
291
|
|
|
263
292
|
try {
|
|
293
|
+
if (!interruptCommand.includes(text?.trim())) {
|
|
294
|
+
sessionStreamSuppressed.delete(effectiveSessionKey)
|
|
295
|
+
streamChunkIdxBySessionKey.set(effectiveSessionKey, 0)
|
|
296
|
+
}
|
|
297
|
+
|
|
264
298
|
if (systemCommand.includes(text?.trim())) {
|
|
265
299
|
dcgLogger(`dispatching /new`)
|
|
266
300
|
await core.channel.reply.dispatchReplyFromConfig({
|
|
@@ -269,21 +303,48 @@ export async function handleDcgchatMessage(msg: InboundMessage, accountId: strin
|
|
|
269
303
|
dispatcher,
|
|
270
304
|
replyOptions: {
|
|
271
305
|
...replyOptions,
|
|
272
|
-
onModelSelected: prefixContext.onModelSelected
|
|
306
|
+
onModelSelected: prefixContext.onModelSelected,
|
|
307
|
+
onAgentRunStart: (runId) => {
|
|
308
|
+
activeRunIdBySessionKey.set(effectiveSessionKey, runId)
|
|
309
|
+
}
|
|
273
310
|
}
|
|
274
311
|
})
|
|
275
312
|
} else if (interruptCommand.includes(text?.trim())) {
|
|
276
313
|
dcgLogger(`interrupt command: ${text}`)
|
|
314
|
+
sendText('会话已终止', outboundCtx)
|
|
315
|
+
safeSendFinal()
|
|
316
|
+
sessionStreamSuppressed.add(effectiveSessionKey)
|
|
317
|
+
const runId = activeRunIdBySessionKey.get(effectiveSessionKey)
|
|
277
318
|
sendMessageToGateway(
|
|
278
319
|
JSON.stringify({
|
|
279
320
|
method: 'chat.abort',
|
|
280
|
-
params: {
|
|
321
|
+
params: {
|
|
322
|
+
sessionKey: effectiveSessionKey,
|
|
323
|
+
...(runId ? { runId } : {})
|
|
324
|
+
}
|
|
281
325
|
})
|
|
282
326
|
)
|
|
283
|
-
|
|
327
|
+
if (runId) activeRunIdBySessionKey.delete(effectiveSessionKey)
|
|
284
328
|
return
|
|
285
329
|
} else {
|
|
286
330
|
dcgLogger(`dispatching to agent (session=${route.sessionKey})`)
|
|
331
|
+
const params = getEffectiveMsgParams(effectiveSessionKey)
|
|
332
|
+
if (!ignoreToolCommand.includes(text?.trim())) {
|
|
333
|
+
// message_received 没有 sessionKey 前置到bot中执行
|
|
334
|
+
wsSendRaw(params, {
|
|
335
|
+
is_finish: -1,
|
|
336
|
+
tool_call_id: Date.now().toString(),
|
|
337
|
+
is_cover: 0,
|
|
338
|
+
thinking_content: JSON.stringify({
|
|
339
|
+
type: 'message_received',
|
|
340
|
+
specialIdentification: 'dcgchat_tool_call_special_identification',
|
|
341
|
+
toolName: '',
|
|
342
|
+
callId: Date.now().toString(),
|
|
343
|
+
params: ''
|
|
344
|
+
}),
|
|
345
|
+
response: ''
|
|
346
|
+
})
|
|
347
|
+
}
|
|
287
348
|
await core.channel.reply.dispatchReplyFromConfig({
|
|
288
349
|
ctx: ctxPayload,
|
|
289
350
|
cfg: config,
|
|
@@ -292,7 +353,12 @@ export async function handleDcgchatMessage(msg: InboundMessage, accountId: strin
|
|
|
292
353
|
...replyOptions,
|
|
293
354
|
// abortSignal: genSignal,
|
|
294
355
|
onModelSelected: prefixContext.onModelSelected,
|
|
356
|
+
onAgentRunStart: (runId) => {
|
|
357
|
+
activeRunIdBySessionKey.set(effectiveSessionKey, runId)
|
|
358
|
+
},
|
|
295
359
|
onPartialReply: async (payload: ReplyPayload) => {
|
|
360
|
+
if (sessionStreamSuppressed.has(effectiveSessionKey)) return
|
|
361
|
+
|
|
296
362
|
// Accumulate full text
|
|
297
363
|
if (payload.text) {
|
|
298
364
|
completeText = payload.text
|
|
@@ -303,8 +369,10 @@ export async function handleDcgchatMessage(msg: InboundMessage, accountId: strin
|
|
|
303
369
|
? payload.text.slice(streamedTextLen)
|
|
304
370
|
: payload.text
|
|
305
371
|
if (delta.trim()) {
|
|
306
|
-
|
|
307
|
-
|
|
372
|
+
const prev = streamChunkIdxBySessionKey.get(effectiveSessionKey) ?? 0
|
|
373
|
+
streamChunkIdxBySessionKey.set(effectiveSessionKey, prev + 1)
|
|
374
|
+
sendChunk(delta, outboundCtx, prev)
|
|
375
|
+
dcgLogger(`[stream]: chunkIdx=${prev} len=${delta.length} user=${msg._userId} ${delta.slice(0, 100)}`)
|
|
308
376
|
}
|
|
309
377
|
streamedTextLen = payload.text.length
|
|
310
378
|
}
|
|
@@ -342,28 +410,31 @@ export async function handleDcgchatMessage(msg: InboundMessage, accountId: strin
|
|
|
342
410
|
dcgLogger(` markRunComplete||markRunComplete error: ${String(err)}`, 'error')
|
|
343
411
|
}
|
|
344
412
|
if (![...systemCommand, ...interruptCommand].includes(text?.trim())) {
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
413
|
+
if (sessionStreamSuppressed.has(effectiveSessionKey)) {
|
|
414
|
+
sessionStreamSuppressed.delete(effectiveSessionKey)
|
|
415
|
+
} else {
|
|
416
|
+
for (const file of extractMobookFiles(completeText)) {
|
|
417
|
+
const candidates: string[] = [file]
|
|
418
|
+
candidates.push(path.join(getWorkspaceDir(), file))
|
|
419
|
+
candidates.push(path.join(getWorkspaceDir(), file.replace(/^\//, '')))
|
|
420
|
+
if (process.platform === 'win32') {
|
|
421
|
+
const underMobook = file.replace(/^\/mobook\//i, '').replace(/^mobook[\\/]/i, '')
|
|
422
|
+
if (underMobook) {
|
|
423
|
+
candidates.push(path.join('C:\\', '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')
|
|
353
432
|
}
|
|
354
|
-
}
|
|
355
|
-
const resolved = candidates.find((p) => fs.existsSync(p))
|
|
356
|
-
if (!resolved) continue
|
|
357
|
-
try {
|
|
358
|
-
await sendDcgchatMedia({ sessionKey: effectiveSessionKey, mediaUrl: resolved, text: '' })
|
|
359
|
-
} catch (err) {
|
|
360
|
-
dcgLogger(` sendDcgchatMedia error: ${String(err)}`, 'error')
|
|
361
433
|
}
|
|
362
434
|
}
|
|
363
435
|
}
|
|
364
436
|
safeSendFinal()
|
|
365
437
|
clearSentMediaKeys(msg.content.message_id)
|
|
366
|
-
setMsgStatus('finished')
|
|
367
438
|
|
|
368
439
|
// Record session metadata
|
|
369
440
|
const storePath = core.channel.session.resolveStorePath(config.session?.store)
|
package/src/channel.ts
CHANGED
|
@@ -2,10 +2,10 @@ 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, getOpenClawConfig, hasSentMediaKey } from './utils/global.js'
|
|
5
|
+
import { addSentMediaKey, getCronMessageId, getOpenClawConfig, hasSentMediaKey } from './utils/global.js'
|
|
6
6
|
import { isWsOpen, mergeDefaultParams, mergeSessionParams, sendFinal, wsSendRaw } from './transport.js'
|
|
7
7
|
import { dcgLogger, setLogger } from './utils/log.js'
|
|
8
|
-
import {
|
|
8
|
+
import { getEffectiveMsgParams, getCurrentSessionKey } from './utils/params.js'
|
|
9
9
|
import { startDcgchatGatewaySocket } from './gateway/socket.js'
|
|
10
10
|
|
|
11
11
|
export type DcgchatMediaSendOptions = {
|
|
@@ -143,13 +143,23 @@ export const dcgchatPlugin: ChannelPlugin<ResolvedDcgchatAccount> = {
|
|
|
143
143
|
textChunkLimit: 4000,
|
|
144
144
|
sendText: async (ctx) => {
|
|
145
145
|
if (isWsOpen()) {
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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
|
+
const newCtx = messageId ? { ...outboundCtx, messageId } : outboundCtx
|
|
162
|
+
wsSendRaw(newCtx, { response: ctx.text, is_finish: -1 })
|
|
153
163
|
dcgLogger(`channel sendText to ${ctx.to} ${ctx.text?.slice(0, 50)}`)
|
|
154
164
|
}
|
|
155
165
|
return {
|
package/src/cron.ts
CHANGED
|
@@ -1,17 +1,11 @@
|
|
|
1
1
|
import path from 'node:path'
|
|
2
2
|
import fs from 'node:fs'
|
|
3
|
-
import crypto from 'node:crypto'
|
|
4
|
-
import { execFile } from 'node:child_process'
|
|
5
|
-
import { promisify } from 'node:util'
|
|
6
|
-
|
|
7
|
-
const execFileAsync = promisify(execFile)
|
|
8
3
|
import type { IMsgParams } from './types.js'
|
|
9
|
-
import { sendEventMessage } from './transport.js'
|
|
10
|
-
import { getWorkspaceDir } from './utils/global.js'
|
|
4
|
+
import { sendEventMessage, sendFinal } from './transport.js'
|
|
5
|
+
import { getWorkspaceDir, removeCronMessageId, setCronMessageId, setMsgStatus } from './utils/global.js'
|
|
11
6
|
import { ossUpload } from './request/oss.js'
|
|
12
7
|
import { dcgLogger } from './utils/log.js'
|
|
13
8
|
import { sendMessageToGateway } from './gateway/socket.js'
|
|
14
|
-
import { channelInfo, ENV } from './utils/constant.js'
|
|
15
9
|
import { getCurrentSessionKey, getEffectiveMsgParams } from './utils/params.js'
|
|
16
10
|
|
|
17
11
|
export function getCronJobsPath(): string {
|
|
@@ -20,6 +14,33 @@ export function getCronJobsPath(): string {
|
|
|
20
14
|
return path.join(cronDir, 'jobs.json')
|
|
21
15
|
}
|
|
22
16
|
|
|
17
|
+
type CronJobsFile = {
|
|
18
|
+
version?: number
|
|
19
|
+
jobs?: Array<{ id?: string; sessionKey?: string }>
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* 在 `jobPath` 指向的 jobs.json(通常为 getCronJobsPath())中按 id 查找任务并返回其 sessionKey。
|
|
24
|
+
*/
|
|
25
|
+
export function readCronJobSessionKey(jobPath: string, jobId: string): string | null {
|
|
26
|
+
const id = jobId?.trim()
|
|
27
|
+
if (!id) return null
|
|
28
|
+
if (!fs.existsSync(jobPath)) {
|
|
29
|
+
dcgLogger(`readCronJobSessionKey: file not found ${jobPath}`, 'error')
|
|
30
|
+
return null
|
|
31
|
+
}
|
|
32
|
+
try {
|
|
33
|
+
const raw = fs.readFileSync(jobPath, 'utf8')
|
|
34
|
+
const data = JSON.parse(raw) as CronJobsFile
|
|
35
|
+
const job = (data.jobs ?? []).find((j) => j.id === id)
|
|
36
|
+
const sk = job?.sessionKey?.trim()
|
|
37
|
+
return sk || null
|
|
38
|
+
} catch (e) {
|
|
39
|
+
dcgLogger(`readCronJobSessionKey: failed to read ${jobPath}: ${String(e)}`, 'error')
|
|
40
|
+
return null
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
23
44
|
function msgParamsToCtx(p: IMsgParams): IMsgParams | null {
|
|
24
45
|
if (!p?.botToken) return null
|
|
25
46
|
return p
|
|
@@ -103,36 +124,35 @@ export const onEnabledCronJob = async (jobId: string) => {
|
|
|
103
124
|
}
|
|
104
125
|
sendMessageToGateway(JSON.stringify({ method: 'cron.update', params: { id: jobId, patch: { enabled: true } } }))
|
|
105
126
|
}
|
|
106
|
-
export const onRunCronJob = async (jobId: string) => {
|
|
127
|
+
export const onRunCronJob = async (jobId: string, messageId: string) => {
|
|
107
128
|
const id = jobId?.trim()
|
|
108
129
|
if (!id) {
|
|
109
130
|
dcgLogger('onRemoveCronJob: empty jobId', 'error')
|
|
110
131
|
return
|
|
111
132
|
}
|
|
112
|
-
|
|
133
|
+
const jobPath = getCronJobsPath()
|
|
134
|
+
const sessionKey = readCronJobSessionKey(jobPath, jobId) || ''
|
|
135
|
+
if (!sessionKey) {
|
|
136
|
+
dcgLogger(`finishedDcgchatCron: no sessionKey for job id=${id}`, 'error')
|
|
137
|
+
return
|
|
138
|
+
}
|
|
139
|
+
setCronMessageId(sessionKey, messageId)
|
|
140
|
+
sendMessageToGateway(JSON.stringify({ method: 'cron.run', params: { id: jobId } }))
|
|
113
141
|
}
|
|
114
|
-
export const
|
|
142
|
+
export const finishedDcgchatCron = async (jobId: string) => {
|
|
115
143
|
const id = jobId?.trim()
|
|
116
144
|
if (!id) {
|
|
117
|
-
dcgLogger('
|
|
145
|
+
dcgLogger('finishedDcgchatCron: empty jobId', 'error')
|
|
118
146
|
return
|
|
119
147
|
}
|
|
120
|
-
const
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
to: params.sessionId,
|
|
131
|
-
accountId: 14,
|
|
132
|
-
bestEffort: true
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
})
|
|
137
|
-
)
|
|
148
|
+
const jobPath = getCronJobsPath()
|
|
149
|
+
const sessionKey = readCronJobSessionKey(jobPath, id)
|
|
150
|
+
if (!sessionKey) {
|
|
151
|
+
dcgLogger(`finishedDcgchatCron: no sessionKey for job id=${id}`, 'error')
|
|
152
|
+
return
|
|
153
|
+
}
|
|
154
|
+
const outboundCtx = getEffectiveMsgParams(sessionKey)
|
|
155
|
+
sendFinal(outboundCtx)
|
|
156
|
+
removeCronMessageId(sessionKey)
|
|
157
|
+
dcgLogger(`finishedDcgchatCron: job=${id} sessionKey=${sessionKey}`)
|
|
138
158
|
}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { channelInfo, ENV } from './utils/constant.js'
|
|
2
|
+
/**
|
|
3
|
+
* cron-delivery-guard — 定时任务投递守护插件
|
|
4
|
+
*
|
|
5
|
+
* 核心机制:通过 before_tool_call 钩子拦截 cron 工具调用,
|
|
6
|
+
* 当 delivery.mode 为 "announce" 且未指定 channel 时,
|
|
7
|
+
* 自动注入 bestEffort: true,使投递失败时静默降级,
|
|
8
|
+
* 不影响 cron 执行结果的保存。
|
|
9
|
+
*
|
|
10
|
+
* 背景:
|
|
11
|
+
* - 定时任务的 delivery 设为 announce 模式,如果没有指定 channel,
|
|
12
|
+
* 投递可能因找不到有效渠道而失败
|
|
13
|
+
* - bestEffort: true 让框架在投递失败时不报错,避免丢失执行结果
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { dcgLogger } from './utils/log.js'
|
|
17
|
+
|
|
18
|
+
const LOG_TAG = 'cron-delivery-guard'
|
|
19
|
+
|
|
20
|
+
// ---- 类型定义 ----
|
|
21
|
+
|
|
22
|
+
interface ToolCallEvent {
|
|
23
|
+
toolName: string
|
|
24
|
+
toolCallId: string
|
|
25
|
+
params: Record<string, unknown>
|
|
26
|
+
result?: { content: string }
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
interface HookContext {
|
|
30
|
+
agentId: string
|
|
31
|
+
sessionKey: string
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
interface BeforeToolCallResult {
|
|
35
|
+
block?: boolean
|
|
36
|
+
blockReason?: string
|
|
37
|
+
params?: Record<string, unknown>
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// ---- delivery 类型 ----
|
|
41
|
+
|
|
42
|
+
interface CronDelivery {
|
|
43
|
+
mode?: string
|
|
44
|
+
channel?: string
|
|
45
|
+
to?: string
|
|
46
|
+
bestEffort?: boolean
|
|
47
|
+
[key: string]: unknown
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* 解析 OpenClaw mobook direct 会话 key。
|
|
51
|
+
* 形如 `agent:main:mobook:direct:14:5466`(大小写不敏感,与路由 toLowerCase 一致)
|
|
52
|
+
* - 倒数第二段:account / peer(delivery.accountId)
|
|
53
|
+
* - 最后一段:会话 id(delivery.to)
|
|
54
|
+
*/
|
|
55
|
+
export function formatterSessionKey(sessionKey: string): { agentId: string; sessionId: string } {
|
|
56
|
+
const parts = sessionKey.split(':').filter((s) => s.length > 0)
|
|
57
|
+
const norm = parts.map((s) => s.toLowerCase())
|
|
58
|
+
if (parts.length >= 6 && norm[0] === 'agent' && norm[2] === 'mobook' && norm[3] === 'direct') {
|
|
59
|
+
return {
|
|
60
|
+
agentId: parts[4] ?? '',
|
|
61
|
+
sessionId: parts[5] ?? ''
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
if (parts.length >= 2) {
|
|
65
|
+
return {
|
|
66
|
+
agentId: parts[parts.length - 2] ?? '',
|
|
67
|
+
sessionId: parts[parts.length - 1] ?? ''
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return { agentId: '', sessionId: '' }
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ---- 辅助函数 ----
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* 判断是否为 cron 工具调用
|
|
77
|
+
*/
|
|
78
|
+
function isCronTool(toolName: string): boolean {
|
|
79
|
+
return toolName === 'cron'
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* 从 cron 参数中提取 delivery 配置
|
|
84
|
+
* cron 工具的参数结构可能是:
|
|
85
|
+
* - params.delivery (顶层)
|
|
86
|
+
* - params.job.delivery (嵌套在 job 中)
|
|
87
|
+
*/
|
|
88
|
+
function extractDelivery(params: Record<string, unknown>): CronDelivery | null {
|
|
89
|
+
// 尝试顶层 delivery
|
|
90
|
+
if (params.delivery && typeof params.delivery === 'object') {
|
|
91
|
+
return params.delivery as CronDelivery
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// 尝试 job.delivery
|
|
95
|
+
const job = params.job as Record<string, unknown> | undefined
|
|
96
|
+
if (job?.delivery && typeof job.delivery === 'object') {
|
|
97
|
+
return job.delivery as CronDelivery
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// 尝试 payload 中的 deliver 相关字段 (兼容 qqbot-cron 风格)
|
|
101
|
+
const payload = job?.payload as Record<string, unknown> | undefined
|
|
102
|
+
if (payload?.deliver === true && payload.channel === undefined) {
|
|
103
|
+
// payload 风格: { deliver: true, channel?: string }
|
|
104
|
+
// 这种情况不是 delivery 对象,跳过
|
|
105
|
+
return null
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return null
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* 判断 delivery 是否需要注入 bestEffort
|
|
113
|
+
* 条件: mode 为 "announce" 且没有 channel
|
|
114
|
+
*/
|
|
115
|
+
function needsBestEffort(delivery: CronDelivery): boolean {
|
|
116
|
+
return delivery.mode === 'announce' && !delivery.channel && !delivery.bestEffort
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* 深拷贝 params 并注入 bestEffort: true
|
|
121
|
+
*/
|
|
122
|
+
function injectBestEffort(params: Record<string, unknown>, sk: string): Record<string, unknown> {
|
|
123
|
+
const newParams = JSON.parse(JSON.stringify(params)) as Record<string, unknown>
|
|
124
|
+
const { agentId, sessionId } = formatterSessionKey(sk)
|
|
125
|
+
// 顶层 delivery
|
|
126
|
+
if (newParams.delivery && typeof newParams.delivery === 'object') {
|
|
127
|
+
;(newParams.delivery as CronDelivery).bestEffort = true
|
|
128
|
+
;(newParams.delivery as CronDelivery).to = sessionId
|
|
129
|
+
;(newParams.delivery as CronDelivery).accountId = agentId
|
|
130
|
+
;(newParams.delivery as CronDelivery).channel = "dcgchat-test"
|
|
131
|
+
newParams.sessionKey = sk
|
|
132
|
+
return newParams
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// job.delivery
|
|
136
|
+
const job = newParams.job as Record<string, unknown> | undefined
|
|
137
|
+
if (job?.delivery && typeof job.delivery === 'object') {
|
|
138
|
+
;(job.delivery as CronDelivery).bestEffort = true
|
|
139
|
+
;(newParams.delivery as CronDelivery).to = sessionId
|
|
140
|
+
;(newParams.delivery as CronDelivery).accountId = agentId
|
|
141
|
+
;(newParams.delivery as CronDelivery).channel = "dcgchat-test"
|
|
142
|
+
newParams.sessionKey = sk
|
|
143
|
+
return newParams
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return newParams
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function cronToolCall(event: { toolName: any; params: any; toolCallId: any }, sk: string) {
|
|
150
|
+
const { toolName, params, toolCallId } = event
|
|
151
|
+
|
|
152
|
+
// 仅处理 cron 工具
|
|
153
|
+
if (isCronTool(toolName)) {
|
|
154
|
+
const delivery = extractDelivery(params)
|
|
155
|
+
if (!delivery) {
|
|
156
|
+
dcgLogger(`[${LOG_TAG}] cron call (${toolCallId}) has no delivery config, skip.`)
|
|
157
|
+
return undefined
|
|
158
|
+
}
|
|
159
|
+
if (!needsBestEffort(delivery)) {
|
|
160
|
+
dcgLogger(
|
|
161
|
+
`[${LOG_TAG}] cron call (${toolCallId}) delivery does not need bestEffort ` +
|
|
162
|
+
`(mode=${String(delivery.mode)}, channel=${String(delivery.channel)}, bestEffort=${String(delivery.bestEffort)}), skip.`
|
|
163
|
+
)
|
|
164
|
+
return undefined
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// ★ 核心:注入 bestEffort: true
|
|
168
|
+
const newParams = injectBestEffort(params, sk)
|
|
169
|
+
dcgLogger(
|
|
170
|
+
`[${LOG_TAG}] cron call (${toolCallId}) injected bestEffort=true ` +
|
|
171
|
+
`(mode=announce, no channel). delivery=${JSON.stringify(newParams.delivery || (newParams.job as Record<string, unknown>)?.delivery)}`
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
return { params: newParams }
|
|
175
|
+
} else if (toolName === 'exec') {
|
|
176
|
+
if (params.command.indexOf('cron create') || params.command.indexOf('cron add')) {
|
|
177
|
+
const newParams = JSON.parse(JSON.stringify(params)) as Record<string, unknown>
|
|
178
|
+
newParams.command =
|
|
179
|
+
params.command.replace('--json', '') + ` --session-key ${sk} --channel ${"dcgchat-test"} --to ${sk} --json`
|
|
180
|
+
return { params: newParams }
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return undefined
|
|
185
|
+
}
|
package/src/gateway/index.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { WebSocket } from 'ws'
|
|
|
3
3
|
import crypto from 'crypto'
|
|
4
4
|
import { deriveDeviceIdFromPublicKey, publicKeyRawBase64UrlFromPem, buildDeviceAuthPayloadV3, signDevicePayload } from './security.js'
|
|
5
5
|
import { dcgLogger } from '../utils/log.js'
|
|
6
|
-
import {
|
|
6
|
+
import { finishedDcgchatCron, sendDcgchatCron } from '../cron.js'
|
|
7
7
|
|
|
8
8
|
export interface GatewayEvent {
|
|
9
9
|
type: string
|
|
@@ -196,7 +196,7 @@ export class GatewayConnection {
|
|
|
196
196
|
dcgLogger('Gateway connection opened(等待 connect.challenge)')
|
|
197
197
|
})
|
|
198
198
|
|
|
199
|
-
this.ws.on('message', (data) => {
|
|
199
|
+
this.ws.on('message', (data, ...args) => {
|
|
200
200
|
try {
|
|
201
201
|
const msg = JSON.parse(data.toString())
|
|
202
202
|
this.handleMessage(
|
|
@@ -357,20 +357,18 @@ export class GatewayConnection {
|
|
|
357
357
|
|
|
358
358
|
if (msg.type === 'event') {
|
|
359
359
|
try {
|
|
360
|
+
// 定时任务相关事件
|
|
360
361
|
if (msg.event === 'cron') {
|
|
361
362
|
dcgLogger(`[Gateway] 收到事件: ${JSON.stringify(msg)}`)
|
|
362
363
|
if (msg.payload?.action === 'added') {
|
|
363
|
-
updateCronJobSessionKey(msg.payload?.jobId as string)
|
|
364
|
-
}
|
|
365
|
-
if (msg.payload?.action === 'updated') {
|
|
366
364
|
sendDcgchatCron()
|
|
367
365
|
}
|
|
368
|
-
if (msg.payload?.action === 'added') {
|
|
369
|
-
updateCronJobSessionKey(msg.payload?.jobId as string)
|
|
370
|
-
}
|
|
371
366
|
if (msg.payload?.action === 'updated') {
|
|
372
367
|
sendDcgchatCron()
|
|
373
368
|
}
|
|
369
|
+
if (msg.payload?.action === 'finished') {
|
|
370
|
+
finishedDcgchatCron(msg.payload?.jobId as string)
|
|
371
|
+
}
|
|
374
372
|
}
|
|
375
373
|
} catch (error) {
|
|
376
374
|
dcgLogger(`[Gateway] 处理事件失败: ${error}`, 'error')
|
package/src/gateway/socket.ts
CHANGED
|
@@ -230,10 +230,8 @@ export function startDcgchatGatewaySocket(): void {
|
|
|
230
230
|
socketStopped = false
|
|
231
231
|
clearReconnectTimer()
|
|
232
232
|
if (startupConnectTimer != null) return
|
|
233
|
-
startupConnectTimer =
|
|
234
|
-
|
|
235
|
-
void connectPersistentGateway()
|
|
236
|
-
}, 10000)
|
|
233
|
+
startupConnectTimer = null
|
|
234
|
+
void connectPersistentGateway()
|
|
237
235
|
}
|
|
238
236
|
|
|
239
237
|
/**
|
package/src/monitor.ts
CHANGED
|
@@ -2,12 +2,12 @@ import type { ClawdbotConfig, RuntimeEnv } from 'openclaw/plugin-sdk'
|
|
|
2
2
|
import WebSocket from 'ws'
|
|
3
3
|
import { handleDcgchatMessage } from './bot.js'
|
|
4
4
|
import { resolveAccount } from './channel.js'
|
|
5
|
-
import { setWsConnection, getOpenClawConfig, setMsgStatus } from './utils/global.js'
|
|
5
|
+
import { setWsConnection, getOpenClawConfig, setMsgStatus, getSessionKey } from './utils/global.js'
|
|
6
6
|
import type { InboundMessage } from './types.js'
|
|
7
7
|
import { installSkill, uninstallSkill } from './skill.js'
|
|
8
8
|
import { dcgLogger } from './utils/log.js'
|
|
9
|
-
import { ignoreToolCommand } from './utils/constant.js'
|
|
10
9
|
import { onDisabledCronJob, onEnabledCronJob, onRemoveCronJob, onRunCronJob } from './cron.js'
|
|
10
|
+
import { ignoreToolCommand } from './utils/constant.js'
|
|
11
11
|
|
|
12
12
|
export type MonitorDcgchatOpts = {
|
|
13
13
|
config?: ClawdbotConfig
|
|
@@ -126,10 +126,13 @@ export async function monitorDcgchatProvider(opts: MonitorDcgchatOpts): Promise<
|
|
|
126
126
|
|
|
127
127
|
if (parsed.messageType == 'openclaw_bot_chat') {
|
|
128
128
|
const msg = parsed as unknown as InboundMessage
|
|
129
|
-
|
|
130
|
-
|
|
129
|
+
// 与 monitor 原逻辑一致:工具类指令不进入 running,避免误触工具链监控
|
|
130
|
+
const effectiveSessionKey = getSessionKey(msg.content, account.accountId)
|
|
131
|
+
if (!ignoreToolCommand.includes(msg.content.text?.trim() ?? '')) {
|
|
132
|
+
setMsgStatus(effectiveSessionKey, 'running')
|
|
133
|
+
} else {
|
|
134
|
+
setMsgStatus(effectiveSessionKey, 'finished')
|
|
131
135
|
}
|
|
132
|
-
|
|
133
136
|
await handleDcgchatMessage(msg, account.accountId)
|
|
134
137
|
} else if (parsed.messageType == 'openclaw_bot_event') {
|
|
135
138
|
const { event_type, operation_type } = parsed.content ? parsed.content : ({} as Record<string, any>)
|
|
@@ -152,15 +155,15 @@ export async function monitorDcgchatProvider(opts: MonitorDcgchatOpts): Promise<
|
|
|
152
155
|
dcgLogger(`openclaw_bot_event unknown event_type: ${event_type}, ${data.toString()}`)
|
|
153
156
|
}
|
|
154
157
|
} else if (event_type === 'cron') {
|
|
155
|
-
const { job_id } = parsed.content
|
|
158
|
+
const { job_id, message_id } = parsed.content
|
|
156
159
|
if (operation_type === 'remove') {
|
|
157
160
|
await onRemoveCronJob(job_id)
|
|
158
161
|
} else if (operation_type === 'enable') {
|
|
159
162
|
await onEnabledCronJob(job_id)
|
|
160
163
|
} else if (operation_type === 'disable') {
|
|
161
164
|
await onDisabledCronJob(job_id)
|
|
162
|
-
} else if (operation_type === '
|
|
163
|
-
await onRunCronJob(job_id)
|
|
165
|
+
} else if (operation_type === 'run') {
|
|
166
|
+
await onRunCronJob(job_id, message_id)
|
|
164
167
|
}
|
|
165
168
|
} else {
|
|
166
169
|
dcgLogger(`openclaw_bot_event unknown operation_type: ${operation_type}, ${data.toString()}`)
|
package/src/tool.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import type { OpenClawPluginApi } from 'openclaw/plugin-sdk'
|
|
2
|
-
import { getMsgStatus
|
|
2
|
+
import { getMsgStatus } from './utils/global.js'
|
|
3
3
|
import { dcgLogger } from './utils/log.js'
|
|
4
|
-
import {
|
|
4
|
+
import { sendFinal, sendText, wsSendRaw } from './transport.js'
|
|
5
5
|
import { getCurrentSessionKey, getEffectiveMsgParams } from './utils/params.js'
|
|
6
|
+
import { channelInfo, ENV } from './utils/constant.js'
|
|
7
|
+
import { cronToolCall } from './cronToolCall.js'
|
|
6
8
|
|
|
7
9
|
let toolCallId = ''
|
|
8
10
|
let toolName = ''
|
|
@@ -31,8 +33,10 @@ type PluginHookName =
|
|
|
31
33
|
| 'subagent_ended'
|
|
32
34
|
| 'gateway_start'
|
|
33
35
|
| 'gateway_stop'
|
|
36
|
+
|
|
37
|
+
// message_received 没有 sessionKey 前置到bot中执行
|
|
34
38
|
const eventList = [
|
|
35
|
-
{ event: 'message_received', message: '' },
|
|
39
|
+
// { event: 'message_received', message: '' },
|
|
36
40
|
// {event: 'before_model_resolve', message: ''},
|
|
37
41
|
// {event: 'before_prompt_build', message: '正在查阅背景资料,构建思考逻辑'},
|
|
38
42
|
// {event: 'before_agent_start', message: '书灵墨宝已就位,准备开始执行任务'},
|
|
@@ -50,75 +54,78 @@ const eventList = [
|
|
|
50
54
|
{ event: 'after_tool_call', message: '' }
|
|
51
55
|
]
|
|
52
56
|
|
|
53
|
-
function sendToolCallMessage(text: string, toolCallId: string, isCover: number) {
|
|
54
|
-
const ws = getWsConnection()
|
|
55
|
-
const sk = getCurrentSessionKey()
|
|
56
|
-
if (!sk) return
|
|
57
|
+
function sendToolCallMessage(sk: string, text: string, toolCallId: string, isCover: number) {
|
|
57
58
|
const params = getEffectiveMsgParams(sk)
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
session_id: params?.sessionId,
|
|
77
|
-
message_id: params?.messageId || Date.now().toString()
|
|
78
|
-
}
|
|
79
|
-
})
|
|
80
|
-
)
|
|
81
|
-
}
|
|
59
|
+
wsSendRaw(params, {
|
|
60
|
+
is_finish: -1,
|
|
61
|
+
tool_call_id: toolCallId,
|
|
62
|
+
is_cover: isCover,
|
|
63
|
+
thinking_content: text,
|
|
64
|
+
response: ''
|
|
65
|
+
})
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* 深拷贝 params 并注入 bestEffort: true
|
|
70
|
+
*/
|
|
71
|
+
interface CronDelivery {
|
|
72
|
+
mode?: string
|
|
73
|
+
channel?: string
|
|
74
|
+
to?: string
|
|
75
|
+
bestEffort?: boolean
|
|
76
|
+
[key: string]: unknown
|
|
82
77
|
}
|
|
83
78
|
|
|
84
79
|
export function monitoringToolMessage(api: OpenClawPluginApi) {
|
|
85
80
|
for (const item of eventList) {
|
|
86
81
|
api.on(item.event as PluginHookName, (event: any, args: any) => {
|
|
87
|
-
const
|
|
88
|
-
if (
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
82
|
+
const sk = args?.sessionKey as string
|
|
83
|
+
if (sk) {
|
|
84
|
+
const status = getMsgStatus(sk)
|
|
85
|
+
if (status === 'running') {
|
|
86
|
+
if (['after_tool_call', 'before_tool_call'].includes(item.event)) {
|
|
87
|
+
const { result: _result, ...rest } = event
|
|
88
|
+
dcgLogger(`工具调用结果: ~ event:${item.event} ~ params:${JSON.stringify(rest)}`)
|
|
89
|
+
|
|
90
|
+
if (item.event === 'before_tool_call') {
|
|
91
|
+
return cronToolCall(rest, sk)
|
|
92
|
+
}
|
|
93
|
+
const text = JSON.stringify({
|
|
94
|
+
type: item.event,
|
|
95
|
+
specialIdentification: 'dcgchat_tool_call_special_identification',
|
|
96
|
+
callId: event.toolCallId || event.runId || Date.now().toString(),
|
|
97
|
+
...rest,
|
|
98
|
+
status: item.event === 'after_tool_call' ? 'finished' : 'running'
|
|
99
|
+
})
|
|
100
|
+
sendToolCallMessage(
|
|
101
|
+
sk,
|
|
102
|
+
text,
|
|
103
|
+
event.toolCallId || event.runId || Date.now().toString(),
|
|
104
|
+
item.event === 'after_tool_call' ? 1 : 0
|
|
105
|
+
)
|
|
106
|
+
} else if (item.event) {
|
|
107
|
+
const msgCtx = getEffectiveMsgParams(sk)
|
|
108
|
+
if (item.event === 'llm_output') {
|
|
109
|
+
if (event.lastAssistant?.errorMessage === '429-账户额度耗尽') {
|
|
110
|
+
const message = '您的积分已消耗完,您可以通过充值积分来继续使用'
|
|
111
|
+
sendText(message, msgCtx, { message_tags: { insufficient_balance: 1 }, is_finish: -1 })
|
|
112
|
+
sendFinal(msgCtx)
|
|
113
|
+
return
|
|
114
|
+
}
|
|
111
115
|
}
|
|
116
|
+
const text = JSON.stringify({
|
|
117
|
+
type: item.event,
|
|
118
|
+
specialIdentification: 'dcgchat_tool_call_special_identification',
|
|
119
|
+
toolName: '',
|
|
120
|
+
callId: event.runId || Date.now().toString(),
|
|
121
|
+
params: item.message
|
|
122
|
+
})
|
|
123
|
+
sendToolCallMessage(sk, text, event.runId || Date.now().toString(), 0)
|
|
124
|
+
dcgLogger(`工具调用结果: ~ event:${item.event} ${status}`)
|
|
112
125
|
}
|
|
113
|
-
const text = JSON.stringify({
|
|
114
|
-
type: item.event,
|
|
115
|
-
specialIdentification: 'dcgchat_tool_call_special_identification',
|
|
116
|
-
toolName: '',
|
|
117
|
-
callId: event.runId || Date.now().toString(),
|
|
118
|
-
params: item.message
|
|
119
|
-
})
|
|
120
|
-
sendToolCallMessage(text, event.runId || Date.now().toString(), 0)
|
|
121
126
|
}
|
|
127
|
+
} else {
|
|
128
|
+
dcgLogger(`工具调用结果: ~ event:${item.event} ~ 没有sessionKey 为执行`)
|
|
122
129
|
}
|
|
123
130
|
})
|
|
124
131
|
}
|
package/src/transport.ts
CHANGED
|
@@ -158,8 +158,8 @@ export function wsSendRaw(ctx: IMsgParams, content: Record<string, unknown>): bo
|
|
|
158
158
|
return true
|
|
159
159
|
}
|
|
160
160
|
|
|
161
|
-
export function sendChunk(text: string, ctx: IMsgParams): boolean {
|
|
162
|
-
return wsSend(ctx, { response: text, state: 'chunk' })
|
|
161
|
+
export function sendChunk(text: string, ctx: IMsgParams, chunkIdx: number): boolean {
|
|
162
|
+
return wsSend(ctx, { response: text, state: 'chunk', chunk_idx: chunkIdx })
|
|
163
163
|
}
|
|
164
164
|
|
|
165
165
|
export function sendFinal(ctx: IMsgParams): boolean {
|
package/src/types.ts
CHANGED
package/src/utils/constant.ts
CHANGED
|
@@ -2,6 +2,6 @@ export const ENV: 'production' | 'test' | 'develop' = 'test'
|
|
|
2
2
|
|
|
3
3
|
|
|
4
4
|
export const systemCommand = ['/new', '/status']
|
|
5
|
-
export const interruptCommand = ['
|
|
5
|
+
export const interruptCommand = ['chat.stop']
|
|
6
6
|
|
|
7
|
-
export const ignoreToolCommand = ['/search', '/abort', '/queue interrupt', ...systemCommand, ...interruptCommand]
|
|
7
|
+
export const ignoreToolCommand = ['/search', '/abort', '/stop', '/queue interrupt', ...systemCommand, ...interruptCommand]
|
package/src/utils/global.ts
CHANGED
|
@@ -68,12 +68,22 @@ export function getDcgchatRuntime(): PluginRuntime {
|
|
|
68
68
|
return runtime as PluginRuntime
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
71
|
+
export type MsgSessionStatus = 'running' | 'finished' | ''
|
|
72
|
+
|
|
73
|
+
const msgStatusBySessionKey = new Map<string, MsgSessionStatus>()
|
|
74
|
+
|
|
75
|
+
export function setMsgStatus(sessionKey: string, status: MsgSessionStatus) {
|
|
76
|
+
if (!sessionKey?.trim()) return
|
|
77
|
+
if (status === '') {
|
|
78
|
+
msgStatusBySessionKey.delete(sessionKey)
|
|
79
|
+
} else {
|
|
80
|
+
msgStatusBySessionKey.set(sessionKey, status)
|
|
81
|
+
}
|
|
74
82
|
}
|
|
75
|
-
|
|
76
|
-
|
|
83
|
+
|
|
84
|
+
export function getMsgStatus(sessionKey: string): MsgSessionStatus {
|
|
85
|
+
if (!sessionKey?.trim()) return ''
|
|
86
|
+
return msgStatusBySessionKey.get(sessionKey) ?? ''
|
|
77
87
|
}
|
|
78
88
|
|
|
79
89
|
const getMediaKey = (url: string) => url.split(/[\\/]/).pop() ?? url
|
|
@@ -106,3 +116,30 @@ export function clearSentMediaKeys(messageId?: string) {
|
|
|
106
116
|
sentMediaKeysBySession.clear()
|
|
107
117
|
}
|
|
108
118
|
}
|
|
119
|
+
|
|
120
|
+
export const getSessionKey = (content: any, accountId: string) => {
|
|
121
|
+
const { real_mobook, agent_id, conversation_id, session_id } = content
|
|
122
|
+
const core = getDcgchatRuntime()
|
|
123
|
+
|
|
124
|
+
const route = core.channel.routing.resolveAgentRoute({
|
|
125
|
+
cfg: getOpenClawConfig() as OpenClawConfig,
|
|
126
|
+
channel: "dcgchat-test",
|
|
127
|
+
accountId: accountId || 'default',
|
|
128
|
+
peer: { kind: 'direct', id: session_id }
|
|
129
|
+
})
|
|
130
|
+
return real_mobook === '1' ? route.sessionKey : `agent:main:mobook:direct:${agent_id}:${session_id}`.toLowerCase()
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const cronMessageIdMap = new Map<string, string>()
|
|
134
|
+
|
|
135
|
+
export function setCronMessageId(sk: string, messageId: string) {
|
|
136
|
+
cronMessageIdMap.set(sk, messageId)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function getCronMessageId(sk: string): string {
|
|
140
|
+
return cronMessageIdMap.get(sk) ?? ''
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function removeCronMessageId(sk: string) {
|
|
144
|
+
cronMessageIdMap.delete(sk)
|
|
145
|
+
}
|
package/src/utils/params.ts
CHANGED
|
@@ -53,7 +53,9 @@ export function getEffectiveMsgParams(sessionKey?: string): IMsgParams {
|
|
|
53
53
|
export function setParamsMessage(sessionKey: string, params: Partial<IMsgParams>) {
|
|
54
54
|
if (!sessionKey) return
|
|
55
55
|
currentSessionKey = sessionKey
|
|
56
|
-
paramsMessageMap.
|
|
56
|
+
const previous = paramsMessageMap.get(sessionKey)
|
|
57
|
+
const base = previous ? resolveParamsMessage(previous) : getParamsDefaults()
|
|
58
|
+
paramsMessageMap.set(sessionKey, resolveParamsMessage({ ...base, ...params, sessionKey }))
|
|
57
59
|
}
|
|
58
60
|
|
|
59
61
|
export function getParamsMessage(sessionKey: string): IMsgParams | undefined {
|