@wenbin_wb/dsh-bridge 2.10.7 → 2.10.9

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,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
- 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
+ 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
+ }