@dcrays/dcgchat-test 0.3.3 → 0.3.4

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.3",
3
+ "version": "0.3.4",
4
4
  "type": "module",
5
5
  "description": "OpenClaw channel plugin for 书灵墨宝 (WebSocket)",
6
6
  "main": "index.ts",
package/src/bot.ts CHANGED
@@ -164,7 +164,6 @@ export async function handleDcgchatMessage(msg: InboundMessage, accountId: strin
164
164
 
165
165
  const effectiveAgentId = embeddedAgentId ?? route.agentId
166
166
  const effectiveSessionKey = getSessionKey(msg.content, account.accountId)
167
- console.log('🚀 ~ handleDcgchatMessage ~ effectiveSessionKey:', effectiveSessionKey)
168
167
 
169
168
  setParamsMessage(effectiveSessionKey, {
170
169
  userId: msg._userId,
@@ -229,7 +228,7 @@ export async function handleDcgchatMessage(msg: InboundMessage, accountId: strin
229
228
  RawBody: text,
230
229
  CommandBody: text,
231
230
  From: userId,
232
- To: conversationId,
231
+ To: effectiveSessionKey,
233
232
  SessionKey: effectiveSessionKey,
234
233
  AccountId: route.accountId,
235
234
  ChatType: 'direct',
@@ -242,7 +241,7 @@ export async function handleDcgchatMessage(msg: InboundMessage, accountId: strin
242
241
  WasMentioned: true,
243
242
  CommandAuthorized: true,
244
243
  OriginatingChannel: "dcgchat-test",
245
- OriginatingTo: `user:${userId}`,
244
+ OriginatingTo: effectiveSessionKey,
246
245
  ...mediaPayload
247
246
  })
248
247
 
@@ -359,10 +358,7 @@ export async function handleDcgchatMessage(msg: InboundMessage, accountId: strin
359
358
  },
360
359
  onPartialReply: async (payload: ReplyPayload) => {
361
360
  if (sessionStreamSuppressed.has(effectiveSessionKey)) return
362
- console.log(
363
- '🚀 ~ handleDcgchatMessage ~ sessionStreamSuppressed.has(effectiveSessionKey):',
364
- sessionStreamSuppressed.has(effectiveSessionKey)
365
- )
361
+
366
362
  // Accumulate full text
367
363
  if (payload.text) {
368
364
  completeText = payload.text
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, getOpenClawConfig, hasSentMediaKey } from './utils/global.js'
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
8
  import { getEffectiveMsgParams, getCurrentSessionKey } from './utils/params.js'
@@ -143,14 +143,23 @@ export const dcgchatPlugin: ChannelPlugin<ResolvedDcgchatAccount> = {
143
143
  textChunkLimit: 4000,
144
144
  sendText: async (ctx) => {
145
145
  if (isWsOpen()) {
146
- const merged = mergeDefaultParams({
147
- agentId: ctx.accountId ?? '',
148
- sessionId: ctx.to,
149
- messageId: `${Date.now()}`,
150
- is_finish: -1
151
- })
152
- wsSendRaw(merged, { response: ctx.text })
153
- sendFinal(merged)
146
+ // if (ctx.to.indexOf('cron:') >= 0) {
147
+ // const sessionInfo = ctx.to.split(':')[1]
148
+ // const sessionId = sessionInfo.split('-')[0]
149
+ // const agentId = sessionInfo.split('-')[1]
150
+ // const merged = mergeDefaultParams({
151
+ // agentId: agentId,
152
+ // sessionId: sessionId,
153
+ // messageId: `${Date.now()}`,
154
+ // is_finish: -1
155
+ // })
156
+ // wsSendRaw(merged, { response: ctx.text })
157
+ // } else {
158
+ // }
159
+ const outboundCtx = getEffectiveMsgParams(ctx.to)
160
+ const messageId = getCronMessageId(ctx.to)
161
+ const newCtx = messageId ? { ...outboundCtx, messageId } : outboundCtx
162
+ wsSendRaw(newCtx, { response: ctx.text, is_finish: -1 })
154
163
  dcgLogger(`channel sendText to ${ctx.to} ${ctx.text?.slice(0, 50)}`)
155
164
  }
156
165
  return {
package/src/cron.ts CHANGED
@@ -1,17 +1,11 @@
1
1
  import path from 'node:path'
2
2
  import fs from 'node:fs'
3
- import crypto from 'node:crypto'
4
- import { execFile } from 'node:child_process'
5
- import { promisify } from 'node:util'
6
-
7
- const execFileAsync = promisify(execFile)
8
3
  import type { IMsgParams } from './types.js'
9
- import { sendEventMessage } from './transport.js'
10
- import { getWorkspaceDir } from './utils/global.js'
4
+ import { sendEventMessage, sendFinal } from './transport.js'
5
+ import { getWorkspaceDir, removeCronMessageId, setCronMessageId, setMsgStatus } from './utils/global.js'
11
6
  import { ossUpload } from './request/oss.js'
12
7
  import { dcgLogger } from './utils/log.js'
13
8
  import { sendMessageToGateway } from './gateway/socket.js'
14
- import { channelInfo, ENV } from './utils/constant.js'
15
9
  import { getCurrentSessionKey, getEffectiveMsgParams } from './utils/params.js'
16
10
 
17
11
  export function getCronJobsPath(): string {
@@ -20,6 +14,33 @@ export function getCronJobsPath(): string {
20
14
  return path.join(cronDir, 'jobs.json')
21
15
  }
22
16
 
17
+ type CronJobsFile = {
18
+ version?: number
19
+ jobs?: Array<{ id?: string; sessionKey?: string }>
20
+ }
21
+
22
+ /**
23
+ * 在 `jobPath` 指向的 jobs.json(通常为 getCronJobsPath())中按 id 查找任务并返回其 sessionKey。
24
+ */
25
+ export function readCronJobSessionKey(jobPath: string, jobId: string): string | null {
26
+ const id = jobId?.trim()
27
+ if (!id) return null
28
+ if (!fs.existsSync(jobPath)) {
29
+ dcgLogger(`readCronJobSessionKey: file not found ${jobPath}`, 'error')
30
+ return null
31
+ }
32
+ try {
33
+ const raw = fs.readFileSync(jobPath, 'utf8')
34
+ const data = JSON.parse(raw) as CronJobsFile
35
+ const job = (data.jobs ?? []).find((j) => j.id === id)
36
+ const sk = job?.sessionKey?.trim()
37
+ return sk || null
38
+ } catch (e) {
39
+ dcgLogger(`readCronJobSessionKey: failed to read ${jobPath}: ${String(e)}`, 'error')
40
+ return null
41
+ }
42
+ }
43
+
23
44
  function msgParamsToCtx(p: IMsgParams): IMsgParams | null {
24
45
  if (!p?.botToken) return null
25
46
  return p
@@ -103,36 +124,35 @@ export const onEnabledCronJob = async (jobId: string) => {
103
124
  }
104
125
  sendMessageToGateway(JSON.stringify({ method: 'cron.update', params: { id: jobId, patch: { enabled: true } } }))
105
126
  }
106
- export const onRunCronJob = async (jobId: string) => {
127
+ export const onRunCronJob = async (jobId: string, messageId: string) => {
107
128
  const id = jobId?.trim()
108
129
  if (!id) {
109
130
  dcgLogger('onRemoveCronJob: empty jobId', 'error')
110
131
  return
111
132
  }
112
- sendMessageToGateway(JSON.stringify({ method: 'cron.update', jobId }))
133
+ const jobPath = getCronJobsPath()
134
+ const sessionKey = readCronJobSessionKey(jobPath, jobId) || ''
135
+ if (!sessionKey) {
136
+ dcgLogger(`finishedDcgchatCron: no sessionKey for job id=${id}`, 'error')
137
+ return
138
+ }
139
+ setCronMessageId(sessionKey, messageId)
140
+ sendMessageToGateway(JSON.stringify({ method: 'cron.run', params: { id: jobId } }))
113
141
  }
114
- export const updateCronJobSessionKey = async (jobId: string) => {
142
+ export const finishedDcgchatCron = async (jobId: string) => {
115
143
  const id = jobId?.trim()
116
144
  if (!id) {
117
- dcgLogger('onRemoveCronJob: empty jobId', 'error')
145
+ dcgLogger('finishedDcgchatCron: empty jobId', 'error')
118
146
  return
119
147
  }
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
- )
148
+ const jobPath = getCronJobsPath()
149
+ const sessionKey = readCronJobSessionKey(jobPath, id)
150
+ if (!sessionKey) {
151
+ dcgLogger(`finishedDcgchatCron: no sessionKey for job id=${id}`, 'error')
152
+ return
153
+ }
154
+ const outboundCtx = getEffectiveMsgParams(sessionKey)
155
+ sendFinal(outboundCtx)
156
+ removeCronMessageId(sessionKey)
157
+ dcgLogger(`finishedDcgchatCron: job=${id} sessionKey=${sessionKey}`)
138
158
  }
@@ -0,0 +1,185 @@
1
+ import { channelInfo, ENV } from './utils/constant.js'
2
+ /**
3
+ * cron-delivery-guard — 定时任务投递守护插件
4
+ *
5
+ * 核心机制:通过 before_tool_call 钩子拦截 cron 工具调用,
6
+ * 当 delivery.mode 为 "announce" 且未指定 channel 时,
7
+ * 自动注入 bestEffort: true,使投递失败时静默降级,
8
+ * 不影响 cron 执行结果的保存。
9
+ *
10
+ * 背景:
11
+ * - 定时任务的 delivery 设为 announce 模式,如果没有指定 channel,
12
+ * 投递可能因找不到有效渠道而失败
13
+ * - bestEffort: true 让框架在投递失败时不报错,避免丢失执行结果
14
+ */
15
+
16
+ import { dcgLogger } from './utils/log.js'
17
+
18
+ const LOG_TAG = 'cron-delivery-guard'
19
+
20
+ // ---- 类型定义 ----
21
+
22
+ interface ToolCallEvent {
23
+ toolName: string
24
+ toolCallId: string
25
+ params: Record<string, unknown>
26
+ result?: { content: string }
27
+ }
28
+
29
+ interface HookContext {
30
+ agentId: string
31
+ sessionKey: string
32
+ }
33
+
34
+ interface BeforeToolCallResult {
35
+ block?: boolean
36
+ blockReason?: string
37
+ params?: Record<string, unknown>
38
+ }
39
+
40
+ // ---- delivery 类型 ----
41
+
42
+ interface CronDelivery {
43
+ mode?: string
44
+ channel?: string
45
+ to?: string
46
+ bestEffort?: boolean
47
+ [key: string]: unknown
48
+ }
49
+ /**
50
+ * 解析 OpenClaw mobook direct 会话 key。
51
+ * 形如 `agent:main:mobook:direct:14:5466`(大小写不敏感,与路由 toLowerCase 一致)
52
+ * - 倒数第二段:account / peer(delivery.accountId)
53
+ * - 最后一段:会话 id(delivery.to)
54
+ */
55
+ export function formatterSessionKey(sessionKey: string): { agentId: string; sessionId: string } {
56
+ const parts = sessionKey.split(':').filter((s) => s.length > 0)
57
+ const norm = parts.map((s) => s.toLowerCase())
58
+ if (parts.length >= 6 && norm[0] === 'agent' && norm[2] === 'mobook' && norm[3] === 'direct') {
59
+ return {
60
+ agentId: parts[4] ?? '',
61
+ sessionId: parts[5] ?? ''
62
+ }
63
+ }
64
+ if (parts.length >= 2) {
65
+ return {
66
+ agentId: parts[parts.length - 2] ?? '',
67
+ sessionId: parts[parts.length - 1] ?? ''
68
+ }
69
+ }
70
+ return { agentId: '', sessionId: '' }
71
+ }
72
+
73
+ // ---- 辅助函数 ----
74
+
75
+ /**
76
+ * 判断是否为 cron 工具调用
77
+ */
78
+ function isCronTool(toolName: string): boolean {
79
+ return toolName === 'cron'
80
+ }
81
+
82
+ /**
83
+ * 从 cron 参数中提取 delivery 配置
84
+ * cron 工具的参数结构可能是:
85
+ * - params.delivery (顶层)
86
+ * - params.job.delivery (嵌套在 job 中)
87
+ */
88
+ function extractDelivery(params: Record<string, unknown>): CronDelivery | null {
89
+ // 尝试顶层 delivery
90
+ if (params.delivery && typeof params.delivery === 'object') {
91
+ return params.delivery as CronDelivery
92
+ }
93
+
94
+ // 尝试 job.delivery
95
+ const job = params.job as Record<string, unknown> | undefined
96
+ if (job?.delivery && typeof job.delivery === 'object') {
97
+ return job.delivery as CronDelivery
98
+ }
99
+
100
+ // 尝试 payload 中的 deliver 相关字段 (兼容 qqbot-cron 风格)
101
+ const payload = job?.payload as Record<string, unknown> | undefined
102
+ if (payload?.deliver === true && payload.channel === undefined) {
103
+ // payload 风格: { deliver: true, channel?: string }
104
+ // 这种情况不是 delivery 对象,跳过
105
+ return null
106
+ }
107
+
108
+ return null
109
+ }
110
+
111
+ /**
112
+ * 判断 delivery 是否需要注入 bestEffort
113
+ * 条件: mode 为 "announce" 且没有 channel
114
+ */
115
+ function needsBestEffort(delivery: CronDelivery): boolean {
116
+ return delivery.mode === 'announce' && !delivery.channel && !delivery.bestEffort
117
+ }
118
+
119
+ /**
120
+ * 深拷贝 params 并注入 bestEffort: true
121
+ */
122
+ function injectBestEffort(params: Record<string, unknown>, sk: string): Record<string, unknown> {
123
+ const newParams = JSON.parse(JSON.stringify(params)) as Record<string, unknown>
124
+ const { agentId, sessionId } = formatterSessionKey(sk)
125
+ // 顶层 delivery
126
+ if (newParams.delivery && typeof newParams.delivery === 'object') {
127
+ ;(newParams.delivery as CronDelivery).bestEffort = true
128
+ ;(newParams.delivery as CronDelivery).to = sessionId
129
+ ;(newParams.delivery as CronDelivery).accountId = agentId
130
+ ;(newParams.delivery as CronDelivery).channel = "dcgchat-test"
131
+ newParams.sessionKey = sk
132
+ return newParams
133
+ }
134
+
135
+ // job.delivery
136
+ const job = newParams.job as Record<string, unknown> | undefined
137
+ if (job?.delivery && typeof job.delivery === 'object') {
138
+ ;(job.delivery as CronDelivery).bestEffort = true
139
+ ;(newParams.delivery as CronDelivery).to = sessionId
140
+ ;(newParams.delivery as CronDelivery).accountId = agentId
141
+ ;(newParams.delivery as CronDelivery).channel = "dcgchat-test"
142
+ newParams.sessionKey = sk
143
+ return newParams
144
+ }
145
+
146
+ return newParams
147
+ }
148
+
149
+ export function cronToolCall(event: { toolName: any; params: any; toolCallId: any }, sk: string) {
150
+ const { toolName, params, toolCallId } = event
151
+
152
+ // 仅处理 cron 工具
153
+ if (isCronTool(toolName)) {
154
+ const delivery = extractDelivery(params)
155
+ if (!delivery) {
156
+ dcgLogger(`[${LOG_TAG}] cron call (${toolCallId}) has no delivery config, skip.`)
157
+ return undefined
158
+ }
159
+ if (!needsBestEffort(delivery)) {
160
+ dcgLogger(
161
+ `[${LOG_TAG}] cron call (${toolCallId}) delivery does not need bestEffort ` +
162
+ `(mode=${String(delivery.mode)}, channel=${String(delivery.channel)}, bestEffort=${String(delivery.bestEffort)}), skip.`
163
+ )
164
+ return undefined
165
+ }
166
+
167
+ // ★ 核心:注入 bestEffort: true
168
+ const newParams = injectBestEffort(params, sk)
169
+ dcgLogger(
170
+ `[${LOG_TAG}] cron call (${toolCallId}) injected bestEffort=true ` +
171
+ `(mode=announce, no channel). delivery=${JSON.stringify(newParams.delivery || (newParams.job as Record<string, unknown>)?.delivery)}`
172
+ )
173
+
174
+ return { params: newParams }
175
+ } else if (toolName === 'exec') {
176
+ if (params.command.indexOf('cron create') || params.command.indexOf('cron add')) {
177
+ const newParams = JSON.parse(JSON.stringify(params)) as Record<string, unknown>
178
+ newParams.command =
179
+ params.command.replace('--json', '') + ` --session-key ${sk} --channel ${"dcgchat-test"} --to ${sk} --json`
180
+ return { params: newParams }
181
+ }
182
+ }
183
+
184
+ return undefined
185
+ }
@@ -3,7 +3,7 @@ import { WebSocket } from 'ws'
3
3
  import crypto from 'crypto'
4
4
  import { deriveDeviceIdFromPublicKey, publicKeyRawBase64UrlFromPem, buildDeviceAuthPayloadV3, signDevicePayload } from './security.js'
5
5
  import { dcgLogger } from '../utils/log.js'
6
- import { sendDcgchatCron, updateCronJobSessionKey } from '../cron.js'
6
+ import { finishedDcgchatCron, sendDcgchatCron } from '../cron.js'
7
7
 
8
8
  export interface GatewayEvent {
9
9
  type: string
@@ -196,7 +196,7 @@ export class GatewayConnection {
196
196
  dcgLogger('Gateway connection opened(等待 connect.challenge)')
197
197
  })
198
198
 
199
- this.ws.on('message', (data) => {
199
+ this.ws.on('message', (data, ...args) => {
200
200
  try {
201
201
  const msg = JSON.parse(data.toString())
202
202
  this.handleMessage(
@@ -357,20 +357,18 @@ export class GatewayConnection {
357
357
 
358
358
  if (msg.type === 'event') {
359
359
  try {
360
+ // 定时任务相关事件
360
361
  if (msg.event === 'cron') {
361
362
  dcgLogger(`[Gateway] 收到事件: ${JSON.stringify(msg)}`)
362
363
  if (msg.payload?.action === 'added') {
363
- updateCronJobSessionKey(msg.payload?.jobId as string)
364
- }
365
- if (msg.payload?.action === 'updated') {
366
364
  sendDcgchatCron()
367
365
  }
368
- if (msg.payload?.action === 'added') {
369
- updateCronJobSessionKey(msg.payload?.jobId as string)
370
- }
371
366
  if (msg.payload?.action === 'updated') {
372
367
  sendDcgchatCron()
373
368
  }
369
+ if (msg.payload?.action === 'finished') {
370
+ finishedDcgchatCron(msg.payload?.jobId as string)
371
+ }
374
372
  }
375
373
  } catch (error) {
376
374
  dcgLogger(`[Gateway] 处理事件失败: ${error}`, 'error')
package/src/monitor.ts CHANGED
@@ -128,7 +128,6 @@ export async function monitorDcgchatProvider(opts: MonitorDcgchatOpts): Promise<
128
128
  const msg = parsed as unknown as InboundMessage
129
129
  // 与 monitor 原逻辑一致:工具类指令不进入 running,避免误触工具链监控
130
130
  const effectiveSessionKey = getSessionKey(msg.content, account.accountId)
131
- console.log('🚀 ~ connect ~ effectiveSessionKey:', effectiveSessionKey)
132
131
  if (!ignoreToolCommand.includes(msg.content.text?.trim() ?? '')) {
133
132
  setMsgStatus(effectiveSessionKey, 'running')
134
133
  } else {
@@ -156,15 +155,15 @@ export async function monitorDcgchatProvider(opts: MonitorDcgchatOpts): Promise<
156
155
  dcgLogger(`openclaw_bot_event unknown event_type: ${event_type}, ${data.toString()}`)
157
156
  }
158
157
  } else if (event_type === 'cron') {
159
- const { job_id } = parsed.content
158
+ const { job_id, message_id } = parsed.content
160
159
  if (operation_type === 'remove') {
161
160
  await onRemoveCronJob(job_id)
162
161
  } else if (operation_type === 'enable') {
163
162
  await onEnabledCronJob(job_id)
164
163
  } else if (operation_type === 'disable') {
165
164
  await onDisabledCronJob(job_id)
166
- } else if (operation_type === 'exec') {
167
- await onRunCronJob(job_id)
165
+ } else if (operation_type === 'run') {
166
+ await onRunCronJob(job_id, message_id)
168
167
  }
169
168
  } else {
170
169
  dcgLogger(`openclaw_bot_event unknown operation_type: ${operation_type}, ${data.toString()}`)
package/src/tool.ts CHANGED
@@ -3,6 +3,8 @@ import { getMsgStatus } from './utils/global.js'
3
3
  import { dcgLogger } from './utils/log.js'
4
4
  import { sendFinal, sendText, wsSendRaw } from './transport.js'
5
5
  import { getCurrentSessionKey, getEffectiveMsgParams } from './utils/params.js'
6
+ import { channelInfo, ENV } from './utils/constant.js'
7
+ import { cronToolCall } from './cronToolCall.js'
6
8
 
7
9
  let toolCallId = ''
8
10
  let toolName = ''
@@ -63,6 +65,17 @@ function sendToolCallMessage(sk: string, text: string, toolCallId: string, isCov
63
65
  })
64
66
  }
65
67
 
68
+ /**
69
+ * 深拷贝 params 并注入 bestEffort: true
70
+ */
71
+ interface CronDelivery {
72
+ mode?: string
73
+ channel?: string
74
+ to?: string
75
+ bestEffort?: boolean
76
+ [key: string]: unknown
77
+ }
78
+
66
79
  export function monitoringToolMessage(api: OpenClawPluginApi) {
67
80
  for (const item of eventList) {
68
81
  api.on(item.event as PluginHookName, (event: any, args: any) => {
@@ -73,6 +86,10 @@ export function monitoringToolMessage(api: OpenClawPluginApi) {
73
86
  if (['after_tool_call', 'before_tool_call'].includes(item.event)) {
74
87
  const { result: _result, ...rest } = event
75
88
  dcgLogger(`工具调用结果: ~ event:${item.event} ~ params:${JSON.stringify(rest)}`)
89
+
90
+ if (item.event === 'before_tool_call') {
91
+ return cronToolCall(rest, sk)
92
+ }
76
93
  const text = JSON.stringify({
77
94
  type: item.event,
78
95
  specialIdentification: 'dcgchat_tool_call_special_identification',
@@ -129,3 +129,17 @@ export const getSessionKey = (content: any, accountId: string) => {
129
129
  })
130
130
  return real_mobook === '1' ? route.sessionKey : `agent:main:mobook:direct:${agent_id}:${session_id}`.toLowerCase()
131
131
  }
132
+
133
+ const cronMessageIdMap = new Map<string, string>()
134
+
135
+ export function setCronMessageId(sk: string, messageId: string) {
136
+ cronMessageIdMap.set(sk, messageId)
137
+ }
138
+
139
+ export function getCronMessageId(sk: string): string {
140
+ return cronMessageIdMap.get(sk) ?? ''
141
+ }
142
+
143
+ export function removeCronMessageId(sk: string) {
144
+ cronMessageIdMap.delete(sk)
145
+ }
@@ -53,7 +53,9 @@ export function getEffectiveMsgParams(sessionKey?: string): IMsgParams {
53
53
  export function setParamsMessage(sessionKey: string, params: Partial<IMsgParams>) {
54
54
  if (!sessionKey) return
55
55
  currentSessionKey = sessionKey
56
- paramsMessageMap.set(sessionKey, resolveParamsMessage({ ...params, sessionKey }))
56
+ const previous = paramsMessageMap.get(sessionKey)
57
+ const base = previous ? resolveParamsMessage(previous) : getParamsDefaults()
58
+ paramsMessageMap.set(sessionKey, resolveParamsMessage({ ...base, ...params, sessionKey }))
57
59
  }
58
60
 
59
61
  export function getParamsMessage(sessionKey: string): IMsgParams | undefined {