@xmanrui/dsh-im 4.19.2 → 4.20.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.
Files changed (40) hide show
  1. package/README.en.md +78 -0
  2. package/README.md +78 -0
  3. package/lib/client.js +594 -217
  4. package/lib/index.js +279 -276
  5. package/package.json +9 -1
  6. package/plugin-src/client/channels/weixin/api.js +35 -17
  7. package/plugin-src/client/channels/weixin/connection-error.js +68 -0
  8. package/plugin-src/client/channels/weixin/index.js +36 -12
  9. package/plugin-src/client/i18n.js +2 -0
  10. package/plugin-src/host/channels/shared/startup.mjs +7 -4
  11. package/plugin-src/host/channels/weixin/connection-supervisor.mjs +13 -1
  12. package/plugin-src/host/channels/weixin/index.mjs +12 -3
  13. package/plugin-src/host/channels/weixin/production.mjs +53 -3
  14. package/plugin-src/host/channels/weixin/rpc.mjs +22 -17
  15. package/src/channels/dingtalk/dingtalk-bridge.mjs +4 -1
  16. package/src/channels/dingtalk/dingtalk-menu.mjs +8 -4
  17. package/src/channels/feishu/bridge.mjs +44 -13
  18. package/src/channels/qq/qq-bridge.mjs +12 -4
  19. package/src/channels/qq/qq-menu.mjs +11 -8
  20. package/src/channels/shared/bot-workspace-store.mjs +532 -40
  21. package/src/channels/shared/command-catalog.mjs +5 -0
  22. package/src/channels/shared/compact-command.mjs +14 -4
  23. package/src/channels/shared/control-command.mjs +1 -1
  24. package/src/channels/shared/deferred-delivery-coordinator.mjs +1 -1
  25. package/src/channels/shared/history-command.mjs +1 -1
  26. package/src/channels/shared/i18n-en/shared-a.mjs +41 -0
  27. package/src/channels/shared/i18n-en/weixin.mjs +2 -0
  28. package/src/channels/shared/model-command.mjs +5 -3
  29. package/src/channels/shared/workspace-command.mjs +114 -9
  30. package/src/channels/shared/workspace-session.mjs +55 -5
  31. package/src/channels/telegram/telegram-runtime.mjs +1 -1
  32. package/src/channels/wecom/wecom-bridge.mjs +2 -2
  33. package/src/channels/weixin/connection-error.en.mjs +116 -0
  34. package/src/channels/weixin/connection-error.mjs +204 -0
  35. package/src/channels/weixin/diagnostic-details.mjs +40 -0
  36. package/src/channels/weixin/state-store.mjs +4 -3
  37. package/src/channels/weixin/weixin-api.mjs +20 -8
  38. package/src/channels/weixin/weixin-bridge.mjs +3 -2
  39. package/src/channels/weixin/weixin-controller.mjs +133 -104
  40. package/src/channels/weixin/weixin-runtime.mjs +35 -24
@@ -1,11 +1,11 @@
1
1
  import { SET_ALIAS_ENDPOINT, validAliasPayload } from '../shared/bot-alias-rpc.mjs';
2
2
  import { registerManagementRpc } from '../../../management-rpc.mjs';
3
3
  import QRCode from 'qrcode';
4
+ import { createWeixinDiagnostics, weixinStageError } from '../../../../src/channels/weixin/connection-error.mjs';
4
5
  import { SET_CONTEXT_ENHANCEMENT_ENDPOINT, validContextEnhancementPayload } from '../shared/context-enhancement-rpc.mjs';
5
6
  import { SET_ACCESS_POLICY_ENDPOINT, validAccessPolicyPayload } from '../shared/access-policy-rpc.mjs';
6
7
  import { resolveRpcAuthority } from '../../rpc-authority.mjs';
7
8
  import {
8
- publicWorkspaceError,
9
9
  SET_WORKSPACE_ENDPOINT,
10
10
  validWorkspacePayload,
11
11
  } from '../shared/workspace-rpc.mjs';
@@ -118,13 +118,6 @@ function cancelled() {
118
118
  return { ok: false, error: { code: 'cancelled', message: 'The request was cancelled.' } };
119
119
  }
120
120
 
121
- function internalFailure() {
122
- return {
123
- ok: false,
124
- error: { code: 'weixin-operation-failed', message: '微信操作失败,请稍后重试。' },
125
- };
126
- }
127
-
128
121
  async function qrDataUrl(value) {
129
122
  return QRCode.toDataURL(value, {
130
123
  type: 'image/png',
@@ -143,7 +136,9 @@ async function withEncodedQr(value, encodeQr) {
143
136
  }
144
137
 
145
138
  async function publicStatus(status, encodeQr) {
146
- const safe = structuredClone(status);
139
+ let safe;
140
+ try { safe = structuredClone(status); }
141
+ catch (error) { throw weixinStageError('status-read-failed', error); }
147
142
  if (safe.provisioning) safe.provisioning = await withEncodedQr(safe.provisioning, encodeQr);
148
143
  return safe;
149
144
  }
@@ -161,14 +156,17 @@ function assertController(controller) {
161
156
  }
162
157
  }
163
158
 
164
- export function createWeixinRpcHandler(controller, { encodeQr = qrDataUrl } = {}) {
159
+ export function createWeixinRpcHandler(controller, { encodeQr = qrDataUrl, logger = console, diagnostics = createWeixinDiagnostics({ logger }) } = {}) {
165
160
  assertController(controller);
166
161
  const qrCache = new Map();
167
162
  const cachedEncode = (url) => {
168
163
  let encoded = qrCache.get(url);
169
164
  if (!encoded) {
170
165
  if (qrCache.size >= 16) qrCache.delete(qrCache.keys().next().value);
171
- encoded = Promise.resolve().then(() => encodeQr(url));
166
+ encoded = Promise.resolve().then(() => encodeQr(url)).catch(error => {
167
+ qrCache.delete(url);
168
+ throw weixinStageError('qr-encode-failed', error);
169
+ });
172
170
  qrCache.set(url, encoded);
173
171
  }
174
172
  return encoded;
@@ -193,7 +191,7 @@ export function createWeixinRpcHandler(controller, { encodeQr = qrDataUrl } = {}
193
191
  value = await withEncodedQr(started, cachedEncode);
194
192
  } else if (endpoint === WEIXIN_ENDPOINTS.pollProvisioning) {
195
193
  const current = await controller.registrationStatus(payload.attemptId);
196
- if (!current) return badRequest('The provisioning attempt no longer exists.');
194
+ if (!current) throw weixinStageError('provision-attempt-not-found', undefined, 'qr.poll');
197
195
  value = await withEncodedQr(current, cachedEncode);
198
196
  } else if (endpoint === WEIXIN_ENDPOINTS.submitVerification) {
199
197
  value = await withEncodedQr(
@@ -202,7 +200,7 @@ export function createWeixinRpcHandler(controller, { encodeQr = qrDataUrl } = {}
202
200
  );
203
201
  } else if (endpoint === WEIXIN_ENDPOINTS.cancelProvisioning) {
204
202
  value = await controller.cancelProvisioning(payload.attemptId);
205
- if (!value) return badRequest('The provisioning attempt no longer exists.');
203
+ if (!value) throw weixinStageError('provision-attempt-not-found', undefined, 'qr.cancel');
206
204
  } else if (endpoint === WEIXIN_ENDPOINTS.reconnectBot) {
207
205
  const snapshot = await controller.reconnectBot(payload.botId);
208
206
  if (signal?.aborted) return cancelled();
@@ -263,10 +261,17 @@ export function createWeixinRpcHandler(controller, { encodeQr = qrDataUrl } = {}
263
261
  }
264
262
  return signal?.aborted ? cancelled() : { ok: true, value };
265
263
  } catch (error) {
266
- const workspaceError = publicWorkspaceError(error);
267
- return signal?.aborted ? cancelled() : workspaceError
268
- ? { ok: false, error: workspaceError }
269
- : internalFailure();
264
+ if (signal?.aborted) return cancelled();
265
+ const context = {
266
+ 'connection.status': ['status.read', 'status-read-failed'],
267
+ 'provision.begin': ['qr.begin', 'qr-start-failed'],
268
+ 'provision.poll': ['qr.poll'], 'provision.verify': ['qr.verify'], 'provision.cancel': ['qr.cancel'],
269
+ 'bot.reconnect': ['connection.start', 'connection-start-failed'],
270
+ 'bot.delete': ['account.remove'], 'bot.workspace.set': ['workspace.write', 'workspace-save-failed'],
271
+ }[endpoint] ?? ['workspace.write', 'workspace-save-failed'];
272
+ return { ok: false, error: diagnostics.report(error, {
273
+ operation: endpoint, stage: context[0], code: context[1], botId: payload.botId,
274
+ }).publicError };
270
275
  }
271
276
  };
272
277
  }
@@ -852,7 +852,10 @@ export class DingtalkHarnessBridge {
852
852
  });
853
853
  return;
854
854
  }
855
+ const sessionWorkspace = typeof this.#harness.currentConversationWorkspace === 'function'
856
+ ? this.#harness.currentConversationWorkspace(key) : this.#harness.currentWorkspace?.();
855
857
  if (entry.workspace !== this.#harness.currentWorkspace?.()
858
+ || entry.sessionWorkspace !== sessionWorkspace
856
859
  || entry.sessionId !== this.#state.sessionFor(key)) {
857
860
  result = { message: t('会话或工作区已变化,菜单已刷新,请重新选择。') };
858
861
  } else if ((this.#queues.has(key) || options.pendingInteraction || this.#batchInputs.status(key).phase !== 'idle')
@@ -1029,7 +1032,7 @@ export class DingtalkHarnessBridge {
1029
1032
  if (quotedAt === null) return { unavailableReason: 'not-delivered' };
1030
1033
  const sessionId = this.#state.sessionFor(key);
1031
1034
  const session = typeof sessionId === 'string' && sessionId
1032
- ? this.#harness.workspaceSession?.(sessionId)
1035
+ ? this.#harness.workspaceSession?.(sessionId, key)
1033
1036
  : null;
1034
1037
  const text = await recoverAssistantTextByTimestamp({
1035
1038
  session,
@@ -19,20 +19,24 @@ const presetCommand = (id) => `/preset ${/^\d+$/u.test(id) ? 'id:' : ''}${id}`;
19
19
  // as a command supplied by the client or as a prompt for the model.
20
20
  export async function dingtalkMenuSnapshot(harness, state, key, signal) {
21
21
  const workspace = harness.currentWorkspace?.();
22
+ const currentSessionWorkspace = () => typeof harness.currentConversationWorkspace === 'function'
23
+ ? harness.currentConversationWorkspace(key) : harness.currentWorkspace?.();
24
+ const sessionWorkspace = currentSessionWorkspace();
22
25
  const sessionId = state.sessionFor(key);
23
26
  const options = { signal };
24
27
  const results = await Promise.allSettled([
25
28
  workspacePathSnapshot(harness, options),
26
- harness.listWorkspaceSessions?.(workspace, options),
29
+ harness.listWorkspaceSessions?.(sessionWorkspace, options),
27
30
  (async () => {
28
- const session = sessionId ? harness.workspaceSession?.(sessionId) : null;
31
+ const session = sessionId ? harness.workspaceSession?.(sessionId, key) : null;
29
32
  return typeof session?.models === 'function'
30
33
  ? session.models(options) : harness.listModels?.(options);
31
34
  })(),
32
35
  harness.agentPresetSettings?.(options),
33
36
  ]);
34
37
  signal?.throwIfAborted();
35
- if (harness.currentWorkspace?.() !== workspace || state.sessionFor(key) !== sessionId) {
38
+ if (harness.currentWorkspace?.() !== workspace
39
+ || currentSessionWorkspace() !== sessionWorkspace || state.sessionFor(key) !== sessionId) {
36
40
  throw new Error(t('会话或工作区已变化,请重新发送 /m。'));
37
41
  }
38
42
  const [paths, listed, catalog, settings] = results.map((r) => r.status === 'fulfilled' ? r.value : null);
@@ -65,7 +69,7 @@ export async function dingtalkMenuSnapshot(harness, state, key, signal) {
65
69
  }));
66
70
  data[`${name}_index`] = entries.findIndex(([, command]) => command === current[name]);
67
71
  }
68
- return { workspace, sessionId, selections, data };
72
+ return { workspace, sessionWorkspace, sessionId, selections, data };
69
73
  }
70
74
 
71
75
  export function dingtalkMenuCommand(entry, callback) {
@@ -2266,6 +2266,7 @@ export class FeishuHarnessBridge {
2266
2266
  chatId,
2267
2267
  key,
2268
2268
  messageId = null,
2269
+ conversationWorkspace,
2269
2270
  sessionWorkspace = null,
2270
2271
  sessionPage = 0,
2271
2272
  sessionLimit = null,
@@ -2513,6 +2514,10 @@ export class FeishuHarnessBridge {
2513
2514
  return;
2514
2515
  }
2515
2516
  if (action.startsWith('use:')) {
2517
+ if (conversationWorkspace !== undefined && conversationWorkspace !== this.#conversationWorkspace(key)) {
2518
+ await reply(t('这个菜单已过期,请回复 /m 重新打开。'));
2519
+ return;
2520
+ }
2516
2521
  await this.#bindSession(key, chatId, action.slice('use:'.length), { updateMessageId: messageId, replyTo: messageId });
2517
2522
  return;
2518
2523
  }
@@ -2594,7 +2599,9 @@ export class FeishuHarnessBridge {
2594
2599
  return;
2595
2600
  }
2596
2601
  // The number label sits on the session (bind) button of the row.
2597
- await this.#handleCardAction(`use:${session.sessionId}`, { chatId, key, messageId: replyTo });
2602
+ await this.#handleCardAction(`use:${session.sessionId}`, {
2603
+ chatId, key, messageId: replyTo, conversationWorkspace: menu.conversationWorkspace,
2604
+ });
2598
2605
  return;
2599
2606
  }
2600
2607
  if (menu.kind === 'workspaces') {
@@ -2624,6 +2631,12 @@ export class FeishuHarnessBridge {
2624
2631
  return sessions;
2625
2632
  }
2626
2633
 
2634
+ #conversationWorkspace(key) {
2635
+ return typeof this.#harness.currentConversationWorkspace === 'function'
2636
+ ? this.#harness.currentConversationWorkspace(key)
2637
+ : this.#harness.currentWorkspace?.();
2638
+ }
2639
+
2627
2640
  async #showSessions(
2628
2641
  { chatId, key, replyTo = null },
2629
2642
  selector,
@@ -2631,13 +2644,20 @@ export class FeishuHarnessBridge {
2631
2644
  { updateMessageId = null, limit = null } = {},
2632
2645
  ) {
2633
2646
  try {
2647
+ const conversationWorkspace = this.#conversationWorkspace(key);
2634
2648
  const signal = this.#cardDataSignal();
2635
- const resolved = await resolveSessionListWorkspace(selector ?? '', this.#harness, { signal });
2649
+ const resolved = await resolveSessionListWorkspace(selector ?? '', this.#harness, {
2650
+ signal, conversationKey: key,
2651
+ });
2636
2652
  if (resolved.error) {
2637
2653
  await this.#send(chatId, resolved.error, { replyTo });
2638
2654
  return;
2639
2655
  }
2640
2656
  const listed = await this.#harness.listWorkspaceSessions(resolved.workspace, { signal });
2657
+ if (conversationWorkspace !== undefined && conversationWorkspace !== this.#conversationWorkspace(key)) {
2658
+ await this.#send(chatId, t('这个菜单已过期,请回复 /m 重新打开。'), { replyTo });
2659
+ return;
2660
+ }
2641
2661
  const visibleSessions = this.#visibleSessions(Array.isArray(listed?.sessions) ? listed.sessions : []);
2642
2662
  const sessionLimit = Number.isSafeInteger(limit) && limit > 0 ? limit : null;
2643
2663
  const sessions = sessionLimit === null
@@ -2656,6 +2676,7 @@ export class FeishuHarnessBridge {
2656
2676
  const pageSlice = sessions.slice(safePage * MENU_PAGE_SIZE, (safePage + 1) * MENU_PAGE_SIZE);
2657
2677
  this.#rememberMenu(key, {
2658
2678
  kind: 'sessions',
2679
+ conversationWorkspace,
2659
2680
  sessions: pageSlice.map((session) => ({ ...session, watched: watchedSet.has(session.sessionId) })),
2660
2681
  });
2661
2682
  await this.#sendCard(
@@ -2665,6 +2686,9 @@ export class FeishuHarnessBridge {
2665
2686
  key,
2666
2687
  updateMessageId,
2667
2688
  replyTo,
2689
+ // The effective conversation workspace is separate from an explicit
2690
+ // list selector, which may intentionally point at another workspace.
2691
+ conversationWorkspace,
2668
2692
  // Keep the canonical selector result for later page callbacks. The
2669
2693
  // list response's workspace is display data and is not authoritative.
2670
2694
  sessionWorkspace: resolved.workspace,
@@ -2732,6 +2756,7 @@ export class FeishuHarnessBridge {
2732
2756
  this.#cardKeys.set(messageId, {
2733
2757
  key: options.key,
2734
2758
  chatId,
2759
+ conversationWorkspace: options.conversationWorkspace,
2735
2760
  sessionWorkspace: typeof options.sessionWorkspace === 'string' && options.sessionWorkspace
2736
2761
  ? options.sessionWorkspace
2737
2762
  : null,
@@ -2850,13 +2875,14 @@ export class FeishuHarnessBridge {
2850
2875
  }
2851
2876
 
2852
2877
  async #sendMenuCard(key, chatId, { updateMessageId = null, replyTo = null } = {}) {
2878
+ const conversationWorkspace = this.#conversationWorkspace(key);
2853
2879
  let currentSessionId = null;
2854
2880
  let directSessionTitle = null;
2855
2881
  try {
2856
2882
  const sessionId = this.#state.sessionFor(key);
2857
2883
  if (typeof sessionId === 'string' && sessionId) {
2858
2884
  currentSessionId = sessionId;
2859
- const session = this.#harness.workspaceSession?.(sessionId);
2885
+ const session = this.#harness.workspaceSession?.(sessionId, key);
2860
2886
  directSessionTitle = nonEmptyString(session?.title)
2861
2887
  ?? nonEmptyString(session?.name)
2862
2888
  ?? nonEmptyString(session?.displayName);
@@ -2874,12 +2900,13 @@ export class FeishuHarnessBridge {
2874
2900
  return { current, paths: current ? [current] : [] };
2875
2901
  });
2876
2902
  const sessionTask = (async () => {
2877
- const current = typeof this.#harness.currentWorkspace === 'function'
2878
- ? this.#harness.currentWorkspace()
2879
- : null;
2880
- if (!current || typeof this.#harness.listWorkspaceSessions !== 'function') return [];
2903
+ if (typeof this.#harness.listWorkspaceSessions !== 'function') return [];
2881
2904
  try {
2882
- const listed = await this.#harness.listWorkspaceSessions(current, { signal: dataSignal });
2905
+ const resolved = await resolveSessionListWorkspace('', this.#harness, {
2906
+ signal: dataSignal, conversationKey: key,
2907
+ });
2908
+ if (resolved.error) return [];
2909
+ const listed = await this.#harness.listWorkspaceSessions(resolved.workspace, { signal: dataSignal });
2883
2910
  return this.#visibleSessions(Array.isArray(listed?.sessions) ? listed.sessions : []);
2884
2911
  } catch {
2885
2912
  return [];
@@ -2896,7 +2923,7 @@ export class FeishuHarnessBridge {
2896
2923
  const modelTask = (async () => {
2897
2924
  try {
2898
2925
  if (currentSessionId) {
2899
- const session = this.#harness.workspaceSession?.(currentSessionId);
2926
+ const session = this.#harness.workspaceSession?.(currentSessionId, key);
2900
2927
  if (typeof session?.models === 'function') {
2901
2928
  return await session.models({ signal: dataSignal });
2902
2929
  }
@@ -2913,6 +2940,10 @@ export class FeishuHarnessBridge {
2913
2940
  presetTask,
2914
2941
  modelTask,
2915
2942
  ]);
2943
+ if (conversationWorkspace !== undefined && conversationWorkspace !== this.#conversationWorkspace(key)) {
2944
+ await this.#send(chatId, t('这个菜单已过期,请回复 /m 重新打开。'), { replyTo });
2945
+ return;
2946
+ }
2916
2947
  const workspaces = Array.isArray(snapshot.paths) ? snapshot.paths : [];
2917
2948
  const currentWorkspace = snapshot.current ?? null;
2918
2949
  const currentMatch = listedSessions.find((session) => session.sessionId === currentSessionId);
@@ -2944,7 +2975,7 @@ export class FeishuHarnessBridge {
2944
2975
  currentSession: currentSessionId ? { id: currentSessionId, title: currentSessionTitle } : null,
2945
2976
  sessions, archiveVisible, presetCatalog, modelCatalog,
2946
2977
  }),
2947
- { key, updateMessageId, replyTo },
2978
+ { key, updateMessageId, replyTo, conversationWorkspace },
2948
2979
  );
2949
2980
  }
2950
2981
 
@@ -2956,7 +2987,7 @@ export class FeishuHarnessBridge {
2956
2987
  async #resolveSessionTitle(key, sessionId) {
2957
2988
  try {
2958
2989
  if (typeof this.#harness.workspaceSession === 'function') {
2959
- const session = this.#harness.workspaceSession(sessionId);
2990
+ const session = this.#harness.workspaceSession(sessionId, key);
2960
2991
  if (session && typeof session === 'object') {
2961
2992
  const direct = nonEmptyString(session.title)
2962
2993
  ?? nonEmptyString(session.name)
@@ -3009,7 +3040,7 @@ export class FeishuHarnessBridge {
3009
3040
  const sessionId = this.#state?.sessionFor?.(key);
3010
3041
  let catalog;
3011
3042
  if (typeof sessionId === 'string' && sessionId) {
3012
- const session = this.#harness.workspaceSession(sessionId);
3043
+ const session = this.#harness.workspaceSession(sessionId, key);
3013
3044
  if (session?.models) {
3014
3045
  catalog = await session.models({ signal });
3015
3046
  }
@@ -3078,7 +3109,7 @@ export class FeishuHarnessBridge {
3078
3109
  try {
3079
3110
  const sessionId = this.#state?.sessionFor?.(key);
3080
3111
  if (typeof sessionId === 'string' && sessionId) {
3081
- const session = this.#harness.workspaceSession(sessionId);
3112
+ const session = this.#harness.workspaceSession(sessionId, key);
3082
3113
  if (session?.models) {
3083
3114
  const cat = await session.models({ signal });
3084
3115
  if (cat.current) info.model = `${cat.current.provider}/${cat.current.model}`;
@@ -702,7 +702,10 @@ export class QqHarnessBridge {
702
702
  }
703
703
 
704
704
  #menuContext(key) {
705
- return { workspace: this.#harness.currentWorkspace?.(), sessionId: this.#state.sessionFor(key) };
705
+ const workspace = this.#harness.currentWorkspace?.();
706
+ const sessionWorkspace = typeof this.#harness.currentConversationWorkspace === 'function'
707
+ ? this.#harness.currentConversationWorkspace(key) : workspace;
708
+ return { workspace, sessionWorkspace, sessionId: this.#state.sessionFor(key) };
706
709
  }
707
710
 
708
711
  async #showMenu(message, key, name, pageView = null) {
@@ -716,7 +719,8 @@ export class QqHarnessBridge {
716
719
  this.#signal?.throwIfAborted();
717
720
  this.#harness.assertWorkspaceScope?.();
718
721
  const current = this.#menuContext(key);
719
- if (current.workspace !== context.workspace || current.sessionId !== context.sessionId) {
722
+ if (current.workspace !== context.workspace || current.sessionWorkspace !== context.sessionWorkspace
723
+ || current.sessionId !== context.sessionId) {
720
724
  return { message: t('会话或工作区已变化,请重新发送 /m。') };
721
725
  }
722
726
  if (!this.#menus.publish(key, actor, entry, view)) return { messages: [] };
@@ -753,7 +757,9 @@ export class QqHarnessBridge {
753
757
  const execute = async () => {
754
758
  this.#signal?.throwIfAborted();
755
759
  const current = this.#menuContext(key);
756
- if (current.workspace !== choice.context.workspace || current.sessionId !== choice.context.sessionId) {
760
+ if (current.workspace !== choice.context.workspace
761
+ || current.sessionWorkspace !== choice.context.sessionWorkspace
762
+ || current.sessionId !== choice.context.sessionId) {
757
763
  return { message: t('会话或工作区已变化,请重新发送 /m。') };
758
764
  }
759
765
  // A prompt may have started while this action waited for a prior menu command.
@@ -764,7 +770,9 @@ export class QqHarnessBridge {
764
770
  if (command === '/new') return withSessionBindingLock(this.#state, key, async () => {
765
771
  if (isBusy()) return { message: t('当前任务仍在运行,请先停止任务或等待任务完成后再执行此操作。') };
766
772
  const locked = this.#menuContext(key);
767
- if (locked.workspace !== choice.context.workspace || locked.sessionId !== choice.context.sessionId) {
773
+ if (locked.workspace !== choice.context.workspace
774
+ || locked.sessionWorkspace !== choice.context.sessionWorkspace
775
+ || locked.sessionId !== choice.context.sessionId) {
768
776
  return { message: t('会话或工作区已变化,请重新发送 /m。') };
769
777
  }
770
778
  await this.#state.clearSession(key);
@@ -55,7 +55,8 @@ export class QqMenuStore {
55
55
  const entry = this.#entries.get(this.#key(route, actor));
56
56
  if (!entry || entry.used || !entry.view || entry.expiresAt <= this.#now()
57
57
  || (token && token !== entry.token)) return { error: t('这个菜单已过期,请回复 /m 重新打开。') };
58
- if (entry.workspace !== context.workspace || entry.sessionId !== context.sessionId) {
58
+ if (entry.workspace !== context.workspace || entry.sessionWorkspace !== context.sessionWorkspace
59
+ || entry.sessionId !== context.sessionId) {
59
60
  entry.used = true;
60
61
  return { error: t('会话或工作区已变化,请重新发送 /m。') };
61
62
  }
@@ -80,13 +81,15 @@ export function qqMenuPage(list, requestedPage = 0) {
80
81
  list.choices.length ? '' : t('暂无可用选项。')].filter(Boolean).join('\n'), entries, columns: 2 };
81
82
  }
82
83
 
83
- async function catalogFor(harness, sessionId, options) {
84
- const session = sessionId ? harness.workspaceSession?.(sessionId) : null;
84
+ async function catalogFor(harness, sessionId, key, options) {
85
+ const session = sessionId ? harness.workspaceSession?.(sessionId, key) : null;
85
86
  return typeof session?.models === 'function' ? session.models(options) : harness.listModels(options);
86
87
  }
87
88
 
88
89
  export async function qqMenuView(name, harness, state, key, { signal, busy = false } = {}) {
89
90
  const workspace = harness.currentWorkspace?.();
91
+ const sessionWorkspace = typeof harness.currentConversationWorkspace === 'function'
92
+ ? harness.currentConversationWorkspace(key) : workspace;
90
93
  const sessionId = state.sessionFor(key);
91
94
  const options = { signal };
92
95
  const archived = state.includesArchivedSessions?.() === true;
@@ -94,8 +97,8 @@ export async function qqMenuView(name, harness, state, key, { signal, busy = fal
94
97
  if (name === 'main' || name === 'status') {
95
98
  if (name === 'status') await harness.ensureRunning(options);
96
99
  const results = await Promise.allSettled([
97
- harness.listWorkspaceSessions?.(workspace, options),
98
- catalogFor(harness, sessionId, options),
100
+ harness.listWorkspaceSessions?.(name === 'main' ? sessionWorkspace : workspace, options),
101
+ catalogFor(harness, sessionId, key, options),
99
102
  harness.agentPresetSettings?.(options),
100
103
  ]);
101
104
  signal?.throwIfAborted();
@@ -125,8 +128,8 @@ export async function qqMenuView(name, harness, state, key, { signal, busy = fal
125
128
  ] };
126
129
  }
127
130
  if (name === 'sessions') {
128
- const listed = await harness.listWorkspaceSessions(workspace, options);
129
- return qqMenuPage({ title: t('📋 会话列表'), detail: clean(workspace), choices: visibleSessions(listed).map((item) =>
131
+ const listed = await harness.listWorkspaceSessions(sessionWorkspace, options);
132
+ return qqMenuPage({ title: t('📋 会话列表'), detail: clean(sessionWorkspace), choices: visibleSessions(listed).map((item) =>
130
133
  command(`${item.sessionId === sessionId ? '✓ ' : ''}${clean(item.title || item.sessionId, 70)}${item.archived ? t('(已归档)') : ''}`, `/session ${item.sessionId}`)) });
131
134
  }
132
135
  if (name === 'workspaces') {
@@ -135,7 +138,7 @@ export async function qqMenuView(name, harness, state, key, { signal, busy = fal
135
138
  command(`${path === workspace ? '✓ ' : ''}${clean(path)}`, `/workspace ${path}`)) });
136
139
  }
137
140
  if (name === 'models') {
138
- const catalog = await catalogFor(harness, sessionId, options);
141
+ const catalog = await catalogFor(harness, sessionId, key, options);
139
142
  return qqMenuPage({ title: t('🧠 模型'), detail: catalog.failures?.length ? t('部分模型暂不可用,可稍后重试。') : '',
140
143
  choices: catalog.groups.flatMap((group) => group.models.map((model) => command(
141
144
  `${catalog.current?.provider === group.id && catalog.current?.model === model.id ? '✓ ' : ''}${clean(group.name, 30)} · ${clean(model.name, 60)}`,