@dcrays/dcgchat-test 0.3.40 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dcrays/dcgchat-test",
3
- "version": "0.3.40",
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
@@ -12,7 +12,6 @@ import {
12
12
  } from './utils/global.js'
13
13
  import { resolveAccount, sendDcgchatMedia } from './channel.js'
14
14
  import { generateSignUrl } from './request/api.js'
15
- import { extractMobookFiles } from './utils/searchFile.js'
16
15
  import { sendChunk, sendFinal, sendText as sendTextMsg, sendError, wsSendRaw, sendText } from './transport.js'
17
16
  import { dcgLogger } from './utils/log.js'
18
17
  import { channelInfo, systemCommand, interruptCommand, ENV, ignoreToolCommand } from './utils/constant.js'
@@ -181,7 +180,7 @@ export async function handleDcgchatMessage(msg: InboundMessage, accountId: strin
181
180
  effectiveAgentId && effectiveAgentId !== 'main' ? config.agents?.list?.find((a) => a.id === effectiveAgentId) : undefined
182
181
  const agentDisplayName = agentEntry?.name ?? (effectiveAgentId && effectiveAgentId !== 'main' ? effectiveAgentId : undefined)
183
182
 
184
- const text = msg.content.text?.trim()
183
+ let text = msg.content.text?.trim()
185
184
 
186
185
  if (!text) {
187
186
  sendTextMsg('你需要我帮你做什么呢?', outboundCtx)
@@ -236,6 +235,12 @@ export async function handleDcgchatMessage(msg: InboundMessage, accountId: strin
236
235
  const getMediaKey = (url: string) => url.split(/[\\/]/).pop() ?? url
237
236
  let streamedTextLen = 0
238
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
+ }
239
244
  const prefixContext = createReplyPrefixContext({
240
245
  cfg: config,
241
246
  agentId: effectiveAgentId ?? '',
@@ -298,10 +303,9 @@ export async function handleDcgchatMessage(msg: InboundMessage, accountId: strin
298
303
  })
299
304
  } else if (interruptCommand.includes(text?.trim())) {
300
305
  dcgLogger(`interrupt command: ${text}`)
301
- sendFinal(outboundCtx, 'abort')
306
+ sendFinal({ ...outboundCtx, messageId: `${Date.now()}` }, 'abort')
302
307
  sendText('会话已终止', outboundCtx)
303
308
  sessionStreamSuppressed.add(dcgSessionKey)
304
-
305
309
  const abortOneSession = async (sessionKey: string) => {
306
310
  try {
307
311
  await sendGatewayRpc({ method: 'chat.abort', params: { sessionKey } })
@@ -309,7 +313,6 @@ export async function handleDcgchatMessage(msg: InboundMessage, accountId: strin
309
313
  dcgLogger(`chat.abort ${sessionKey}: ${String(e)}`, 'error')
310
314
  }
311
315
  }
312
-
313
316
  const keysToAbort = new Set<string>(getChildSessionKeysTrackedForRequester(dcgSessionKey))
314
317
  try {
315
318
  const listed = await sendGatewayRpc<{ sessions?: Array<{ key?: string }> }>({
@@ -327,17 +330,6 @@ export async function handleDcgchatMessage(msg: InboundMessage, accountId: strin
327
330
  await abortOneSession(sk)
328
331
  }
329
332
  await abortOneSession(dcgSessionKey)
330
-
331
- try {
332
- await sendGatewayRpc({
333
- method: 'sessions.reset',
334
- params: { key: dcgSessionKey, reason: 'reset' }
335
- })
336
- } catch (e) {
337
- dcgLogger(`sessions.reset: ${String(e)}`, 'error')
338
- }
339
-
340
- activeRunIdBySessionKey.delete(dcgSessionKey)
341
333
  streamChunkIdxBySessionKey.delete(dcgSessionKey)
342
334
  resetSubagentStateForRequesterSession(dcgSessionKey)
343
335
  setMsgStatus(dcgSessionKey, 'finished')
@@ -393,7 +385,7 @@ export async function handleDcgchatMessage(msg: InboundMessage, accountId: strin
393
385
  const prev = streamChunkIdxBySessionKey.get(dcgSessionKey) ?? 0
394
386
  streamChunkIdxBySessionKey.set(dcgSessionKey, prev + 1)
395
387
  sendChunk(delta, outboundCtx, prev)
396
- 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)}`)
397
389
  }
398
390
  streamedTextLen = payload.text.length
399
391
  } else {
@@ -425,9 +417,7 @@ export async function handleDcgchatMessage(msg: InboundMessage, accountId: strin
425
417
  const storePath = core.channel.session.resolveStorePath(config.session?.store)
426
418
  await waitUntilSubagentsIdle(dcgSessionKey, { timeoutMs: 600_000 })
427
419
  sendFinal(outboundCtx, 'end')
428
- dcgLogger(
429
- `record session route: rawTarget=${userId}, normalizedTarget=${dcgSessionKey}, updateLastRoute.to=${dcgSessionKey}, accountId=${route.accountId}`
430
- )
420
+ dcgLogger(`record session route: updateLastRoute.to=${dcgSessionKey}, accountId=${route.accountId}`)
431
421
  core.channel.session
432
422
  .recordInboundSession({
433
423
  storePath,
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
@@ -115,16 +156,25 @@ export const dcgchatPlugin: ChannelPlugin<ResolvedDcgchatAccount> = {
115
156
  enabled: { type: 'boolean' },
116
157
  wsUrl: { type: 'string' },
117
158
  botToken: { type: 'string' },
118
- userId: { type: 'string', description: 'WebSocket 连接参数 _userId,与 message 工具的 target(dcgSessionKey)无关' },
159
+ userId: { type: 'string', description: 'WebSocket 连接参数 _userId,与 dcgchat_message 工具的 target(dcgSessionKey)无关' },
119
160
  appId: { type: 'string' },
120
161
  domainId: { type: 'string' },
121
- 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
+ }
122
168
  }
123
169
  },
124
170
  uiHints: {
125
171
  userId: {
126
172
  label: 'WS 连接 _userId',
127
173
  help: '仅用于拼接网关 WebSocket URL 的查询参数,不是 Agent 发消息时的 target。发消息请使用 dcgSessionKey(与入站上下文 SessionKey 相同)。'
174
+ },
175
+ strictMessageToolTarget: {
176
+ label: '严格 message.target',
177
+ help: '开启后由通道目标解析层拒绝纯数字等非 sessionKey 的 target(推荐开启);关闭则与旧版行为一致。'
128
178
  }
129
179
  }
130
180
  },
@@ -134,8 +184,7 @@ export const dcgchatPlugin: ChannelPlugin<ResolvedDcgchatAccount> = {
134
184
  defaultAccountId: () => DEFAULT_ACCOUNT_ID,
135
185
  setAccountEnabled: ({ cfg, enabled }) => {
136
186
  const channelKey = "dcgchat-test"
137
- const prev =
138
- (cfg.channels?.[channelKey as keyof NonNullable<typeof cfg.channels>] as Record<string, unknown> | undefined) ?? {}
187
+ const prev = (cfg.channels?.[channelKey as keyof NonNullable<typeof cfg.channels>] as Record<string, unknown> | undefined) ?? {}
139
188
  return {
140
189
  ...cfg,
141
190
  channels: {
@@ -159,16 +208,26 @@ export const dcgchatPlugin: ChannelPlugin<ResolvedDcgchatAccount> = {
159
208
  messaging: {
160
209
  normalizeTarget: (raw) => raw || undefined,
161
210
  targetResolver: {
162
- looksLikeId: (raw) => Boolean(raw?.trim()),
163
- hint: 'dcgSessionKey(与 SessionKey 一致;勿填配置里的 WS userId'
211
+ looksLikeId: dcgchatMessageTargetLooksLikeId,
212
+ hint: '须为完整 dcgSessionKey(与 SessionKey 一致,形如 agent:…:mobook:direct:… 或 agent: 前缀路由键);禁止填 WS userId 等纯数字。可在通道配置 strictMessageToolTarget=false 关闭校验。'
164
213
  }
165
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
+ },
166
225
  agentPrompt: {
167
226
  messageToolHints: () => [
168
227
  '生成文件后,**尽可能不要**把文件路径、地址直接告诉用户。',
169
228
  '生成文件后,把文件名告诉用户。',
170
- '调用 message 工具时,target 必须填写 dcgSessionKey(即 SessionKey),绝不能填写 WS userId',
171
- '生成文件后,必须调用 message 工具发送文件,不可以直接在文本回复里包含文件路径、文件名、地址。'
229
+ '使用内置 `message` 或 `dcgchat_message` 发消息时,target 必须是完整 dcgSessionKey(与上下文 SessionKey 相同),禁止填 From、SenderId、WS userId 等纯数字。',
230
+ '生成文件后,须通过工具发送文件,勿在文本里直接输出路径或地址。'
172
231
  ]
173
232
  },
174
233
  outbound: {
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
@@ -349,7 +349,7 @@ export function monitoringToolMessage(api: OpenClawPluginApi) {
349
349
  dcgLogger(`工具调用结果: ~ event:${item.event} ${status}`)
350
350
  }
351
351
  }
352
- } else {
352
+ } else if (item.event !== 'before_tool_call') {
353
353
  dcgLogger(`工具调用结果: ~ event:${item.event} ~ 没有sessionKey 为执行`)
354
354
  }
355
355
  })
@@ -72,13 +72,14 @@ function isSafeFile(filepath: string) {
72
72
  }
73
73
 
74
74
  /**
75
- * 书灵墨宝 message 工具:须符合 OpenClaw `AgentTool`(execute 返回 `AgentToolResult`)。
75
+ * 书灵墨宝出站消息工具:须符合 OpenClaw `AgentTool`(execute 返回 `AgentToolResult`)。
76
+ * 工具名使用 `dcgchat_message`,避免与核心内置 `message` 冲突。
76
77
  * 通过注册时的 `OpenClawPluginToolContext.sessionKey` 出站,不再使用非标准的 `execute(args, ctx)`。
77
78
  */
78
79
  export function createDcgchatMessageTool(pluginCtx: DcgchatMessageToolContext): AnyAgentTool {
79
80
  return {
80
- name: 'message',
81
- label: 'message',
81
+ name: 'dcgchat_message',
82
+ label: 'dcgchat_message',
82
83
  description: `
83
84
  向用户发送消息。
84
85
  若传 target,target 必须是 sessionKey,不能是 userId。
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
@@ -47,9 +47,8 @@ export function getEffectiveMsgParams(sessionKey?: string): IMsgParams {
47
47
  }
48
48
 
49
49
  /**
50
- * Agent `message` 工具的 `target` 应为 `dcgSessionKey`(如 `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()