@dcrays/dcgchat 0.3.30 → 0.3.33

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",
3
- "version": "0.3.30",
3
+ "version": "0.3.33",
4
4
  "type": "module",
5
5
  "description": "OpenClaw channel plugin for 书灵墨宝 (WebSocket)",
6
6
  "main": "index.ts",
@@ -42,4 +42,4 @@
42
42
  "defaultChoice": "npm"
43
43
  }
44
44
  }
45
- }
45
+ }
package/src/bot.ts CHANGED
@@ -180,8 +180,11 @@ export async function handleDcgchatMessage(msg: InboundMessage, accountId: strin
180
180
  }
181
181
  setParamsMessage(effectiveSessionKey, mergedParams)
182
182
  // 与 OpenClaw 会话投递里仍可能出现的 ctx.to=SenderId(userId)对齐,便于 getOutboundMsgParams 命中
183
- console.log('🚀 ~ handleDcgchatMessage ~ mergedParams:', mergedParams)
183
+ dcgLogger(
184
+ `target normalize: rawTarget=${userId}, normalizedTarget=${effectiveSessionKey}, conversationId=${conversationId ?? ''}, messageId=${msg.content.message_id}`
185
+ )
184
186
  setParamsMessage(userId, mergedParams)
187
+ dcgLogger(`target alias bound: aliasTarget=${userId} -> sessionKey=${effectiveSessionKey}`)
185
188
  const outboundCtx = getEffectiveMsgParams(effectiveSessionKey)
186
189
  const agentEntry =
187
190
  effectiveAgentId && effectiveAgentId !== 'main' ? config.agents?.list?.find((a) => a.id === effectiveAgentId) : undefined
@@ -249,6 +252,9 @@ export async function handleDcgchatMessage(msg: InboundMessage, accountId: strin
249
252
  OriginatingTo: effectiveSessionKey,
250
253
  ...mediaPayload
251
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
+ )
252
258
 
253
259
  const sentMediaKeys = new Set<string>()
254
260
  const getMediaKey = (url: string) => url.split(/[\\/]/).pop() ?? url
@@ -442,6 +448,9 @@ export async function handleDcgchatMessage(msg: InboundMessage, accountId: strin
442
448
 
443
449
  // Record session metadata
444
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
+ )
445
454
  core.channel.session
446
455
  .recordInboundSession({
447
456
  storePath,
package/src/channel.ts CHANGED
@@ -2,7 +2,7 @@ import type { ChannelPlugin, OpenClawConfig } from 'openclaw/plugin-sdk'
2
2
  import { DEFAULT_ACCOUNT_ID } from 'openclaw/plugin-sdk'
3
3
  import type { ResolvedDcgchatAccount, DcgchatConfig } from './types.js'
4
4
  import { ossUpload } from './request/oss.js'
5
- import { addSentMediaKey, getCronMessageId, getOpenClawConfig, hasSentMediaKey } from './utils/global.js'
5
+ import { addSentMediaKey, getCronMessageId, getInfoBySessionKey, 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
8
  import { getOutboundMsgParams, getParamsMessage } from './utils/params.js'
@@ -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"]?.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 session=${opts.sessionKey}, 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 session=${opts.sessionKey}: ${String(error)}`, 'error')
59
+ dcgLogger(`dcgchat: error sendMedia session=${sessionKey}: ${String(error)}`, 'error')
54
60
  }
55
61
  }
56
62
 
@@ -141,7 +147,7 @@ export const dcgchatPlugin: ChannelPlugin<ResolvedDcgchatAccount> = {
141
147
  })
142
148
  },
143
149
  messaging: {
144
- normalizeTarget: (raw) => raw?.trim() || undefined,
150
+ normalizeTarget: (raw) => raw || undefined,
145
151
  targetResolver: {
146
152
  looksLikeId: (raw) => Boolean(raw?.trim()),
147
153
  hint: 'effectiveSessionKey(与 SessionKey 一致;勿填配置里的 WS userId)'
@@ -155,30 +161,41 @@ export const dcgchatPlugin: ChannelPlugin<ResolvedDcgchatAccount> = {
155
161
  },
156
162
  outbound: {
157
163
  deliveryMode: 'direct',
164
+ resolveTarget: ({ to }) => {
165
+ if (!to) {
166
+ return { ok: false, error: new Error('target is empty') }
167
+ }
168
+ return { ok: true, to: to }
169
+ },
158
170
  textChunkLimit: 4000,
159
171
  sendText: async (ctx) => {
160
- const isCron = ctx.to.indexOf('dcg-cron:') >= 0
161
- const to = ctx.to.replace('dcg-cron:', '')
162
172
  dcgLogger(`channel sendText to ${ctx.to} `)
163
- const outboundCtx = getOutboundMsgParams(to)
164
- const cronMsgId = getCronMessageId(to)
165
- const messageId = !!cronMsgId ? cronMsgId : isCron ? `${Date.now()}` : outboundCtx?.messageId
173
+ let messageId = ''
174
+ const to = normalizeSessionTarget(ctx.to)
166
175
  if (isWsOpen()) {
167
- if (outboundCtx?.sessionId) {
168
- const newCtx = { ...outboundCtx, messageId }
169
- wsSendRaw(newCtx, { response: ctx.text, is_finish: -1, message_tags: { source: 'channel' } })
170
- } else {
171
- const sessionInfo = to.split(':')
172
- const sessionId = sessionInfo.at(-1) ?? ''
173
- const agentId = sessionInfo.at(-2) ?? ''
176
+ const isCron = ctx.to.indexOf('dcg-cron:') >= 0
177
+ const outboundCtx = getOutboundMsgParams(to)
178
+ const content: Record<string, unknown> = { response: ctx.text }
179
+ if (isCron) {
180
+ messageId = getCronMessageId(to) || `${Date.now()}`
181
+ const { sessionId, agentId } = getInfoBySessionKey(to)
182
+ content.is_finish = -1
183
+ content.message_tags = { source: 'cron' }
174
184
  const merged = mergeDefaultParams({
175
185
  agentId: agentId,
176
186
  sessionId: `${sessionId}`,
177
187
  messageId: messageId,
178
- is_finish: -1,
179
188
  real_mobook: !sessionId ? 1 : ''
180
189
  })
181
- wsSendRaw(merged, { response: ctx.text, message_tags: { source: 'channel' } })
190
+ wsSendRaw(merged, content)
191
+ } else {
192
+ if (outboundCtx?.sessionId) {
193
+ messageId = outboundCtx?.messageId || `${Date.now()}`
194
+ const newCtx = { ...outboundCtx, messageId }
195
+ wsSendRaw(newCtx, content)
196
+ } else {
197
+ dcgLogger(`channel sendText to ${ctx.to} -> sessionId not found`, 'error')
198
+ }
182
199
  }
183
200
  }
184
201
  return {
@@ -189,7 +206,7 @@ export const dcgchatPlugin: ChannelPlugin<ResolvedDcgchatAccount> = {
189
206
  },
190
207
  sendMedia: async (ctx) => {
191
208
  const isCron = ctx.to.indexOf('dcg-cron:') >= 0
192
- const to = ctx.to.replace('dcg-cron:', '')
209
+ const to = normalizeSessionTarget(ctx.to)
193
210
  const outboundCtx = getOutboundMsgParams(to)
194
211
  const msgCtx = getParamsMessage(to) ?? outboundCtx
195
212
  const cronMsgId = getCronMessageId(to)
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()
@@ -45,23 +45,31 @@ function msgParamsToCtx(p: IMsgParams): IMsgParams | null {
45
45
  return p
46
46
  }
47
47
 
48
- const CRON_UPLOAD_DEBOUNCE_MS = 6600
48
+ const CRON_UPLOAD_DEBOUNCE_MS = 2400
49
49
 
50
- /** 待合并的上传上下文(短时间内多次调用只保留最后一次) */
51
- let pendingCronUploadCtx: IMsgParams | null = null
52
50
  let cronUploadFlushTimer: ReturnType<typeof setTimeout> | null = null
53
51
 
54
- async function runCronJobsUpload(msgCtx: IMsgParams): Promise<void> {
52
+ async function runCronJobsUpload(sessionKey: string): Promise<void> {
55
53
  const jobPath = getCronJobsPath()
54
+ const botToken = getParamsDefaults().botToken
56
55
  if (fs.existsSync(jobPath)) {
57
56
  try {
58
- const url = await ossUpload(jobPath, msgCtx.botToken ?? '', 0)
57
+ const url = await ossUpload(jobPath, botToken ?? '', 0)
59
58
  dcgLogger(`定时任务创建成功: ${url}`)
60
- if (!msgCtx.sessionKey) {
61
- dcgLogger(`runCronJobsUpload: missing sessionKey ${JSON.stringify(msgCtx)} on msgCtx`, 'error')
59
+ if (!sessionKey) {
60
+ dcgLogger(`runCronJobsUpload: missing sessionKey on msgCtx`, 'error')
62
61
  return
63
62
  }
64
- sendEventMessage(url, msgCtx.sessionKey)
63
+ const sessionInfo = sessionKey.split(':')
64
+ const sessionId = sessionInfo.at(-1) ?? ''
65
+ const agentId = sessionInfo.at(-2) ?? ''
66
+ const params = {
67
+ event_type: 'cron',
68
+ operation_type: 'install',
69
+ session_id: sessionId,
70
+ agent_id: agentId
71
+ }
72
+ sendEventMessage(url, params)
65
73
  } catch (error) {
66
74
  dcgLogger(`${jobPath} upload failed: ${error}`, 'error')
67
75
  }
@@ -70,14 +78,6 @@ async function runCronJobsUpload(msgCtx: IMsgParams): Promise<void> {
70
78
  }
71
79
  }
72
80
 
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
81
  /**
82
82
  * 将 jobs.json 同步到 OSS 并推送事件。30s 内多次调用合并为一次上传;定时触发后清空待处理项,避免重复执行。
83
83
  * @param msgCtx 可选;省略时使用当前会话 getEffectiveMsgParams(sessionKey) 快照
@@ -85,17 +85,17 @@ function flushCronUploadQueue(): void {
85
85
  export function sendDcgchatCron(jobId: string): void {
86
86
  const jobPath = getCronJobsPath()
87
87
  const { sessionKey } = readCronJob(jobPath, jobId) || {}
88
- const ctx = msgParamsToCtx(getEffectiveMsgParams(sessionKey))
89
- if (!ctx) {
88
+ if (!sessionKey) {
90
89
  dcgLogger('sendDcgchatCron: no message context (missing token / params)', 'error')
91
90
  return
92
91
  }
93
92
  dcgLogger(`sessionKey: ${sessionKey}, jobId: ${jobId}`)
94
- pendingCronUploadCtx = ctx
95
93
  if (cronUploadFlushTimer !== null) {
96
94
  clearTimeout(cronUploadFlushTimer)
97
95
  }
98
- cronUploadFlushTimer = setTimeout(flushCronUploadQueue, CRON_UPLOAD_DEBOUNCE_MS)
96
+ cronUploadFlushTimer = setTimeout(() => {
97
+ runCronJobsUpload(sessionKey)
98
+ }, CRON_UPLOAD_DEBOUNCE_MS)
99
99
  }
100
100
 
101
101
  /**
@@ -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
  )
@@ -143,3 +143,8 @@ export function getCronMessageId(sk: string): string {
143
143
  export function removeCronMessageId(sk: string) {
144
144
  cronMessageIdMap.delete(sk)
145
145
  }
146
+
147
+ export function getInfoBySessionKey(sk: string): { sessionId: string; agentId: string } {
148
+ const sessionInfo = sk.split(':')
149
+ return { sessionId: sessionInfo.at(-1) ?? '', agentId: sessionInfo.at(-2) ?? '' }
150
+ }
@@ -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"] as DcgchatConfig | undefined) ?? {}
13
13
  return {
14
14
  userId: Number(ch.userId ?? 0),
15
15
  botToken: ch.botToken ?? '',