@wenbin_wb/dsh-bridge 2.10.9 → 2.10.11

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.
@@ -1,221 +1,235 @@
1
- // 斜杠命令解释器(/sessions /use /new /workspaces /status /help …)
2
- // 自 conversation-bridge.js 拆出。routeCommand(node, text, senderId) 只通过 node 参数
3
- // 访问会话桥能力,目录/渲染逻辑来自 session-catalog.js。
4
- import {
5
- listSessions, listWorkspaces, validateWorkspacePath, renderSessions,
6
- sessionsInDisplayOrder, describeTurnEnd, helpText, fmtTime, fmtSessionId,
7
- sessionLabel, getWorkspaceBasename,
8
- } from './session-catalog.js'
9
- import { isSafeWorkspacePath } from '../security/path-validator.js'
10
- import { basename, normalize } from 'node:path'
11
-
12
- export async function routeCommand(node, text, senderId = null) {
13
- const trimmed = text.trim()
14
-
15
- if (trimmed === '/yes' || trimmed === '/no' || /^[12]$/.test(trimmed)) {
16
- if (node.resolveApproval(trimmed, senderId)) return true
17
- }
18
-
19
- if (!trimmed.startsWith('/')) return false
20
-
21
- const [command, ...rest] = trimmed.slice(1).split(/\s+/)
22
- switch (command) {
23
- case 'help':
24
- await node.sendText(helpText())
25
- return true
26
- case 'sessions':
27
- case 'list':
28
- await node.sendText(await renderSessions(node))
29
- return true
30
- case 'use':
31
- case 'resume': {
32
- const index = Number(rest[0])
33
- const sessions = sessionsInDisplayOrder(await listSessions(node))
34
- if (!Number.isInteger(index) || index < 1 || index > sessions.length) {
35
- await node.sendText(`❌ **无效会话编号**:\`${rest[0] ?? ''}\`\n\n> 可用编号范围:\`1 – ${sessions.length}\`(发送 \`/sessions\` 查看会话列表)`)
36
- return true
37
- }
38
- const session = sessions[index - 1]
39
- node.setActiveSessionById(session.id)
40
- const title = session.title || (session.events ? sessionLabel(session) : '')
41
- const titleLine = title ? `\n- **标题**:${title}` : ''
42
- await node.sendText(`✓ **已切换到会话 #${index}**${titleLine}\n- **会话 ID**:\`${fmtSessionId(session.id)}\``)
43
- return true
44
- }
45
- case 'rename': {
46
- if (!node.activeSessionId) {
47
- await node.sendText(`❌ **当前没有活动会话**\n\n> 请先使用 \`/sessions\` 查看会话列表并通过 \`/use 编号\` 切换到目标会话,或通过 \`/new <提示词>\` 创建新会话。`)
48
- return true
49
- }
50
- const newTitle = rest.join(' ').trim()
51
- if (!newTitle) {
52
- await node.sendText(`❌ **缺少新标题参数**\n\n> 用法:\`/rename <新标题>\`\n> 示例:\`/rename 优化登录交互逻辑\``)
53
- return true
54
- }
55
-
56
- try {
57
- const session = node.activeSession()
58
- if (session) {
59
- session.title = newTitle
60
- }
61
- if (node.ctx.sessionPersistence?.update) {
62
- await node.ctx.sessionPersistence.update(node.activeSessionId, { title: newTitle }).catch(() => {})
63
- }
64
- await node.sendText(`✓ **会话重命名成功**\n- **会话 ID**:\`${fmtSessionId(node.activeSessionId)}\`\n- **新标题**:${newTitle}`)
65
- } catch (err) {
66
- await node.sendText(`❌ **重命名失败**:${err instanceof Error ? err.message : String(err)}`)
67
- }
68
- return true
69
- }
70
- case 'workspaces': {
71
- const workspaces = await listWorkspaces(node)
72
- if (workspaces.length === 0) {
73
- await node.sendText(`## 🗂️ 可用工作区\n\n> 当前没有已注册的工作区。可使用 \`/new <提示词> @<路径>\` 指定项目目录。`)
74
- return true
75
- }
76
- const rows = workspaces.map((w, i) => {
77
- const titleStr = w.title && w.title !== w.path ? w.title : getWorkspaceBasename(w.path)
78
- const safeTitle = titleStr.replace(/\|/g, '|')
79
- return `| **@${i + 1}** | ${safeTitle} | \`${w.path}\` |`
80
- })
81
- await node.sendText([
82
- `## 🗂️ 可用工作区 (共 ${workspaces.length} 个)`,
83
- `> 新建会话:发送 \`/new <提示词> @序号\` 或 \`/new <提示词> @路径\``,
84
- '',
85
- '| 序号 | 工作区名称 | 目录路径 |',
86
- '| :--- | :--- | :--- |',
87
- ...rows,
88
- ].join('\n'))
89
- return true
90
- }
91
- case 'addworkspace': {
92
- const targetPath = rest.join(' ').trim()
93
- if (!targetPath) {
94
- await node.sendText(`❌ **缺少工作区路径**\n\n> 用法:\`/addworkspace <电脑绝对路径>\`\n> 示例:\`/addworkspace D:\\IdeaProjects\\my-app\``)
95
- return true
96
- }
97
- try {
98
- const safetyCheck = await isSafeWorkspacePath(targetPath)
99
- if (!safetyCheck.valid) {
100
- await node.sendText(`⚠️ **${safetyCheck.error || '路径安全校验未通过'}**:\`${targetPath}\`\n\n> 出于安全考虑,禁止将系统关键目录或敏感配置文件所在路径登记为工作区。`)
101
- return true
102
- }
103
- const resolved = safetyCheck.path
104
- const title = basename(resolved) || resolved
105
- if (node.ctx.workspaceRegistry?.add) {
106
- await node.ctx.workspaceRegistry.add({ path: resolved, title }).catch(() => {})
107
- } else if (node.ctx.workspaceRegistry?.register) {
108
- await node.ctx.workspaceRegistry.register({ path: resolved, title }).catch(() => {})
109
- }
110
- const workspaces = await listWorkspaces(node)
111
- const foundIndex = workspaces.findIndex(w => normalize(w.path) === normalize(resolved))
112
- const numStr = foundIndex >= 0 ? `@${foundIndex + 1}` : ''
113
- await node.sendText([
114
- `✓ **工作区添加成功**!`,
115
- `- **名称**:${title}`,
116
- `- **路径**:\`${resolved}\``,
117
- foundIndex >= 0 ? `- **快捷编号**:\`${numStr}\`` : '',
118
- '',
119
- `> 发送 \`/new <提示词> ${numStr || '@' + resolved}\` 即可直接在此工作区创建会话。`,
120
- ].filter(Boolean).join('\n'))
121
- } catch (err) {
122
- await node.sendText(`❌ **添加工作区失败**:${err instanceof Error ? err.message : String(err)}`)
123
- }
124
- return true
125
- }
126
- case 'new': {
127
- // 解析尾部 @N 或 @路径 作为工作区 cwd
128
- const args = rest.join(' ').trim()
129
- let cwd
130
- let prompt = args
131
- const atMatch = args.match(/\s+@(\S+)$/)
132
- if (atMatch) {
133
- prompt = args.slice(0, atMatch.index).trim()
134
- const sel = atMatch[1]
135
- const workspaces = await listWorkspaces(node)
136
- if (/^\d+$/.test(sel)) {
137
- const idx = Number(sel)
138
- const ws = workspaces[idx - 1]
139
- if (ws) cwd = ws.path
140
- else {
141
- await node.sendText(`❌ **无效工作区编号**:\`${sel}\`\n\n> 请发送 \`/workspaces\` 查看可用工作区列表与编号。`)
142
- return true
143
- }
144
- } else {
145
- // 直接指定路径时,规范化并校验(必须完全匹配已注册工作区)
146
- const validation = await validateWorkspacePath(node, sel)
147
- if (!validation.valid) {
148
- await node.sendText(validation.error)
149
- return true
150
- }
151
- cwd = validation.path
152
- }
153
- }
154
- await node.createSession(prompt, cwd)
155
- return true
156
- }
157
- case 'stop': {
158
- node.stopAllHeartbeats()
159
- const agent = node.activeAgent()
160
- if (!agent) {
161
- await node.sendText(`ℹ️ **当前没有正在运行的 Agent 任务**`)
162
- } else {
163
- agent.cancel({ kind: 'user' })
164
- await node.sendText(`⏹ **已请求停止当前任务**`)
165
- }
166
- return true
167
- }
168
- case 'end': {
169
- node.stopAllHeartbeats()
170
- // 结束当前会话:停止 agent 并清除活动会话(进入"没有活动会话"状态)
171
- const agent = node.activeAgent()
172
- if (agent) agent.cancel({ kind: 'user' })
173
- node.activeSessionId = null
174
- await node.onActiveSessionChange?.(null)
175
- await node.sendText(`✓ **已结束当前会话**(没有活动会话)。\n\n> **后续操作**:\n> - \`/new <提示词>\` — 新建会话并开始\n> - \`/sessions\` — 查看历史会话列表\n> - \`/help\` — 查看常用指令帮助`)
176
- return true
177
- }
178
- case 'status': {
179
- const agent = node.activeAgent()
180
- const session = node.activeSession()
181
- if (!session) {
182
- await node.sendText(`## 📊 Agent 状态看板\n\n> 当前没有活动会话。\n> 发送 \`/new <提示词>\` 开始新任务,或发送 \`/sessions\` 查看已有会话。`)
183
- return true
184
- }
185
- const statusMap = {
186
- idle: '空闲 (idle)',
187
- running: '运行中 (running)',
188
- paused: '已暂停 (paused)',
189
- error: '异常 (error)',
190
- }
191
- const status = statusMap[agent?.status] || (agent?.status ?? '空闲 (idle)')
192
- const lastTurn = [...(session.events ?? [])].reverse().find((e) => e.type === 'turn/end')
193
- const reason = lastTurn ? describeTurnEnd(lastTurn.data.reason) : '尚未运行'
194
- const title = session.title || (session.events ? sessionLabel(session) : '')
195
- const shortId = fmtSessionId(session.id)
196
- const cwd = session.header?.cwd || node.config?.cwd || ''
197
-
198
- const content = [
199
- `## 📊 Agent 状态看板`,
200
- '',
201
- '| 属性 | 当前状态 / 参数 |',
202
- '| :--- | :--- |',
203
- `| **会话 ID** | \`${shortId}\` |`,
204
- ...(title ? [`| **会话标题** | ${title.replace(/\|/g, '|')} |`] : []),
205
- ...(cwd ? [`| **工作区** | \`${cwd}\` |`] : []),
206
- `| **Agent 状态** | ${status} |`,
207
- `| **累计事件** | ${session.seq ?? 0} 条 |`,
208
- `| **最近执行** | ${reason} |`,
209
- ].join('\n')
210
-
211
- await node.sendText(content)
212
- return true
213
- }
214
- case 'start': // 别名:首次扫码自动开始一个会话
215
- await node.createSession('')
216
- return true
217
- default:
218
- await node.sendText(`❌ **未知指令**:\`/${command}\`\n\n${helpText()}`)
219
- return true
220
- }
221
- }
1
+ // 斜杠命令解释器(/sessions /use /new /workspaces /status /help …)
2
+ // 自 conversation-bridge.js 拆出。routeCommand(node, text, senderId) 只通过 node 参数
3
+ // 访问会话桥能力,目录/渲染逻辑来自 session-catalog.js。
4
+ import {
5
+ listSessions, listWorkspaces, validateWorkspacePath, renderSessions,
6
+ sessionsInDisplayOrder, describeTurnEnd, helpText, fmtTime, fmtSessionId,
7
+ sessionLabel, getWorkspaceBasename,
8
+ } from './session-catalog.js'
9
+ import { isSafeWorkspacePath } from '../security/path-validator.js'
10
+ import { basename, normalize } from 'node:path'
11
+
12
+ export async function routeCommand(node, text, senderId = null) {
13
+ const trimmed = text.trim()
14
+
15
+ if (trimmed === '/yes' || trimmed === '/no' || /^[12]$/.test(trimmed)) {
16
+ if (node.resolveApproval(trimmed, senderId)) return true
17
+ }
18
+
19
+ if (!trimmed.startsWith('/')) return false
20
+
21
+ const [command, ...rest] = trimmed.slice(1).split(/\s+/)
22
+ switch (command) {
23
+ case 'help':
24
+ await node.sendText(helpText())
25
+ return true
26
+ case 'sessions':
27
+ case 'list':
28
+ await node.sendText(await renderSessions(node))
29
+ return true
30
+ case 'use':
31
+ case 'resume': {
32
+ const index = Number(rest[0])
33
+ const sessions = sessionsInDisplayOrder(await listSessions(node))
34
+ if (!Number.isInteger(index) || index < 1 || index > sessions.length) {
35
+ await node.sendText(`❌ **无效会话编号**:\`${rest[0] ?? ''}\`\n\n> 可用编号范围:\`1 – ${sessions.length}\`(发送 \`/sessions\` 查看会话列表)`)
36
+ return true
37
+ }
38
+ const session = sessions[index - 1]
39
+ node.setActiveSessionById(session.id)
40
+ const title = session.title || (session.events ? sessionLabel(session) : '')
41
+ const titleLine = title ? `\n- **标题**:${title}` : ''
42
+ await node.sendText(`✓ **已切换到会话 #${index}**${titleLine}\n- **会话 ID**:\`${fmtSessionId(session.id)}\``)
43
+ return true
44
+ }
45
+ case 'rename': {
46
+ if (!node.activeSessionId) {
47
+ await node.sendText(`❌ **当前没有活动会话**\n\n> 请先使用 \`/sessions\` 查看会话列表并通过 \`/use 编号\` 切换到目标会话,或通过 \`/new <提示词>\` 创建新会话。`)
48
+ return true
49
+ }
50
+ const newTitle = rest.join(' ').trim()
51
+ if (!newTitle) {
52
+ await node.sendText(`❌ **缺少新标题参数**\n\n> 用法:\`/rename <新标题>\`\n> 示例:\`/rename 优化登录交互逻辑\``)
53
+ return true
54
+ }
55
+
56
+ try {
57
+ const session = node.activeSession()
58
+ if (!session) {
59
+ // 会话尚未在内存中恢复(如宿主刚重启):DSH 的会话标题服务只接受 live 会话,
60
+ // 此时无法真正改名,明确告知而不是回复一句"成功"却什么都没发生
61
+ await node.sendText(`❌ **无法重命名**:当前会话尚未恢复(宿主重启后需先发一条消息重新挂载)。\n\n> 发送任意消息后再次执行 \`/rename <新标题>\` 即可。`)
62
+ return true
63
+ }
64
+ // 首选 DSH 原生会话标题服务:rename() 会追加 session/title 事件并落盘,
65
+ // 宿主重启与 Web 侧边栏都能看到。注意 sessionPersistence 并不提供 update()
66
+ // (真实方法只有 create/open/stat/list/...),旧写法被可选链静默跳过,
67
+ // 导致 /rename 只改了内存对象、重启即失效。
68
+ const titleService = node.ctx.get?.('sessionTitle')
69
+ if (titleService?.rename) {
70
+ titleService.rename(session, newTitle)
71
+ } else {
72
+ // 旧版 DSH 回退:内存标题立即可见,并尝试历史持久化接口(存在才用)
73
+ session.title = newTitle
74
+ if (node.ctx.sessionPersistence?.update) {
75
+ await node.ctx.sessionPersistence.update(node.activeSessionId, { title: newTitle }).catch(() => {})
76
+ }
77
+ }
78
+ await node.sendText(`✓ **会话重命名成功**\n- **会话 ID**:\`${fmtSessionId(node.activeSessionId)}\`\n- **新标题**:${newTitle}`)
79
+ } catch (err) {
80
+ await node.sendText(`❌ **重命名失败**:${err instanceof Error ? err.message : String(err)}`)
81
+ }
82
+ return true
83
+ }
84
+ case 'workspaces': {
85
+ const workspaces = await listWorkspaces(node)
86
+ if (workspaces.length === 0) {
87
+ await node.sendText(`## 🗂️ 可用工作区\n\n> 当前没有已注册的工作区。可使用 \`/new <提示词> @<路径>\` 指定项目目录。`)
88
+ return true
89
+ }
90
+ const rows = workspaces.map((w, i) => {
91
+ const titleStr = w.title && w.title !== w.path ? w.title : getWorkspaceBasename(w.path)
92
+ const safeTitle = titleStr.replace(/\|/g, '|')
93
+ return `| **@${i + 1}** | ${safeTitle} | \`${w.path}\` |`
94
+ })
95
+ await node.sendText([
96
+ `## 🗂️ 可用工作区 (共 ${workspaces.length} 个)`,
97
+ `> 新建会话:发送 \`/new <提示词> @序号\` 或 \`/new <提示词> @路径\``,
98
+ '',
99
+ '| 序号 | 工作区名称 | 目录路径 |',
100
+ '| :--- | :--- | :--- |',
101
+ ...rows,
102
+ ].join('\n'))
103
+ return true
104
+ }
105
+ case 'addworkspace': {
106
+ const targetPath = rest.join(' ').trim()
107
+ if (!targetPath) {
108
+ await node.sendText(`❌ **缺少工作区路径**\n\n> 用法:\`/addworkspace <电脑绝对路径>\`\n> 示例:\`/addworkspace D:\\IdeaProjects\\my-app\``)
109
+ return true
110
+ }
111
+ try {
112
+ const safetyCheck = await isSafeWorkspacePath(targetPath)
113
+ if (!safetyCheck.valid) {
114
+ await node.sendText(`⚠️ **${safetyCheck.error || '路径安全校验未通过'}**:\`${targetPath}\`\n\n> 出于安全考虑,禁止将系统关键目录或敏感配置文件所在路径登记为工作区。`)
115
+ return true
116
+ }
117
+ const resolved = safetyCheck.path
118
+ const title = basename(resolved) || resolved
119
+ if (node.ctx.workspaceRegistry?.add) {
120
+ await node.ctx.workspaceRegistry.add({ path: resolved, title }).catch(() => {})
121
+ } else if (node.ctx.workspaceRegistry?.register) {
122
+ await node.ctx.workspaceRegistry.register({ path: resolved, title }).catch(() => {})
123
+ }
124
+ const workspaces = await listWorkspaces(node)
125
+ const foundIndex = workspaces.findIndex(w => normalize(w.path) === normalize(resolved))
126
+ const numStr = foundIndex >= 0 ? `@${foundIndex + 1}` : ''
127
+ await node.sendText([
128
+ `✓ **工作区添加成功**!`,
129
+ `- **名称**:${title}`,
130
+ `- **路径**:\`${resolved}\``,
131
+ foundIndex >= 0 ? `- **快捷编号**:\`${numStr}\`` : '',
132
+ '',
133
+ `> 发送 \`/new <提示词> ${numStr || '@' + resolved}\` 即可直接在此工作区创建会话。`,
134
+ ].filter(Boolean).join('\n'))
135
+ } catch (err) {
136
+ await node.sendText(`❌ **添加工作区失败**:${err instanceof Error ? err.message : String(err)}`)
137
+ }
138
+ return true
139
+ }
140
+ case 'new': {
141
+ // 解析尾部 @N 或 @路径 作为工作区 cwd
142
+ const args = rest.join(' ').trim()
143
+ let cwd
144
+ let prompt = args
145
+ const atMatch = args.match(/\s+@(\S+)$/)
146
+ if (atMatch) {
147
+ prompt = args.slice(0, atMatch.index).trim()
148
+ const sel = atMatch[1]
149
+ const workspaces = await listWorkspaces(node)
150
+ if (/^\d+$/.test(sel)) {
151
+ const idx = Number(sel)
152
+ const ws = workspaces[idx - 1]
153
+ if (ws) cwd = ws.path
154
+ else {
155
+ await node.sendText(`❌ **无效工作区编号**:\`${sel}\`\n\n> 请发送 \`/workspaces\` 查看可用工作区列表与编号。`)
156
+ return true
157
+ }
158
+ } else {
159
+ // 直接指定路径时,规范化并校验(必须完全匹配已注册工作区)
160
+ const validation = await validateWorkspacePath(node, sel)
161
+ if (!validation.valid) {
162
+ await node.sendText(validation.error)
163
+ return true
164
+ }
165
+ cwd = validation.path
166
+ }
167
+ }
168
+ await node.createSession(prompt, cwd)
169
+ return true
170
+ }
171
+ case 'stop': {
172
+ node.stopAllHeartbeats()
173
+ const agent = node.activeAgent()
174
+ if (!agent) {
175
+ await node.sendText(`ℹ️ **当前没有正在运行的 Agent 任务**`)
176
+ } else {
177
+ agent.cancel({ kind: 'user' })
178
+ await node.sendText(`⏹ **已请求停止当前任务**`)
179
+ }
180
+ return true
181
+ }
182
+ case 'end': {
183
+ node.stopAllHeartbeats()
184
+ // 结束当前会话:停止 agent 并清除活动会话(进入"没有活动会话"状态)
185
+ const agent = node.activeAgent()
186
+ if (agent) agent.cancel({ kind: 'user' })
187
+ node.activeSessionId = null
188
+ await node.onActiveSessionChange?.(null)
189
+ await node.sendText(`✓ **已结束当前会话**(没有活动会话)。\n\n> **后续操作**:\n> - \`/new <提示词>\` — 新建会话并开始\n> - \`/sessions\` — 查看历史会话列表\n> - \`/help\` — 查看常用指令帮助`)
190
+ return true
191
+ }
192
+ case 'status': {
193
+ const agent = node.activeAgent()
194
+ const session = node.activeSession()
195
+ if (!session) {
196
+ await node.sendText(`## 📊 Agent 状态看板\n\n> 当前没有活动会话。\n> 发送 \`/new <提示词>\` 开始新任务,或发送 \`/sessions\` 查看已有会话。`)
197
+ return true
198
+ }
199
+ const statusMap = {
200
+ idle: '空闲 (idle)',
201
+ running: '运行中 (running)',
202
+ paused: '已暂停 (paused)',
203
+ error: '异常 (error)',
204
+ }
205
+ const status = statusMap[agent?.status] || (agent?.status ?? '空闲 (idle)')
206
+ const lastTurn = [...(session.events ?? [])].reverse().find((e) => e.type === 'turn/end')
207
+ const reason = lastTurn ? describeTurnEnd(lastTurn.data.reason) : '尚未运行'
208
+ const title = session.title || (session.events ? sessionLabel(session) : '')
209
+ const shortId = fmtSessionId(session.id)
210
+ const cwd = session.header?.cwd || node.config?.cwd || ''
211
+
212
+ const content = [
213
+ `## 📊 Agent 状态看板`,
214
+ '',
215
+ '| 属性 | 当前状态 / 参数 |',
216
+ '| :--- | :--- |',
217
+ `| **会话 ID** | \`${shortId}\` |`,
218
+ ...(title ? [`| **会话标题** | ${title.replace(/\|/g, '|')} |`] : []),
219
+ ...(cwd ? [`| **工作区** | \`${cwd}\` |`] : []),
220
+ `| **Agent 状态** | ${status} |`,
221
+ `| **累计事件** | ${session.seq ?? 0} 条 |`,
222
+ `| **最近执行** | ${reason} |`,
223
+ ].join('\n')
224
+
225
+ await node.sendText(content)
226
+ return true
227
+ }
228
+ case 'start': // 别名:首次扫码自动开始一个会话
229
+ await node.createSession('')
230
+ return true
231
+ default:
232
+ await node.sendText(`❌ **未知指令**:\`/${command}\`\n\n${helpText()}`)
233
+ return true
234
+ }
235
+ }
@@ -0,0 +1,69 @@
1
+ // 平台配置恢复(从 ~/.dsh/dsh-bridge/config.json 回写到会话桥 config)
2
+ //
3
+ // 背景:平台配置的写入链路把整段平台配置落在 config.json 里,但启动/重启时的
4
+ // 读回链路只挑了几个白名单字段,导致 agentPreset / cwd / agentProvider / agentModel
5
+ // 这类字符串配置"配了、重启就丢",会话随后落到 DSH 默认 preset 或空 preset 层。
6
+ //
7
+ // 这里把恢复规则收敛成一个纯函数,既供 lib/index.js 的 restorePlatform 使用,
8
+ // 也让"只恢复白名单字段"这条不变量可以被单测钉住。
9
+
10
+ /**
11
+ * 恢复平台配置时必须透传的字符串字段白名单。
12
+ *
13
+ * 这四个键直接决定远程会话在哪里、以什么预设和模型启动:
14
+ * - cwd 会话工作区(`/new` 建在哪个目录)
15
+ * - agentPreset DSH agent preset(决定该会话挂载的工具/提示词/技能目录)
16
+ * - agentProvider / agentModel 会话默认模型路由
17
+ */
18
+ export const RESTORED_STRING_FIELDS = ['agentPreset', 'cwd', 'agentProvider', 'agentModel']
19
+
20
+ /**
21
+ * 各平台 `setConfig()` 会持久化、因此恢复时必须一并回写的数值字段。
22
+ *
23
+ * 写入侧(wechat / qq / feishu / telegram 的 setConfig)统一持久化这三个会话节奏参数;
24
+ * 恢复侧若漏掉,用户在设置页调好的摘要间隔 / 审批超时 / 分块延时只活到下一次重启。
25
+ * 写入侧新增数值字段时,必须同步这里。
26
+ */
27
+ export const PLATFORM_TIMING_FIELDS = ['digestIntervalSec', 'approvalTimeoutSec', 'sendChunkDelayMs']
28
+
29
+ /**
30
+ * 把 config.json 中某个平台的持久化配置回写到会话桥的 node.config。
31
+ *
32
+ * 规则(与磁盘上的写入契约一致):
33
+ * - allowFrom 一律以数组形式落回,缺失即空数组;
34
+ * - numericFields 与 maxMessageChars 走数值归一,maxMessageChars 低于 200 视为无效并回落默认值;
35
+ * - groupAutoApprove 仅在显式写入时按布尔解释;
36
+ * - stringFields 只接受非空字符串,空串/非字符串一律忽略(保留构造期配置)。
37
+ *
38
+ * @param {object} nodeConfig 会话桥的 config 对象(就地修改)
39
+ * @param {object} cfg config.json 里该平台的持久化配置
40
+ * @param {object} [opts]
41
+ * @param {string[]} [opts.numericFields] 需要数值归一化的字段名
42
+ * @param {string[]} [opts.stringFields] 需要字符串透传的字段名
43
+ * @param {number} [opts.defaultMaxMessageChars] maxMessageChars 无效时的默认值
44
+ * @returns {object} 同一个 nodeConfig(便于串联)
45
+ */
46
+ export function applyRestoredPlatformConfig(nodeConfig, cfg, {
47
+ numericFields = [],
48
+ stringFields = [],
49
+ defaultMaxMessageChars = 2000,
50
+ } = {}) {
51
+ if (!nodeConfig || !cfg) return nodeConfig
52
+
53
+ nodeConfig.allowFrom = Array.isArray(cfg.allowFrom) ? cfg.allowFrom : []
54
+ for (const field of numericFields) {
55
+ if (cfg[field] != null) nodeConfig[field] = Number(cfg[field])
56
+ }
57
+ if (cfg.maxMessageChars != null) {
58
+ const val = Number(cfg.maxMessageChars)
59
+ nodeConfig.maxMessageChars = (val >= 200) ? val : defaultMaxMessageChars
60
+ }
61
+ if (cfg.groupAutoApprove != null) nodeConfig.groupAutoApprove = cfg.groupAutoApprove === true
62
+
63
+ for (const field of stringFields) {
64
+ const val = cfg[field]
65
+ // 只接受非空(且非纯空白)字符串:空串/垃圾值不应清掉或污染构造期配置
66
+ if (typeof val === 'string' && val.trim().length > 0) nodeConfig[field] = val
67
+ }
68
+ return nodeConfig
69
+ }