@dcrays/dcgchat 0.2.34 → 0.3.18

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/src/tool.ts CHANGED
@@ -1,7 +1,10 @@
1
1
  import type { OpenClawPluginApi } from 'openclaw/plugin-sdk'
2
- import { getMsgParams, getMsgStatus, getWsConnection } from './utils/global.js'
2
+ import { getMsgStatus } from './utils/global.js'
3
3
  import { dcgLogger } from './utils/log.js'
4
- import { isWsOpen, sendFinal, sendText } from './transport.js'
4
+ import { sendFinal, sendText, wsSendRaw } from './transport.js'
5
+ import { getCurrentSessionKey, getEffectiveMsgParams } from './utils/params.js'
6
+ import { channelInfo, ENV } from './utils/constant.js'
7
+ import { cronToolCall } from './cronToolCall.js'
5
8
 
6
9
  let toolCallId = ''
7
10
  let toolName = ''
@@ -30,8 +33,10 @@ type PluginHookName =
30
33
  | 'subagent_ended'
31
34
  | 'gateway_start'
32
35
  | 'gateway_stop'
36
+
37
+ // message_received 没有 sessionKey 前置到bot中执行
33
38
  const eventList = [
34
- { event: 'message_received', message: '' },
39
+ // { event: 'message_received', message: '' },
35
40
  // {event: 'before_model_resolve', message: ''},
36
41
  // {event: 'before_prompt_build', message: '正在查阅背景资料,构建思考逻辑'},
37
42
  // {event: 'before_agent_start', message: '书灵墨宝已就位,准备开始执行任务'},
@@ -49,80 +54,78 @@ const eventList = [
49
54
  { event: 'after_tool_call', message: '' }
50
55
  ]
51
56
 
52
- function sendToolCallMessage(text: string, toolCallId: string, isCover: number) {
53
- const ws = getWsConnection()
54
- const params = getMsgParams()
55
- if (isWsOpen()) {
56
- ws?.send(
57
- JSON.stringify({
58
- messageType: 'openclaw_bot_chat',
59
- _userId: params?.userId,
60
- source: 'client',
61
- is_finish: -1,
62
- content: {
63
- is_finish: -1,
64
- bot_token: params?.token,
65
- domain_id: params?.domainId,
66
- app_id: params?.appId,
67
- bot_id: params?.botId,
68
- agent_id: params?.agentId,
69
- tool_call_id: toolCallId,
70
- is_cover: isCover,
71
- thinking_content: text,
72
- response: '',
73
- session_id: params?.sessionId,
74
- message_id: params?.messageId || Date.now().toString()
75
- }
76
- })
77
- )
78
- }
57
+ function sendToolCallMessage(sk: string, text: string, toolCallId: string, isCover: number) {
58
+ const params = getEffectiveMsgParams(sk)
59
+ wsSendRaw(params, {
60
+ is_finish: -1,
61
+ tool_call_id: toolCallId,
62
+ is_cover: isCover,
63
+ thinking_content: text,
64
+ response: ''
65
+ })
66
+ }
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
79
77
  }
80
78
 
81
79
  export function monitoringToolMessage(api: OpenClawPluginApi) {
82
80
  for (const item of eventList) {
83
- api.on(item.event as PluginHookName, (event: any) => {
84
- const status = getMsgStatus()
85
- if (status === 'running') {
86
- dcgLogger(`工具调用结果: ~ event:${item.event}`)
87
- if (['after_tool_call', 'before_tool_call'].includes(item.event)) {
88
- const { result: _result, ...rest } = event
89
- const text = JSON.stringify({
90
- type: item.event,
91
- specialIdentification: 'dcgchat_tool_call_special_identification',
92
- callId: event.toolCallId || event.runId || Date.now().toString(),
93
- ...rest,
94
- status: item.event === 'after_tool_call' ? 'finished' : 'running'
95
- })
96
- sendToolCallMessage(text, event.toolCallId || event.runId || Date.now().toString(), item.event === 'after_tool_call' ? 1 : 0)
97
- } else if (item.event) {
98
- if (item.event === 'llm_output') {
99
- if (event.lastAssistant?.errorMessage === '429-账户额度耗尽') {
100
- const params = getMsgParams()
101
- const message = '您的积分已消耗完,您可以通过充值积分来继续使用'
102
- const ctx = {
103
- userId: params.userId,
104
- botToken: params.token,
105
- domainId: params.domainId,
106
- appId: params.appId,
107
- botId: params.botId,
108
- agentId: params.agentId,
109
- sessionId: params.sessionId,
110
- messageId: params.messageId
81
+ api.on(item.event as PluginHookName, (event: any, args: any) => {
82
+ const sk = args?.sessionKey as string
83
+ if (sk) {
84
+ const status = getMsgStatus(sk)
85
+ if (status === 'running') {
86
+ if (['after_tool_call', 'before_tool_call'].includes(item.event)) {
87
+ const { result: _result, ...rest } = event
88
+ dcgLogger(`工具调用结果: ~ event:${item.event} ~ params:${JSON.stringify(rest)}`)
89
+
90
+ if (item.event === 'before_tool_call') {
91
+ return cronToolCall(rest, sk)
92
+ }
93
+ const text = JSON.stringify({
94
+ type: item.event,
95
+ specialIdentification: 'dcgchat_tool_call_special_identification',
96
+ callId: event.toolCallId || event.runId || Date.now().toString(),
97
+ ...rest,
98
+ status: item.event === 'after_tool_call' ? 'finished' : 'running'
99
+ })
100
+ sendToolCallMessage(
101
+ sk,
102
+ text,
103
+ event.toolCallId || event.runId || Date.now().toString(),
104
+ item.event === 'after_tool_call' ? 1 : 0
105
+ )
106
+ } else if (item.event) {
107
+ const msgCtx = getEffectiveMsgParams(sk)
108
+ if (item.event === 'llm_output') {
109
+ if (event.lastAssistant?.errorMessage === '1003-额度不足请充值') {
110
+ const message = '您的积分已消耗完,您可以通过充值积分来继续使用'
111
+ sendText(message, msgCtx, { message_tags: { insufficient_balance: 1 }, is_finish: -1 })
112
+ sendFinal(msgCtx, '积分不足')
113
+ return
111
114
  }
112
- sendText(ctx, message, { message_tags: { insufficient_balance: 1 }, is_finish: -1 })
113
- sendFinal(ctx)
114
- return
115
115
  }
116
+ const text = JSON.stringify({
117
+ type: item.event,
118
+ specialIdentification: 'dcgchat_tool_call_special_identification',
119
+ toolName: '',
120
+ callId: event.runId || Date.now().toString(),
121
+ params: item.message
122
+ })
123
+ sendToolCallMessage(sk, text, event.runId || Date.now().toString(), 0)
124
+ dcgLogger(`工具调用结果: ~ event:${item.event} ${status}`)
116
125
  }
117
- const text = JSON.stringify({
118
- type: item.event,
119
- specialIdentification: 'dcgchat_tool_call_special_identification',
120
- toolName: '',
121
- callId: event.runId || Date.now().toString(),
122
- params: item.message
123
- })
124
- sendToolCallMessage(text, event.runId || Date.now().toString(), 0)
125
126
  }
127
+ } else {
128
+ dcgLogger(`工具调用结果: ~ event:${item.event} ~ 没有sessionKey 为执行`)
126
129
  }
127
130
  })
128
131
  }
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,86 +26,176 @@ 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 {
59
+ return {
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
65
+ }
66
+ }
67
+
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
40
76
  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
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
49
85
  }
50
86
  }
51
87
 
52
- function buildEnvelope(ctx: DcgchatMsgContext, extra: Record<string, unknown>) {
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
53
96
  return {
54
- messageType: 'openclaw_bot_chat' as const,
55
- _userId: ctx.userId,
56
- source: 'client' as const,
57
- content: buildContent(ctx, extra)
97
+ messageType: 'openclaw_bot_chat',
98
+ _userId: base.userId,
99
+ source: 'client',
100
+ content: buildWireContent(base, d, content)
58
101
  }
59
102
  }
60
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 {
94
- return wsSend(ctx, { response: text, state: 'chunk' })
161
+ export function sendChunk(text: string, ctx: IMsgParams, chunkIdx: number): boolean {
162
+ return wsSend(ctx, { response: text, state: 'chunk', chunk_idx: chunkIdx })
95
163
  }
96
164
 
97
- export function sendFinal(ctx: DcgchatMsgContext): boolean {
98
- dcgLogger(` message handling complete state: final`)
165
+ export function sendFinal(ctx: IMsgParams, tag?: string): boolean {
166
+ dcgLogger(` message handling complete state: final tag:${tag}`)
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
  }
177
+
178
+ export function sendEventMessage(url: string, sessionKey: string) {
179
+ const ctx = getEffectiveMsgParams(sessionKey)
180
+ const ws = getWsConnection()
181
+ if (isWsOpen()) {
182
+ ws?.send(
183
+ JSON.stringify({
184
+ messageType: 'openclaw_bot_event',
185
+ source: 'client',
186
+ content: {
187
+ event_type: 'cron',
188
+ operation_type: 'install',
189
+ bot_token: ctx.botToken,
190
+ domain_id: ctx.domainId,
191
+ app_id: ctx.appId,
192
+ oss_url: url,
193
+ bot_id: ctx.botId,
194
+ agent_id: ctx.agentId,
195
+ session_id: ctx.sessionId,
196
+ message_id: ctx.messageId || Date.now().toString()
197
+ }
198
+ })
199
+ )
200
+ }
201
+ }
package/src/types.ts CHANGED
@@ -44,6 +44,7 @@ export type InboundMessage = {
44
44
  bot_id?: string
45
45
  agent_id?: string
46
46
  session_id: string
47
+ real_mobook: string | number
47
48
  message_id: string
48
49
  text: string
49
50
  files?: {
@@ -115,12 +116,16 @@ export interface IStsTokenReq {
115
116
  }
116
117
 
117
118
  export interface IMsgParams {
118
- userId: number
119
- token: string
120
- sessionId: string
121
- messageId: string
122
- domainId: string
123
- appId: string
124
- botId: string
125
- agentId: 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
130
+ is_finish?: number
126
131
  }
@@ -2,6 +2,6 @@ export const ENV: 'production' | 'test' | 'develop' = 'production'
2
2
 
3
3
 
4
4
  export const systemCommand = ['/new', '/status']
5
- export const interruptCommand = ['/stop']
5
+ export const interruptCommand = ['chat.stop']
6
6
 
7
- export const ignoreToolCommand = ['/search', '/abort', '/queue interrupt', ...systemCommand, ...interruptCommand]
7
+ export const ignoreToolCommand = ['/search', '/abort', '/stop', '/queue interrupt', ...systemCommand, ...interruptCommand]
@@ -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,20 +68,22 @@ 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
- }
71
+ export type MsgSessionStatus = 'running' | 'finished' | ''
79
72
 
80
- let msgStatus: 'running' | 'finished' | '' = ''
81
- export function setMsgStatus(status: 'running' | 'finished' | '') {
82
- msgStatus = status
73
+ const msgStatusBySessionKey = new Map<string, MsgSessionStatus>()
74
+
75
+ export function setMsgStatus(sessionKey: string, status: MsgSessionStatus) {
76
+ if (!sessionKey?.trim()) return
77
+ if (status === '') {
78
+ msgStatusBySessionKey.delete(sessionKey)
79
+ } else {
80
+ msgStatusBySessionKey.set(sessionKey, status)
81
+ }
83
82
  }
84
- export function getMsgStatus() {
85
- return msgStatus
83
+
84
+ export function getMsgStatus(sessionKey: string): MsgSessionStatus {
85
+ if (!sessionKey?.trim()) return ''
86
+ return msgStatusBySessionKey.get(sessionKey) ?? ''
86
87
  }
87
88
 
88
89
  const getMediaKey = (url: string) => url.split(/[\\/]/).pop() ?? url
@@ -115,3 +116,30 @@ export function clearSentMediaKeys(messageId?: string) {
115
116
  sentMediaKeysBySession.clear()
116
117
  }
117
118
  }
119
+
120
+ export const getSessionKey = (content: any, accountId: string) => {
121
+ const { real_mobook, agent_id, conversation_id, session_id } = content
122
+ const core = getDcgchatRuntime()
123
+
124
+ const route = core.channel.routing.resolveAgentRoute({
125
+ cfg: getOpenClawConfig() as OpenClawConfig,
126
+ channel: "dcgchat",
127
+ accountId: accountId || 'default',
128
+ peer: { kind: 'direct', id: session_id }
129
+ })
130
+ return real_mobook == '1' ? route.sessionKey : `agent:main:mobook:direct:${agent_id}:${session_id}`.toLowerCase()
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
+ }
@@ -0,0 +1,67 @@
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"] 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
+ const previous = paramsMessageMap.get(sessionKey)
57
+ const base = previous ? resolveParamsMessage(previous) : getParamsDefaults()
58
+ paramsMessageMap.set(sessionKey, resolveParamsMessage({ ...base, ...params, sessionKey }))
59
+ }
60
+
61
+ export function getParamsMessage(sessionKey: string): IMsgParams | undefined {
62
+ return paramsMessageMap.get(sessionKey)
63
+ }
64
+
65
+ export function getCurrentSessionKey(): string | null {
66
+ return currentSessionKey
67
+ }
@@ -94,11 +94,9 @@ function stripMobookNoise(s: string) {
94
94
  }
95
95
 
96
96
  /**
97
- * 从文本中扫描 `/mobook/` 片段,按最长后缀匹配合法扩展名(兜底,不依赖 FILE_NAME 字符集)
97
+ * 从文本中扫描 `.../mobook/...` `...\mobook\...` 片段,按最长后缀匹配合法扩展名(兜底)
98
98
  */
99
- function collectMobookPathsByScan(text: string, result: Set<string>): void {
100
- const lower = text.toLowerCase()
101
- const needle = '/mobook/'
99
+ function collectMobookPathsAfterNeedle(text: string, lower: string, needle: string, result: Set<string>): void {
102
100
  let from = 0
103
101
  while (from < text.length) {
104
102
  const i = lower.indexOf(needle, from)
@@ -136,8 +134,16 @@ function collectMobookPathsByScan(text: string, result: Set<string>): void {
136
134
  }
137
135
  }
138
136
 
137
+ function collectMobookPathsByScan(text: string, result: Set<string>): void {
138
+ const lower = text.toLowerCase()
139
+ collectMobookPathsAfterNeedle(text, lower, '/mobook/', result)
140
+ collectMobookPathsAfterNeedle(text, lower, '\\mobook\\', result)
141
+ }
142
+
139
143
  export function extractMobookFiles(text = '') {
140
144
  if (typeof text !== 'string' || !text.trim()) return []
145
+ // 全角冒号(中文输入常见)→ 半角,便于匹配 c:\mobook\
146
+ text = text.replace(/\uFF1A/g, ':')
141
147
  const result = new Set<string>()
142
148
  // ✅ 扩展名(必须长扩展名优先,见 EXT_SORTED_FOR_REGEX)
143
149
  const EXT = `(${EXT_SORTED_FOR_REGEX.join('|')})`
@@ -157,6 +163,14 @@ export function extractMobookFiles(text = '') {
157
163
  ;(text.match(fullPathReg) || []).forEach((p) => {
158
164
  result.add(normalizePath(p))
159
165
  })
166
+ // 2️⃣b Windows 实际保存路径:C:\mobook\xxx、c:/mobook/xxx、\mobook\xxx(模型常写反斜杠,原先无法识别)
167
+ const winMobookReg = new RegExp(`(?:[a-zA-Z]:)?[/\\\\]mobook[/\\\\]${FILE_NAME}\\.${EXT}`, 'gi')
168
+ ;(text.match(winMobookReg) || []).forEach((full) => {
169
+ const name = full.replace(/^(?:[a-zA-Z]:)?[/\\\\]mobook[/\\\\]/i, '').trim()
170
+ if (isValidFileName(name)) {
171
+ result.add(normalizePath(`/mobook/${name}`))
172
+ }
173
+ })
160
174
  // 3️⃣ mobook下的 xxx.xxx
161
175
  const inlineReg = new RegExp(`mobook下的\\s*(${FILE_NAME}\\.${EXT})`, 'gi')
162
176
  ;(text.match(inlineReg) || []).forEach((item) => {