@xmanrui/dsh-im 0.6.0 → 0.7.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/README.md +22 -4
- package/lib/client.js +649 -357
- package/lib/index.js +108 -96
- package/package.json +1 -1
- package/plugin-src/client/build.mjs +1 -1
- package/plugin-src/client/i18n.js +14 -0
- package/plugin-src/client/index.js +11 -3
- package/plugin-src/client/styles.js +49 -8
- package/plugin-src/client/workspace-directory-picker.js +230 -0
- package/plugin-src/client/workspace-editor.js +38 -65
- package/scripts/verify-package.mjs +5 -0
- package/src/channels/dingtalk/dingtalk-bridge.mjs +3 -1
- package/src/channels/dingtalk/harness-client.mjs +56 -0
- package/src/channels/discord/discord-api.mjs +1 -1
- package/src/channels/feishu/bridge.mjs +3 -1
- package/src/channels/feishu/harness-client.mjs +56 -0
- package/src/channels/qq/qq-bridge.mjs +3 -1
- package/src/channels/shared/bot-workspace-store.mjs +185 -4
- package/src/channels/shared/harness-session-binding.mjs +110 -0
- package/src/channels/shared/text-harness-bridge.mjs +4 -2
- package/src/channels/shared/workspace-command.mjs +212 -16
- package/src/channels/shared/workspace-session.mjs +22 -9
- package/src/channels/wecom/wecom-bridge.mjs +3 -1
- package/src/channels/weixin/harness-client.mjs +56 -0
- package/src/channels/weixin/weixin-api.mjs +1 -1
- package/src/channels/weixin/weixin-bridge.mjs +5 -3
|
@@ -1,10 +1,25 @@
|
|
|
1
|
-
import { stat } from 'node:fs/promises';
|
|
1
|
+
import { realpath, stat } from 'node:fs/promises';
|
|
2
2
|
import { isAbsolute, resolve } from 'node:path';
|
|
3
3
|
|
|
4
|
+
import { WORKSPACE_SESSION_STALE } from './workspace-session.mjs';
|
|
5
|
+
|
|
4
6
|
const WORKSPACE_COMMAND = /^\/workspace(?:\s+([\s\S]+))?$/i;
|
|
5
7
|
const WORKSPACE_LIST_COMMAND = /^\/workspacelist(?:\s+([\s\S]+))?$/i;
|
|
8
|
+
const SESSION_LIST_COMMAND = /^\/sessionlist(?:\s+([\s\S]+))?$/i;
|
|
9
|
+
const SESSION_BIND_PREFIX = /^\/session(?=$|\s)/i;
|
|
10
|
+
const SESSION_BIND_COMMAND = /^\/session[ \t]+([^\s]+)$/i;
|
|
6
11
|
const MAX_WORKSPACE_PATH_LENGTH = 4_096;
|
|
7
12
|
const MAX_COMMAND_MESSAGE_LENGTH = 1_800;
|
|
13
|
+
const MAX_SESSION_ID_LENGTH = 256;
|
|
14
|
+
const UNSAFE_DISPLAY_TEXT = /[\p{Cc}\p{Cf}\p{Zl}\p{Zp}]/u;
|
|
15
|
+
const UNSAFE_DISPLAY_TEXT_GLOBAL = /[\p{Cc}\p{Cf}\p{Zl}\p{Zp}]+/gu;
|
|
16
|
+
const SESSION_BIND_USAGE = '用法:/session Session ID';
|
|
17
|
+
const SESSION_LIST_USAGE = [
|
|
18
|
+
'用法:',
|
|
19
|
+
'/sessionlist 列出当前工作区会话',
|
|
20
|
+
'/sessionlist 工作区序号 按 /workspacelist 序号列出会话',
|
|
21
|
+
'/sessionlist 工作区绝对路径 列出指定工作区会话',
|
|
22
|
+
].join('\n');
|
|
8
23
|
|
|
9
24
|
function commandResult(message, messages = [message]) {
|
|
10
25
|
return { handled: true, message, messages };
|
|
@@ -12,20 +27,74 @@ function commandResult(message, messages = [message]) {
|
|
|
12
27
|
|
|
13
28
|
function normalizedWorkspacePath(value) {
|
|
14
29
|
if (typeof value !== 'string' || value.length > MAX_WORKSPACE_PATH_LENGTH
|
|
15
|
-
|| !isAbsolute(value) ||
|
|
30
|
+
|| !isAbsolute(value) || UNSAFE_DISPLAY_TEXT.test(value)) return null;
|
|
16
31
|
return resolve(value);
|
|
17
32
|
}
|
|
18
33
|
|
|
34
|
+
function safeDisplayText(value) {
|
|
35
|
+
if (typeof value !== 'string') return '';
|
|
36
|
+
return value.replace(UNSAFE_DISPLAY_TEXT_GLOBAL, ' ').replace(/\s+/gu, ' ').trim();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function validSessionId(value) {
|
|
40
|
+
return typeof value === 'string'
|
|
41
|
+
&& value.length > 0
|
|
42
|
+
&& value.length <= MAX_SESSION_ID_LENGTH
|
|
43
|
+
&& !/\p{White_Space}/u.test(value)
|
|
44
|
+
&& !UNSAFE_DISPLAY_TEXT.test(value);
|
|
45
|
+
}
|
|
46
|
+
|
|
19
47
|
async function existingWorkspacePaths(values) {
|
|
20
|
-
const
|
|
21
|
-
|
|
48
|
+
const checked = await Promise.all(values.map(async (value) => {
|
|
49
|
+
const workspace = normalizedWorkspacePath(value);
|
|
50
|
+
if (!workspace) return null;
|
|
22
51
|
try {
|
|
23
|
-
|
|
52
|
+
if (!(await stat(workspace)).isDirectory()) return null;
|
|
53
|
+
return normalizedWorkspacePath(await realpath(workspace));
|
|
24
54
|
} catch {
|
|
25
55
|
return null;
|
|
26
56
|
}
|
|
27
57
|
}));
|
|
28
|
-
return checked.filter(Boolean);
|
|
58
|
+
return [...new Set(checked.filter(Boolean))];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function selectedWorkspacePath(value) {
|
|
62
|
+
if (typeof value !== 'string' || !isAbsolute(value.trim())) {
|
|
63
|
+
return { error: `工作区必须是绝对路径。\n${SESSION_LIST_USAGE}` };
|
|
64
|
+
}
|
|
65
|
+
const workspace = normalizedWorkspacePath(value.trim());
|
|
66
|
+
if (!workspace) {
|
|
67
|
+
return { error: `工作区路径包含不支持的字符或长度超过限制。\n${SESSION_LIST_USAGE}` };
|
|
68
|
+
}
|
|
69
|
+
let info;
|
|
70
|
+
try {
|
|
71
|
+
info = await stat(workspace);
|
|
72
|
+
} catch {
|
|
73
|
+
return { error: `工作区路径不存在。\n${SESSION_LIST_USAGE}` };
|
|
74
|
+
}
|
|
75
|
+
if (!info.isDirectory()) {
|
|
76
|
+
return { error: `工作区路径必须指向一个目录。\n${SESSION_LIST_USAGE}` };
|
|
77
|
+
}
|
|
78
|
+
try {
|
|
79
|
+
const canonical = normalizedWorkspacePath(await realpath(workspace));
|
|
80
|
+
return canonical
|
|
81
|
+
? { workspace: canonical }
|
|
82
|
+
: { error: `工作区路径包含不支持的字符或长度超过限制。\n${SESSION_LIST_USAGE}` };
|
|
83
|
+
} catch {
|
|
84
|
+
return { error: `工作区路径不存在。\n${SESSION_LIST_USAGE}` };
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async function workspacePathSnapshot(harness) {
|
|
89
|
+
const listed = await harness.listWorkspaces();
|
|
90
|
+
const currentValue = typeof harness?.currentWorkspace === 'function'
|
|
91
|
+
? harness.currentWorkspace()
|
|
92
|
+
: null;
|
|
93
|
+
const [current] = currentValue ? await existingWorkspacePaths([currentValue]) : [];
|
|
94
|
+
const registered = await existingWorkspacePaths(Array.isArray(listed) ? listed : []);
|
|
95
|
+
const paths = [...new Set([...(current ? [current] : []), ...registered])];
|
|
96
|
+
harness.assertWorkspaceScope?.();
|
|
97
|
+
return { current: current ?? null, paths };
|
|
29
98
|
}
|
|
30
99
|
|
|
31
100
|
export function splitWorkspaceCommandMessage(message) {
|
|
@@ -56,15 +125,7 @@ async function runWorkspaceListCommand(match, harness) {
|
|
|
56
125
|
return commandResult('当前机器人暂不支持列出工作区。');
|
|
57
126
|
}
|
|
58
127
|
try {
|
|
59
|
-
const
|
|
60
|
-
const current = typeof harness.currentWorkspace === 'function'
|
|
61
|
-
? normalizedWorkspacePath(harness.currentWorkspace())
|
|
62
|
-
: null;
|
|
63
|
-
const paths = await existingWorkspacePaths([
|
|
64
|
-
...(current ? [current] : []),
|
|
65
|
-
...(Array.isArray(listed) ? listed : []),
|
|
66
|
-
]);
|
|
67
|
-
harness.assertWorkspaceScope?.();
|
|
128
|
+
const { current, paths } = await workspacePathSnapshot(harness);
|
|
68
129
|
if (paths.length === 0) {
|
|
69
130
|
return commandResult('当前 Harness Host 上没有仍然存在的已登记工作区。');
|
|
70
131
|
}
|
|
@@ -75,6 +136,7 @@ async function runWorkspaceListCommand(match, harness) {
|
|
|
75
136
|
)),
|
|
76
137
|
'',
|
|
77
138
|
'切换用法:/workspace 工作区绝对路径',
|
|
139
|
+
'查看会话:/sessionlist 工作区序号或绝对路径',
|
|
78
140
|
];
|
|
79
141
|
const message = lines.join('\n');
|
|
80
142
|
return commandResult(message, splitWorkspaceCommandMessage(message));
|
|
@@ -86,9 +148,143 @@ async function runWorkspaceListCommand(match, harness) {
|
|
|
86
148
|
}
|
|
87
149
|
}
|
|
88
150
|
|
|
89
|
-
|
|
151
|
+
async function resolveSessionListWorkspace(selector, harness) {
|
|
152
|
+
if (!selector) {
|
|
153
|
+
if (typeof harness?.currentWorkspace !== 'function') {
|
|
154
|
+
return { error: '当前机器人没有可用的工作区。' };
|
|
155
|
+
}
|
|
156
|
+
const selected = await selectedWorkspacePath(harness.currentWorkspace());
|
|
157
|
+
harness.assertWorkspaceScope?.();
|
|
158
|
+
return selected;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (/^\d+$/u.test(selector)) {
|
|
162
|
+
if (typeof harness?.listWorkspaces !== 'function') {
|
|
163
|
+
return { error: '当前机器人暂不支持按序号选择工作区。' };
|
|
164
|
+
}
|
|
165
|
+
const { paths } = await workspacePathSnapshot(harness);
|
|
166
|
+
const position = Number(selector);
|
|
167
|
+
if (!Number.isSafeInteger(position) || position < 1 || position > paths.length) {
|
|
168
|
+
return { error: '工作区序号不存在,请先执行 /workspacelist。' };
|
|
169
|
+
}
|
|
170
|
+
return { workspace: paths[position - 1] };
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const selected = await selectedWorkspacePath(selector);
|
|
174
|
+
harness.assertWorkspaceScope?.();
|
|
175
|
+
return selected;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function sessionListMessage(workspace, sessions) {
|
|
179
|
+
const rows = sessions.map((session) => {
|
|
180
|
+
const sessionId = safeDisplayText(session?.sessionId);
|
|
181
|
+
if (!sessionId) throw new TypeError('Harness returned an invalid session id');
|
|
182
|
+
const title = session?.summaryAvailable === false
|
|
183
|
+
? '标题暂不可用'
|
|
184
|
+
: safeDisplayText(session?.title) || '暂无标题';
|
|
185
|
+
return `${title}${session?.archived === true ? '(已归档)' : ''}\n ID: ${sessionId}`;
|
|
186
|
+
});
|
|
187
|
+
if (rows.length === 0) return `工作区:${workspace}\n该工作区暂无会话。`;
|
|
188
|
+
return [
|
|
189
|
+
`工作区:${workspace}`,
|
|
190
|
+
`会话(${rows.length}):`,
|
|
191
|
+
'',
|
|
192
|
+
...rows.map((row, index) => `${index + 1}. ${row}`),
|
|
193
|
+
'',
|
|
194
|
+
'绑定用法:/session Session ID',
|
|
195
|
+
].join('\n');
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
async function runSessionListCommand(match, harness) {
|
|
199
|
+
if (typeof harness?.listWorkspaceSessions !== 'function') {
|
|
200
|
+
return commandResult('当前机器人暂不支持列出工作区会话。');
|
|
201
|
+
}
|
|
202
|
+
const selector = match[1]?.trim() ?? '';
|
|
203
|
+
try {
|
|
204
|
+
const resolved = await resolveSessionListWorkspace(selector, harness);
|
|
205
|
+
if (resolved.error) return commandResult(resolved.error);
|
|
206
|
+
const listed = await harness.listWorkspaceSessions(resolved.workspace);
|
|
207
|
+
if (!listed || !Array.isArray(listed.sessions)) {
|
|
208
|
+
throw new TypeError('Harness returned an invalid workspace session list');
|
|
209
|
+
}
|
|
210
|
+
harness.assertWorkspaceScope?.();
|
|
211
|
+
const workspace = normalizedWorkspacePath(listed.workspace) ?? resolved.workspace;
|
|
212
|
+
const message = sessionListMessage(workspace, listed.sessions);
|
|
213
|
+
return commandResult(message, splitWorkspaceCommandMessage(message));
|
|
214
|
+
} catch (error) {
|
|
215
|
+
if (error?.code === 'workspace-bot-not-found') {
|
|
216
|
+
return commandResult('机器人正在移除或已重新接入,无法列出原会话的工作区会话。');
|
|
217
|
+
}
|
|
218
|
+
return commandResult('暂时无法获取工作区会话列表,请稍后重试。');
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function sessionBindErrorMessage(error) {
|
|
223
|
+
if (error?.code === 'session-id-invalid') {
|
|
224
|
+
return `Session ID 格式无效。\n${SESSION_BIND_USAGE}`;
|
|
225
|
+
}
|
|
226
|
+
if (['session-not-registered', 'session-not-found'].includes(error?.code)) {
|
|
227
|
+
return '未找到该会话,请先执行 /sessionlist 确认 Session ID。';
|
|
228
|
+
}
|
|
229
|
+
if (error?.code === 'session-subagent-unsupported') {
|
|
230
|
+
return '子代理会话不能绑定到机器人对话,请选择普通会话。';
|
|
231
|
+
}
|
|
232
|
+
if (error?.code === 'session-workspace-ambiguous') {
|
|
233
|
+
return '该会话的工作区归属不明确,暂时无法绑定。';
|
|
234
|
+
}
|
|
235
|
+
if (error?.code === 'session-summary-unavailable') {
|
|
236
|
+
return '暂时无法读取该会话的信息,请稍后重试。';
|
|
237
|
+
}
|
|
238
|
+
if (error?.code === 'workspace-bot-not-found') {
|
|
239
|
+
return '机器人正在移除或已重新接入,无法绑定原对话的会话。';
|
|
240
|
+
}
|
|
241
|
+
if ([WORKSPACE_SESSION_STALE, 'agent-busy', 'session-conflict', 'workspace-conflict']
|
|
242
|
+
.includes(error?.code)) {
|
|
243
|
+
return '工作区或会话状态已发生变化,请重试。';
|
|
244
|
+
}
|
|
245
|
+
return '暂时无法绑定会话,请稍后重试。';
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
async function runSessionBindCommand(command, harness, conversationKey) {
|
|
249
|
+
const match = SESSION_BIND_COMMAND.exec(command);
|
|
250
|
+
const sessionId = match?.[1];
|
|
251
|
+
if (!validSessionId(sessionId)) return commandResult(SESSION_BIND_USAGE);
|
|
252
|
+
if (typeof harness?.bindWorkspaceSession !== 'function') {
|
|
253
|
+
return commandResult('当前机器人暂不支持绑定已有会话。');
|
|
254
|
+
}
|
|
255
|
+
if (typeof conversationKey !== 'string' || !conversationKey) {
|
|
256
|
+
return commandResult('当前消息缺少可绑定的会话上下文。');
|
|
257
|
+
}
|
|
258
|
+
try {
|
|
259
|
+
const bound = await harness.bindWorkspaceSession(conversationKey, sessionId);
|
|
260
|
+
harness.assertWorkspaceScope?.();
|
|
261
|
+
const workspace = normalizedWorkspacePath(bound?.workspace);
|
|
262
|
+
const boundSessionId = safeDisplayText(bound?.sessionId);
|
|
263
|
+
if (!workspace || !boundSessionId) {
|
|
264
|
+
throw new TypeError('Harness returned an invalid bound session');
|
|
265
|
+
}
|
|
266
|
+
const title = safeDisplayText(bound?.title) || '暂无标题';
|
|
267
|
+
const message = [
|
|
268
|
+
'当前聊天已绑定会话:',
|
|
269
|
+
`工作区:${workspace}`,
|
|
270
|
+
`标题:${title}`,
|
|
271
|
+
`ID:${boundSessionId}`,
|
|
272
|
+
`归档:${bound?.archived === true ? '是' : '否'}`,
|
|
273
|
+
].join('\n');
|
|
274
|
+
return commandResult(message, splitWorkspaceCommandMessage(message));
|
|
275
|
+
} catch (error) {
|
|
276
|
+
return commandResult(sessionBindErrorMessage(error));
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
export async function runWorkspaceCommand(text, harness, conversationKey) {
|
|
90
281
|
if (typeof text !== 'string') return null;
|
|
91
282
|
const command = text.trim();
|
|
283
|
+
if (SESSION_BIND_PREFIX.test(command)) {
|
|
284
|
+
return runSessionBindCommand(command, harness, conversationKey);
|
|
285
|
+
}
|
|
286
|
+
const sessionListMatch = SESSION_LIST_COMMAND.exec(command);
|
|
287
|
+
if (sessionListMatch) return runSessionListCommand(sessionListMatch, harness);
|
|
92
288
|
const listMatch = WORKSPACE_LIST_COMMAND.exec(command);
|
|
93
289
|
if (listMatch) return runWorkspaceListCommand(listMatch, harness);
|
|
94
290
|
const match = WORKSPACE_COMMAND.exec(command);
|
|
@@ -1,9 +1,20 @@
|
|
|
1
1
|
export const WORKSPACE_SESSION_STALE = 'workspace-session-stale';
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
function workspaceSession(harness, sessionId) {
|
|
4
|
+
if (typeof harness.workspaceSession === 'function') {
|
|
5
|
+
return harness.workspaceSession(sessionId);
|
|
6
|
+
}
|
|
7
|
+
return Object.freeze({
|
|
8
|
+
sessionId,
|
|
9
|
+
sessionExists: (...args) => harness.sessionExists(sessionId, ...args),
|
|
10
|
+
ask: (...args) => harness.ask(sessionId, ...args),
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async function sessionExists(session, options) {
|
|
4
15
|
return options === undefined
|
|
5
|
-
?
|
|
6
|
-
:
|
|
16
|
+
? session.sessionExists()
|
|
17
|
+
: session.sessionExists(options);
|
|
7
18
|
}
|
|
8
19
|
|
|
9
20
|
async function createSession(harness, options) {
|
|
@@ -27,15 +38,17 @@ export async function askInWorkspaceSession({
|
|
|
27
38
|
askOptions,
|
|
28
39
|
}) {
|
|
29
40
|
while (true) {
|
|
30
|
-
let sessionId = state.sessionFor(key);
|
|
31
|
-
if (!sessionId || !(await sessionExists(harness, sessionId, existsOptions))) {
|
|
32
|
-
sessionId = await createSession(harness, createOptions);
|
|
33
|
-
if (await state.setSession(key, sessionId) === false) continue;
|
|
34
|
-
}
|
|
35
41
|
try {
|
|
42
|
+
let sessionId = state.sessionFor(key);
|
|
43
|
+
let session = sessionId ? workspaceSession(harness, sessionId) : null;
|
|
44
|
+
if (!session || !(await sessionExists(session, existsOptions))) {
|
|
45
|
+
sessionId = await createSession(harness, createOptions);
|
|
46
|
+
if (await state.setSession(key, sessionId) === false) continue;
|
|
47
|
+
session = workspaceSession(harness, sessionId);
|
|
48
|
+
}
|
|
36
49
|
return {
|
|
37
50
|
sessionId,
|
|
38
|
-
answer: await
|
|
51
|
+
answer: await session.ask(text, askOptions),
|
|
39
52
|
};
|
|
40
53
|
} catch (error) {
|
|
41
54
|
if (error?.code !== WORKSPACE_SESSION_STALE) throw error;
|
|
@@ -9,6 +9,8 @@ const HELP_TEXT = [
|
|
|
9
9
|
'/new 开启一个全新会话',
|
|
10
10
|
'/workspace 工作区绝对路径 切换工作区',
|
|
11
11
|
'/workspacelist 列出工作区绝对路径',
|
|
12
|
+
'/sessionlist [工作区序号或绝对路径] 列出会话 ID 和标题',
|
|
13
|
+
'/session Session ID 将当前聊天绑定到指定会话',
|
|
12
14
|
'/status 检查连接状态',
|
|
13
15
|
'/help 显示本帮助',
|
|
14
16
|
].join('\n');
|
|
@@ -186,7 +188,7 @@ export class WecomHarnessBridge {
|
|
|
186
188
|
await this.#state.markSeen(messageId);
|
|
187
189
|
return;
|
|
188
190
|
}
|
|
189
|
-
const workspaceCommand = await runWorkspaceCommand(text, this.#harness);
|
|
191
|
+
const workspaceCommand = await runWorkspaceCommand(text, this.#harness, key);
|
|
190
192
|
if (workspaceCommand) {
|
|
191
193
|
for (const reply of workspaceCommand.messages ?? [workspaceCommand.message]) {
|
|
192
194
|
await this.#sendImmediate(frame, chatId, reply);
|
|
@@ -2,6 +2,8 @@ import { spawn } from 'node:child_process';
|
|
|
2
2
|
import { randomUUID } from 'node:crypto';
|
|
3
3
|
import { isAbsolute } from 'node:path';
|
|
4
4
|
|
|
5
|
+
import { adoptRegisteredWorkspaceSession } from '../shared/harness-session-binding.mjs';
|
|
6
|
+
|
|
5
7
|
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
6
8
|
|
|
7
9
|
function workspacePaths(value) {
|
|
@@ -11,6 +13,47 @@ function workspacePaths(value) {
|
|
|
11
13
|
));
|
|
12
14
|
}
|
|
13
15
|
|
|
16
|
+
function workspaceFromList(workspacePath, workspaceList) {
|
|
17
|
+
if (!Array.isArray(workspaceList?.items)
|
|
18
|
+
|| !Array.isArray(workspaceList?.archivedSessionIds)) {
|
|
19
|
+
throw new Error('Harness returned an invalid response for workspace.list');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const workspace = workspaceList.items.find((item) => item?.path === workspacePath);
|
|
23
|
+
if (!workspace) return null;
|
|
24
|
+
if (!Array.isArray(workspace.sessionIds)
|
|
25
|
+
|| workspace.sessionIds.some((sessionId) => typeof sessionId !== 'string')) {
|
|
26
|
+
throw new Error('Harness returned invalid session IDs for workspace.list');
|
|
27
|
+
}
|
|
28
|
+
return workspace;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function workspaceSessions(workspace, archivedSessionIds, sessionList) {
|
|
32
|
+
if (!Array.isArray(sessionList?.items)) {
|
|
33
|
+
throw new Error('Harness returned an invalid response for session.list');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const archived = new Set(archivedSessionIds);
|
|
37
|
+
const summaries = new Map(sessionList.items.flatMap((item) => (
|
|
38
|
+
typeof item?.sessionId === 'string' ? [[item.sessionId, item]] : []
|
|
39
|
+
)));
|
|
40
|
+
return {
|
|
41
|
+
workspace: workspace.path,
|
|
42
|
+
sessions: workspace.sessionIds.map((sessionId) => {
|
|
43
|
+
const summary = summaries.get(sessionId);
|
|
44
|
+
const title = summary?.projections?.values?.title;
|
|
45
|
+
return {
|
|
46
|
+
sessionId,
|
|
47
|
+
title: typeof title === 'string' ? title : null,
|
|
48
|
+
archived: archived.has(sessionId),
|
|
49
|
+
blank: summary?.blank === true,
|
|
50
|
+
origin: summary?.origin === 'subagent' ? 'subagent' : null,
|
|
51
|
+
summaryAvailable: summary !== undefined,
|
|
52
|
+
};
|
|
53
|
+
}),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
14
57
|
function assistantMessageText(event) {
|
|
15
58
|
return (event?.data?.message?.content ?? [])
|
|
16
59
|
.filter((part) => part.type === 'text' && typeof part.text === 'string')
|
|
@@ -200,6 +243,19 @@ export class HarnessClient {
|
|
|
200
243
|
return workspacePaths(await this.rpc('workspace.list', {}, 30_000, options));
|
|
201
244
|
}
|
|
202
245
|
|
|
246
|
+
async listWorkspaceSessions(workspacePath, options = {}) {
|
|
247
|
+
await this.ensureRunning();
|
|
248
|
+
const workspaceList = await this.rpc('workspace.list', {}, 30_000, options);
|
|
249
|
+
const workspace = workspaceFromList(workspacePath, workspaceList);
|
|
250
|
+
if (!workspace) return { workspace: workspacePath, sessions: [] };
|
|
251
|
+
const sessionList = await this.rpc('session.list', {}, 30_000, options);
|
|
252
|
+
return workspaceSessions(workspace, workspaceList.archivedSessionIds, sessionList);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
async adoptWorkspaceSession(value, options = {}) {
|
|
256
|
+
return adoptRegisteredWorkspaceSession(this, value, options);
|
|
257
|
+
}
|
|
258
|
+
|
|
203
259
|
async workspaceId(options = {}) {
|
|
204
260
|
const workspace = options.workspace ?? this.#workspace;
|
|
205
261
|
const { items } = await this.rpc('workspace.list', {});
|
|
@@ -13,6 +13,8 @@ const HELP_TEXT = [
|
|
|
13
13
|
'/new 开启一个全新会话',
|
|
14
14
|
'/workspace 工作区绝对路径 切换工作区',
|
|
15
15
|
'/workspacelist 列出工作区绝对路径',
|
|
16
|
+
'/sessionlist [工作区序号或绝对路径] 列出会话 ID 和标题',
|
|
17
|
+
'/session Session ID 将当前聊天绑定到指定会话',
|
|
16
18
|
'/status 检查连接状态',
|
|
17
19
|
'/help 显示本帮助',
|
|
18
20
|
].join('\n');
|
|
@@ -120,6 +122,7 @@ export class WeixinHarnessBridge {
|
|
|
120
122
|
}
|
|
121
123
|
|
|
122
124
|
const command = text.trim().toLowerCase();
|
|
125
|
+
const key = conversationKey(sender);
|
|
123
126
|
if (command === '/help') {
|
|
124
127
|
await this.#send(sender, HELP_TEXT, contextToken, runId);
|
|
125
128
|
await this.#state.markSeen(messageId);
|
|
@@ -132,12 +135,12 @@ export class WeixinHarnessBridge {
|
|
|
132
135
|
return;
|
|
133
136
|
}
|
|
134
137
|
if (command === '/new') {
|
|
135
|
-
await this.#state.clearSession(
|
|
138
|
+
await this.#state.clearSession(key);
|
|
136
139
|
await this.#send(sender, '已开启新会话。请发送你的问题。', contextToken, runId);
|
|
137
140
|
await this.#state.markSeen(messageId);
|
|
138
141
|
return;
|
|
139
142
|
}
|
|
140
|
-
const workspaceCommand = await runWorkspaceCommand(text, this.#harness);
|
|
143
|
+
const workspaceCommand = await runWorkspaceCommand(text, this.#harness, key);
|
|
141
144
|
if (workspaceCommand) {
|
|
142
145
|
for (const reply of workspaceCommand.messages ?? [workspaceCommand.message]) {
|
|
143
146
|
await this.#send(sender, reply, contextToken, runId);
|
|
@@ -146,7 +149,6 @@ export class WeixinHarnessBridge {
|
|
|
146
149
|
return;
|
|
147
150
|
}
|
|
148
151
|
|
|
149
|
-
const key = conversationKey(sender);
|
|
150
152
|
const { answer } = await askInWorkspaceSession({
|
|
151
153
|
harness: this.#harness,
|
|
152
154
|
state: this.#state,
|