@dcrays/dcgchat-test 0.3.39 → 0.3.41
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 -2
- package/package.json +1 -1
- package/src/bot.ts +56 -75
- package/src/channel.ts +78 -15
- package/src/gateway/socket.ts +5 -3
- package/src/monitor.ts +0 -1
- package/src/tool.ts +1 -2
- package/src/tools/{meeageToll.ts → messageTool.ts} +17 -8
- package/src/transport.ts +8 -5
- package/src/types.ts +6 -0
- package/src/utils/gatewayMsgHanlder.ts +4 -2
- package/src/utils/global.ts +13 -14
- package/src/utils/params.ts +2 -3
package/index.ts
CHANGED
|
@@ -4,8 +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 {
|
|
8
|
-
import { createDcgchatMessageTool } from './src/tools/meeageToll.js'
|
|
7
|
+
import { createDcgchatMessageTool } from './src/tools/messageTool.js'
|
|
9
8
|
|
|
10
9
|
const plugin = {
|
|
11
10
|
id: "dcgchat-test",
|
package/package.json
CHANGED
package/src/bot.ts
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import path from 'node:path'
|
|
2
|
-
import
|
|
3
|
-
import
|
|
4
|
-
import type { PluginRuntime, ReplyPayload } from 'openclaw/plugin-sdk'
|
|
5
|
-
import { createPluginRuntimeStore, createReplyPrefixContext, createTypingCallbacks } from 'openclaw/plugin-sdk'
|
|
2
|
+
import type { ReplyPayload } from 'openclaw/plugin-sdk'
|
|
3
|
+
import { createReplyPrefixContext, createTypingCallbacks } from 'openclaw/plugin-sdk'
|
|
6
4
|
import type { InboundMessage } from './types.js'
|
|
7
5
|
import {
|
|
8
6
|
clearSentMediaKeys,
|
|
@@ -14,7 +12,6 @@ import {
|
|
|
14
12
|
} from './utils/global.js'
|
|
15
13
|
import { resolveAccount, sendDcgchatMedia } from './channel.js'
|
|
16
14
|
import { generateSignUrl } from './request/api.js'
|
|
17
|
-
import { extractMobookFiles } from './utils/searchFile.js'
|
|
18
15
|
import { sendChunk, sendFinal, sendText as sendTextMsg, sendError, wsSendRaw, sendText } from './transport.js'
|
|
19
16
|
import { dcgLogger } from './utils/log.js'
|
|
20
17
|
import { channelInfo, systemCommand, interruptCommand, ENV, ignoreToolCommand } from './utils/constant.js'
|
|
@@ -126,20 +123,18 @@ function resolveReplyMediaList(payload: ReplyPayload): string[] {
|
|
|
126
123
|
if (payload.mediaUrls?.length) return payload.mediaUrls.filter(Boolean)
|
|
127
124
|
return payload.mediaUrl ? [payload.mediaUrl] : []
|
|
128
125
|
}
|
|
126
|
+
|
|
129
127
|
const typingCallbacks = createTypingCallbacks({
|
|
130
|
-
start: async () => {
|
|
131
|
-
console.log('typing start')
|
|
132
|
-
},
|
|
128
|
+
start: async () => {},
|
|
133
129
|
onStartError: (err) => {
|
|
134
|
-
|
|
130
|
+
dcgLogger(`typing start error: ${String(err)}`, 'error')
|
|
135
131
|
}
|
|
136
132
|
})
|
|
133
|
+
|
|
137
134
|
/**
|
|
138
135
|
* 处理一条用户消息,调用 Agent 并返回回复
|
|
139
136
|
*/
|
|
140
137
|
export async function handleDcgchatMessage(msg: InboundMessage, accountId: string): Promise<void> {
|
|
141
|
-
let finalSent = false
|
|
142
|
-
|
|
143
138
|
let completeText = ''
|
|
144
139
|
const config = getOpenClawConfig()
|
|
145
140
|
if (!config) {
|
|
@@ -164,7 +159,7 @@ export async function handleDcgchatMessage(msg: InboundMessage, accountId: strin
|
|
|
164
159
|
const embeddedAgentId = extractAgentIdFromConversationId(conversationId)
|
|
165
160
|
|
|
166
161
|
const effectiveAgentId = embeddedAgentId ?? route.agentId
|
|
167
|
-
const
|
|
162
|
+
const dcgSessionKey = getSessionKey(msg.content, account.accountId)
|
|
168
163
|
|
|
169
164
|
const mergedParams = {
|
|
170
165
|
userId: msg._userId,
|
|
@@ -175,18 +170,17 @@ export async function handleDcgchatMessage(msg: InboundMessage, accountId: strin
|
|
|
175
170
|
appId: msg.content.app_id,
|
|
176
171
|
botId: msg.content.bot_id ?? '',
|
|
177
172
|
agentId: msg.content.agent_id ?? '',
|
|
178
|
-
sessionKey:
|
|
173
|
+
sessionKey: dcgSessionKey,
|
|
179
174
|
real_mobook
|
|
180
175
|
}
|
|
181
|
-
setParamsMessage(
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
const outboundCtx = getEffectiveMsgParams(effectiveSessionKey)
|
|
176
|
+
setParamsMessage(dcgSessionKey, mergedParams)
|
|
177
|
+
dcgLogger(`target alias bound: aliasTarget=${userId} -> sessionKey=${dcgSessionKey}`)
|
|
178
|
+
const outboundCtx = getEffectiveMsgParams(dcgSessionKey)
|
|
185
179
|
const agentEntry =
|
|
186
180
|
effectiveAgentId && effectiveAgentId !== 'main' ? config.agents?.list?.find((a) => a.id === effectiveAgentId) : undefined
|
|
187
181
|
const agentDisplayName = agentEntry?.name ?? (effectiveAgentId && effectiveAgentId !== 'main' ? effectiveAgentId : undefined)
|
|
188
182
|
|
|
189
|
-
|
|
183
|
+
let text = msg.content.text?.trim()
|
|
190
184
|
|
|
191
185
|
if (!text) {
|
|
192
186
|
sendTextMsg('你需要我帮你做什么呢?', outboundCtx)
|
|
@@ -218,8 +212,8 @@ export async function handleDcgchatMessage(msg: InboundMessage, accountId: strin
|
|
|
218
212
|
RawBody: text,
|
|
219
213
|
CommandBody: text,
|
|
220
214
|
From: userId,
|
|
221
|
-
To:
|
|
222
|
-
SessionKey:
|
|
215
|
+
To: dcgSessionKey,
|
|
216
|
+
SessionKey: dcgSessionKey,
|
|
223
217
|
AccountId: route.accountId,
|
|
224
218
|
ChatType: 'direct',
|
|
225
219
|
SenderName: agentDisplayName,
|
|
@@ -231,9 +225,9 @@ export async function handleDcgchatMessage(msg: InboundMessage, accountId: strin
|
|
|
231
225
|
WasMentioned: true,
|
|
232
226
|
CommandAuthorized: true,
|
|
233
227
|
OriginatingChannel: "dcgchat-test",
|
|
234
|
-
OriginatingTo:
|
|
235
|
-
Target:
|
|
236
|
-
SourceTarget:
|
|
228
|
+
OriginatingTo: dcgSessionKey,
|
|
229
|
+
Target: dcgSessionKey,
|
|
230
|
+
SourceTarget: dcgSessionKey,
|
|
237
231
|
...mediaPayload
|
|
238
232
|
})
|
|
239
233
|
|
|
@@ -241,6 +235,12 @@ export async function handleDcgchatMessage(msg: InboundMessage, accountId: strin
|
|
|
241
235
|
const getMediaKey = (url: string) => url.split(/[\\/]/).pop() ?? url
|
|
242
236
|
let streamedTextLen = 0
|
|
243
237
|
|
|
238
|
+
if (msg.content.skills_scope.length > 0) {
|
|
239
|
+
const workspaceDir = getWorkspaceDir()
|
|
240
|
+
const skillCode = msg.content.skills_scope.map((skill) => `${workspaceDir}/skills/${skill.skill_code}`).join('\n')
|
|
241
|
+
const skillText = `在这个目录${skillCode}下有你需要的技能,`
|
|
242
|
+
text = skillText ? `${skillText} \n ${text}` : text
|
|
243
|
+
}
|
|
244
244
|
const prefixContext = createReplyPrefixContext({
|
|
245
245
|
cfg: config,
|
|
246
246
|
agentId: effectiveAgentId ?? '',
|
|
@@ -254,26 +254,22 @@ export async function handleDcgchatMessage(msg: InboundMessage, accountId: strin
|
|
|
254
254
|
humanDelay: core.channel.reply.resolveHumanDelayConfig(config, route.agentId),
|
|
255
255
|
onReplyStart: async () => {},
|
|
256
256
|
deliver: async (payload: ReplyPayload, info) => {
|
|
257
|
-
if (sessionStreamSuppressed.has(
|
|
257
|
+
if (sessionStreamSuppressed.has(dcgSessionKey)) return
|
|
258
258
|
const mediaList = resolveReplyMediaList(payload)
|
|
259
259
|
for (const mediaUrl of mediaList) {
|
|
260
260
|
const key = getMediaKey(mediaUrl)
|
|
261
261
|
if (sentMediaKeys.has(key)) continue
|
|
262
262
|
sentMediaKeys.add(key)
|
|
263
|
-
await sendDcgchatMedia({ sessionKey:
|
|
263
|
+
await sendDcgchatMedia({ sessionKey: dcgSessionKey, mediaUrl, text: '' })
|
|
264
264
|
}
|
|
265
265
|
},
|
|
266
266
|
onError: (err: unknown, info: { kind: string }) => {
|
|
267
|
-
setMsgStatus(
|
|
267
|
+
setMsgStatus(dcgSessionKey, 'finished')
|
|
268
268
|
sendFinal(outboundCtx, 'error')
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
dcgLogger(`${info.kind} reply failed (stream suppressed): ${String(err)}`, 'error')
|
|
274
|
-
return
|
|
275
|
-
}
|
|
276
|
-
dcgLogger(`${info.kind} reply failed: ${String(err)}`, 'error')
|
|
269
|
+
activeRunIdBySessionKey.delete(dcgSessionKey)
|
|
270
|
+
streamChunkIdxBySessionKey.delete(dcgSessionKey)
|
|
271
|
+
const suppressed = sessionStreamSuppressed.has(dcgSessionKey)
|
|
272
|
+
dcgLogger(`${info.kind} reply failed${suppressed ? ' (stream suppressed)' : ''}: ${String(err)}`, 'error')
|
|
277
273
|
},
|
|
278
274
|
onIdle: () => {
|
|
279
275
|
typingCallbacks.onIdle?.()
|
|
@@ -282,8 +278,8 @@ export async function handleDcgchatMessage(msg: InboundMessage, accountId: strin
|
|
|
282
278
|
|
|
283
279
|
try {
|
|
284
280
|
if (!interruptCommand.includes(text?.trim())) {
|
|
285
|
-
sessionStreamSuppressed.delete(
|
|
286
|
-
streamChunkIdxBySessionKey.set(
|
|
281
|
+
sessionStreamSuppressed.delete(dcgSessionKey)
|
|
282
|
+
streamChunkIdxBySessionKey.set(dcgSessionKey, 0)
|
|
287
283
|
}
|
|
288
284
|
|
|
289
285
|
if (systemCommand.includes(text?.trim())) {
|
|
@@ -300,17 +296,16 @@ export async function handleDcgchatMessage(msg: InboundMessage, accountId: strin
|
|
|
300
296
|
...replyOptions,
|
|
301
297
|
onModelSelected: prefixContext.onModelSelected,
|
|
302
298
|
onAgentRunStart: (runId) => {
|
|
303
|
-
activeRunIdBySessionKey.set(
|
|
299
|
+
activeRunIdBySessionKey.set(dcgSessionKey, runId)
|
|
304
300
|
}
|
|
305
301
|
}
|
|
306
302
|
})
|
|
307
303
|
})
|
|
308
304
|
} else if (interruptCommand.includes(text?.trim())) {
|
|
309
305
|
dcgLogger(`interrupt command: ${text}`)
|
|
310
|
-
sendFinal(outboundCtx, 'abort')
|
|
306
|
+
sendFinal({ ...outboundCtx, messageId: `${Date.now()}` }, 'abort')
|
|
311
307
|
sendText('会话已终止', outboundCtx)
|
|
312
|
-
sessionStreamSuppressed.add(
|
|
313
|
-
|
|
308
|
+
sessionStreamSuppressed.add(dcgSessionKey)
|
|
314
309
|
const abortOneSession = async (sessionKey: string) => {
|
|
315
310
|
try {
|
|
316
311
|
await sendGatewayRpc({ method: 'chat.abort', params: { sessionKey } })
|
|
@@ -318,12 +313,11 @@ export async function handleDcgchatMessage(msg: InboundMessage, accountId: strin
|
|
|
318
313
|
dcgLogger(`chat.abort ${sessionKey}: ${String(e)}`, 'error')
|
|
319
314
|
}
|
|
320
315
|
}
|
|
321
|
-
|
|
322
|
-
const keysToAbort = new Set<string>(getChildSessionKeysTrackedForRequester(effectiveSessionKey))
|
|
316
|
+
const keysToAbort = new Set<string>(getChildSessionKeysTrackedForRequester(dcgSessionKey))
|
|
323
317
|
try {
|
|
324
318
|
const listed = await sendGatewayRpc<{ sessions?: Array<{ key?: string }> }>({
|
|
325
319
|
method: 'sessions.list',
|
|
326
|
-
params: { spawnedBy:
|
|
320
|
+
params: { spawnedBy: dcgSessionKey, limit: 256 }
|
|
327
321
|
})
|
|
328
322
|
for (const s of listed?.sessions ?? []) {
|
|
329
323
|
const k = typeof s?.key === 'string' ? s.key.trim() : ''
|
|
@@ -335,29 +329,18 @@ export async function handleDcgchatMessage(msg: InboundMessage, accountId: strin
|
|
|
335
329
|
for (const sk of keysToAbort) {
|
|
336
330
|
await abortOneSession(sk)
|
|
337
331
|
}
|
|
338
|
-
await abortOneSession(
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
method: 'sessions.reset',
|
|
343
|
-
params: { key: effectiveSessionKey, reason: 'reset' }
|
|
344
|
-
})
|
|
345
|
-
} catch (e) {
|
|
346
|
-
dcgLogger(`sessions.reset: ${String(e)}`, 'error')
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
activeRunIdBySessionKey.delete(effectiveSessionKey)
|
|
350
|
-
streamChunkIdxBySessionKey.delete(effectiveSessionKey)
|
|
351
|
-
resetSubagentStateForRequesterSession(effectiveSessionKey)
|
|
352
|
-
setMsgStatus(effectiveSessionKey, 'finished')
|
|
332
|
+
await abortOneSession(dcgSessionKey)
|
|
333
|
+
streamChunkIdxBySessionKey.delete(dcgSessionKey)
|
|
334
|
+
resetSubagentStateForRequesterSession(dcgSessionKey)
|
|
335
|
+
setMsgStatus(dcgSessionKey, 'finished')
|
|
353
336
|
clearSentMediaKeys(msg.content.message_id)
|
|
354
|
-
clearParamsMessage(
|
|
337
|
+
clearParamsMessage(dcgSessionKey)
|
|
355
338
|
clearParamsMessage(userId)
|
|
356
339
|
|
|
357
340
|
sendFinal(outboundCtx, 'stop')
|
|
358
341
|
return
|
|
359
342
|
} else {
|
|
360
|
-
const params = getEffectiveMsgParams(
|
|
343
|
+
const params = getEffectiveMsgParams(dcgSessionKey)
|
|
361
344
|
if (!ignoreToolCommand.includes(text?.trim())) {
|
|
362
345
|
wsSendRaw(params, {
|
|
363
346
|
is_finish: -1,
|
|
@@ -385,10 +368,10 @@ export async function handleDcgchatMessage(msg: InboundMessage, accountId: strin
|
|
|
385
368
|
...replyOptions,
|
|
386
369
|
onModelSelected: prefixContext.onModelSelected,
|
|
387
370
|
onAgentRunStart: (runId) => {
|
|
388
|
-
activeRunIdBySessionKey.set(
|
|
371
|
+
activeRunIdBySessionKey.set(dcgSessionKey, runId)
|
|
389
372
|
},
|
|
390
373
|
onPartialReply: async (payload: ReplyPayload) => {
|
|
391
|
-
if (sessionStreamSuppressed.has(
|
|
374
|
+
if (sessionStreamSuppressed.has(dcgSessionKey)) return
|
|
392
375
|
|
|
393
376
|
if (payload.text) {
|
|
394
377
|
completeText = payload.text
|
|
@@ -399,10 +382,10 @@ export async function handleDcgchatMessage(msg: InboundMessage, accountId: strin
|
|
|
399
382
|
? payload.text.slice(streamedTextLen)
|
|
400
383
|
: payload.text
|
|
401
384
|
if (delta.trim()) {
|
|
402
|
-
const prev = streamChunkIdxBySessionKey.get(
|
|
403
|
-
streamChunkIdxBySessionKey.set(
|
|
385
|
+
const prev = streamChunkIdxBySessionKey.get(dcgSessionKey) ?? 0
|
|
386
|
+
streamChunkIdxBySessionKey.set(dcgSessionKey, prev + 1)
|
|
404
387
|
sendChunk(delta, outboundCtx, prev)
|
|
405
|
-
dcgLogger(`[stream]: chunkIdx=${prev} len=${delta.length} user=${
|
|
388
|
+
dcgLogger(`[stream]: chunkIdx=${prev} len=${delta.length} user=${outboundCtx.sessionId} ${delta.slice(0, 100)}`)
|
|
406
389
|
}
|
|
407
390
|
streamedTextLen = payload.text.length
|
|
408
391
|
} else {
|
|
@@ -414,7 +397,7 @@ export async function handleDcgchatMessage(msg: InboundMessage, accountId: strin
|
|
|
414
397
|
const key = getMediaKey(mediaUrl)
|
|
415
398
|
if (sentMediaKeys.has(key)) continue
|
|
416
399
|
sentMediaKeys.add(key)
|
|
417
|
-
await sendDcgchatMedia({ sessionKey:
|
|
400
|
+
await sendDcgchatMedia({ sessionKey: dcgSessionKey, mediaUrl, text: '' })
|
|
418
401
|
}
|
|
419
402
|
}
|
|
420
403
|
}
|
|
@@ -426,26 +409,24 @@ export async function handleDcgchatMessage(msg: InboundMessage, accountId: strin
|
|
|
426
409
|
}
|
|
427
410
|
|
|
428
411
|
if (![...systemCommand, ...interruptCommand].includes(text?.trim())) {
|
|
429
|
-
if (sessionStreamSuppressed.has(
|
|
430
|
-
sessionStreamSuppressed.delete(
|
|
412
|
+
if (sessionStreamSuppressed.has(dcgSessionKey)) {
|
|
413
|
+
sessionStreamSuppressed.delete(dcgSessionKey)
|
|
431
414
|
}
|
|
432
415
|
}
|
|
433
416
|
clearSentMediaKeys(msg.content.message_id)
|
|
434
417
|
const storePath = core.channel.session.resolveStorePath(config.session?.store)
|
|
435
|
-
await waitUntilSubagentsIdle(
|
|
418
|
+
await waitUntilSubagentsIdle(dcgSessionKey, { timeoutMs: 600_000 })
|
|
436
419
|
sendFinal(outboundCtx, 'end')
|
|
437
|
-
dcgLogger(
|
|
438
|
-
`record session route: rawTarget=${userId}, normalizedTarget=${effectiveSessionKey}, updateLastRoute.to=${effectiveSessionKey}, accountId=${route.accountId}`
|
|
439
|
-
)
|
|
420
|
+
dcgLogger(`record session route: updateLastRoute.to=${dcgSessionKey}, accountId=${route.accountId}`)
|
|
440
421
|
core.channel.session
|
|
441
422
|
.recordInboundSession({
|
|
442
423
|
storePath,
|
|
443
|
-
sessionKey:
|
|
424
|
+
sessionKey: dcgSessionKey,
|
|
444
425
|
ctx: ctxPayload,
|
|
445
426
|
updateLastRoute: {
|
|
446
|
-
sessionKey:
|
|
427
|
+
sessionKey: dcgSessionKey,
|
|
447
428
|
channel: "dcgchat-test",
|
|
448
|
-
to:
|
|
429
|
+
to: dcgSessionKey,
|
|
449
430
|
accountId: route.accountId
|
|
450
431
|
},
|
|
451
432
|
onRecordError: (err) => {
|
package/src/channel.ts
CHANGED
|
@@ -15,6 +15,47 @@ import { dcgLogger, setLogger } from './utils/log.js'
|
|
|
15
15
|
import { getOutboundMsgParams, getParamsMessage } from './utils/params.js'
|
|
16
16
|
import { startDcgchatGatewaySocket } from './gateway/socket.js'
|
|
17
17
|
|
|
18
|
+
function dcgchatChannelCfg(): DcgchatConfig {
|
|
19
|
+
return (getOpenClawConfig()?.channels?.["dcgchat-test"] as DcgchatConfig | undefined) ?? {}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** `agent:<code>:mobook:direct:<agentId>:<sessionId>`(与 getSessionKey 非 real_mobook 分支一致) */
|
|
23
|
+
function isMobookDirectSessionKey(s: string): boolean {
|
|
24
|
+
const parts = s.split(':').filter((p) => p.length > 0)
|
|
25
|
+
const low = parts.map((p) => p.toLowerCase())
|
|
26
|
+
return parts.length >= 6 && low[0] === 'agent' && low[2] === 'mobook' && low[3] === 'direct'
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** real_mobook 等线路下 Core 分配的 `agent:<agentId>:…` sessionKey */
|
|
30
|
+
function isAgentPrefixedSessionKey(s: string): boolean {
|
|
31
|
+
const parts = s.split(':').filter((p) => p.length > 0)
|
|
32
|
+
return parts.length >= 3 && parts[0].toLowerCase() === 'agent'
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* 供 `messaging.targetResolver.looksLikeId` 使用:与 OpenClaw `resolveMessagingTarget` 对齐,
|
|
37
|
+
* 仅当 target「像合法会话路由键」时才走 id 类解析;纯数字不会命中,从而在系统层拒绝误填 userId。
|
|
38
|
+
*/
|
|
39
|
+
function looksLikeDcgchatMessageToolTarget(raw: string): boolean {
|
|
40
|
+
let s = raw.trim()
|
|
41
|
+
if (!s) return false
|
|
42
|
+
const prefix = 'dcg-cron:'
|
|
43
|
+
if (s.startsWith(prefix)) {
|
|
44
|
+
s = s.slice(prefix.length).trim()
|
|
45
|
+
if (!s) return false
|
|
46
|
+
}
|
|
47
|
+
if (isMobookDirectSessionKey(s)) return true
|
|
48
|
+
if (isAgentPrefixedSessionKey(s)) return true
|
|
49
|
+
return false
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function dcgchatMessageTargetLooksLikeId(raw: string, _normalized?: string): boolean {
|
|
53
|
+
if (dcgchatChannelCfg().strictMessageToolTarget === false) {
|
|
54
|
+
return Boolean(raw?.trim())
|
|
55
|
+
}
|
|
56
|
+
return looksLikeDcgchatMessageToolTarget(raw)
|
|
57
|
+
}
|
|
58
|
+
|
|
18
59
|
export type DcgchatMediaSendOptions = {
|
|
19
60
|
/** 与 setParamsMessage / map 一致,用于 getOutboundMsgParams */
|
|
20
61
|
sessionKey: string
|
|
@@ -105,7 +146,8 @@ export const dcgchatPlugin: ChannelPlugin<ResolvedDcgchatAccount> = {
|
|
|
105
146
|
effects: true
|
|
106
147
|
// blockStreaming: true,
|
|
107
148
|
},
|
|
108
|
-
|
|
149
|
+
/** 当前构建的 channel id + 兼容旧配置键 `channels.dcgchat` */
|
|
150
|
+
reload: { configPrefixes: [`channels.${"dcgchat-test"}`, 'channels.dcgchat'] },
|
|
109
151
|
configSchema: {
|
|
110
152
|
schema: {
|
|
111
153
|
type: 'object',
|
|
@@ -114,16 +156,25 @@ export const dcgchatPlugin: ChannelPlugin<ResolvedDcgchatAccount> = {
|
|
|
114
156
|
enabled: { type: 'boolean' },
|
|
115
157
|
wsUrl: { type: 'string' },
|
|
116
158
|
botToken: { type: 'string' },
|
|
117
|
-
userId: { type: 'string', description: 'WebSocket 连接参数 _userId,与
|
|
159
|
+
userId: { type: 'string', description: 'WebSocket 连接参数 _userId,与 dcgchat_message 工具的 target(dcgSessionKey)无关' },
|
|
118
160
|
appId: { type: 'string' },
|
|
119
161
|
domainId: { type: 'string' },
|
|
120
|
-
capabilities: { type: 'array', items: { type: 'string' } }
|
|
162
|
+
capabilities: { type: 'array', items: { type: 'string' } },
|
|
163
|
+
strictMessageToolTarget: {
|
|
164
|
+
type: 'boolean',
|
|
165
|
+
description:
|
|
166
|
+
'默认 true:内置 message 工具的 target 须为 sessionKey 形态(如 agent:…:mobook:direct:… 或 agent: 前缀多段),禁止纯数字 WS userId。设为 false 关闭此校验。'
|
|
167
|
+
}
|
|
121
168
|
}
|
|
122
169
|
},
|
|
123
170
|
uiHints: {
|
|
124
171
|
userId: {
|
|
125
172
|
label: 'WS 连接 _userId',
|
|
126
|
-
help: '仅用于拼接网关 WebSocket URL 的查询参数,不是 Agent 发消息时的 target。发消息请使用
|
|
173
|
+
help: '仅用于拼接网关 WebSocket URL 的查询参数,不是 Agent 发消息时的 target。发消息请使用 dcgSessionKey(与入站上下文 SessionKey 相同)。'
|
|
174
|
+
},
|
|
175
|
+
strictMessageToolTarget: {
|
|
176
|
+
label: '严格 message.target',
|
|
177
|
+
help: '开启后由通道目标解析层拒绝纯数字等非 sessionKey 的 target(推荐开启);关闭则与旧版行为一致。'
|
|
127
178
|
}
|
|
128
179
|
}
|
|
129
180
|
},
|
|
@@ -131,16 +182,17 @@ export const dcgchatPlugin: ChannelPlugin<ResolvedDcgchatAccount> = {
|
|
|
131
182
|
listAccountIds: () => [DEFAULT_ACCOUNT_ID],
|
|
132
183
|
resolveAccount: (cfg, accountId) => resolveAccount(cfg, accountId),
|
|
133
184
|
defaultAccountId: () => DEFAULT_ACCOUNT_ID,
|
|
134
|
-
setAccountEnabled: ({ cfg, enabled }) =>
|
|
135
|
-
|
|
136
|
-
channels
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
185
|
+
setAccountEnabled: ({ cfg, enabled }) => {
|
|
186
|
+
const channelKey = "dcgchat-test"
|
|
187
|
+
const prev = (cfg.channels?.[channelKey as keyof NonNullable<typeof cfg.channels>] as Record<string, unknown> | undefined) ?? {}
|
|
188
|
+
return {
|
|
189
|
+
...cfg,
|
|
190
|
+
channels: {
|
|
191
|
+
...cfg.channels,
|
|
192
|
+
[channelKey]: { ...prev, enabled }
|
|
141
193
|
}
|
|
142
194
|
}
|
|
143
|
-
}
|
|
195
|
+
},
|
|
144
196
|
isConfigured: (account) => account.configured,
|
|
145
197
|
describeAccount: (account) => ({
|
|
146
198
|
accountId: account.accountId,
|
|
@@ -156,15 +208,26 @@ export const dcgchatPlugin: ChannelPlugin<ResolvedDcgchatAccount> = {
|
|
|
156
208
|
messaging: {
|
|
157
209
|
normalizeTarget: (raw) => raw || undefined,
|
|
158
210
|
targetResolver: {
|
|
159
|
-
looksLikeId:
|
|
160
|
-
hint: '
|
|
211
|
+
looksLikeId: dcgchatMessageTargetLooksLikeId,
|
|
212
|
+
hint: '须为完整 dcgSessionKey(与 SessionKey 一致,形如 agent:…:mobook:direct:… 或 agent: 前缀路由键);禁止填 WS userId 等纯数字。可在通道配置 strictMessageToolTarget=false 关闭校验。'
|
|
161
213
|
}
|
|
162
214
|
},
|
|
215
|
+
/**
|
|
216
|
+
* 与 Telegram 等通道一致:用入站路由键 `To`(即 SessionKey / OriginatingTo)作为 message 工具默认 `currentChannelId`。
|
|
217
|
+
* 显式 target 由 `messaging.targetResolver.looksLikeId` + OpenClaw `resolveMessagingTarget` 校验(见 strictMessageToolTarget)。
|
|
218
|
+
*/
|
|
219
|
+
threading: {
|
|
220
|
+
buildToolContext: ({ context, hasRepliedRef }) => ({
|
|
221
|
+
currentChannelId: context.To?.trim() || undefined,
|
|
222
|
+
hasRepliedRef
|
|
223
|
+
})
|
|
224
|
+
},
|
|
163
225
|
agentPrompt: {
|
|
164
226
|
messageToolHints: () => [
|
|
165
227
|
'生成文件后,**尽可能不要**把文件路径、地址直接告诉用户。',
|
|
166
228
|
'生成文件后,把文件名告诉用户。',
|
|
167
|
-
'
|
|
229
|
+
'使用内置 `message` 或 `dcgchat_message` 发消息时,target 必须是完整 dcgSessionKey(与上下文 SessionKey 相同),禁止填 From、SenderId、WS userId 等纯数字。',
|
|
230
|
+
'生成文件后,须通过工具发送文件,勿在文本里直接输出路径或地址。'
|
|
168
231
|
]
|
|
169
232
|
},
|
|
170
233
|
outbound: {
|
package/src/gateway/socket.ts
CHANGED
|
@@ -212,7 +212,7 @@ async function connectPersistentGateway(): Promise<void> {
|
|
|
212
212
|
startPingTimer(gw)
|
|
213
213
|
dcgLogger(`Gateway 持久连接成功 connId=${gw.getConnId() ?? '?'}`)
|
|
214
214
|
} catch (e) {
|
|
215
|
-
dcgLogger(`Gateway
|
|
215
|
+
dcgLogger(`Gateway 连接失败: ${e}`, 'error')
|
|
216
216
|
persistentConn = null
|
|
217
217
|
clearPingTimer()
|
|
218
218
|
if (!socketStopped) {
|
|
@@ -230,8 +230,10 @@ export function startDcgchatGatewaySocket(): void {
|
|
|
230
230
|
socketStopped = false
|
|
231
231
|
clearReconnectTimer()
|
|
232
232
|
if (startupConnectTimer != null) return
|
|
233
|
-
startupConnectTimer =
|
|
234
|
-
|
|
233
|
+
startupConnectTimer = setTimeout(() => {
|
|
234
|
+
startupConnectTimer = null
|
|
235
|
+
void connectPersistentGateway()
|
|
236
|
+
}, 0)
|
|
235
237
|
}
|
|
236
238
|
|
|
237
239
|
/**
|
package/src/monitor.ts
CHANGED
package/src/tool.ts
CHANGED
|
@@ -205,7 +205,6 @@ export function getActiveSubagentCount(sessionKey: string): number {
|
|
|
205
205
|
* 注意:须在收到 `subagent_spawned` 之后才会计入;仅 spawning 未 spawned 的不会阻塞。
|
|
206
206
|
*/
|
|
207
207
|
export function waitUntilSubagentsIdle(sessionKey: string, opts?: { timeoutMs?: number; signal?: AbortSignal }): Promise<void> {
|
|
208
|
-
console.log('🚀 ~ waitUntilSubagentsIdle ~ sessionKey:', sessionKey)
|
|
209
208
|
const sk = sessionKey?.trim()
|
|
210
209
|
if (!sk) return Promise.resolve()
|
|
211
210
|
|
|
@@ -350,7 +349,7 @@ export function monitoringToolMessage(api: OpenClawPluginApi) {
|
|
|
350
349
|
dcgLogger(`工具调用结果: ~ event:${item.event} ${status}`)
|
|
351
350
|
}
|
|
352
351
|
}
|
|
353
|
-
} else {
|
|
352
|
+
} else if (item.event !== 'before_tool_call') {
|
|
354
353
|
dcgLogger(`工具调用结果: ~ event:${item.event} ~ 没有sessionKey 为执行`)
|
|
355
354
|
}
|
|
356
355
|
})
|
|
@@ -2,15 +2,15 @@ import fs from 'node:fs'
|
|
|
2
2
|
import path from 'node:path'
|
|
3
3
|
import type { AnyAgentTool } from 'openclaw/plugin-sdk'
|
|
4
4
|
import { jsonResult } from 'openclaw/plugin-sdk'
|
|
5
|
+
import { sendDcgchatMedia } from '../channel.js'
|
|
6
|
+
import { getOutboundMsgParams } from '../utils/params.js'
|
|
7
|
+
import { sendText } from '../transport.js'
|
|
5
8
|
|
|
6
9
|
/** 与 `registerTool` 工厂入参一致(主包未导出 `OpenClawPluginToolContext` 时仅用所需字段)。 */
|
|
7
10
|
export type DcgchatMessageToolContext = {
|
|
8
11
|
sessionKey?: string
|
|
9
12
|
workspaceDir?: string
|
|
10
13
|
}
|
|
11
|
-
import { sendDcgchatMedia } from '../channel.js'
|
|
12
|
-
import { getOutboundMsgParams } from '../utils/params.js'
|
|
13
|
-
import { sendText } from '../transport.js'
|
|
14
14
|
|
|
15
15
|
const SAFE_PREFIXES = ['/workspace/', '/mobook/']
|
|
16
16
|
|
|
@@ -22,7 +22,13 @@ const SAFE_EXTENSIONS = new Set([...fileType1, ...fileType2, ...fileType3, ...fi
|
|
|
22
22
|
|
|
23
23
|
const messageToolParameters = {
|
|
24
24
|
type: 'object',
|
|
25
|
+
additionalProperties: false,
|
|
25
26
|
properties: {
|
|
27
|
+
target: {
|
|
28
|
+
type: 'string',
|
|
29
|
+
description:
|
|
30
|
+
'目标会话键(sessionKey),必须与当前会话 SessionKey 一致,禁止填写 userId。'
|
|
31
|
+
},
|
|
26
32
|
content: {
|
|
27
33
|
type: 'string',
|
|
28
34
|
description: '发送文本内容'
|
|
@@ -32,6 +38,7 @@ const messageToolParameters = {
|
|
|
32
38
|
description: '发送附件',
|
|
33
39
|
items: {
|
|
34
40
|
type: 'object',
|
|
41
|
+
additionalProperties: false,
|
|
35
42
|
properties: {
|
|
36
43
|
file: {
|
|
37
44
|
type: 'string',
|
|
@@ -41,7 +48,8 @@ const messageToolParameters = {
|
|
|
41
48
|
required: ['file']
|
|
42
49
|
}
|
|
43
50
|
}
|
|
44
|
-
}
|
|
51
|
+
},
|
|
52
|
+
oneOf: [{ required: ['content'] }, { required: ['media'] }]
|
|
45
53
|
}
|
|
46
54
|
|
|
47
55
|
function extractPaths(text?: string) {
|
|
@@ -64,16 +72,17 @@ function isSafeFile(filepath: string) {
|
|
|
64
72
|
}
|
|
65
73
|
|
|
66
74
|
/**
|
|
67
|
-
*
|
|
75
|
+
* 书灵墨宝出站消息工具:须符合 OpenClaw `AgentTool`(execute 返回 `AgentToolResult`)。
|
|
76
|
+
* 工具名使用 `dcgchat_message`,避免与核心内置 `message` 冲突。
|
|
68
77
|
* 通过注册时的 `OpenClawPluginToolContext.sessionKey` 出站,不再使用非标准的 `execute(args, ctx)`。
|
|
69
78
|
*/
|
|
70
79
|
export function createDcgchatMessageTool(pluginCtx: DcgchatMessageToolContext): AnyAgentTool {
|
|
71
|
-
console.log('🚀 ~ createDcgchatMessageTool ~ pluginCtx:', pluginCtx)
|
|
72
80
|
return {
|
|
73
|
-
name: '
|
|
74
|
-
label: '
|
|
81
|
+
name: 'dcgchat_message',
|
|
82
|
+
label: 'dcgchat_message',
|
|
75
83
|
description: `
|
|
76
84
|
向用户发送消息。
|
|
85
|
+
若传 target,target 必须是 sessionKey,不能是 userId。
|
|
77
86
|
如果发送附件:必须使用 media 字段
|
|
78
87
|
支持路径目录:
|
|
79
88
|
/workspace/
|
package/src/transport.ts
CHANGED
|
@@ -152,11 +152,14 @@ export function wsSend(ctx: IMsgParams, content: Record<string, unknown>): boole
|
|
|
152
152
|
*/
|
|
153
153
|
export function wsSendRaw(ctx: IMsgParams, content: Record<string, unknown>, isLog = true): boolean {
|
|
154
154
|
const ws = getWsConnection()
|
|
155
|
-
if (
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
155
|
+
if (ws?.readyState !== WebSocket.OPEN) {
|
|
156
|
+
dcgLogger(`server socket not ready ${ws?.readyState}`, 'error')
|
|
157
|
+
return false
|
|
158
|
+
}
|
|
159
|
+
const envelope = buildOpenclawBotChat(ctx, content, { mergeChannelDefaults: true })
|
|
160
|
+
ws.send(JSON.stringify(envelope))
|
|
161
|
+
if (isLog) {
|
|
162
|
+
dcgLogger('已发送:' + JSON.stringify(envelope))
|
|
160
163
|
}
|
|
161
164
|
return true
|
|
162
165
|
}
|
package/src/types.ts
CHANGED
|
@@ -11,6 +11,11 @@ export type DcgchatConfig = {
|
|
|
11
11
|
userId?: string
|
|
12
12
|
domainId?: string
|
|
13
13
|
appId?: string
|
|
14
|
+
/**
|
|
15
|
+
* 内置 `message` 工具走 OpenClaw 目标解析:`true`(默认)时仅将符合 sessionKey 形态的字符串视为合法 target,
|
|
16
|
+
* 纯数字(WS userId 等)会解析失败;设为 `false` 恢复旧版宽松行为(不推荐)。
|
|
17
|
+
*/
|
|
18
|
+
strictMessageToolTarget?: boolean
|
|
14
19
|
}
|
|
15
20
|
|
|
16
21
|
export type ResolvedDcgchatAccount = {
|
|
@@ -38,6 +43,7 @@ export type InboundMessage = {
|
|
|
38
43
|
source: string // 'server',
|
|
39
44
|
// content: string;
|
|
40
45
|
content: {
|
|
46
|
+
skills_scope: Record<string, any>[]
|
|
41
47
|
bot_token: string
|
|
42
48
|
agent_clone_code?: string
|
|
43
49
|
domain_id?: string
|
|
@@ -10,11 +10,13 @@ import { sendChunk } from '../transport.js'
|
|
|
10
10
|
export function handleGatewayEventMessage(msg: { event?: string; payload?: Record<string, unknown>; seq?: number }): GatewayEvent {
|
|
11
11
|
try {
|
|
12
12
|
if (msg.event === 'agent') {
|
|
13
|
-
dcgLogger(`[Gateway] 收到agent事件: ${JSON.stringify(msg).slice(0, 100)}`)
|
|
14
13
|
const pl = msg.payload as { runId: string; data?: { delta?: unknown } }
|
|
15
14
|
const sessionKey = getSessionKeyBySubAgentRunId(pl.runId)
|
|
16
15
|
const outboundCtx = getEffectiveMsgParams(sessionKey)
|
|
17
|
-
if (
|
|
16
|
+
if (pl.data?.delta) {
|
|
17
|
+
dcgLogger(`[Gateway] 收到agent事件: ${JSON.stringify(msg).slice(0, 100)}`)
|
|
18
|
+
if (outboundCtx.sessionId) sendChunk(pl.data.delta as string, outboundCtx, 0)
|
|
19
|
+
}
|
|
18
20
|
}
|
|
19
21
|
if (msg.event === 'cron') {
|
|
20
22
|
const p = msg.payload
|
package/src/utils/global.ts
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
|
-
/** socket connection */
|
|
2
1
|
import type WebSocket from 'ws'
|
|
2
|
+
import fs from 'node:fs'
|
|
3
|
+
import os from 'node:os'
|
|
4
|
+
import path from 'node:path'
|
|
5
|
+
import { createPluginRuntimeStore, type OpenClawConfig, type PluginRuntime } from 'openclaw/plugin-sdk'
|
|
6
|
+
import { channelInfo, ENV } from './constant.js'
|
|
7
|
+
import { dcgLogger } from './log.js'
|
|
3
8
|
|
|
9
|
+
/** socket connection */
|
|
4
10
|
let ws: WebSocket | null = null
|
|
5
11
|
|
|
6
12
|
export function setWsConnection(next: WebSocket | null) {
|
|
@@ -11,7 +17,6 @@ export function getWsConnection(): WebSocket | null {
|
|
|
11
17
|
return ws
|
|
12
18
|
}
|
|
13
19
|
|
|
14
|
-
// OpenClawConfig
|
|
15
20
|
let config: OpenClawConfig | null = null
|
|
16
21
|
|
|
17
22
|
export function setOpenClawConfig(next: OpenClawConfig | null) {
|
|
@@ -22,15 +27,7 @@ export function getOpenClawConfig(): OpenClawConfig | null {
|
|
|
22
27
|
return config
|
|
23
28
|
}
|
|
24
29
|
|
|
25
|
-
|
|
26
|
-
import { dcgLogger } from './log.js'
|
|
27
|
-
import { channelInfo, ENV } from './constant.js'
|
|
28
|
-
|
|
29
|
-
const path = require('path')
|
|
30
|
-
const fs = require('fs')
|
|
31
|
-
const os = require('os')
|
|
32
|
-
|
|
33
|
-
function getWorkspacePath() {
|
|
30
|
+
function getWorkspacePath(): string | null {
|
|
34
31
|
const workspacePath = path.join(
|
|
35
32
|
os.homedir(),
|
|
36
33
|
config?.channels?.["dcgchat-test"]?.appId == 110 ? '.mobook' : '.openclaw',
|
|
@@ -42,19 +39,21 @@ function getWorkspacePath() {
|
|
|
42
39
|
return null
|
|
43
40
|
}
|
|
44
41
|
|
|
45
|
-
let workspaceDir: string = getWorkspacePath()
|
|
42
|
+
let workspaceDir: string = getWorkspacePath() ?? ''
|
|
46
43
|
|
|
47
44
|
export function setWorkspaceDir(dir?: string) {
|
|
48
45
|
if (dir) {
|
|
49
46
|
workspaceDir = dir
|
|
50
47
|
}
|
|
51
48
|
}
|
|
49
|
+
|
|
52
50
|
export function getWorkspaceDir(): string {
|
|
53
51
|
if (!workspaceDir) {
|
|
54
52
|
dcgLogger?.('Workspace directory not initialized', 'error')
|
|
55
53
|
}
|
|
56
54
|
return workspaceDir
|
|
57
55
|
}
|
|
56
|
+
|
|
58
57
|
const { setRuntime: setDcgchatRuntime, getRuntime: getDcgchatRuntime } = createPluginRuntimeStore<PluginRuntime>(
|
|
59
58
|
`${"dcgchat-test"} runtime not initialized`
|
|
60
59
|
)
|
|
@@ -127,7 +126,7 @@ export const getSessionKey = (content: any, accountId: string) => {
|
|
|
127
126
|
const { real_mobook, agent_id, agent_clone_code, session_id } = content
|
|
128
127
|
const core = getDcgchatRuntime()
|
|
129
128
|
|
|
130
|
-
const
|
|
129
|
+
const agentCode = agent_clone_code || 'main'
|
|
131
130
|
|
|
132
131
|
const route = core.channel.routing.resolveAgentRoute({
|
|
133
132
|
cfg: getOpenClawConfig() as OpenClawConfig,
|
|
@@ -135,7 +134,7 @@ export const getSessionKey = (content: any, accountId: string) => {
|
|
|
135
134
|
accountId: accountId || 'default',
|
|
136
135
|
peer: { kind: 'direct', id: session_id }
|
|
137
136
|
})
|
|
138
|
-
return real_mobook == '1' ? route.sessionKey : `agent:${
|
|
137
|
+
return real_mobook == '1' ? route.sessionKey : `agent:${agentCode}:mobook:direct:${agent_id}:${session_id}`.toLowerCase()
|
|
139
138
|
}
|
|
140
139
|
|
|
141
140
|
export function getInfoBySessionKey(sk: string): { sessionId: string; agentId: string } {
|
package/src/utils/params.ts
CHANGED
|
@@ -47,9 +47,8 @@ export function getEffectiveMsgParams(sessionKey?: string): IMsgParams {
|
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
/**
|
|
50
|
-
* Agent `
|
|
51
|
-
* `setParamsMessage`
|
|
52
|
-
* 则回落到当前会话 `currentSessionKey`,避免拿到空 `messageId` / `sessionId` 导致无文件卡片、WS 上下文错误。
|
|
50
|
+
* Agent `dcgchat_message` / 出站 `target` 应为 `dcgSessionKey`(如 `agent:main:mobook:direct:...`)。
|
|
51
|
+
* `setParamsMessage` 的 key 与此一致;查不到 map 时回落到配置缺省(无会话级 messageId/sessionId)。
|
|
53
52
|
*/
|
|
54
53
|
export function getOutboundMsgParams(preferredKey: string): IMsgParams {
|
|
55
54
|
const k = preferredKey?.trim()
|