@dcrays/dcgchat-test 0.3.28 → 0.3.32

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.28",
3
+ "version": "0.3.32",
4
4
  "type": "module",
5
5
  "description": "OpenClaw channel plugin for 书灵墨宝 (WebSocket)",
6
6
  "main": "index.ts",
package/src/bot.ts CHANGED
@@ -166,7 +166,7 @@ export async function handleDcgchatMessage(msg: InboundMessage, accountId: strin
166
166
  const effectiveAgentId = embeddedAgentId ?? route.agentId
167
167
  const effectiveSessionKey = getSessionKey(msg.content, account.accountId)
168
168
 
169
- setParamsMessage(effectiveSessionKey, {
169
+ const mergedParams = {
170
170
  userId: msg._userId,
171
171
  botToken: msg.content.bot_token,
172
172
  sessionId: conversationId,
@@ -177,7 +177,14 @@ export async function handleDcgchatMessage(msg: InboundMessage, accountId: strin
177
177
  agentId: msg.content.agent_id ?? '',
178
178
  sessionKey: effectiveSessionKey,
179
179
  real_mobook
180
- })
180
+ }
181
+ setParamsMessage(effectiveSessionKey, mergedParams)
182
+ // 与 OpenClaw 会话投递里仍可能出现的 ctx.to=SenderId(userId)对齐,便于 getOutboundMsgParams 命中
183
+ dcgLogger(
184
+ `target normalize: rawTarget=${userId}, normalizedTarget=${effectiveSessionKey}, conversationId=${conversationId ?? ''}, messageId=${msg.content.message_id}`
185
+ )
186
+ setParamsMessage(userId, mergedParams)
187
+ dcgLogger(`target alias bound: aliasTarget=${userId} -> sessionKey=${effectiveSessionKey}`)
181
188
  const outboundCtx = getEffectiveMsgParams(effectiveSessionKey)
182
189
  const agentEntry =
183
190
  effectiveAgentId && effectiveAgentId !== 'main' ? config.agents?.list?.find((a) => a.id === effectiveAgentId) : undefined
@@ -245,6 +252,9 @@ export async function handleDcgchatMessage(msg: InboundMessage, accountId: strin
245
252
  OriginatingTo: effectiveSessionKey,
246
253
  ...mediaPayload
247
254
  })
255
+ dcgLogger(
256
+ `inbound context target: rawTarget=${userId}, normalizedTarget=${effectiveSessionKey}, ctx.To=${String(ctxPayload.To ?? '')}, ctx.SessionKey=${String(ctxPayload.SessionKey ?? '')}, ctx.OriginatingTo=${String(ctxPayload.OriginatingTo ?? '')}`
257
+ )
248
258
 
249
259
  const sentMediaKeys = new Set<string>()
250
260
  const getMediaKey = (url: string) => url.split(/[\\/]/).pop() ?? url
@@ -438,11 +448,21 @@ export async function handleDcgchatMessage(msg: InboundMessage, accountId: strin
438
448
 
439
449
  // Record session metadata
440
450
  const storePath = core.channel.session.resolveStorePath(config.session?.store)
451
+ dcgLogger(
452
+ `record session route: rawTarget=${userId}, normalizedTarget=${effectiveSessionKey}, updateLastRoute.to=${effectiveSessionKey}, accountId=${route.accountId}`
453
+ )
441
454
  core.channel.session
442
455
  .recordInboundSession({
443
456
  storePath,
444
457
  sessionKey: effectiveSessionKey,
445
458
  ctx: ctxPayload,
459
+ // 与 Telegram/Discord 等一致:写入 deliveryContext.to,否则投递可能回退为 From(userId),channel sendMedia 里 ctx.to 会变成数字 userId
460
+ updateLastRoute: {
461
+ sessionKey: effectiveSessionKey,
462
+ channel: "dcgchat-test",
463
+ to: effectiveSessionKey,
464
+ accountId: route.accountId
465
+ },
446
466
  onRecordError: (err) => {
447
467
  dcgLogger(` session record error: ${String(err)}`, 'error')
448
468
  }
package/src/channel.ts CHANGED
@@ -5,7 +5,7 @@ import { ossUpload } from './request/oss.js'
5
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 { getOutboundMsgParams } from './utils/params.js'
8
+ import { getOutboundMsgParams, getParamsMessage } from './utils/params.js'
9
9
  import { startDcgchatGatewaySocket } from './gateway/socket.js'
10
10
 
11
11
  export type DcgchatMediaSendOptions = {
@@ -15,8 +15,15 @@ export type DcgchatMediaSendOptions = {
15
15
  text?: string
16
16
  }
17
17
 
18
+ function normalizeSessionTarget(rawTo: string): string {
19
+ const cleaned = rawTo.replace('dcg-cron:', '').trim()
20
+ if (!cleaned) return ''
21
+ return getParamsMessage(cleaned)?.sessionKey?.trim() || cleaned
22
+ }
23
+
18
24
  export async function sendDcgchatMedia(opts: DcgchatMediaSendOptions): Promise<void> {
19
- const msgCtx = getOutboundMsgParams(opts.sessionKey ?? '')
25
+ const sessionKey = normalizeSessionTarget(opts.sessionKey ?? '')
26
+ const msgCtx = getOutboundMsgParams(sessionKey)
20
27
  if (!isWsOpen()) {
21
28
  dcgLogger(`outbound media skipped -> ws not open: ${opts.mediaUrl ?? ''}`)
22
29
  return
@@ -36,21 +43,20 @@ export async function sendDcgchatMedia(opts: DcgchatMediaSendOptions): Promise<v
36
43
 
37
44
  try {
38
45
  const botToken = msgCtx.botToken ?? getOpenClawConfig()?.channels?.["dcgchat-test"]?.botToken ?? ''
39
- console.log('🚀 ~ sendDcgchatMedia ~ botToken:', botToken)
40
46
  const url = opts.mediaUrl ? await ossUpload(opts.mediaUrl, botToken, 1) : ''
41
47
  wsSendRaw(msgCtx, {
42
48
  response: opts.text ?? '',
43
49
  message_tags: { source: 'file' },
44
50
  files: [{ url, name: fileName }]
45
51
  })
46
- dcgLogger(`dcgchat: sendMedia to user ${msgCtx.userId}, file=${fileName}`)
52
+ dcgLogger(`dcgchat: sendMedia session=${sessionKey}, file=${fileName}`)
47
53
  } catch (error) {
48
54
  wsSendRaw(msgCtx, {
49
55
  response: opts.text ?? '',
50
56
  message_tags: { source: 'file' },
51
57
  files: [{ url: opts.mediaUrl ?? '', name: fileName }]
52
58
  })
53
- dcgLogger(`dcgchat: error sendMedia to user ${msgCtx.userId}: ${String(error)}`, 'error')
59
+ dcgLogger(`dcgchat: error sendMedia session=${sessionKey}: ${String(error)}`, 'error')
54
60
  }
55
61
  }
56
62
 
@@ -101,11 +107,17 @@ export const dcgchatPlugin: ChannelPlugin<ResolvedDcgchatAccount> = {
101
107
  enabled: { type: 'boolean' },
102
108
  wsUrl: { type: 'string' },
103
109
  botToken: { type: 'string' },
104
- userId: { type: 'string' },
110
+ userId: { type: 'string', description: 'WebSocket 连接参数 _userId,与 message 工具的 target(effectiveSessionKey)无关' },
105
111
  appId: { type: 'string' },
106
112
  domainId: { type: 'string' },
107
113
  capabilities: { type: 'array', items: { type: 'string' } }
108
114
  }
115
+ },
116
+ uiHints: {
117
+ userId: {
118
+ label: 'WS 连接 _userId',
119
+ help: '仅用于拼接网关 WebSocket URL 的查询参数,不是 Agent 发消息时的 target。发消息请使用 effectiveSessionKey(与入站上下文 SessionKey 相同,格式如 agent:main:mobook:direct:…)。'
120
+ }
109
121
  }
110
122
  },
111
123
  config: {
@@ -135,26 +147,44 @@ export const dcgchatPlugin: ChannelPlugin<ResolvedDcgchatAccount> = {
135
147
  })
136
148
  },
137
149
  messaging: {
138
- normalizeTarget: (raw) => raw?.trim() || undefined,
150
+ normalizeTarget: (raw) => normalizeSessionTarget(raw ?? '') || undefined,
139
151
  targetResolver: {
140
152
  looksLikeId: (raw) => Boolean(raw?.trim()),
141
- hint: 'userId'
153
+ hint: 'effectiveSessionKey(与 SessionKey 一致;勿填配置里的 WS userId'
142
154
  }
143
155
  },
156
+ agentPrompt: {
157
+ messageToolHints: () => [
158
+ '书灵墨宝:message 工具的 target 必须填 effectiveSessionKey(与当前会话 SessionKey / OriginatingTo 相同),形如 agent:main:mobook:direct:<agent_id>:<session_id>;不要填 channels.dcgchat.userId 或纯数字 userId。',
159
+ 'OpenClaw 自带的 target 字段说明里仍可能出现 “user id”,在本频道请忽略该字样,一律按 effectiveSessionKey 理解。'
160
+ ]
161
+ },
144
162
  outbound: {
145
163
  deliveryMode: 'direct',
164
+ resolveTarget: ({ to }) => {
165
+ const normalized = normalizeSessionTarget(to ?? '')
166
+ if (!normalized) {
167
+ return { ok: false, error: new Error('target is empty') }
168
+ }
169
+ return { ok: true, to: normalized }
170
+ },
146
171
  textChunkLimit: 4000,
147
172
  sendText: async (ctx) => {
148
173
  const isCron = ctx.to.indexOf('dcg-cron:') >= 0
149
- const to = ctx.to.replace('dcg-cron:', '')
174
+ const to = normalizeSessionTarget(ctx.to)
150
175
  dcgLogger(`channel sendText to ${ctx.to} `)
151
176
  const outboundCtx = getOutboundMsgParams(to)
152
177
  const cronMsgId = getCronMessageId(to)
153
178
  const messageId = !!cronMsgId ? cronMsgId : isCron ? `${Date.now()}` : outboundCtx?.messageId
179
+ const content: Record<string, unknown> = { response: ctx.text }
180
+ if (isCron || !outboundCtx?.messageId) {
181
+ content.is_finish = -1
182
+ content.message_tags = { source: 'cron' }
183
+ }
154
184
  if (isWsOpen()) {
155
185
  if (outboundCtx?.sessionId) {
156
186
  const newCtx = { ...outboundCtx, messageId }
157
- wsSendRaw(newCtx, { response: ctx.text, is_finish: -1, message_tags: { source: 'channel' } })
187
+ wsSendRaw(newCtx, content)
158
188
  } else {
159
189
  const sessionInfo = to.split(':')
160
190
  const sessionId = sessionInfo.at(-1) ?? ''
@@ -163,10 +193,9 @@ export const dcgchatPlugin: ChannelPlugin<ResolvedDcgchatAccount> = {
163
193
  agentId: agentId,
164
194
  sessionId: `${sessionId}`,
165
195
  messageId: messageId,
166
- is_finish: -1,
167
196
  real_mobook: !sessionId ? 1 : ''
168
197
  })
169
- wsSendRaw(merged, { response: ctx.text, message_tags: { source: 'channel' } })
198
+ wsSendRaw(merged, content)
170
199
  }
171
200
  }
172
201
  return {
@@ -176,17 +205,29 @@ export const dcgchatPlugin: ChannelPlugin<ResolvedDcgchatAccount> = {
176
205
  }
177
206
  },
178
207
  sendMedia: async (ctx) => {
179
- const to = ctx.to.replace('dcg-cron:', '')
180
- const msgCtx = getOutboundMsgParams(to)
181
- const cronMsgId = getCronMessageId(to)
182
208
  const isCron = ctx.to.indexOf('dcg-cron:') >= 0
183
- const messageId = !!cronMsgId ? cronMsgId : isCron ? `${Date.now()}` : msgCtx?.messageId
209
+ const to = normalizeSessionTarget(ctx.to)
210
+ const outboundCtx = getOutboundMsgParams(to)
211
+ const msgCtx = getParamsMessage(to) ?? outboundCtx
212
+ const cronMsgId = getCronMessageId(to)
213
+ const fallbackMessageId = `${Date.now()}`
214
+ const messageId = cronMsgId || (isCron ? fallbackMessageId : msgCtx?.messageId || fallbackMessageId)
215
+
216
+ if (!outboundCtx?.sessionId) {
217
+ dcgLogger(`channel sendMedia to ${ctx.to} -> sessionId not found`, 'error')
218
+ return {
219
+ channel: "dcgchat-test",
220
+ messageId,
221
+ chatId: to || ''
222
+ }
223
+ }
224
+
184
225
  dcgLogger(`channel sendMedia to ${ctx.to}`)
185
- await sendDcgchatMedia({ sessionKey: to ?? '', mediaUrl: ctx.mediaUrl ?? '' })
226
+ await sendDcgchatMedia({ sessionKey: to || '', mediaUrl: ctx.mediaUrl || '' })
186
227
  return {
187
228
  channel: "dcgchat-test",
188
- messageId: `${messageId}`,
189
- chatId: msgCtx.userId?.toString()
229
+ messageId,
230
+ chatId: to || ''
190
231
  }
191
232
  }
192
233
  },
package/src/cron.ts CHANGED
@@ -6,7 +6,7 @@ import { getCronMessageId, getWorkspaceDir, getWsConnection, removeCronMessageId
6
6
  import { ossUpload } from './request/oss.js'
7
7
  import { dcgLogger } from './utils/log.js'
8
8
  import { sendMessageToGateway } from './gateway/socket.js'
9
- import { getEffectiveMsgParams } from './utils/params.js'
9
+ import { getEffectiveMsgParams, getParamsDefaults } from './utils/params.js'
10
10
 
11
11
  export function getCronJobsPath(): string {
12
12
  const workspaceDir = getWorkspaceDir()
@@ -51,17 +51,27 @@ const CRON_UPLOAD_DEBOUNCE_MS = 6600
51
51
  let pendingCronUploadCtx: IMsgParams | null = null
52
52
  let cronUploadFlushTimer: ReturnType<typeof setTimeout> | null = null
53
53
 
54
- async function runCronJobsUpload(msgCtx: IMsgParams): Promise<void> {
54
+ async function runCronJobsUpload(sessionKey: string): Promise<void> {
55
55
  const jobPath = getCronJobsPath()
56
+ const botToken = getParamsDefaults().botToken
56
57
  if (fs.existsSync(jobPath)) {
57
58
  try {
58
- const url = await ossUpload(jobPath, msgCtx.botToken ?? '', 0)
59
+ const url = await ossUpload(jobPath, botToken ?? '', 0)
59
60
  dcgLogger(`定时任务创建成功: ${url}`)
60
- if (!msgCtx.sessionKey) {
61
- dcgLogger(`runCronJobsUpload: missing sessionKey ${JSON.stringify(msgCtx)} on msgCtx`, 'error')
61
+ if (!sessionKey) {
62
+ dcgLogger(`runCronJobsUpload: missing sessionKey on msgCtx`, 'error')
62
63
  return
63
64
  }
64
- sendEventMessage(url, msgCtx.sessionKey)
65
+ const sessionInfo = sessionKey.split(':')
66
+ const sessionId = sessionInfo.at(-1) ?? ''
67
+ const agentId = sessionInfo.at(-2) ?? ''
68
+ const params = {
69
+ event_type: 'cron',
70
+ operation_type: 'install',
71
+ session_id: sessionId,
72
+ agent_id: agentId
73
+ }
74
+ sendEventMessage(url, params)
65
75
  } catch (error) {
66
76
  dcgLogger(`${jobPath} upload failed: ${error}`, 'error')
67
77
  }
@@ -70,14 +80,6 @@ async function runCronJobsUpload(msgCtx: IMsgParams): Promise<void> {
70
80
  }
71
81
  }
72
82
 
73
- function flushCronUploadQueue(): void {
74
- cronUploadFlushTimer = null
75
- const ctx = pendingCronUploadCtx
76
- pendingCronUploadCtx = null
77
- if (!ctx) return
78
- void runCronJobsUpload(ctx)
79
- }
80
-
81
83
  /**
82
84
  * 将 jobs.json 同步到 OSS 并推送事件。30s 内多次调用合并为一次上传;定时触发后清空待处理项,避免重复执行。
83
85
  * @param msgCtx 可选;省略时使用当前会话 getEffectiveMsgParams(sessionKey) 快照
@@ -85,17 +87,17 @@ function flushCronUploadQueue(): void {
85
87
  export function sendDcgchatCron(jobId: string): void {
86
88
  const jobPath = getCronJobsPath()
87
89
  const { sessionKey } = readCronJob(jobPath, jobId) || {}
88
- const ctx = msgParamsToCtx(getEffectiveMsgParams(sessionKey))
89
- if (!ctx) {
90
+ if (!sessionKey) {
90
91
  dcgLogger('sendDcgchatCron: no message context (missing token / params)', 'error')
91
92
  return
92
93
  }
93
94
  dcgLogger(`sessionKey: ${sessionKey}, jobId: ${jobId}`)
94
- pendingCronUploadCtx = ctx
95
95
  if (cronUploadFlushTimer !== null) {
96
96
  clearTimeout(cronUploadFlushTimer)
97
97
  }
98
- cronUploadFlushTimer = setTimeout(flushCronUploadQueue, CRON_UPLOAD_DEBOUNCE_MS)
98
+ cronUploadFlushTimer = setTimeout(() => {
99
+ runCronJobsUpload(sessionKey)
100
+ }, CRON_UPLOAD_DEBOUNCE_MS)
99
101
  }
100
102
 
101
103
  /**
@@ -363,10 +363,10 @@ export class GatewayConnection {
363
363
  if (msg.event === 'cron') {
364
364
  dcgLogger(`[Gateway] 收到事件: ${JSON.stringify(msg)}`)
365
365
  if (msg.payload?.action === 'added') {
366
- sendDcgchatCron()
366
+ sendDcgchatCron(msg.payload?.jobId)
367
367
  }
368
368
  if (msg.payload?.action === 'updated') {
369
- sendDcgchatCron()
369
+ sendDcgchatCron(msg.payload?.jobId as string)
370
370
  }
371
371
  if (msg.payload?.action === 'finished') {
372
372
  finishedDcgchatCron(msg.payload?.jobId as string)
@@ -40,7 +40,7 @@ export const queryUserTokenByBotToken = async (botToken: string): Promise<string
40
40
  const response = await post<{ botToken: string }, { token: string }>('/organization/queryUserTokenByBotToken', { botToken })
41
41
 
42
42
  if (!response || !response.data || !response.data.token) {
43
- dcgLogger('获取绑定的用户信息失败: ' + JSON.stringify(response), 'error')
43
+ dcgLogger('获取绑定的用户信息失败: token:' + botToken + '|' + JSON.stringify(response), 'error')
44
44
  return ''
45
45
  }
46
46
 
package/src/transport.ts CHANGED
@@ -178,8 +178,8 @@ export function sendError(errorMsg: string, ctx: IMsgParams): boolean {
178
178
  return wsSend(ctx, { response: `[错误] ${errorMsg}`, state: 'final' })
179
179
  }
180
180
 
181
- export function sendEventMessage(url: string, sessionKey: string) {
182
- const ctx = getEffectiveMsgParams(sessionKey)
181
+ export function sendEventMessage(url: string, params: Record<string, string> = {}) {
182
+ const ctx = getParamsDefaults()
183
183
  const ws = getWsConnection()
184
184
  if (isWsOpen()) {
185
185
  ws?.send(
@@ -187,16 +187,12 @@ export function sendEventMessage(url: string, sessionKey: string) {
187
187
  messageType: 'openclaw_bot_event',
188
188
  source: 'client',
189
189
  content: {
190
- event_type: 'cron',
191
- operation_type: 'install',
192
190
  bot_token: ctx.botToken,
193
191
  domain_id: ctx.domainId,
194
192
  app_id: ctx.appId,
195
193
  oss_url: url,
196
194
  bot_id: ctx.botId,
197
- agent_id: ctx.agentId,
198
- session_id: ctx.sessionId,
199
- message_id: Date.now().toString()
195
+ ...params
200
196
  }
201
197
  })
202
198
  )
@@ -9,7 +9,7 @@ const paramsMessageMap = new Map<string, IMsgParams>()
9
9
 
10
10
  /** 从 OpenClaw 配置读取当前 channel 的基础参数(唯一来源,供 transport / resolve 等复用) */
11
11
  export function getParamsDefaults(): IMsgParams {
12
- const ch = (getOpenClawConfig()?.channels?.['dcgchat'] as DcgchatConfig | undefined) ?? {}
12
+ const ch = (getOpenClawConfig()?.channels?.["dcgchat-test"] as DcgchatConfig | undefined) ?? {}
13
13
  return {
14
14
  userId: Number(ch.userId ?? 0),
15
15
  botToken: ch.botToken ?? '',
@@ -47,8 +47,8 @@ export function getEffectiveMsgParams(sessionKey?: string): IMsgParams {
47
47
  }
48
48
 
49
49
  /**
50
- * Agent `message` 工具常把 `target` 设为用户 ID(如 "150"),而 `setParamsMessage` 使用的 key 是
51
- * `effectiveSessionKey`(如 `agent:main:mobook:direct:...`)。若按 preferredKey 查不到 map,
50
+ * Agent `message` 工具的 `target` 应为 `effectiveSessionKey`(如 `agent:main:mobook:direct:...`)。
51
+ * `setParamsMessage` 使用的 key 与此一致。若按 preferredKey 查不到 map,
52
52
  * 则回落到当前会话 `currentSessionKey`,避免拿到空 `messageId` / `sessionId` 导致无文件卡片、WS 上下文错误。
53
53
  */
54
54
  export function getOutboundMsgParams(preferredKey: string): IMsgParams {