@wenbin_wb/dsh-bridge 2.8.6 → 2.9.0
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/CHANGELOG.md +295 -235
- package/README.en.md +408 -572
- package/README.md +430 -570
- package/client/client.js +3654 -3487
- package/client/index.js +220 -877
- package/client/mobile-styles.js +802 -0
- package/docs/fix-plan-202608.md +108 -0
- package/lib/auth/login-template.js +381 -381
- package/lib/auth/manager.js +531 -469
- package/lib/bridge-rpc-constants.js +1 -0
- package/lib/bridge-rpc.js +436 -502
- package/lib/cloudflared-manager.mjs +361 -345
- package/lib/compat.js +129 -0
- package/lib/feishu/index.js +225 -222
- package/lib/feishu/node.js +433 -409
- package/lib/index.js +1765 -1739
- package/lib/platform/base.js +147 -156
- package/lib/platform/commands.js +221 -0
- package/lib/platform/conversation-bridge.js +816 -1570
- package/lib/platform/dsh-storage.js +117 -0
- package/lib/platform/index.js +10 -10
- package/lib/platform/message-split.js +191 -0
- package/lib/platform/session-catalog.js +372 -0
- package/lib/platform/stream-slices.js +21 -0
- package/lib/qq/index.js +312 -309
- package/lib/qq/node.js +532 -533
- package/lib/telegram/index.js +215 -212
- package/lib/telegram/node.js +348 -350
- package/lib/tunnel-client.mjs +39 -15
- package/lib/wechat/gateway.js +973 -960
- package/lib/wechat/index.js +244 -241
- package/lib/wechat/media.js +285 -281
- package/lib/wechat/node.js +352 -350
- package/package.json +106 -102
package/lib/platform/base.js
CHANGED
|
@@ -1,156 +1,147 @@
|
|
|
1
|
-
// dsh-bridge 平台抽象基类
|
|
2
|
-
//
|
|
3
|
-
// 定义 IM 平台适配器的统一接口。每个平台(微信/QQ/飞书/Telegram…)继承本类,
|
|
4
|
-
// 实现协议层(登录、收发消息、typing)。平台无关的会话桥逻辑在 ConversationBridge
|
|
5
|
-
// (lib/platform/conversation-bridge.js)中实现,通过 platform 注入到 bridge。
|
|
6
|
-
//
|
|
7
|
-
// 生命周期:constructor → start() → stop() → dispose()
|
|
8
|
-
// 消息抽象:sendText / sendTyping / sendMedia(由子类实现)
|
|
9
|
-
|
|
10
|
-
export class Platform {
|
|
11
|
-
/**
|
|
12
|
-
* @param {object} opts
|
|
13
|
-
* @param {object} opts.ctx Cordis 上下文
|
|
14
|
-
* @param {object} opts.logger 日志器
|
|
15
|
-
* @param {object} [opts.config] 已持久化的平台配置(凭证等)
|
|
16
|
-
* @param {(patch: object) => (void|Promise<void>)} [opts.onPersist] 主插件保存回调
|
|
17
|
-
* @param {import('./conversation-bridge.js').ConversationBridge} [opts.bridge] 会话桥实例
|
|
18
|
-
*/
|
|
19
|
-
constructor({ ctx, logger, config = {}, onPersist, bridge } = {}) {
|
|
20
|
-
this.ctx = ctx
|
|
21
|
-
this.logger = logger
|
|
22
|
-
this.config = { ...config }
|
|
23
|
-
this.onPersist = onPersist ?? (() => {})
|
|
24
|
-
this.bridge = bridge ?? null
|
|
25
|
-
|
|
26
|
-
// 平台标识(子类必须设置)
|
|
27
|
-
this.id = ''
|
|
28
|
-
this.name = ''
|
|
29
|
-
|
|
30
|
-
// 连接状态与账号:子类可能用 getter 覆盖(如委托给 gateway),
|
|
31
|
-
// 因此仅在未被子类覆盖时才初始化默认值。
|
|
32
|
-
if (!('status' in this)) this.status = 'idle'
|
|
33
|
-
if (!('accountId' in this)) this.accountId = null
|
|
34
|
-
|
|
35
|
-
// 扫码/登录的流式状态(RPC 轮询读取)
|
|
36
|
-
this.loginState = {
|
|
37
|
-
phase: 'idle', // idle | qr | scaned | confirmed | done | error
|
|
38
|
-
qrPayload: null, // 待渲染内容:dataURL 图片 或 二维码文本
|
|
39
|
-
qrKind: null, // 'img' | 'text'
|
|
40
|
-
error: null,
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
this.disposers = []
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
// ---- 平台能力声明(子类可覆盖)----
|
|
47
|
-
|
|
48
|
-
get capabilities() {
|
|
49
|
-
return {
|
|
50
|
-
supportsGroup: false, // 是否支持群聊
|
|
51
|
-
supportsMedia: false, // 是否支持媒体收发
|
|
52
|
-
supportsVoice: false, // 是否支持语音
|
|
53
|
-
supportsTyping: false, // 是否支持 typing 状态
|
|
54
|
-
maxMessageChars: 2000, // 单条消息最大字符数
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
get configured() {
|
|
59
|
-
return false
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
// ---- 生命周期(子类必须实现 start/stop;dispose 已提供默认实现)----
|
|
63
|
-
|
|
64
|
-
async start() {
|
|
65
|
-
throw new Error(`${this.id || 'platform'}: start() not implemented`)
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
async stop() {
|
|
69
|
-
throw new Error(`${this.id || 'platform'}: stop() not implemented`)
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
dispose() {
|
|
73
|
-
for (const disposer of this.disposers) {
|
|
74
|
-
try { disposer() } catch { /* 忽略 */ }
|
|
75
|
-
}
|
|
76
|
-
this.disposers = []
|
|
77
|
-
this.bridge?.dispose?.()
|
|
78
|
-
this.bridge = null
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
// ---- 消息抽象(子类必须实现)----
|
|
82
|
-
|
|
83
|
-
async sendText(peerId, text, opts = {}) {
|
|
84
|
-
throw new Error(`${this.id || 'platform'}: sendText() not implemented`)
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
async sendTyping(peerId, state) {
|
|
88
|
-
return Promise.resolve()
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
async sendMedia(peerId, media, opts = {}) {
|
|
92
|
-
throw new Error(`${this.id || 'platform'}: sendMedia() not implemented`)
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
// ---- 登录(子类必须实现 login;getLoginState 已提供默认)----
|
|
96
|
-
|
|
97
|
-
async login(opts = {}) {
|
|
98
|
-
throw new Error(`${this.id || 'platform'}: login() not implemented`)
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
getLoginState() {
|
|
102
|
-
return { ...this.loginState }
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
// ---- 状态汇总(供 RPC/UI 读取)----
|
|
106
|
-
|
|
107
|
-
getStatus() {
|
|
108
|
-
return {
|
|
109
|
-
id: this.id,
|
|
110
|
-
name: this.name,
|
|
111
|
-
status: this.status,
|
|
112
|
-
configured: this.configured,
|
|
113
|
-
accountId: this.accountId,
|
|
114
|
-
login: this.getLoginState(),
|
|
115
|
-
peerId: this.bridge?.peerId ?? null,
|
|
116
|
-
sessionId: this.bridge?.activeSessionId ?? null,
|
|
117
|
-
config: this.getEditableConfig?.(),
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
/** 可编辑配置(供 UI 设置面板读取);子类可覆盖返回具体字段。 */
|
|
122
|
-
getEditableConfig() {
|
|
123
|
-
return {}
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
// ---- 工具 ----
|
|
127
|
-
|
|
128
|
-
setStatus(status) {
|
|
129
|
-
if (this.status === status) return
|
|
130
|
-
this.status = status
|
|
131
|
-
try {
|
|
132
|
-
this.ctx.emit?.(`${this.id}/status`, status)
|
|
133
|
-
} catch { /* emit 失败不致命 */ }
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
async persist(patch) {
|
|
137
|
-
try {
|
|
138
|
-
await this.onPersist(patch)
|
|
139
|
-
} catch (err) {
|
|
140
|
-
this.logger?.warn?.(`[dsh-bridge ${this.id}] persist failed: %s`, err?.message ?? err)
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
this.disposers = []
|
|
149
|
-
this.bridge?.dispose?.()
|
|
150
|
-
this.bridge = null
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
async destroy() {
|
|
154
|
-
this.dispose()
|
|
155
|
-
}
|
|
156
|
-
}
|
|
1
|
+
// dsh-bridge 平台抽象基类
|
|
2
|
+
//
|
|
3
|
+
// 定义 IM 平台适配器的统一接口。每个平台(微信/QQ/飞书/Telegram…)继承本类,
|
|
4
|
+
// 实现协议层(登录、收发消息、typing)。平台无关的会话桥逻辑在 ConversationBridge
|
|
5
|
+
// (lib/platform/conversation-bridge.js)中实现,通过 platform 注入到 bridge。
|
|
6
|
+
//
|
|
7
|
+
// 生命周期:constructor → start() → stop() → dispose()
|
|
8
|
+
// 消息抽象:sendText / sendTyping / sendMedia(由子类实现)
|
|
9
|
+
|
|
10
|
+
export class Platform {
|
|
11
|
+
/**
|
|
12
|
+
* @param {object} opts
|
|
13
|
+
* @param {object} opts.ctx Cordis 上下文
|
|
14
|
+
* @param {object} opts.logger 日志器
|
|
15
|
+
* @param {object} [opts.config] 已持久化的平台配置(凭证等)
|
|
16
|
+
* @param {(patch: object) => (void|Promise<void>)} [opts.onPersist] 主插件保存回调
|
|
17
|
+
* @param {import('./conversation-bridge.js').ConversationBridge} [opts.bridge] 会话桥实例
|
|
18
|
+
*/
|
|
19
|
+
constructor({ ctx, logger, config = {}, onPersist, bridge } = {}) {
|
|
20
|
+
this.ctx = ctx
|
|
21
|
+
this.logger = logger
|
|
22
|
+
this.config = { ...config }
|
|
23
|
+
this.onPersist = onPersist ?? (() => {})
|
|
24
|
+
this.bridge = bridge ?? null
|
|
25
|
+
|
|
26
|
+
// 平台标识(子类必须设置)
|
|
27
|
+
this.id = ''
|
|
28
|
+
this.name = ''
|
|
29
|
+
|
|
30
|
+
// 连接状态与账号:子类可能用 getter 覆盖(如委托给 gateway),
|
|
31
|
+
// 因此仅在未被子类覆盖时才初始化默认值。
|
|
32
|
+
if (!('status' in this)) this.status = 'idle'
|
|
33
|
+
if (!('accountId' in this)) this.accountId = null
|
|
34
|
+
|
|
35
|
+
// 扫码/登录的流式状态(RPC 轮询读取)
|
|
36
|
+
this.loginState = {
|
|
37
|
+
phase: 'idle', // idle | qr | scaned | confirmed | done | error
|
|
38
|
+
qrPayload: null, // 待渲染内容:dataURL 图片 或 二维码文本
|
|
39
|
+
qrKind: null, // 'img' | 'text'
|
|
40
|
+
error: null,
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
this.disposers = []
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ---- 平台能力声明(子类可覆盖)----
|
|
47
|
+
|
|
48
|
+
get capabilities() {
|
|
49
|
+
return {
|
|
50
|
+
supportsGroup: false, // 是否支持群聊
|
|
51
|
+
supportsMedia: false, // 是否支持媒体收发
|
|
52
|
+
supportsVoice: false, // 是否支持语音
|
|
53
|
+
supportsTyping: false, // 是否支持 typing 状态
|
|
54
|
+
maxMessageChars: 2000, // 单条消息最大字符数
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
get configured() {
|
|
59
|
+
return false
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ---- 生命周期(子类必须实现 start/stop;dispose 已提供默认实现)----
|
|
63
|
+
|
|
64
|
+
async start() {
|
|
65
|
+
throw new Error(`${this.id || 'platform'}: start() not implemented`)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async stop() {
|
|
69
|
+
throw new Error(`${this.id || 'platform'}: stop() not implemented`)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
dispose() {
|
|
73
|
+
for (const disposer of this.disposers) {
|
|
74
|
+
try { disposer() } catch { /* 忽略 */ }
|
|
75
|
+
}
|
|
76
|
+
this.disposers = []
|
|
77
|
+
this.bridge?.dispose?.()
|
|
78
|
+
this.bridge = null
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// ---- 消息抽象(子类必须实现)----
|
|
82
|
+
|
|
83
|
+
async sendText(peerId, text, opts = {}) {
|
|
84
|
+
throw new Error(`${this.id || 'platform'}: sendText() not implemented`)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async sendTyping(peerId, state) {
|
|
88
|
+
return Promise.resolve()
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async sendMedia(peerId, media, opts = {}) {
|
|
92
|
+
throw new Error(`${this.id || 'platform'}: sendMedia() not implemented`)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ---- 登录(子类必须实现 login;getLoginState 已提供默认)----
|
|
96
|
+
|
|
97
|
+
async login(opts = {}) {
|
|
98
|
+
throw new Error(`${this.id || 'platform'}: login() not implemented`)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
getLoginState() {
|
|
102
|
+
return { ...this.loginState }
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// ---- 状态汇总(供 RPC/UI 读取)----
|
|
106
|
+
|
|
107
|
+
getStatus() {
|
|
108
|
+
return {
|
|
109
|
+
id: this.id,
|
|
110
|
+
name: this.name,
|
|
111
|
+
status: this.status,
|
|
112
|
+
configured: this.configured,
|
|
113
|
+
accountId: this.accountId,
|
|
114
|
+
login: this.getLoginState(),
|
|
115
|
+
peerId: this.bridge?.peerId ?? null,
|
|
116
|
+
sessionId: this.bridge?.activeSessionId ?? null,
|
|
117
|
+
config: this.getEditableConfig?.(),
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** 可编辑配置(供 UI 设置面板读取);子类可覆盖返回具体字段。 */
|
|
122
|
+
getEditableConfig() {
|
|
123
|
+
return {}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// ---- 工具 ----
|
|
127
|
+
|
|
128
|
+
setStatus(status) {
|
|
129
|
+
if (this.status === status) return
|
|
130
|
+
this.status = status
|
|
131
|
+
try {
|
|
132
|
+
this.ctx.emit?.(`${this.id}/status`, status)
|
|
133
|
+
} catch { /* emit 失败不致命 */ }
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async persist(patch) {
|
|
137
|
+
try {
|
|
138
|
+
await this.onPersist(patch)
|
|
139
|
+
} catch (err) {
|
|
140
|
+
this.logger?.warn?.(`[dsh-bridge ${this.id}] persist failed: %s`, err?.message ?? err)
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async destroy() {
|
|
145
|
+
this.dispose()
|
|
146
|
+
}
|
|
147
|
+
}
|
|
@@ -0,0 +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
|
+
}
|