@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 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 { startDcgchatGatewaySocket } from './src/gateway/socket.js'
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dcrays/dcgchat-test",
3
- "version": "0.3.39",
3
+ "version": "0.3.41",
4
4
  "type": "module",
5
5
  "description": "OpenClaw channel plugin for 书灵墨宝 (WebSocket)",
6
6
  "main": "index.ts",
package/src/bot.ts CHANGED
@@ -1,8 +1,6 @@
1
1
  import path from 'node:path'
2
- import fs from 'node:fs'
3
- import os from 'node:os'
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
- console.log('typing start error', err)
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 effectiveSessionKey = getSessionKey(msg.content, account.accountId)
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: effectiveSessionKey,
173
+ sessionKey: dcgSessionKey,
179
174
  real_mobook
180
175
  }
181
- setParamsMessage(effectiveSessionKey, mergedParams)
182
- setParamsMessage(userId, mergedParams)
183
- dcgLogger(`target alias bound: aliasTarget=${userId} -> sessionKey=${effectiveSessionKey}`)
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
- const text = msg.content.text?.trim()
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: effectiveSessionKey,
222
- SessionKey: effectiveSessionKey,
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: effectiveSessionKey,
235
- Target: effectiveSessionKey,
236
- SourceTarget: effectiveSessionKey,
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(effectiveSessionKey)) return
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: effectiveSessionKey, mediaUrl, text: '' })
263
+ await sendDcgchatMedia({ sessionKey: dcgSessionKey, mediaUrl, text: '' })
264
264
  }
265
265
  },
266
266
  onError: (err: unknown, info: { kind: string }) => {
267
- setMsgStatus(effectiveSessionKey, 'finished')
267
+ setMsgStatus(dcgSessionKey, 'finished')
268
268
  sendFinal(outboundCtx, 'error')
269
- dcgLogger(`${info.kind} reply failed: ${String(err)}`, 'error')
270
- activeRunIdBySessionKey.delete(effectiveSessionKey)
271
- streamChunkIdxBySessionKey.delete(effectiveSessionKey)
272
- if (sessionStreamSuppressed.has(effectiveSessionKey)) {
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(effectiveSessionKey)
286
- streamChunkIdxBySessionKey.set(effectiveSessionKey, 0)
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(effectiveSessionKey, runId)
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(effectiveSessionKey)
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: effectiveSessionKey, limit: 256 }
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(effectiveSessionKey)
339
-
340
- try {
341
- await sendGatewayRpc({
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(effectiveSessionKey)
337
+ clearParamsMessage(dcgSessionKey)
355
338
  clearParamsMessage(userId)
356
339
 
357
340
  sendFinal(outboundCtx, 'stop')
358
341
  return
359
342
  } else {
360
- const params = getEffectiveMsgParams(effectiveSessionKey)
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(effectiveSessionKey, runId)
371
+ activeRunIdBySessionKey.set(dcgSessionKey, runId)
389
372
  },
390
373
  onPartialReply: async (payload: ReplyPayload) => {
391
- if (sessionStreamSuppressed.has(effectiveSessionKey)) return
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(effectiveSessionKey) ?? 0
403
- streamChunkIdxBySessionKey.set(effectiveSessionKey, prev + 1)
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=${msg._userId} ${delta.slice(0, 100)}`)
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: effectiveSessionKey, mediaUrl, text: '' })
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(effectiveSessionKey)) {
430
- sessionStreamSuppressed.delete(effectiveSessionKey)
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(effectiveSessionKey, { timeoutMs: 600_000 })
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: effectiveSessionKey,
424
+ sessionKey: dcgSessionKey,
444
425
  ctx: ctxPayload,
445
426
  updateLastRoute: {
446
- sessionKey: effectiveSessionKey,
427
+ sessionKey: dcgSessionKey,
447
428
  channel: "dcgchat-test",
448
- to: effectiveSessionKey,
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
- reload: { configPrefixes: ['channels.dcgchat'] },
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,与 message 工具的 target(effectiveSessionKey)无关' },
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。发消息请使用 effectiveSessionKey(与入站上下文 SessionKey 相同,格式如 agent:main:mobook:direct:<agent_id>:<session_id>)。'
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
- ...cfg,
136
- channels: {
137
- ...cfg.channels,
138
- dcgchat: {
139
- ...(cfg.channels?.["dcgchat-test"] as Record<string, unknown> | undefined),
140
- enabled
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: (raw) => Boolean(raw?.trim()),
160
- hint: 'effectiveSessionKey(与 SessionKey 一致;勿填配置里的 WS userId'
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
- '生成文件后,必须调用 message 工具发送文件,不可以直接在文本回复里包含文件路径、文件名、地址。'
229
+ '使用内置 `message` 或 `dcgchat_message` 发消息时,target 必须是完整 dcgSessionKey(与上下文 SessionKey 相同),禁止填 From、SenderId、WS userId 等纯数字。',
230
+ '生成文件后,须通过工具发送文件,勿在文本里直接输出路径或地址。'
168
231
  ]
169
232
  },
170
233
  outbound: {
@@ -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 连接失败11: ${e}`, 'error')
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 = null
234
- void connectPersistentGateway()
233
+ startupConnectTimer = setTimeout(() => {
234
+ startupConnectTimer = null
235
+ void connectPersistentGateway()
236
+ }, 0)
235
237
  }
236
238
 
237
239
  /**
package/src/monitor.ts CHANGED
@@ -123,7 +123,6 @@ export async function monitorDcgchatProvider(opts: MonitorDcgchatOpts): Promise<
123
123
  dcgLogger(`用户消息content字段解析失败: invalid JSON received`, 'error')
124
124
  return
125
125
  }
126
-
127
126
  await handleParsedWsMessage(parsed, payloadStr, account.accountId)
128
127
  })
129
128
 
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
- * 书灵墨宝 message 工具:须符合 OpenClaw `AgentTool`(execute 返回 `AgentToolResult`)。
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: 'message',
74
- label: 'message',
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 (isWsOpen()) {
156
- ws?.send(JSON.stringify(buildOpenclawBotChat(ctx, content, { mergeChannelDefaults: true })))
157
- if (isLog) {
158
- dcgLogger('已发送:' + JSON.stringify(buildOpenclawBotChat(ctx, content, { mergeChannelDefaults: true })))
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 (outboundCtx.sessionId && pl.data?.delta) sendChunk(pl.data.delta as string, outboundCtx, 0)
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
@@ -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
- import { createPluginRuntimeStore, type OpenClawConfig, type PluginRuntime } from 'openclaw/plugin-sdk'
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 anentCode = agent_clone_code || 'main'
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:${anentCode}:mobook:direct:${agent_id}:${session_id}`.toLowerCase()
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 } {
@@ -47,9 +47,8 @@ export function getEffectiveMsgParams(sessionKey?: string): IMsgParams {
47
47
  }
48
48
 
49
49
  /**
50
- * Agent `message` 工具的 `target` 应为 `effectiveSessionKey`(如 `agent:main:mobook:direct:...`)。
51
- * `setParamsMessage` 使用的 key 与此一致。若按 preferredKey 查不到 map,
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()