@dcrays/dcgchat-test 0.3.0 → 0.3.1

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.0",
3
+ "version": "0.3.1",
4
4
  "type": "module",
5
5
  "description": "OpenClaw channel plugin for 书灵墨宝 (WebSocket)",
6
6
  "main": "index.ts",
package/src/bot.ts CHANGED
@@ -3,21 +3,15 @@ import path from 'node:path'
3
3
  import type { ReplyPayload } from 'openclaw/plugin-sdk'
4
4
  import { createReplyPrefixContext } from 'openclaw/plugin-sdk'
5
5
  import type { InboundMessage } from './types.js'
6
- import {
7
- clearSentMediaKeys,
8
- getDcgchatRuntime,
9
- getOpenClawConfig,
10
- getWorkspaceDir,
11
- setMsgParamsSessionKey,
12
- setMsgStatus
13
- } from './utils/global.js'
6
+ import { clearSentMediaKeys, getDcgchatRuntime, getOpenClawConfig, getWorkspaceDir, setMsgStatus } from './utils/global.js'
14
7
  import { resolveAccount, sendDcgchatMedia } from './channel.js'
15
8
  import { generateSignUrl } from './request/api.js'
16
9
  import { extractMobookFiles } from './utils/searchFile.js'
17
- import { createMsgContext, sendChunk, sendFinal, sendText as sendTextMsg, sendError, sendText } from './transport.js'
10
+ import { sendChunk, sendFinal, sendText as sendTextMsg, sendError } from './transport.js'
18
11
  import { dcgLogger } from './utils/log.js'
19
12
  import { channelInfo, systemCommand, interruptCommand, ENV } from './utils/constant.js'
20
13
  import { sendMessageToGateway } from './gateway/socket.js'
14
+ import { getEffectiveMsgParams, setParamsMessage } from './utils/params.js'
21
15
 
22
16
  type MediaInfo = {
23
17
  path: string
@@ -33,15 +27,6 @@ const mediaMaxBytes = 300 * 1024 * 1024
33
27
  /** Active LLM generation abort controllers, keyed by conversationId */
34
28
  // const activeGenerations = new Map<string, AbortController>()
35
29
 
36
- // /** Abort an in-progress LLM generation for a given conversationId */
37
- // export function abortMobookappGeneration(conversationId: string): void {
38
- // const ctrl = activeGenerations.get(conversationId)
39
- // if (ctrl) {
40
- // ctrl.abort()
41
- // activeGenerations.delete(conversationId)
42
- // }
43
- // }
44
-
45
30
  /**
46
31
  * Extract agentId from conversation_id formatted as "agentId::suffix".
47
32
  * Returns null if the conversation_id does not contain the "::" separator.
@@ -132,16 +117,8 @@ function resolveReplyMediaList(payload: ReplyPayload): string[] {
132
117
  * 处理一条用户消息,调用 Agent 并返回回复
133
118
  */
134
119
  export async function handleDcgchatMessage(msg: InboundMessage, accountId: string): Promise<void> {
135
- const msgCtx = createMsgContext(msg)
136
-
137
120
  let finalSent = false
138
121
 
139
- const safeSendFinal = () => {
140
- if (finalSent) return
141
- finalSent = true
142
- sendFinal(msgCtx)
143
- }
144
-
145
122
  let completeText = ''
146
123
  const config = getOpenClawConfig()
147
124
  if (!config) {
@@ -150,39 +127,58 @@ export async function handleDcgchatMessage(msg: InboundMessage, accountId: strin
150
127
  }
151
128
  const account = resolveAccount(config, accountId)
152
129
  const userId = msg._userId.toString()
130
+
131
+ const core = getDcgchatRuntime()
132
+
133
+ const conversationId = msg.content.session_id?.trim()
134
+ const agentId = msg.content.agent_id?.trim()
135
+ const real_mobook = msg.content.real_mobook?.toString().trim()
136
+
137
+ const route = core.channel.routing.resolveAgentRoute({
138
+ cfg: config,
139
+ channel: "dcgchat-test",
140
+ accountId: account.accountId,
141
+ peer: { kind: 'direct', id: conversationId }
142
+ })
143
+
144
+ const embeddedAgentId = extractAgentIdFromConversationId(conversationId)
145
+
146
+ const effectiveAgentId = embeddedAgentId ?? route.agentId
147
+ const effectiveSessionKey =
148
+ real_mobook === '1' ? route.sessionKey : `agent:main:mobook:direct:${agentId}:${conversationId}`.toLowerCase()
149
+
150
+ setParamsMessage(effectiveSessionKey, {
151
+ userId: msg._userId,
152
+ botToken: msg.content.bot_token,
153
+ sessionId: conversationId,
154
+ messageId: msg.content.message_id,
155
+ domainId: msg.content.domain_id,
156
+ appId: msg.content.app_id,
157
+ botId: msg.content.bot_id ?? '',
158
+ agentId: msg.content.agent_id ?? '',
159
+ sessionKey: effectiveSessionKey,
160
+ real_mobook
161
+ })
162
+ const outboundCtx = getEffectiveMsgParams(effectiveSessionKey)
163
+ const agentEntry =
164
+ effectiveAgentId && effectiveAgentId !== 'main' ? config.agents?.list?.find((a) => a.id === effectiveAgentId) : undefined
165
+ const agentDisplayName = agentEntry?.name ?? (effectiveAgentId && effectiveAgentId !== 'main' ? effectiveAgentId : undefined)
166
+
167
+ const safeSendFinal = () => {
168
+ if (finalSent) return
169
+ finalSent = true
170
+ sendFinal(outboundCtx)
171
+ }
172
+
153
173
  const text = msg.content.text?.trim()
154
174
 
155
175
  if (!text) {
156
- sendTextMsg(msgCtx, '你需要我帮你做什么呢?')
176
+ sendTextMsg('你需要我帮你做什么呢?', outboundCtx)
157
177
  safeSendFinal()
158
178
  return
159
179
  }
160
180
 
161
181
  try {
162
- const core = getDcgchatRuntime()
163
-
164
- const conversationId = msg.content.session_id?.trim()
165
- const agentId = msg.content.agent_id?.trim()
166
- const realMobook = msg.content.real_mobook?.toString().trim()
167
-
168
- const route = core.channel.routing.resolveAgentRoute({
169
- cfg: config,
170
- channel: "dcgchat-test",
171
- accountId: account.accountId,
172
- peer: { kind: 'direct', id: conversationId }
173
- })
174
-
175
- // If conversation_id encodes an agentId prefix ("agentId::suffix"), override the route.
176
- const embeddedAgentId = extractAgentIdFromConversationId(conversationId)
177
- const effectiveAgentId = embeddedAgentId ?? route.agentId
178
- const effectiveSessionKey =
179
- realMobook === '1' ? route.sessionKey : `agent:main:mobook:direct:${agentId}:${conversationId}`.toLowerCase()
180
- setMsgParamsSessionKey(effectiveSessionKey)
181
-
182
- const agentEntry =
183
- effectiveAgentId && effectiveAgentId !== 'main' ? config.agents?.list?.find((a) => a.id === effectiveAgentId) : undefined
184
- const agentDisplayName = agentEntry?.name ?? (effectiveAgentId && effectiveAgentId !== 'main' ? effectiveAgentId : undefined)
185
-
186
182
  // Abort any existing generation for this conversation, then start a new one
187
183
  // const existingCtrl = activeGenerations.get(conversationId)
188
184
  // if (existingCtrl) existingCtrl.abort()
@@ -252,7 +248,7 @@ export async function handleDcgchatMessage(msg: InboundMessage, accountId: strin
252
248
  const key = getMediaKey(mediaUrl)
253
249
  if (sentMediaKeys.has(key)) continue
254
250
  sentMediaKeys.add(key)
255
- await sendDcgchatMedia({ msgCtx, mediaUrl, text: '' })
251
+ await sendDcgchatMedia({ sessionKey: effectiveSessionKey, mediaUrl, text: '' })
256
252
  }
257
253
  },
258
254
  onError: (err: unknown, info: { kind: string }) => {
@@ -278,14 +274,13 @@ export async function handleDcgchatMessage(msg: InboundMessage, accountId: strin
278
274
  })
279
275
  } else if (interruptCommand.includes(text?.trim())) {
280
276
  dcgLogger(`interrupt command: ${text}`)
281
- // abortMobookappGeneration(conversationId)
282
277
  sendMessageToGateway(
283
278
  JSON.stringify({
284
279
  method: 'chat.abort',
285
280
  params: { sessionKey: effectiveSessionKey }
286
281
  })
287
282
  )
288
- sendFinal(msgCtx)
283
+ safeSendFinal()
289
284
  return
290
285
  } else {
291
286
  dcgLogger(`dispatching to agent (session=${route.sessionKey})`)
@@ -308,7 +303,7 @@ export async function handleDcgchatMessage(msg: InboundMessage, accountId: strin
308
303
  ? payload.text.slice(streamedTextLen)
309
304
  : payload.text
310
305
  if (delta.trim()) {
311
- sendChunk(msgCtx, delta)
306
+ sendChunk(delta, outboundCtx)
312
307
  dcgLogger(`[stream]: chunk ${delta.length} chars to user ${msg._userId} ${delta.slice(0, 100)}`)
313
308
  }
314
309
  streamedTextLen = payload.text.length
@@ -319,7 +314,7 @@ export async function handleDcgchatMessage(msg: InboundMessage, accountId: strin
319
314
  const key = getMediaKey(mediaUrl)
320
315
  if (sentMediaKeys.has(key)) continue
321
316
  sentMediaKeys.add(key)
322
- await sendDcgchatMedia({ msgCtx, mediaUrl, text: '' })
317
+ await sendDcgchatMedia({ sessionKey: effectiveSessionKey, mediaUrl, text: '' })
323
318
  }
324
319
  }
325
320
  }
@@ -360,7 +355,7 @@ export async function handleDcgchatMessage(msg: InboundMessage, accountId: strin
360
355
  const resolved = candidates.find((p) => fs.existsSync(p))
361
356
  if (!resolved) continue
362
357
  try {
363
- await sendDcgchatMedia({ msgCtx, mediaUrl: resolved, text: '' })
358
+ await sendDcgchatMedia({ sessionKey: effectiveSessionKey, mediaUrl: resolved, text: '' })
364
359
  } catch (err) {
365
360
  dcgLogger(` sendDcgchatMedia error: ${String(err)}`, 'error')
366
361
  }
@@ -386,7 +381,7 @@ export async function handleDcgchatMessage(msg: InboundMessage, accountId: strin
386
381
  })
387
382
  } catch (err) {
388
383
  dcgLogger(` handle message failed: ${String(err)}`, 'error')
389
- sendError(msgCtx, err instanceof Error ? err.message : String(err))
384
+ sendError(err instanceof Error ? err.message : String(err), outboundCtx)
390
385
  } finally {
391
386
  safeSendFinal()
392
387
  }
package/src/channel.ts CHANGED
@@ -2,36 +2,40 @@ 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, getMsgParams, hasSentMediaKey } from './utils/global.js'
6
- import { type DcgchatMsgContext, isWsOpen, sendFinal, wsSendRaw } from './transport.js'
5
+ import { addSentMediaKey, getOpenClawConfig, hasSentMediaKey } from './utils/global.js'
6
+ import { isWsOpen, mergeDefaultParams, mergeSessionParams, sendFinal, wsSendRaw } from './transport.js'
7
7
  import { dcgLogger, setLogger } from './utils/log.js'
8
+ import { getParamsDefaults, getEffectiveMsgParams, getCurrentSessionKey } from './utils/params.js'
8
9
 
9
10
  export type DcgchatMediaSendOptions = {
10
- msgCtx: DcgchatMsgContext
11
+ /** 与 setParamsMessage / map 一致,用于 getEffectiveMsgParams */
12
+ sessionKey: string
11
13
  mediaUrl?: string
12
14
  text?: string
13
15
  }
14
16
 
15
17
  export async function sendDcgchatMedia(opts: DcgchatMediaSendOptions): Promise<void> {
16
- const { msgCtx } = opts
18
+ const msgCtx = getEffectiveMsgParams(opts.sessionKey)
17
19
  if (!isWsOpen()) {
18
20
  dcgLogger(`outbound media skipped -> ws not open: ${opts.mediaUrl ?? ''}`)
19
21
  return
20
22
  }
21
23
 
22
24
  const mediaUrl = opts.mediaUrl
23
- if (mediaUrl && hasSentMediaKey(msgCtx.messageId, mediaUrl)) {
25
+ const dedupeId = msgCtx.messageId
26
+ if (mediaUrl && dedupeId && hasSentMediaKey(dedupeId, mediaUrl)) {
24
27
  dcgLogger(`dcgchat: sendMedia skipped (duplicate in session): ${mediaUrl}`)
25
28
  return
26
29
  }
27
- if (mediaUrl) {
28
- addSentMediaKey(msgCtx.messageId, mediaUrl)
30
+ if (mediaUrl && dedupeId) {
31
+ addSentMediaKey(dedupeId, mediaUrl)
29
32
  }
30
33
 
31
34
  const fileName = mediaUrl?.split(/[\\/]/).pop() || ''
32
35
 
33
36
  try {
34
- const url = opts.mediaUrl ? await ossUpload(opts.mediaUrl, msgCtx.botToken, 1) : ''
37
+ const botToken = msgCtx.botToken ?? getOpenClawConfig()?.channels?.["dcgchat-test"]?.botToken ?? ''
38
+ const url = opts.mediaUrl ? await ossUpload(opts.mediaUrl, botToken, 1) : ''
35
39
  wsSendRaw(msgCtx, {
36
40
  response: opts.text ?? '',
37
41
  files: [{ url, name: fileName }]
@@ -61,22 +65,6 @@ export function resolveAccount(cfg: OpenClawConfig, accountId?: string | null):
61
65
  }
62
66
  }
63
67
 
64
- /** Build a DcgchatMsgContext for the outbound pipeline (uses global msgParams). */
65
- function createOutboundMsgContext(cfg: OpenClawConfig, accountId?: string | null): DcgchatMsgContext {
66
- const params = getMsgParams()
67
- const { botToken } = resolveAccount(cfg, accountId)
68
- return {
69
- userId: params.userId,
70
- botToken,
71
- domainId: params.domainId,
72
- appId: params.appId,
73
- botId: params.botId,
74
- agentId: params.agentId,
75
- sessionId: params.sessionId,
76
- messageId: params.messageId
77
- }
78
- }
79
-
80
68
  export const dcgchatPlugin: ChannelPlugin<ResolvedDcgchatAccount> = {
81
69
  id: "dcgchat-test",
82
70
  meta: {
@@ -153,24 +141,30 @@ export const dcgchatPlugin: ChannelPlugin<ResolvedDcgchatAccount> = {
153
141
  deliveryMode: 'direct',
154
142
  textChunkLimit: 4000,
155
143
  sendText: async (ctx) => {
156
- const msgCtx = createOutboundMsgContext(ctx.cfg, ctx.accountId)
157
144
  if (isWsOpen()) {
158
- wsSendRaw(msgCtx, { response: ctx.text })
159
- dcgLogger(`channel sendText to ${msgCtx.userId} ${ctx.text?.slice(0, 50)}`)
145
+ const merged = mergeDefaultParams({
146
+ agentId: ctx.accountId ?? '',
147
+ sessionId: ctx.to,
148
+ messageId: `${Date.now()}`
149
+ })
150
+ wsSendRaw(merged, { response: ctx.text })
151
+ sendFinal(merged)
152
+ dcgLogger(`channel sendText to ${ctx.to} ${ctx.text?.slice(0, 50)}`)
160
153
  }
161
154
  return {
162
155
  channel: "dcgchat-test",
163
156
  messageId: `dcg-${Date.now()}`,
164
- chatId: msgCtx.userId.toString()
157
+ chatId: ctx.to
165
158
  }
166
159
  },
167
160
  sendMedia: async (ctx) => {
168
- const msgCtx = createOutboundMsgContext(ctx.cfg, ctx.accountId)
169
- await sendDcgchatMedia({ msgCtx, mediaUrl: ctx.mediaUrl })
161
+ const sk = getCurrentSessionKey()
162
+ const msgCtx = getEffectiveMsgParams('sk')
163
+ await sendDcgchatMedia({ sessionKey: sk ?? '', mediaUrl: ctx.mediaUrl ?? '' })
170
164
  return {
171
165
  channel: "dcgchat-test",
172
166
  messageId: `dcg-${Date.now()}`,
173
- chatId: msgCtx.userId.toString()
167
+ chatId: msgCtx.userId?.toString()
174
168
  }
175
169
  }
176
170
  },
package/src/cron.ts CHANGED
@@ -6,11 +6,13 @@ import { promisify } from 'node:util'
6
6
 
7
7
  const execFileAsync = promisify(execFile)
8
8
  import type { IMsgParams } from './types.js'
9
- import { DcgchatMsgContext, sendEventMessage } from './transport.js'
10
- import { getMsgParams, getWorkspaceDir } from './utils/global.js'
9
+ import { sendEventMessage } from './transport.js'
10
+ import { getWorkspaceDir } from './utils/global.js'
11
11
  import { ossUpload } from './request/oss.js'
12
12
  import { dcgLogger } from './utils/log.js'
13
13
  import { sendMessageToGateway } from './gateway/socket.js'
14
+ import { channelInfo, ENV } from './utils/constant.js'
15
+ import { getCurrentSessionKey, getEffectiveMsgParams } from './utils/params.js'
14
16
 
15
17
  export function getCronJobsPath(): string {
16
18
  const workspaceDir = getWorkspaceDir()
@@ -18,33 +20,28 @@ export function getCronJobsPath(): string {
18
20
  return path.join(cronDir, 'jobs.json')
19
21
  }
20
22
 
21
- function msgParamsToCtx(p: IMsgParams): DcgchatMsgContext | null {
22
- if (!p?.token) return null
23
- return {
24
- userId: p.userId,
25
- botToken: p.token,
26
- domainId: p.domainId,
27
- appId: p.appId,
28
- botId: p.botId,
29
- agentId: p.agentId,
30
- sessionId: p.sessionId,
31
- messageId: p.messageId
32
- }
23
+ function msgParamsToCtx(p: IMsgParams): IMsgParams | null {
24
+ if (!p?.botToken) return null
25
+ return p
33
26
  }
34
27
 
35
28
  const CRON_UPLOAD_DEBOUNCE_MS = 30_000
36
29
 
37
30
  /** 待合并的上传上下文(短时间内多次调用只保留最后一次) */
38
- let pendingCronUploadCtx: DcgchatMsgContext | null = null
31
+ let pendingCronUploadCtx: IMsgParams | null = null
39
32
  let cronUploadFlushTimer: ReturnType<typeof setTimeout> | null = null
40
33
 
41
- async function runCronJobsUpload(msgCtx: DcgchatMsgContext): Promise<void> {
34
+ async function runCronJobsUpload(msgCtx: IMsgParams): Promise<void> {
42
35
  const jobPath = getCronJobsPath()
43
36
  if (fs.existsSync(jobPath)) {
44
37
  try {
45
- const url = await ossUpload(jobPath, msgCtx.botToken, 0)
38
+ const url = await ossUpload(jobPath, msgCtx.botToken ?? '', 0)
46
39
  dcgLogger(`定时任务创建成功: ${url}`)
47
- sendEventMessage(msgCtx, url)
40
+ if (!msgCtx.sessionKey) {
41
+ dcgLogger('runCronJobsUpload: missing sessionKey on msgCtx', 'error')
42
+ return
43
+ }
44
+ sendEventMessage(url, msgCtx.sessionKey)
48
45
  } catch (error) {
49
46
  dcgLogger(`${jobPath} upload failed: ${error}`, 'error')
50
47
  }
@@ -63,10 +60,10 @@ function flushCronUploadQueue(): void {
63
60
 
64
61
  /**
65
62
  * 将 jobs.json 同步到 OSS 并推送事件。30s 内多次调用合并为一次上传;定时触发后清空待处理项,避免重复执行。
66
- * @param msgCtx 可选;省略时使用当前 getMsgParams() 快照
63
+ * @param msgCtx 可选;省略时使用当前会话 getEffectiveMsgParams(sessionKey) 快照
67
64
  */
68
65
  export function sendDcgchatCron(): void {
69
- const ctx = msgParamsToCtx(getMsgParams() as IMsgParams)
66
+ const ctx = msgParamsToCtx(getEffectiveMsgParams(getCurrentSessionKey() ?? ''))
70
67
  if (!ctx) {
71
68
  dcgLogger('sendDcgchatCron: no message context (missing token / params)', 'error')
72
69
  return
@@ -120,6 +117,22 @@ export const updateCronJobSessionKey = async (jobId: string) => {
120
117
  dcgLogger('onRemoveCronJob: empty jobId', 'error')
121
118
  return
122
119
  }
123
- const params = getMsgParams()
124
- sendMessageToGateway(JSON.stringify({ method: 'cron.update', params: { id: jobId, patch: { sessionKey: params.sessionKey } } }))
120
+ const params = getEffectiveMsgParams(getCurrentSessionKey() ?? '')
121
+ sendMessageToGateway(
122
+ JSON.stringify({
123
+ method: 'cron.update',
124
+ params: {
125
+ id: jobId,
126
+ patch: {
127
+ sessionKey: params.sessionKey,
128
+ delivery: {
129
+ channel: "dcgchat-test",
130
+ to: params.sessionId,
131
+ accountId: 14,
132
+ bestEffort: true
133
+ }
134
+ }
135
+ }
136
+ })
137
+ )
125
138
  }
@@ -91,7 +91,7 @@ export class GatewayConnection {
91
91
  /** 服务端 connect.challenge 提供的 nonce,须与签名载荷一致 */
92
92
  private connectChallengeNonce: string | null = null
93
93
  private connectSent: boolean = false
94
- private messageHandlers: Map<string, (response: GatewayResponse) => void> = new Map()
94
+ private pendingRpcById: Map<string, (response: GatewayResponse) => void> = new Map()
95
95
  private eventHandlers: Set<(event: GatewayEvent) => void> = new Set()
96
96
 
97
97
  constructor(config: GatewayConfig) {
@@ -347,23 +347,33 @@ export class GatewayConnection {
347
347
  return
348
348
  }
349
349
  if (msg.type === 'res') {
350
- const handler = this.messageHandlers.get(msg.id as string)
350
+ const handler = this.pendingRpcById.get(msg.id as string)
351
351
  if (handler) {
352
- this.messageHandlers.delete(msg.id as string)
352
+ this.pendingRpcById.delete(msg.id as string)
353
353
  handler(msg as unknown as GatewayResponse)
354
354
  }
355
355
  return
356
356
  }
357
357
 
358
358
  if (msg.type === 'event') {
359
- if (msg.event === 'cron') {
360
- dcgLogger(`[Gateway] 收到事件: ${JSON.stringify(msg)}`)
361
- if (msg.payload?.action === 'added') {
362
- updateCronJobSessionKey(msg.payload?.jobId as string)
363
- }
364
- if (msg.payload?.action === 'updated') {
365
- sendDcgchatCron()
359
+ try {
360
+ if (msg.event === 'cron') {
361
+ dcgLogger(`[Gateway] 收到事件: ${JSON.stringify(msg)}`)
362
+ if (msg.payload?.action === 'added') {
363
+ updateCronJobSessionKey(msg.payload?.jobId as string)
364
+ }
365
+ if (msg.payload?.action === 'updated') {
366
+ sendDcgchatCron()
367
+ }
368
+ if (msg.payload?.action === 'added') {
369
+ updateCronJobSessionKey(msg.payload?.jobId as string)
370
+ }
371
+ if (msg.payload?.action === 'updated') {
372
+ sendDcgchatCron()
373
+ }
366
374
  }
375
+ } catch (error) {
376
+ dcgLogger(`[Gateway] 处理事件失败: ${error}`, 'error')
367
377
  }
368
378
  const event: GatewayEvent = {
369
379
  type: msg.event as string,
@@ -387,11 +397,11 @@ export class GatewayConnection {
387
397
  }
388
398
 
389
399
  const timeout = setTimeout(() => {
390
- this.messageHandlers.delete(id)
400
+ this.pendingRpcById.delete(id)
391
401
  reject(new Error('Method call timeout'))
392
402
  }, 30000)
393
403
 
394
- this.messageHandlers.set(id, (response) => {
404
+ this.pendingRpcById.set(id, (response) => {
395
405
  clearTimeout(timeout)
396
406
  if (response.ok) {
397
407
  const body = response.result !== undefined ? response.result : (response as GatewayResponse).payload
@@ -1,8 +1,5 @@
1
1
  // Security utilities for the tunnel plugin
2
2
  import crypto from 'crypto'
3
- import fs from 'fs'
4
- import jwt from 'jsonwebtoken'
5
- import { z } from 'zod'
6
3
 
7
4
  // ED25519 SubjectPublicKeyInfo prefix (must match openclaw gateway `ED25519_SPKI_PREFIX`)
8
5
  const ED25519_SPKI_PREFIX = Buffer.from('302a300506032b6570032100', 'hex')
@@ -12,10 +9,7 @@ const ED25519_SPKI_PREFIX = Buffer.from('302a300506032b6570032100', 'hex')
12
9
  */
13
10
  export function derivePublicKeyRawFromPem(publicKeyPem: string): Buffer {
14
11
  const spki = crypto.createPublicKey(publicKeyPem).export({ type: 'spki', format: 'der' }) as Buffer
15
- if (
16
- spki.length === ED25519_SPKI_PREFIX.length + 32 &&
17
- spki.subarray(0, ED25519_SPKI_PREFIX.length).equals(ED25519_SPKI_PREFIX)
18
- ) {
12
+ if (spki.length === ED25519_SPKI_PREFIX.length + 32 && spki.subarray(0, ED25519_SPKI_PREFIX.length).equals(ED25519_SPKI_PREFIX)) {
19
13
  return spki.subarray(ED25519_SPKI_PREFIX.length)
20
14
  }
21
15
  return spki
@@ -99,6 +99,8 @@ export type GatewayRpcPayload = {
99
99
  let persistentConn: GatewayConnection | null = null
100
100
  let pingTimer: ReturnType<typeof setInterval> | null = null
101
101
  let reconnectTimer: ReturnType<typeof setTimeout> | null = null
102
+ /** register 可能被调用多次,只保留一个「延迟首次连接」定时器,避免同一时刻触发两次 connect */
103
+ let startupConnectTimer: ReturnType<typeof setTimeout> | null = null
102
104
  let connectInFlight = false
103
105
  let socketStopped = true
104
106
  /** 用于忽略「已被替换的旧连接」上的 close/error */
@@ -118,6 +120,13 @@ function clearReconnectTimer(): void {
118
120
  }
119
121
  }
120
122
 
123
+ function clearStartupConnectTimer(): void {
124
+ if (startupConnectTimer) {
125
+ clearTimeout(startupConnectTimer)
126
+ startupConnectTimer = null
127
+ }
128
+ }
129
+
121
130
  function startPingTimer(gw: GatewayConnection): void {
122
131
  clearPingTimer()
123
132
  pingTimer = setInterval(() => {
@@ -159,7 +168,9 @@ function attachSocketLifecycle(gw: GatewayConnection, generation: number): void
159
168
  }
160
169
 
161
170
  async function connectPersistentGateway(): Promise<void> {
162
- if (socketStopped || connectInFlight) return
171
+ if (socketStopped) return
172
+ if (persistentConn?.isConnected()) return
173
+ if (connectInFlight) return
163
174
 
164
175
  const cfg = resolveConfigSafe()
165
176
  if (!cfg) return
@@ -218,7 +229,9 @@ async function connectPersistentGateway(): Promise<void> {
218
229
  export function startDcgchatGatewaySocket(): void {
219
230
  socketStopped = false
220
231
  clearReconnectTimer()
221
- setTimeout(() => {
232
+ if (startupConnectTimer != null) return
233
+ startupConnectTimer = setTimeout(() => {
234
+ startupConnectTimer = null
222
235
  void connectPersistentGateway()
223
236
  }, 10000)
224
237
  }
@@ -229,6 +242,7 @@ export function startDcgchatGatewaySocket(): void {
229
242
  export function stopDcgchatGatewaySocket(): void {
230
243
  socketStopped = true
231
244
  clearReconnectTimer()
245
+ clearStartupConnectTimer()
232
246
  clearPingTimer()
233
247
  if (persistentConn) {
234
248
  try {
package/src/monitor.ts CHANGED
@@ -2,9 +2,8 @@ import type { ClawdbotConfig, RuntimeEnv } from 'openclaw/plugin-sdk'
2
2
  import WebSocket from 'ws'
3
3
  import { handleDcgchatMessage } from './bot.js'
4
4
  import { resolveAccount } from './channel.js'
5
- import { setWsConnection, getOpenClawConfig } from './utils/global.js'
5
+ import { setWsConnection, getOpenClawConfig, setMsgStatus } from './utils/global.js'
6
6
  import type { InboundMessage } from './types.js'
7
- import { setMsgParams, setMsgStatus } from './utils/global.js'
8
7
  import { installSkill, uninstallSkill } from './skill.js'
9
8
  import { dcgLogger } from './utils/log.js'
10
9
  import { ignoreToolCommand } from './utils/constant.js'
@@ -130,19 +129,6 @@ export async function monitorDcgchatProvider(opts: MonitorDcgchatOpts): Promise<
130
129
  if (!ignoreToolCommand.includes(msg.content.text?.trim())) {
131
130
  setMsgStatus('running')
132
131
  }
133
- // 设置获取用户消息消息参数
134
- setMsgParams({
135
- userId: msg._userId,
136
- token: msg.content.bot_token,
137
- sessionId: msg.content.session_id,
138
- messageId: msg.content.message_id,
139
- domainId: account.domainId || 1000,
140
- appId: account.appId || '100',
141
- botId: msg.content.bot_id,
142
- agentId: msg.content.agent_id
143
- })
144
- msg.content.app_id = account.appId || '100'
145
- msg.content.domain_id = account.domainId || '1000'
146
132
 
147
133
  await handleDcgchatMessage(msg, account.accountId)
148
134
  } else if (parsed.messageType == 'openclaw_bot_event') {
@@ -3,7 +3,7 @@ import axios from 'axios'
3
3
  import md5 from 'md5'
4
4
  import type { IResponse } from '../types.js'
5
5
  import { getUserTokenCache } from './userInfo.js'
6
- import { getMsgParams } from '../utils/global.js'
6
+ import { getCurrentSessionKey, getEffectiveMsgParams } from '../utils/params.js'
7
7
  import { ENV } from '../utils/constant.js'
8
8
  import { dcgLogger } from '../utils/log.js'
9
9
 
@@ -172,7 +172,7 @@ export function post<T = Record<string, unknown>, R = unknown>(
172
172
  botToken?: string
173
173
  }
174
174
  ): Promise<IResponse<R>> {
175
- const params = getMsgParams() || {}
175
+ const params = getEffectiveMsgParams(getCurrentSessionKey() ?? '') || {}
176
176
  const config: any = {
177
177
  method: 'POST',
178
178
  url,
package/src/tool.ts CHANGED
@@ -1,7 +1,8 @@
1
1
  import type { OpenClawPluginApi } from 'openclaw/plugin-sdk'
2
- import { getMsgParams, getMsgStatus, getWsConnection } from './utils/global.js'
2
+ import { getMsgStatus, getWsConnection } from './utils/global.js'
3
3
  import { dcgLogger } from './utils/log.js'
4
4
  import { isWsOpen, sendFinal, sendText } from './transport.js'
5
+ import { getCurrentSessionKey, getEffectiveMsgParams } from './utils/params.js'
5
6
 
6
7
  let toolCallId = ''
7
8
  let toolName = ''
@@ -51,7 +52,9 @@ const eventList = [
51
52
 
52
53
  function sendToolCallMessage(text: string, toolCallId: string, isCover: number) {
53
54
  const ws = getWsConnection()
54
- const params = getMsgParams()
55
+ const sk = getCurrentSessionKey()
56
+ if (!sk) return
57
+ const params = getEffectiveMsgParams(sk)
55
58
  if (isWsOpen()) {
56
59
  ws?.send(
57
60
  JSON.stringify({
@@ -61,7 +64,7 @@ function sendToolCallMessage(text: string, toolCallId: string, isCover: number)
61
64
  is_finish: -1,
62
65
  content: {
63
66
  is_finish: -1,
64
- bot_token: params?.token,
67
+ bot_token: params?.botToken,
65
68
  domain_id: params?.domainId,
66
69
  app_id: params?.appId,
67
70
  bot_id: params?.botId,
@@ -85,7 +88,7 @@ export function monitoringToolMessage(api: OpenClawPluginApi) {
85
88
  if (status === 'running') {
86
89
  if (['after_tool_call', 'before_tool_call'].includes(item.event)) {
87
90
  const { result: _result, ...rest } = event
88
- dcgLogger(`工具调用结果: ~ event:${item.event} ~ params:${JSON.stringify(rest)}`)
91
+ dcgLogger(`工具调用结果: ~ event:${item.event} ~ params:${JSON.stringify(rest)}, args: ${JSON.stringify(args)}`)
89
92
  const text = JSON.stringify({
90
93
  type: item.event,
91
94
  specialIdentification: 'dcgchat_tool_call_special_identification',
@@ -97,22 +100,14 @@ export function monitoringToolMessage(api: OpenClawPluginApi) {
97
100
  } else if (item.event) {
98
101
  dcgLogger(`工具调用结果: ~ event:${item.event}`)
99
102
  if (item.event === 'llm_output') {
100
- dcgLogger(`llm_output工具调用结果: ~ event:${JSON.stringify(event)}`)
103
+ dcgLogger(`llm_output工具调用结果: ~ event:${JSON.stringify(event)}, args: ${JSON.stringify(args)}`)
101
104
  if (event.lastAssistant?.errorMessage === '429-账户额度耗尽') {
102
- const params = getMsgParams()
103
105
  const message = '您的积分已消耗完,您可以通过充值积分来继续使用'
104
- const ctx = {
105
- userId: params.userId,
106
- botToken: params.token,
107
- domainId: params.domainId,
108
- appId: params.appId,
109
- botId: params.botId,
110
- agentId: params.agentId,
111
- sessionId: params.sessionId,
112
- messageId: params.messageId
113
- }
114
- sendText(ctx, message, { message_tags: { insufficient_balance: 1 }, is_finish: -1 })
115
- sendFinal(ctx)
106
+ const sk = ((args?.sessionKey as string | undefined) ?? getCurrentSessionKey()) || ''
107
+ if (!sk) return
108
+ const msgCtx = getEffectiveMsgParams(sk)
109
+ sendText(message, msgCtx, { message_tags: { insufficient_balance: 1 }, is_finish: -1 })
110
+ sendFinal(msgCtx)
116
111
  return
117
112
  }
118
113
  }
package/src/transport.ts CHANGED
@@ -1,19 +1,22 @@
1
1
  import { getWsConnection } from './utils/global.js'
2
2
  import { dcgLogger } from './utils/log.js'
3
+ import type { IMsgParams } from './types.js'
4
+ import { getEffectiveMsgParams, getParamsDefaults } from './utils/params.js'
3
5
 
4
- export type DcgchatMsgContext = {
5
- userId: number
6
- botToken: string
7
- domainId?: string
8
- appId?: string
9
- botId?: string
10
- agentId?: string
11
- sessionId: string
12
- messageId: string
6
+ /** sessionKey map 取参,再合并 overrides(channel 出站、媒体等) */
7
+ export function mergeSessionParams(sessionKey: string, overrides?: Partial<IMsgParams>): IMsgParams {
8
+ const base = getEffectiveMsgParams(sessionKey)
9
+ if (!overrides) return base
10
+ return { ...base, ...overrides }
11
+ }
12
+ export function mergeDefaultParams(overrides?: Partial<IMsgParams>): IMsgParams {
13
+ const base = getParamsDefaults()
14
+ if (!overrides) return base
15
+ return { ...base, ...overrides }
13
16
  }
14
17
 
15
- export function createMsgContext(msg: {
16
- _userId: number
18
+ export type InboundMsgForContext = {
19
+ _userId: number | string
17
20
  content: {
18
21
  bot_token: string
19
22
  domain_id?: string
@@ -23,90 +26,157 @@ export function createMsgContext(msg: {
23
26
  session_id: string
24
27
  message_id: string
25
28
  }
26
- }): DcgchatMsgContext {
29
+ }
30
+
31
+ export type OpenclawBotChatEnvelope = {
32
+ messageType: 'openclaw_bot_chat'
33
+ _userId: number | undefined
34
+ source: 'client'
35
+ content: Record<string, unknown>
36
+ }
37
+
38
+ function isInboundWire(arg: unknown): arg is InboundMsgForContext {
39
+ return Boolean(arg && typeof arg === 'object' && '_userId' in arg && 'content' in arg)
40
+ }
41
+
42
+ /** 下行 WebSocket 帧 → 内部上下文(字段缺省用 channel 配置补) */
43
+ function inboundToCtx(msg: InboundMsgForContext, d: IMsgParams): IMsgParams {
44
+ const c = msg.content
27
45
  return {
28
- userId: msg._userId,
29
- botToken: msg.content.bot_token,
30
- domainId: msg.content.domain_id,
31
- appId: msg.content.app_id,
32
- botId: msg.content.bot_id,
33
- agentId: msg.content.agent_id,
34
- sessionId: msg.content.session_id,
35
- messageId: msg.content.message_id
46
+ userId: Number(msg._userId ?? d.userId),
47
+ botToken: c.bot_token ?? d.botToken,
48
+ domainId: String(c.domain_id ?? d.domainId),
49
+ appId: String(c.app_id ?? d.appId),
50
+ botId: c.bot_id,
51
+ agentId: c.agent_id,
52
+ sessionId: c.session_id,
53
+ messageId: c.message_id
36
54
  }
37
55
  }
38
56
 
39
- function buildContent(ctx: DcgchatMsgContext, extra: Record<string, unknown>) {
57
+ /** 上行:与配置合并缺省后再 `...ctx` 覆盖(原 wsSendRaw) */
58
+ function mergeOutboundWithDefaults(ctx: IMsgParams, d: IMsgParams): IMsgParams {
40
59
  return {
41
- bot_token: ctx.botToken,
42
- domain_id: ctx.domainId,
43
- app_id: ctx.appId,
44
- bot_id: ctx.botId,
45
- agent_id: ctx.agentId,
46
- session_id: ctx.sessionId,
47
- message_id: ctx.messageId || Date.now().toString(),
48
- ...extra
60
+ userId: Number(ctx.userId ?? d.userId),
61
+ botToken: ctx.botToken ?? d.botToken,
62
+ domainId: String(ctx.domainId ?? d.domainId),
63
+ appId: String(ctx.appId ?? d.appId),
64
+ ...ctx
49
65
  }
50
66
  }
51
67
 
52
- function buildEnvelope(ctx: DcgchatMsgContext, extra: Record<string, unknown>) {
68
+ /**
69
+ * 组装完整 wire `content` 对象:先写会话/机器人基础字段(回落到 d),再合并调用方传入的 payload。
70
+ * `content` 在使用处构造(如 response、state、files),同名键可覆盖基础字段。
71
+ */
72
+ export function buildWireContent(base: IMsgParams, d: IMsgParams, content: Record<string, unknown>): Record<string, unknown> {
73
+ const resolvedBotToken = base.botToken ?? d.botToken
74
+ const domain = base.domainId ?? d.domainId
75
+ const app = base.appId ?? d.appId
53
76
  return {
54
- messageType: 'openclaw_bot_chat' as const,
55
- _userId: ctx.userId,
56
- source: 'client' as const,
57
- content: buildContent(ctx, extra)
77
+ bot_token: base.botToken || resolvedBotToken,
78
+ domain_id: base.domainId || domain,
79
+ app_id: base.appId || app,
80
+ bot_id: base.botId,
81
+ agent_id: base.agentId,
82
+ session_id: base.sessionId,
83
+ message_id: base.messageId || Date.now().toString(),
84
+ ...content
58
85
  }
59
86
  }
60
87
 
88
+ /** 上行:在已合并的 ctx 上套 openclaw_bot_chat 信封(messageType / _userId / source + content) */
89
+ function buildOutboundOpenclawBotChatEnvelope(
90
+ ctx: IMsgParams,
91
+ content: Record<string, unknown>,
92
+ opts?: { mergeChannelDefaults?: boolean }
93
+ ): OpenclawBotChatEnvelope {
94
+ const d = getParamsDefaults()
95
+ const base = opts?.mergeChannelDefaults ? mergeOutboundWithDefaults(ctx, d) : ctx
96
+ return {
97
+ messageType: 'openclaw_bot_chat',
98
+ _userId: base.userId,
99
+ source: 'client',
100
+ content: buildWireContent(base, d, content)
101
+ }
102
+ }
103
+
104
+ /**
105
+ * 下行解析为 DcgchatMsgContext,或上行组装 openclaw_bot_chat 信封。
106
+ * 上行时 `content` 由调用方传入;基础参数来自 `ctx` 与 `getParamsDefaults()`(可选 mergeChannelDefaults,同原 wsSendRaw)。
107
+ */
108
+ export function buildOpenclawBotChat(msg: InboundMsgForContext): IMsgParams
109
+ export function buildOpenclawBotChat(
110
+ ctx: IMsgParams,
111
+ content: Record<string, unknown>,
112
+ opts?: { mergeChannelDefaults?: boolean }
113
+ ): OpenclawBotChatEnvelope
114
+ export function buildOpenclawBotChat(
115
+ arg1: InboundMsgForContext | IMsgParams,
116
+ arg2?: Record<string, unknown>,
117
+ opts?: { mergeChannelDefaults?: boolean }
118
+ ): IMsgParams | OpenclawBotChatEnvelope {
119
+ const d = getParamsDefaults()
120
+
121
+ if (arg2 === undefined && isInboundWire(arg1)) {
122
+ return inboundToCtx(arg1, d)
123
+ }
124
+
125
+ const ctx = arg1 as IMsgParams
126
+ return buildOutboundOpenclawBotChatEnvelope(ctx, arg2 ?? {}, opts)
127
+ }
128
+
61
129
  export function isWsOpen(): boolean {
62
130
  const isOpen = getWsConnection()?.readyState === WebSocket.OPEN
63
131
  if (!isOpen) {
64
- dcgLogger(`socket not ready ${getWsConnection()?.readyState}`, 'error')
132
+ dcgLogger(`server socket not ready ${getWsConnection()?.readyState}`, 'error')
65
133
  }
66
134
  return isOpen
67
135
  }
68
136
 
69
137
  /**
70
- * Content is stringified separately (double-encoded) to match the
71
- * dcgchat wire protocol used by the chat stream path.
138
+ * 聊天流路径:content 单独 JSON.stringify(双重编码),符合 dcgchat 协议。
139
+ * `ctx` 须由调用方用 getEffectiveMsgParams(sessionKey) 等解析好;`content` 为完整业务 payload。
72
140
  */
73
- export function wsSend(ctx: DcgchatMsgContext, extra: Record<string, unknown>): boolean {
141
+ export function wsSend(ctx: IMsgParams, content: Record<string, unknown>): boolean {
74
142
  const ws = getWsConnection()
75
143
  if (ws?.readyState !== WebSocket.OPEN) return false
76
- const envelope = buildEnvelope(ctx, extra)
144
+ const envelope = buildOpenclawBotChat(ctx, content)
77
145
  ws.send(JSON.stringify({ ...envelope, content: JSON.stringify(envelope.content) }))
78
146
  return true
79
147
  }
80
148
 
81
149
  /**
82
- * Content stays as a nested object (single-encoded).
83
- * Matches the legacy wire format used by media and outbound-pipeline messages.
150
+ * 媒体 / channel 出站:content 保持嵌套对象(单次编码)。
151
+ * `ctx` 须由调用方解析(如需合并覆盖可先 mergeSessionParams)。
84
152
  */
85
- export function wsSendRaw(ctx: DcgchatMsgContext, extra: Record<string, unknown>): boolean {
153
+ export function wsSendRaw(ctx: IMsgParams, content: Record<string, unknown>): boolean {
86
154
  const ws = getWsConnection()
87
155
  if (isWsOpen()) {
88
- ws?.send(JSON.stringify(buildEnvelope(ctx, extra)))
156
+ ws?.send(JSON.stringify(buildOpenclawBotChat(ctx, content, { mergeChannelDefaults: true })))
89
157
  }
90
158
  return true
91
159
  }
92
160
 
93
- export function sendChunk(ctx: DcgchatMsgContext, text: string): boolean {
161
+ export function sendChunk(text: string, ctx: IMsgParams): boolean {
94
162
  return wsSend(ctx, { response: text, state: 'chunk' })
95
163
  }
96
164
 
97
- export function sendFinal(ctx: DcgchatMsgContext): boolean {
165
+ export function sendFinal(ctx: IMsgParams): boolean {
98
166
  dcgLogger(` message handling complete state: final`)
99
167
  return wsSend(ctx, { response: '', state: 'final' })
100
168
  }
101
169
 
102
- export function sendText(ctx: DcgchatMsgContext, text: string, event?: Record<string, unknown>): boolean {
170
+ export function sendText(text: string, ctx: IMsgParams, event?: Record<string, unknown>): boolean {
103
171
  return wsSend(ctx, { response: text, ...event })
104
172
  }
105
173
 
106
- export function sendError(ctx: DcgchatMsgContext, errorMsg: string): boolean {
174
+ export function sendError(errorMsg: string, ctx: IMsgParams): boolean {
107
175
  return wsSend(ctx, { response: `[错误] ${errorMsg}`, state: 'final' })
108
176
  }
109
- export function sendEventMessage(ctx: DcgchatMsgContext, url: string) {
177
+
178
+ export function sendEventMessage(url: string, sessionKey: string) {
179
+ const ctx = getEffectiveMsgParams(sessionKey)
110
180
  const ws = getWsConnection()
111
181
  if (isWsOpen()) {
112
182
  ws?.send(
package/src/types.ts CHANGED
@@ -116,13 +116,15 @@ export interface IStsTokenReq {
116
116
  }
117
117
 
118
118
  export interface IMsgParams {
119
- userId: number
120
- token: string
121
- sessionId: string
122
- messageId: string
123
- domainId: string
124
- appId: string
125
- botId: string
126
- agentId: string
127
- sessionKey: string
119
+ userId?: number
120
+ botToken?: string
121
+ sessionId?: string
122
+ messageId?: string
123
+ domainId?: string
124
+ appId?: string
125
+ botId?: string
126
+ agentId?: string
127
+ /** 与 OpenClaw 路由一致,用于 map 与异步链路(工具 / HTTP / cron)对齐当前会话 */
128
+ sessionKey?: string
129
+ real_mobook?: string | number
128
130
  }
@@ -24,7 +24,6 @@ export function getOpenClawConfig(): OpenClawConfig | null {
24
24
 
25
25
  import type { OpenClawConfig, PluginRuntime } from 'openclaw/plugin-sdk'
26
26
  import { dcgLogger } from './log.js'
27
- import { IMsgParams } from '../types.js'
28
27
  import { channelInfo, ENV } from './constant.js'
29
28
 
30
29
  const path = require('path')
@@ -69,17 +68,6 @@ export function getDcgchatRuntime(): PluginRuntime {
69
68
  return runtime as PluginRuntime
70
69
  }
71
70
 
72
- let msgParams = {} as IMsgParams
73
- export function setMsgParams(params: any) {
74
- msgParams = params
75
- }
76
- export function getMsgParams() {
77
- return msgParams
78
- }
79
- export function setMsgParamsSessionKey(sessionKey: string) {
80
- if (sessionKey) msgParams.sessionKey = sessionKey
81
- }
82
-
83
71
  let msgStatus: 'running' | 'finished' | '' = ''
84
72
  export function setMsgStatus(status: 'running' | 'finished' | '') {
85
73
  msgStatus = status
@@ -0,0 +1,65 @@
1
+ import { channelInfo, ENV } from './constant.js'
2
+ import { getOpenClawConfig } from './global.js'
3
+ import type { DcgchatConfig, IMsgParams } from '../types.js'
4
+
5
+ /**
6
+ * map key 是 session_key,value 为该会话下已 merge 后的消息参数。
7
+ */
8
+ const paramsMessageMap = new Map<string, IMsgParams>()
9
+
10
+ /** 最近一次 setParamsMessage 的 key,供不传参的 getEffectiveMsgParams() 使用 */
11
+ let currentSessionKey: string | null = null
12
+
13
+ /** 从 OpenClaw 配置读取当前 channel 的基础参数(唯一来源,供 transport / resolve 等复用) */
14
+ export function getParamsDefaults(): IMsgParams {
15
+ const ch = (getOpenClawConfig()?.channels?.["dcgchat-test"] as DcgchatConfig | undefined) ?? {}
16
+ return {
17
+ userId: Number(ch.userId ?? 0),
18
+ botToken: ch.botToken ?? '',
19
+ sessionId: '',
20
+ messageId: '',
21
+ domainId: String(ch.domainId ?? '1000'),
22
+ appId: String(ch.appId ?? '100'),
23
+ botId: '',
24
+ agentId: '',
25
+ sessionKey: ''
26
+ }
27
+ }
28
+
29
+ export function resolveParamsMessage(params: Partial<IMsgParams>): IMsgParams {
30
+ const defaults = getParamsDefaults()
31
+ return {
32
+ userId: Number(params.userId ?? defaults.userId),
33
+ botToken: params.botToken ?? defaults.botToken,
34
+ sessionId: params.sessionId ?? defaults.sessionId,
35
+ messageId: params.messageId ?? defaults.messageId,
36
+ domainId: String(params.domainId ?? defaults.domainId),
37
+ appId: String(params.appId ?? defaults.appId),
38
+ botId: params.botId ?? defaults.botId,
39
+ agentId: params.agentId ?? defaults.agentId,
40
+ sessionKey: params.sessionKey ?? defaults.sessionKey
41
+ }
42
+ }
43
+
44
+ /**
45
+ * 统一取值入口:显式 sessionKey,或回落到当前会话;再与配置缺省 merge,保证字段完整。
46
+ */
47
+ export function getEffectiveMsgParams(sessionKey?: string): IMsgParams {
48
+ const key = sessionKey ?? currentSessionKey
49
+ const stored = key ? paramsMessageMap.get(key) : undefined
50
+ return stored ? resolveParamsMessage(stored) : getParamsDefaults()
51
+ }
52
+
53
+ export function setParamsMessage(sessionKey: string, params: Partial<IMsgParams>) {
54
+ if (!sessionKey) return
55
+ currentSessionKey = sessionKey
56
+ paramsMessageMap.set(sessionKey, resolveParamsMessage({ ...params, sessionKey }))
57
+ }
58
+
59
+ export function getParamsMessage(sessionKey: string): IMsgParams | undefined {
60
+ return paramsMessageMap.get(sessionKey)
61
+ }
62
+
63
+ export function getCurrentSessionKey(): string | null {
64
+ return currentSessionKey
65
+ }