@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
|
@@ -108,7 +108,7 @@ export class DiscordApi {
|
|
|
108
108
|
headers: {
|
|
109
109
|
authorization: `Bot ${this.#token}`,
|
|
110
110
|
'content-type': 'application/json',
|
|
111
|
-
'user-agent': 'DeepSeek-Harness-dsh-im (https://github.com/xmanrui/dsh-im, 0.
|
|
111
|
+
'user-agent': 'DeepSeek-Harness-dsh-im (https://github.com/xmanrui/dsh-im, 0.7.0)',
|
|
112
112
|
},
|
|
113
113
|
...(body === undefined ? {} : { body: JSON.stringify(body) }),
|
|
114
114
|
signal: requestSignal(signal, timeoutMs),
|
|
@@ -15,6 +15,8 @@ const HELP_TEXT = [
|
|
|
15
15
|
'/new 开启一个全新会话',
|
|
16
16
|
'/workspace 工作区绝对路径 切换工作区',
|
|
17
17
|
'/workspacelist 列出工作区绝对路径',
|
|
18
|
+
'/sessionlist [工作区序号或绝对路径] 列出会话 ID 和标题',
|
|
19
|
+
'/session Session ID 将当前聊天绑定到指定会话',
|
|
18
20
|
'/status 检查连接状态',
|
|
19
21
|
'/help 显示本帮助',
|
|
20
22
|
].join('\n');
|
|
@@ -110,7 +112,7 @@ export class FeishuHarnessBridge {
|
|
|
110
112
|
await this.#send(event.message.chat_id, '飞书机器人与 DeepSeek Harness 连接正常。');
|
|
111
113
|
return;
|
|
112
114
|
}
|
|
113
|
-
const workspaceCommand = await runWorkspaceCommand(text, this.#harness);
|
|
115
|
+
const workspaceCommand = await runWorkspaceCommand(text, this.#harness, key);
|
|
114
116
|
if (workspaceCommand) {
|
|
115
117
|
for (const reply of workspaceCommand.messages ?? [workspaceCommand.message]) {
|
|
116
118
|
await this.#send(event.message.chat_id, 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 messageText(event) {
|
|
15
58
|
return (event?.data?.message?.content ?? [])
|
|
16
59
|
.filter((part) => part.type === 'text' && typeof part.text === 'string')
|
|
@@ -210,6 +253,19 @@ export class HarnessClient {
|
|
|
210
253
|
return workspacePaths(await this.rpc('workspace.list', {}, 30000, options));
|
|
211
254
|
}
|
|
212
255
|
|
|
256
|
+
async listWorkspaceSessions(workspacePath, options = {}) {
|
|
257
|
+
await this.ensureRunning();
|
|
258
|
+
const workspaceList = await this.rpc('workspace.list', {}, 30000, options);
|
|
259
|
+
const workspace = workspaceFromList(workspacePath, workspaceList);
|
|
260
|
+
if (!workspace) return { workspace: workspacePath, sessions: [] };
|
|
261
|
+
const sessionList = await this.rpc('session.list', {}, 30000, options);
|
|
262
|
+
return workspaceSessions(workspace, workspaceList.archivedSessionIds, sessionList);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
async adoptWorkspaceSession(value, options = {}) {
|
|
266
|
+
return adoptRegisteredWorkspaceSession(this, value, options, 30000);
|
|
267
|
+
}
|
|
268
|
+
|
|
213
269
|
async workspaceId(options = {}) {
|
|
214
270
|
const workspace = options.workspace ?? this.#workspace;
|
|
215
271
|
const { items } = await this.rpc('workspace.list', {});
|
|
@@ -8,6 +8,8 @@ const HELP_TEXT = [
|
|
|
8
8
|
'/new 开启一个全新会话',
|
|
9
9
|
'/workspace 工作区绝对路径 切换工作区',
|
|
10
10
|
'/workspacelist 列出工作区绝对路径',
|
|
11
|
+
'/sessionlist [工作区序号或绝对路径] 列出会话 ID 和标题',
|
|
12
|
+
'/session Session ID 将当前聊天绑定到指定会话',
|
|
11
13
|
'/status 检查连接状态',
|
|
12
14
|
'/help 显示本帮助',
|
|
13
15
|
].join('\n');
|
|
@@ -126,7 +128,7 @@ export class QqHarnessBridge {
|
|
|
126
128
|
await this.#state.markSeen(messageId);
|
|
127
129
|
return;
|
|
128
130
|
}
|
|
129
|
-
const workspaceCommand = await runWorkspaceCommand(text, this.#harness);
|
|
131
|
+
const workspaceCommand = await runWorkspaceCommand(text, this.#harness, key);
|
|
130
132
|
if (workspaceCommand) {
|
|
131
133
|
for (const reply of workspaceCommand.messages ?? [workspaceCommand.message]) {
|
|
132
134
|
await this.#bot.sendText(target, reply);
|
|
@@ -1,10 +1,37 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
mkdir,
|
|
3
|
+
readFile,
|
|
4
|
+
realpath,
|
|
5
|
+
rename,
|
|
6
|
+
stat,
|
|
7
|
+
unlink,
|
|
8
|
+
writeFile,
|
|
9
|
+
} from 'node:fs/promises';
|
|
2
10
|
import { dirname, isAbsolute, resolve } from 'node:path';
|
|
3
11
|
|
|
4
12
|
import { WORKSPACE_SESSION_STALE } from './workspace-session.mjs';
|
|
5
13
|
|
|
6
14
|
const EMPTY_DOCUMENT = Object.freeze({ version: 1, workspaces: Object.freeze({}) });
|
|
7
15
|
|
|
16
|
+
function workspaceSessionStale(message) {
|
|
17
|
+
const error = new Error(message);
|
|
18
|
+
error.code = WORKSPACE_SESSION_STALE;
|
|
19
|
+
return error;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async function canonicalWorkspacePath(value) {
|
|
23
|
+
return resolve(await realpath(value));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async function sameWorkspacePath(left, right) {
|
|
27
|
+
if (left === right) return true;
|
|
28
|
+
try {
|
|
29
|
+
return await canonicalWorkspacePath(left) === await canonicalWorkspacePath(right);
|
|
30
|
+
} catch {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
8
35
|
function botIdOf(value) {
|
|
9
36
|
if (typeof value !== 'string' || !/^[A-Za-z0-9_-]{1,128}$/.test(value)) {
|
|
10
37
|
throw new TypeError('Invalid bot id');
|
|
@@ -179,6 +206,72 @@ export class BotWorkspaceStore {
|
|
|
179
206
|
});
|
|
180
207
|
}
|
|
181
208
|
|
|
209
|
+
async bindWorkspaceSession(botId, value, {
|
|
210
|
+
conversationKey,
|
|
211
|
+
sessionId,
|
|
212
|
+
clearSessions,
|
|
213
|
+
setSession,
|
|
214
|
+
incarnation,
|
|
215
|
+
expectedGeneration,
|
|
216
|
+
} = {}) {
|
|
217
|
+
const id = botIdOf(botId);
|
|
218
|
+
if (typeof conversationKey !== 'string' || !conversationKey
|
|
219
|
+
|| typeof sessionId !== 'string' || !sessionId) {
|
|
220
|
+
throw new TypeError('conversationKey and sessionId are required');
|
|
221
|
+
}
|
|
222
|
+
if (typeof clearSessions !== 'function' || typeof setSession !== 'function') {
|
|
223
|
+
throw new TypeError('session state callbacks are required');
|
|
224
|
+
}
|
|
225
|
+
if (!this.has(id)
|
|
226
|
+
|| (incarnation !== undefined && incarnation !== this.incarnationFor(id))) {
|
|
227
|
+
const error = new Error('找不到要修改的机器人。');
|
|
228
|
+
error.code = 'workspace-bot-not-found';
|
|
229
|
+
throw error;
|
|
230
|
+
}
|
|
231
|
+
const workspace = await canonicalWorkspacePath(await validateWorkspacePath(value));
|
|
232
|
+
return this.#enqueue(id, async () => {
|
|
233
|
+
if (!this.has(id)
|
|
234
|
+
|| (incarnation !== undefined && incarnation !== this.incarnationFor(id))) {
|
|
235
|
+
const error = new Error('找不到要修改的机器人。');
|
|
236
|
+
error.code = 'workspace-bot-not-found';
|
|
237
|
+
throw error;
|
|
238
|
+
}
|
|
239
|
+
if (expectedGeneration !== undefined
|
|
240
|
+
&& expectedGeneration !== this.generationFor(id)) {
|
|
241
|
+
throw workspaceSessionStale(
|
|
242
|
+
'The bot workspace changed before the session binding could be committed.',
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (!(await sameWorkspacePath(workspace, this.workspaceFor(id)))) {
|
|
247
|
+
const previous = this.#workspaces[id];
|
|
248
|
+
// Fence every session resolved before this transition, then remove
|
|
249
|
+
// the old workspace mappings before publishing the new workspace.
|
|
250
|
+
this.#generations.set(id, this.#freshGeneration());
|
|
251
|
+
await clearSessions();
|
|
252
|
+
this.#workspaces[id] = workspace;
|
|
253
|
+
try {
|
|
254
|
+
await this.#persist();
|
|
255
|
+
} catch (error) {
|
|
256
|
+
// Session mappings stay cleared and the advanced generation stays
|
|
257
|
+
// fenced. Restoring either could pair an old session with a state
|
|
258
|
+
// transition whose durable outcome is unknown.
|
|
259
|
+
this.#workspaces[id] = previous;
|
|
260
|
+
throw error;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// This write remains inside the same bot transition as the workspace
|
|
265
|
+
// mutation, so another switch or bind cannot interleave between them.
|
|
266
|
+
await setSession(conversationKey, sessionId);
|
|
267
|
+
return {
|
|
268
|
+
workspace,
|
|
269
|
+
sessionId,
|
|
270
|
+
generation: this.#generations.get(id),
|
|
271
|
+
};
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
|
|
182
275
|
async invalidateSessions(botId, { clearSessions } = {}) {
|
|
183
276
|
const id = botIdOf(botId);
|
|
184
277
|
return this.#enqueue(id, async () => {
|
|
@@ -417,20 +510,21 @@ export function createBotWorkspaceScope(harness, { botId, workspaces, state }) {
|
|
|
417
510
|
}
|
|
418
511
|
};
|
|
419
512
|
}
|
|
420
|
-
if (property === 'listWorkspaces'
|
|
513
|
+
if ((property === 'listWorkspaces' || property === 'listWorkspaceSessions')
|
|
514
|
+
&& typeof target[property] === 'function') {
|
|
421
515
|
return async (...args) => {
|
|
422
516
|
if (!isCurrentScope()) {
|
|
423
517
|
const error = new Error('找不到要修改的机器人。');
|
|
424
518
|
error.code = 'workspace-bot-not-found';
|
|
425
519
|
throw error;
|
|
426
520
|
}
|
|
427
|
-
const
|
|
521
|
+
const result = await target[property](...args);
|
|
428
522
|
if (!isCurrentScope()) {
|
|
429
523
|
const error = new Error('找不到要修改的机器人。');
|
|
430
524
|
error.code = 'workspace-bot-not-found';
|
|
431
525
|
throw error;
|
|
432
526
|
}
|
|
433
|
-
return
|
|
527
|
+
return result;
|
|
434
528
|
};
|
|
435
529
|
}
|
|
436
530
|
if (property === 'switchWorkspace') {
|
|
@@ -446,6 +540,62 @@ export function createBotWorkspaceScope(harness, { botId, workspaces, state }) {
|
|
|
446
540
|
});
|
|
447
541
|
};
|
|
448
542
|
}
|
|
543
|
+
if (property === 'bindWorkspaceSession') {
|
|
544
|
+
return async (conversationKey, sessionId) => {
|
|
545
|
+
if (typeof conversationKey !== 'string' || !conversationKey
|
|
546
|
+
|| typeof sessionId !== 'string' || !sessionId) {
|
|
547
|
+
throw new TypeError('conversationKey and sessionId are required');
|
|
548
|
+
}
|
|
549
|
+
if (!isCurrentScope()) {
|
|
550
|
+
const error = new Error('找不到要修改的机器人。');
|
|
551
|
+
error.code = 'workspace-bot-not-found';
|
|
552
|
+
throw error;
|
|
553
|
+
}
|
|
554
|
+
if (typeof target.adoptWorkspaceSession !== 'function') {
|
|
555
|
+
throw new TypeError('Harness does not support adopting workspace sessions');
|
|
556
|
+
}
|
|
557
|
+
const expectedGeneration = workspaces.generationFor(botId);
|
|
558
|
+
const adopted = await target.adoptWorkspaceSession(sessionId);
|
|
559
|
+
if (!isCurrentScope()) {
|
|
560
|
+
const error = new Error('找不到要修改的机器人。');
|
|
561
|
+
error.code = 'workspace-bot-not-found';
|
|
562
|
+
throw error;
|
|
563
|
+
}
|
|
564
|
+
if (expectedGeneration !== workspaces.generationFor(botId)) {
|
|
565
|
+
throw workspaceSessionStale(
|
|
566
|
+
'The bot workspace changed while the session was being adopted.',
|
|
567
|
+
);
|
|
568
|
+
}
|
|
569
|
+
if (!adopted || typeof adopted !== 'object'
|
|
570
|
+
|| adopted.sessionId !== sessionId || typeof adopted.workspace !== 'string') {
|
|
571
|
+
throw new TypeError('Harness returned an invalid adopted workspace session');
|
|
572
|
+
}
|
|
573
|
+
const bound = await workspaces.bindWorkspaceSession(botId, adopted.workspace, {
|
|
574
|
+
conversationKey,
|
|
575
|
+
sessionId,
|
|
576
|
+
clearSessions: () => state.clearSessions(),
|
|
577
|
+
setSession: (key, selectedSessionId) => state.setSession(key, selectedSessionId),
|
|
578
|
+
incarnation,
|
|
579
|
+
expectedGeneration,
|
|
580
|
+
});
|
|
581
|
+
if (!isCurrentScope()) {
|
|
582
|
+
const error = new Error('找不到要修改的机器人。');
|
|
583
|
+
error.code = 'workspace-bot-not-found';
|
|
584
|
+
throw error;
|
|
585
|
+
}
|
|
586
|
+
if (bound.generation !== workspaces.generationFor(botId)) {
|
|
587
|
+
throw workspaceSessionStale(
|
|
588
|
+
'The bot workspace changed before the session binding completed.',
|
|
589
|
+
);
|
|
590
|
+
}
|
|
591
|
+
sessionGenerations.set(sessionId, bound.generation);
|
|
592
|
+
return {
|
|
593
|
+
...adopted,
|
|
594
|
+
workspace: bound.workspace,
|
|
595
|
+
sessionId: bound.sessionId,
|
|
596
|
+
};
|
|
597
|
+
};
|
|
598
|
+
}
|
|
449
599
|
if (property === 'createSession') {
|
|
450
600
|
return async (options = {}) => {
|
|
451
601
|
await workspaces.whenBotIdle(botId);
|
|
@@ -463,6 +613,37 @@ export function createBotWorkspaceScope(harness, { botId, workspaces, state }) {
|
|
|
463
613
|
return sessionId;
|
|
464
614
|
};
|
|
465
615
|
}
|
|
616
|
+
if (property === 'workspaceSession') {
|
|
617
|
+
return (sessionId) => {
|
|
618
|
+
if (typeof sessionId !== 'string' || !sessionId) {
|
|
619
|
+
throw new TypeError('sessionId is required');
|
|
620
|
+
}
|
|
621
|
+
const generation = sessionGenerations.get(sessionId)
|
|
622
|
+
?? workspaces.generationFor(botId);
|
|
623
|
+
// Transfer the mutable provenance entry into this immutable handle.
|
|
624
|
+
// A later handle for the same id captures its own generation instead
|
|
625
|
+
// of sharing deletion or rebinding state with this call.
|
|
626
|
+
sessionGenerations.delete(sessionId);
|
|
627
|
+
const isCurrentSession = () => isCurrentScope()
|
|
628
|
+
&& generation === workspaces.generationFor(botId);
|
|
629
|
+
return Object.freeze({
|
|
630
|
+
sessionId,
|
|
631
|
+
async sessionExists(...args) {
|
|
632
|
+
if (!isCurrentSession()) return false;
|
|
633
|
+
const exists = await target.sessionExists(sessionId, ...args);
|
|
634
|
+
return isCurrentSession() && exists;
|
|
635
|
+
},
|
|
636
|
+
ask(...args) {
|
|
637
|
+
if (!isCurrentSession()) {
|
|
638
|
+
throw workspaceSessionStale(
|
|
639
|
+
'The bot workspace changed before this prompt started.',
|
|
640
|
+
);
|
|
641
|
+
}
|
|
642
|
+
return target.ask(sessionId, ...args);
|
|
643
|
+
},
|
|
644
|
+
});
|
|
645
|
+
};
|
|
646
|
+
}
|
|
466
647
|
if (property === 'sessionExists') {
|
|
467
648
|
return (sessionId, ...args) => {
|
|
468
649
|
if (!isCurrentScope()) return false;
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { isAbsolute } from 'node:path';
|
|
2
|
+
|
|
3
|
+
const MAX_SESSION_ID_LENGTH = 256;
|
|
4
|
+
const UNSAFE_SESSION_ID = /[\p{White_Space}\p{Cc}\p{Cf}\p{Zl}\p{Zp}]/u;
|
|
5
|
+
const UNSAFE_WORKSPACE_PATH = /[\p{Cc}\p{Cf}\p{Zl}\p{Zp}]/u;
|
|
6
|
+
|
|
7
|
+
function bindingError(code, message) {
|
|
8
|
+
const error = new Error(message);
|
|
9
|
+
error.code = code;
|
|
10
|
+
return error;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function validatedSessionId(value) {
|
|
14
|
+
if (typeof value !== 'string' || !value || value.length > MAX_SESSION_ID_LENGTH
|
|
15
|
+
|| UNSAFE_SESSION_ID.test(value)) {
|
|
16
|
+
throw bindingError('session-id-invalid', 'A non-empty, safe session id is required');
|
|
17
|
+
}
|
|
18
|
+
return value;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function sessionWorkspace(sessionId, value) {
|
|
22
|
+
if (!Array.isArray(value?.items) || !Array.isArray(value?.archivedSessionIds)
|
|
23
|
+
|| value.archivedSessionIds.some((id) => typeof id !== 'string' || !id)) {
|
|
24
|
+
throw new Error('Harness returned an invalid response for workspace.list');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const owners = [];
|
|
28
|
+
for (const workspace of value.items) {
|
|
29
|
+
if (typeof workspace?.workspaceId !== 'string' || !workspace.workspaceId
|
|
30
|
+
|| typeof workspace.path !== 'string' || !isAbsolute(workspace.path)
|
|
31
|
+
|| !Array.isArray(workspace.sessionIds)
|
|
32
|
+
|| workspace.sessionIds.some((id) => typeof id !== 'string' || !id)) {
|
|
33
|
+
throw new Error('Harness returned an invalid response for workspace.list');
|
|
34
|
+
}
|
|
35
|
+
for (const accountedId of workspace.sessionIds) {
|
|
36
|
+
if (accountedId === sessionId) owners.push(workspace);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (owners.length === 0) {
|
|
41
|
+
throw bindingError('session-not-registered', 'The session is not registered to a Harness workspace');
|
|
42
|
+
}
|
|
43
|
+
if (owners.length !== 1) {
|
|
44
|
+
throw bindingError(
|
|
45
|
+
'session-workspace-ambiguous',
|
|
46
|
+
'The session is registered to more than one Harness workspace',
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
if (UNSAFE_WORKSPACE_PATH.test(owners[0].path)) {
|
|
50
|
+
throw new Error('Harness returned an unsafe workspace path for the session');
|
|
51
|
+
}
|
|
52
|
+
return {
|
|
53
|
+
workspace: owners[0],
|
|
54
|
+
archived: value.archivedSessionIds.includes(sessionId),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function sessionSummary(sessionId, value) {
|
|
59
|
+
if (!Array.isArray(value?.items)
|
|
60
|
+
|| value.items.some((item) => typeof item?.sessionId !== 'string' || !item.sessionId)) {
|
|
61
|
+
throw new Error('Harness returned an invalid response for session.list');
|
|
62
|
+
}
|
|
63
|
+
const matches = value.items.filter((item) => item.sessionId === sessionId);
|
|
64
|
+
if (matches.length === 0) {
|
|
65
|
+
throw bindingError('session-summary-unavailable', 'The session is no longer available from Harness');
|
|
66
|
+
}
|
|
67
|
+
if (matches.length !== 1) {
|
|
68
|
+
throw new Error('Harness returned duplicate session summaries for session.list');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const [summary] = matches;
|
|
72
|
+
if (summary.origin === 'subagent') {
|
|
73
|
+
throw bindingError(
|
|
74
|
+
'session-subagent-unsupported',
|
|
75
|
+
'Subagent sessions cannot be adopted as a bot conversation',
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
if (summary.origin !== undefined) {
|
|
79
|
+
throw new Error('Harness returned an invalid session origin for session.list');
|
|
80
|
+
}
|
|
81
|
+
const title = summary.projections?.values?.title;
|
|
82
|
+
if (title !== undefined && title !== null && typeof title !== 'string') {
|
|
83
|
+
throw new Error('Harness returned an invalid session title for session.list');
|
|
84
|
+
}
|
|
85
|
+
return { title: typeof title === 'string' ? title : null };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export async function adoptRegisteredWorkspaceSession(client, value, options = {}, timeoutMs = 30_000) {
|
|
89
|
+
const sessionId = validatedSessionId(value);
|
|
90
|
+
await client.ensureRunning(options);
|
|
91
|
+
const workspaceList = await client.rpc('workspace.list', {}, timeoutMs, options);
|
|
92
|
+
const { workspace, archived } = sessionWorkspace(sessionId, workspaceList);
|
|
93
|
+
const summary = sessionSummary(
|
|
94
|
+
sessionId,
|
|
95
|
+
await client.rpc('session.list', {}, timeoutMs, options),
|
|
96
|
+
);
|
|
97
|
+
const adopted = await client.rpc('session.create', {
|
|
98
|
+
workspaceId: workspace.workspaceId,
|
|
99
|
+
sessionId,
|
|
100
|
+
}, timeoutMs, options);
|
|
101
|
+
if (!adopted || adopted.sessionId !== sessionId) {
|
|
102
|
+
throw new Error('Harness returned an invalid response for session.create');
|
|
103
|
+
}
|
|
104
|
+
return {
|
|
105
|
+
sessionId,
|
|
106
|
+
workspace: workspace.path,
|
|
107
|
+
title: summary.title,
|
|
108
|
+
archived,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
@@ -102,6 +102,8 @@ export class TextHarnessBridge {
|
|
|
102
102
|
'/new 开启一个全新会话',
|
|
103
103
|
'/workspace 工作区绝对路径 切换工作区',
|
|
104
104
|
'/workspacelist 列出工作区绝对路径',
|
|
105
|
+
'/sessionlist [工作区序号或绝对路径] 列出会话 ID 和标题',
|
|
106
|
+
'/session Session ID 将当前聊天绑定到指定会话',
|
|
105
107
|
'/status 检查连接状态',
|
|
106
108
|
'/help 显示本帮助',
|
|
107
109
|
].join('\n'));
|
|
@@ -114,7 +116,8 @@ export class TextHarnessBridge {
|
|
|
114
116
|
await this.#state.markSeen(messageId);
|
|
115
117
|
return;
|
|
116
118
|
}
|
|
117
|
-
const
|
|
119
|
+
const conversationKey = `${message.kind}:${message.conversationId}`;
|
|
120
|
+
const workspaceCommand = await runWorkspaceCommand(text, this.#harness, conversationKey);
|
|
118
121
|
if (workspaceCommand) {
|
|
119
122
|
for (const reply of workspaceCommand.messages ?? [workspaceCommand.message]) {
|
|
120
123
|
await this.#bot.sendText(target, reply);
|
|
@@ -122,7 +125,6 @@ export class TextHarnessBridge {
|
|
|
122
125
|
await this.#state.markSeen(messageId);
|
|
123
126
|
return;
|
|
124
127
|
}
|
|
125
|
-
const conversationKey = `${message.kind}:${message.conversationId}`;
|
|
126
128
|
if (command === '/new') {
|
|
127
129
|
await this.#state.clearSession(conversationKey);
|
|
128
130
|
await this.#bot.sendText(target, '已开启新会话。请发送你的问题。');
|