@xmanrui/dsh-im 0.5.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.
@@ -1,5 +1,56 @@
1
1
  import { spawn } from 'node:child_process';
2
2
  import { randomUUID } from 'node:crypto';
3
+ import { isAbsolute } from 'node:path';
4
+
5
+ import { adoptRegisteredWorkspaceSession } from '../shared/harness-session-binding.mjs';
6
+
7
+ function workspacePaths(value) {
8
+ if (!Array.isArray(value?.items)) return [];
9
+ return value.items.flatMap((item) => (
10
+ typeof item?.path === 'string' && isAbsolute(item.path) ? [item.path] : []
11
+ ));
12
+ }
13
+
14
+ function workspaceFromList(workspacePath, workspaceList) {
15
+ if (!Array.isArray(workspaceList?.items)
16
+ || !Array.isArray(workspaceList?.archivedSessionIds)) {
17
+ throw new Error('Harness returned an invalid response for workspace.list');
18
+ }
19
+
20
+ const workspace = workspaceList.items.find((item) => item?.path === workspacePath);
21
+ if (!workspace) return null;
22
+ if (!Array.isArray(workspace.sessionIds)
23
+ || workspace.sessionIds.some((sessionId) => typeof sessionId !== 'string')) {
24
+ throw new Error('Harness returned invalid session IDs for workspace.list');
25
+ }
26
+ return workspace;
27
+ }
28
+
29
+ function workspaceSessions(workspace, archivedSessionIds, sessionList) {
30
+ if (!Array.isArray(sessionList?.items)) {
31
+ throw new Error('Harness returned an invalid response for session.list');
32
+ }
33
+
34
+ const archived = new Set(archivedSessionIds);
35
+ const summaries = new Map(sessionList.items.flatMap((item) => (
36
+ typeof item?.sessionId === 'string' ? [[item.sessionId, item]] : []
37
+ )));
38
+ return {
39
+ workspace: workspace.path,
40
+ sessions: workspace.sessionIds.map((sessionId) => {
41
+ const summary = summaries.get(sessionId);
42
+ const title = summary?.projections?.values?.title;
43
+ return {
44
+ sessionId,
45
+ title: typeof title === 'string' ? title : null,
46
+ archived: archived.has(sessionId),
47
+ blank: summary?.blank === true,
48
+ origin: summary?.origin === 'subagent' ? 'subagent' : null,
49
+ summaryAvailable: summary !== undefined,
50
+ };
51
+ }),
52
+ };
53
+ }
3
54
 
4
55
  function sleep(ms, signal) {
5
56
  return new Promise((resolve, reject) => {
@@ -216,6 +267,24 @@ export class HarnessClient {
216
267
  throw new Error(`Harness did not become ready: ${lastError?.message ?? 'timeout'}`);
217
268
  }
218
269
 
270
+ async listWorkspaces(options = {}) {
271
+ await this.ensureRunning(options);
272
+ return workspacePaths(await this.rpc('workspace.list', {}, 30_000, options));
273
+ }
274
+
275
+ async listWorkspaceSessions(workspacePath, options = {}) {
276
+ await this.ensureRunning(options);
277
+ const workspaceList = await this.rpc('workspace.list', {}, 30_000, options);
278
+ const workspace = workspaceFromList(workspacePath, workspaceList);
279
+ if (!workspace) return { workspace: workspacePath, sessions: [] };
280
+ const sessionList = await this.rpc('session.list', {}, 30_000, options);
281
+ return workspaceSessions(workspace, workspaceList.archivedSessionIds, sessionList);
282
+ }
283
+
284
+ async adoptWorkspaceSession(value, options = {}) {
285
+ return adoptRegisteredWorkspaceSession(this, value, options);
286
+ }
287
+
219
288
  async workspaceId(options = {}) {
220
289
  const { workspace = this.#workspace, ...rpcOptions } = options;
221
290
  const { items } = await this.rpc('workspace.list', {}, 30_000, rpcOptions);
@@ -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.5.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),
@@ -14,6 +14,9 @@ const HELP_TEXT = [
14
14
  '直接发送问题即可继续当前会话。',
15
15
  '/new 开启一个全新会话',
16
16
  '/workspace 工作区绝对路径 切换工作区',
17
+ '/workspacelist 列出工作区绝对路径',
18
+ '/sessionlist [工作区序号或绝对路径] 列出会话 ID 和标题',
19
+ '/session Session ID 将当前聊天绑定到指定会话',
17
20
  '/status 检查连接状态',
18
21
  '/help 显示本帮助',
19
22
  ].join('\n');
@@ -109,9 +112,11 @@ export class FeishuHarnessBridge {
109
112
  await this.#send(event.message.chat_id, '飞书机器人与 DeepSeek Harness 连接正常。');
110
113
  return;
111
114
  }
112
- const workspaceCommand = await runWorkspaceCommand(text, this.#harness);
115
+ const workspaceCommand = await runWorkspaceCommand(text, this.#harness, key);
113
116
  if (workspaceCommand) {
114
- await this.#send(event.message.chat_id, workspaceCommand.message);
117
+ for (const reply of workspaceCommand.messages ?? [workspaceCommand.message]) {
118
+ await this.#send(event.message.chat_id, reply);
119
+ }
115
120
  return;
116
121
  }
117
122
 
@@ -1,8 +1,59 @@
1
1
  import { spawn } from 'node:child_process';
2
2
  import { randomUUID } from 'node:crypto';
3
+ import { isAbsolute } from 'node:path';
4
+
5
+ import { adoptRegisteredWorkspaceSession } from '../shared/harness-session-binding.mjs';
3
6
 
4
7
  const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
5
8
 
9
+ function workspacePaths(value) {
10
+ if (!Array.isArray(value?.items)) return [];
11
+ return value.items.flatMap((item) => (
12
+ typeof item?.path === 'string' && isAbsolute(item.path) ? [item.path] : []
13
+ ));
14
+ }
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
+
6
57
  function messageText(event) {
7
58
  return (event?.data?.message?.content ?? [])
8
59
  .filter((part) => part.type === 'text' && typeof part.text === 'string')
@@ -197,6 +248,24 @@ export class HarnessClient {
197
248
  throw new Error(`Harness did not become ready: ${lastError?.message ?? 'timeout'}`);
198
249
  }
199
250
 
251
+ async listWorkspaces(options = {}) {
252
+ await this.ensureRunning();
253
+ return workspacePaths(await this.rpc('workspace.list', {}, 30000, options));
254
+ }
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
+
200
269
  async workspaceId(options = {}) {
201
270
  const workspace = options.workspace ?? this.#workspace;
202
271
  const { items } = await this.rpc('workspace.list', {});
@@ -7,6 +7,9 @@ const HELP_TEXT = [
7
7
  '直接发送文字即可继续当前会话。',
8
8
  '/new 开启一个全新会话',
9
9
  '/workspace 工作区绝对路径 切换工作区',
10
+ '/workspacelist 列出工作区绝对路径',
11
+ '/sessionlist [工作区序号或绝对路径] 列出会话 ID 和标题',
12
+ '/session Session ID 将当前聊天绑定到指定会话',
10
13
  '/status 检查连接状态',
11
14
  '/help 显示本帮助',
12
15
  ].join('\n');
@@ -125,9 +128,11 @@ export class QqHarnessBridge {
125
128
  await this.#state.markSeen(messageId);
126
129
  return;
127
130
  }
128
- const workspaceCommand = await runWorkspaceCommand(text, this.#harness);
131
+ const workspaceCommand = await runWorkspaceCommand(text, this.#harness, key);
129
132
  if (workspaceCommand) {
130
- await this.#bot.sendText(target, workspaceCommand.message);
133
+ for (const reply of workspaceCommand.messages ?? [workspaceCommand.message]) {
134
+ await this.#bot.sendText(target, reply);
135
+ }
131
136
  await this.#state.markSeen(messageId);
132
137
  return;
133
138
  }
@@ -1,10 +1,37 @@
1
- import { mkdir, readFile, rename, stat, unlink, writeFile } from 'node:fs/promises';
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 () => {
@@ -398,7 +491,42 @@ export function createBotWorkspaceScope(harness, { botId, workspaces, state }) {
398
491
  const sessionGenerations = new Map();
399
492
  const scopedHarness = new Proxy(harness, {
400
493
  get(target, property) {
401
- if (property === 'currentWorkspace') return () => workspaces.workspaceFor(botId);
494
+ if (property === 'currentWorkspace') {
495
+ return () => {
496
+ if (!isCurrentScope()) {
497
+ const error = new Error('找不到要修改的机器人。');
498
+ error.code = 'workspace-bot-not-found';
499
+ throw error;
500
+ }
501
+ return workspaces.workspaceFor(botId);
502
+ };
503
+ }
504
+ if (property === 'assertWorkspaceScope') {
505
+ return () => {
506
+ if (!isCurrentScope()) {
507
+ const error = new Error('找不到要修改的机器人。');
508
+ error.code = 'workspace-bot-not-found';
509
+ throw error;
510
+ }
511
+ };
512
+ }
513
+ if ((property === 'listWorkspaces' || property === 'listWorkspaceSessions')
514
+ && typeof target[property] === 'function') {
515
+ return async (...args) => {
516
+ if (!isCurrentScope()) {
517
+ const error = new Error('找不到要修改的机器人。');
518
+ error.code = 'workspace-bot-not-found';
519
+ throw error;
520
+ }
521
+ const result = await target[property](...args);
522
+ if (!isCurrentScope()) {
523
+ const error = new Error('找不到要修改的机器人。');
524
+ error.code = 'workspace-bot-not-found';
525
+ throw error;
526
+ }
527
+ return result;
528
+ };
529
+ }
402
530
  if (property === 'switchWorkspace') {
403
531
  return (workspace) => {
404
532
  if (!isCurrentScope()) {
@@ -412,6 +540,62 @@ export function createBotWorkspaceScope(harness, { botId, workspaces, state }) {
412
540
  });
413
541
  };
414
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
+ }
415
599
  if (property === 'createSession') {
416
600
  return async (options = {}) => {
417
601
  await workspaces.whenBotIdle(botId);
@@ -429,6 +613,37 @@ export function createBotWorkspaceScope(harness, { botId, workspaces, state }) {
429
613
  return sessionId;
430
614
  };
431
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
+ }
432
647
  if (property === 'sessionExists') {
433
648
  return (sessionId, ...args) => {
434
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
+ }
@@ -101,6 +101,9 @@ export class TextHarnessBridge {
101
101
  '直接发送文字即可继续当前会话。',
102
102
  '/new 开启一个全新会话',
103
103
  '/workspace 工作区绝对路径 切换工作区',
104
+ '/workspacelist 列出工作区绝对路径',
105
+ '/sessionlist [工作区序号或绝对路径] 列出会话 ID 和标题',
106
+ '/session Session ID 将当前聊天绑定到指定会话',
104
107
  '/status 检查连接状态',
105
108
  '/help 显示本帮助',
106
109
  ].join('\n'));
@@ -113,13 +116,15 @@ export class TextHarnessBridge {
113
116
  await this.#state.markSeen(messageId);
114
117
  return;
115
118
  }
116
- const workspaceCommand = await runWorkspaceCommand(text, this.#harness);
119
+ const conversationKey = `${message.kind}:${message.conversationId}`;
120
+ const workspaceCommand = await runWorkspaceCommand(text, this.#harness, conversationKey);
117
121
  if (workspaceCommand) {
118
- await this.#bot.sendText(target, workspaceCommand.message);
122
+ for (const reply of workspaceCommand.messages ?? [workspaceCommand.message]) {
123
+ await this.#bot.sendText(target, reply);
124
+ }
119
125
  await this.#state.markSeen(messageId);
120
126
  return;
121
127
  }
122
- const conversationKey = `${message.kind}:${message.conversationId}`;
123
128
  if (command === '/new') {
124
129
  await this.#state.clearSession(conversationKey);
125
130
  await this.#bot.sendText(target, '已开启新会话。请发送你的问题。');