@keyou007/agent-bot 0.1.10 → 0.1.11

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 (68) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/README.md +16 -10
  3. package/README.zh.md +16 -10
  4. package/assets/agent-bot-logo.png +0 -0
  5. package/dist/acp/AcpSessionManager.js +78 -46
  6. package/dist/acp/AcpSessionManager.js.map +1 -1
  7. package/dist/codex/AppServerConnection.js +15 -1
  8. package/dist/codex/AppServerConnection.js.map +1 -1
  9. package/dist/codex/CodexLocalActivityDetector.js +4 -1
  10. package/dist/codex/CodexLocalActivityDetector.js.map +1 -1
  11. package/dist/codex/CodexProcessManager.js +1 -0
  12. package/dist/codex/CodexProcessManager.js.map +1 -1
  13. package/dist/codex/CodexRuntime.js +77 -2
  14. package/dist/codex/CodexRuntime.js.map +1 -1
  15. package/dist/commands/CommandRouter.js +64 -49
  16. package/dist/commands/CommandRouter.js.map +1 -1
  17. package/dist/console/ConsoleTurnPresenter.js +1 -1
  18. package/dist/console/ConsoleTurnPresenter.js.map +1 -1
  19. package/dist/feishu/CardRenderer.js +345 -11
  20. package/dist/feishu/CardRenderer.js.map +1 -1
  21. package/dist/feishu/FeishuMessageClient.js +51 -5
  22. package/dist/feishu/FeishuMessageClient.js.map +1 -1
  23. package/dist/feishu/FeishuTurnPresenter.js +8 -3
  24. package/dist/feishu/FeishuTurnPresenter.js.map +1 -1
  25. package/dist/index.js +18 -17
  26. package/dist/index.js.map +1 -1
  27. package/dist/proxy/ProxySessionController.js +589 -205
  28. package/dist/proxy/ProxySessionController.js.map +1 -1
  29. package/dist/runtime/AcpRuntimeAdapter.js +1 -0
  30. package/dist/runtime/AcpRuntimeAdapter.js.map +1 -1
  31. package/dist/runtime/AgentRuntimeRegistry.js +7 -4
  32. package/dist/runtime/AgentRuntimeRegistry.js.map +1 -1
  33. package/dist/runtime/createAgentRuntimeRegistry.js +22 -0
  34. package/dist/runtime/createAgentRuntimeRegistry.js.map +1 -0
  35. package/dist/startup/SessionMetadataHydrator.js +2 -2
  36. package/dist/startup/SessionMetadataHydrator.js.map +1 -1
  37. package/dist/startup/StartupNotifier.js +1 -0
  38. package/dist/startup/StartupNotifier.js.map +1 -1
  39. package/dist/state/StateStore.js +21 -15
  40. package/dist/state/StateStore.js.map +1 -1
  41. package/dist/supervision/SafeRestartScheduler.js +5 -0
  42. package/dist/supervision/SafeRestartScheduler.js.map +1 -1
  43. package/docs/technical-reference.md +15 -5
  44. package/docs/technical-reference.zh.md +15 -5
  45. package/npm-shrinkwrap.json +2 -2
  46. package/package.json +2 -1
  47. package/skills/agent-bot/SKILL.md +13 -3
  48. package/src/acp/AcpSessionManager.ts +73 -54
  49. package/src/codex/AppServerConnection.ts +21 -1
  50. package/src/codex/CodexLocalActivityDetector.ts +8 -1
  51. package/src/codex/CodexProcessManager.ts +1 -0
  52. package/src/codex/CodexRuntime.ts +91 -2
  53. package/src/commands/CommandRouter.ts +71 -49
  54. package/src/commands/commandTypes.ts +6 -11
  55. package/src/console/ConsoleTurnPresenter.ts +1 -1
  56. package/src/feishu/CardRenderer.ts +418 -11
  57. package/src/feishu/FeishuMessageClient.ts +66 -3
  58. package/src/feishu/FeishuTurnPresenter.ts +7 -2
  59. package/src/index.ts +21 -22
  60. package/src/proxy/ProxySessionController.ts +706 -208
  61. package/src/runtime/AcpRuntimeAdapter.ts +1 -0
  62. package/src/runtime/AgentRuntimeRegistry.ts +7 -6
  63. package/src/runtime/createAgentRuntimeRegistry.ts +25 -0
  64. package/src/runtime/types.ts +18 -0
  65. package/src/startup/SessionMetadataHydrator.ts +2 -2
  66. package/src/startup/StartupNotifier.ts +1 -0
  67. package/src/state/StateStore.ts +29 -15
  68. package/src/supervision/SafeRestartScheduler.ts +5 -0
@@ -1,9 +1,10 @@
1
1
  import path from "node:path";
2
2
  import os from "node:os";
3
3
  import { createProjectlessWorkspace, detectProjectlessWorkspace } from "../codex/ProjectlessWorkspace.js";
4
+ import { resolveUserPath } from "../config/paths.js";
4
5
  import { CommandRouter } from "../commands/CommandRouter.js";
5
6
  import { baseChatContextKey, isThreadContextKey } from "../feishu/contextKey.js";
6
- import { CardRenderer } from "../feishu/CardRenderer.js";
7
+ import { CardRenderer, } from "../feishu/CardRenderer.js";
7
8
  import { generateGroupAvatarPng, resolveGroupAvatarProjectName } from "../feishu/GroupAvatarGenerator.js";
8
9
  import { createId } from "../utils/id.js";
9
10
  import { asInlineCode, codeBlock, truncateMiddle, truncateText } from "../utils/markdown.js";
@@ -14,6 +15,7 @@ const MESSAGE_COMPLETED_REACTION = "DONE";
14
15
  const MESSAGE_FAILED_REACTION = "ERROR";
15
16
  const MESSAGE_CANCELLED_REACTION = "CrossMark";
16
17
  const SESSION_PAGE_SIZE = 5;
18
+ const REMOTE_SESSION_REFERENCE_PREFIX = "agent-runtime:";
17
19
  export class ProxySessionController {
18
20
  config;
19
21
  store;
@@ -46,8 +48,8 @@ export class ProxySessionController {
46
48
  if (interruptedAcpSessions.length > 0) {
47
49
  this.logger.warn({ sessionIds: interruptedAcpSessions.map((session) => session.localSessionId) }, "Marked interrupted ACP sessions as failed after process restart.");
48
50
  }
49
- for (const kind of ["acp", "codex"]) {
50
- this.unsubscribe.push(this.runtimes.get(kind).onEvent((event) => {
51
+ for (const [, runtime] of this.runtimes.entries()) {
52
+ this.unsubscribe.push(runtime.onEvent((event) => {
51
53
  void this.handleRuntimeEvent(event).catch((error) => {
52
54
  this.logger.warn({ error, event }, "Failed to present runtime event.");
53
55
  });
@@ -102,7 +104,7 @@ export class ProxySessionController {
102
104
  let command;
103
105
  try {
104
106
  command = imageCount > 0 && !message.text.trimStart().startsWith("/")
105
- ? { type: "prompt", text: message.text.trim() || "请分析这张图片。" }
107
+ ? { type: "prompt", text: message.text.trim() || "请查看这张图片" }
106
108
  : this.router.parse(message.text);
107
109
  }
108
110
  catch (error) {
@@ -169,7 +171,7 @@ export class ProxySessionController {
169
171
  else if (kind === "activity_history") {
170
172
  const requestedPage = String(scopedAction.value.page ?? "0");
171
173
  const numericPage = Number(requestedPage);
172
- await this.outbound.showActivityPage(contextKey, String(scopedAction.value.turnId ?? ""), requestedPage === "latest" ? "latest" : Number.isFinite(numericPage) ? numericPage : 0, scopedAction.messageId);
174
+ await this.outbound.showActivityPage(contextKey, String(scopedAction.value.turnId ?? ""), requestedPage === "latest" ? "latest" : Number.isFinite(numericPage) ? numericPage : 0, requiredCardMessageId(scopedAction.messageId));
173
175
  }
174
176
  else if (kind === "turn_cancel") {
175
177
  const sessionId = String(scopedAction.value.sessionId ?? "");
@@ -236,6 +238,43 @@ export class ProxySessionController {
236
238
  else if (kind === "session_status_refresh") {
237
239
  await this.refreshStatusCardFromAction(scopedAction);
238
240
  }
241
+ else if (kind === "settings_tab_open") {
242
+ await this.openExecutionSettings(contextKey, executionSettingsTabValue(scopedAction.value.tab), {
243
+ sessionId: String(scopedAction.value.sessionId ?? ""),
244
+ updateMessageId: scopedAction.messageId,
245
+ });
246
+ }
247
+ else if (kind === "settings_agent_select") {
248
+ await this.setDefaultAgent(contextKey, String(scopedAction.value.agent ?? ""), {
249
+ sessionId: String(scopedAction.value.sessionId ?? ""),
250
+ updateMessageId: scopedAction.messageId,
251
+ });
252
+ }
253
+ else if (kind === "settings_provider_select") {
254
+ await this.selectProvider(contextKey, String(scopedAction.value.provider ?? ""), {
255
+ sessionId: String(scopedAction.value.sessionId ?? ""),
256
+ updateMessageId: scopedAction.messageId,
257
+ });
258
+ }
259
+ else if (kind === "settings_model_select") {
260
+ await this.model(contextKey, String(scopedAction.value.model ?? ""), {
261
+ sessionId: String(scopedAction.value.sessionId ?? ""),
262
+ updateMessageId: scopedAction.messageId,
263
+ });
264
+ }
265
+ else if (kind === "settings_thinking_select") {
266
+ await this.thinking(contextKey, String(scopedAction.value.effort ?? ""), {
267
+ sessionId: String(scopedAction.value.sessionId ?? ""),
268
+ updateMessageId: scopedAction.messageId,
269
+ expectedModel: String(scopedAction.value.model ?? ""),
270
+ });
271
+ }
272
+ else if (kind === "settings_permission_select") {
273
+ await this.permissions(contextKey, permissionModeValue(scopedAction.value.permissionMode), {
274
+ sessionId: String(scopedAction.value.sessionId ?? ""),
275
+ updateMessageId: scopedAction.messageId,
276
+ });
277
+ }
239
278
  else if (kind === "model_select") {
240
279
  const sessionId = String(scopedAction.value.sessionId ?? "");
241
280
  const model = String(scopedAction.value.model ?? "");
@@ -261,6 +300,32 @@ export class ProxySessionController {
261
300
  expectedModel: model,
262
301
  });
263
302
  }
303
+ else if (kind === "provider_open") {
304
+ await this.openExecutionSettings(contextKey, "provider", {
305
+ sessionId: String(scopedAction.value.sessionId ?? ""),
306
+ updateMessageId: scopedAction.messageId,
307
+ });
308
+ }
309
+ else if (kind === "provider_select" || kind === "provider_model_open") {
310
+ await this.openProviderModelSelector(contextKey, String(scopedAction.value.provider ?? ""), permissionModeValue(scopedAction.value.permissionMode), String(scopedAction.value.sessionId ?? ""), requiredCardMessageId(scopedAction.messageId));
311
+ }
312
+ else if (kind === "provider_model_select" || kind === "provider_reasoning_open") {
313
+ await this.openProviderReasoningSelector(contextKey, String(scopedAction.value.provider ?? ""), String(scopedAction.value.model ?? ""), permissionModeValue(scopedAction.value.permissionMode), String(scopedAction.value.sessionId ?? ""), requiredCardMessageId(scopedAction.messageId));
314
+ }
315
+ else if (kind === "provider_reasoning_select") {
316
+ await this.openProviderPermissionSelector(contextKey, String(scopedAction.value.provider ?? ""), String(scopedAction.value.model ?? ""), String(scopedAction.value.effort ?? ""), permissionModeValue(scopedAction.value.permissionMode), String(scopedAction.value.sessionId ?? ""), requiredCardMessageId(scopedAction.messageId));
317
+ }
318
+ else if (kind === "provider_permission_select") {
319
+ await this.applyProviderSettings(contextKey, {
320
+ provider: String(scopedAction.value.provider ?? ""),
321
+ model: String(scopedAction.value.model ?? ""),
322
+ effort: String(scopedAction.value.effort ?? ""),
323
+ mode: permissionModeValue(scopedAction.value.permissionMode),
324
+ }, {
325
+ sessionId: String(scopedAction.value.sessionId ?? ""),
326
+ updateMessageId: scopedAction.messageId,
327
+ });
328
+ }
264
329
  else if (kind === "approval") {
265
330
  await this.resolveApproval(scopedAction);
266
331
  }
@@ -304,7 +369,7 @@ export class ProxySessionController {
304
369
  throw new Error(`Task not found: ${localSessionId}`);
305
370
  let remote;
306
371
  if (record.remoteSessionId) {
307
- const runtime = this.runtimes.forAgent(this.ensureAgent(record.agentName));
372
+ const runtime = this.runtimes.forAgent(record.agentName);
308
373
  if (runtime.readRemoteSession) {
309
374
  try {
310
375
  remote = await runtime.readRemoteSession(record.remoteSessionId);
@@ -348,7 +413,7 @@ export class ProxySessionController {
348
413
  const record = this.store.getSession(localSessionId);
349
414
  if (!record || record.status === "closed")
350
415
  throw new Error(`Task not found: ${localSessionId}`);
351
- const runtime = this.runtimes.forAgent(this.ensureAgent(record.agentName));
416
+ const runtime = this.runtimes.forAgent(record.agentName);
352
417
  const activeTurnId = runtime.getSession(localSessionId)?.activeTurnId;
353
418
  const routedContextKey = this.outbound.getSessionContextKey(localSessionId);
354
419
  const responseContextKey = (activeTurnId ? this.store.getTurnContextKey(activeTurnId) ?? routedContextKey : undefined)
@@ -402,7 +467,7 @@ export class ProxySessionController {
402
467
  return explicit;
403
468
  const sessionReference = typeof action.value.sessionId === "string" ? action.value.sessionId : undefined;
404
469
  const session = sessionReference
405
- ? this.store.getSession(sessionReference) ?? this.store.findSessionByRemoteSessionId(sessionReference)
470
+ ? this.store.getSession(sessionReference) ?? this.findStoredSessionByReference(sessionReference)
406
471
  : undefined;
407
472
  if (session && baseChatContextKey(session.contextKey) === baseChatContextKey(action.contextKey)) {
408
473
  return session.contextKey;
@@ -424,10 +489,17 @@ export class ProxySessionController {
424
489
  if (command.projectless && this.ensureAgent(context.defaultAgent).kind !== "codex") {
425
490
  throw new Error("/new --nodir 仅支持 Codex Agent。");
426
491
  }
427
- await this.createSession(contextKey, context.defaultAgent, command.projectless ? undefined : command.cwd ?? this.inheritedNewTaskCwd(contextKey), true, false, undefined, undefined, command.title);
492
+ await this.createSession(contextKey, context.defaultAgent, command.projectless
493
+ ? undefined
494
+ : command.cwd === undefined
495
+ ? this.inheritedNewTaskCwd(contextKey)
496
+ : resolveUserPath(command.cwd), true, false, undefined, undefined, command.title, this.inheritedExecutionSettings(contextKey, context.defaultAgent));
428
497
  return;
429
498
  case "newgroup":
430
- await this.createFeishuGroup(contextKey, context.defaultAgent, command.title, userId);
499
+ if (command.projectless && this.ensureAgent(context.defaultAgent).kind !== "codex") {
500
+ throw new Error("/newgroup --nodir 仅支持 Codex Agent。");
501
+ }
502
+ await this.createFeishuGroup(contextKey, context.defaultAgent, command.title, userId, command.cwd === undefined ? undefined : resolveUserPath(command.cwd), command.projectless === true);
431
503
  return;
432
504
  case "forkgroup":
433
505
  await this.forkCurrentSessionToFeishuGroup(contextKey, command.title, userId, incomingMessage);
@@ -438,7 +510,6 @@ export class ProxySessionController {
438
510
  case "title":
439
511
  await this.setTitle(contextKey, command.title);
440
512
  return;
441
- case "ask":
442
513
  case "prompt":
443
514
  await this.prompt(contextKey, command.text, messageId, replyTarget, localImagePaths);
444
515
  return;
@@ -455,11 +526,7 @@ export class ProxySessionController {
455
526
  if (command.agent)
456
527
  await this.setDefaultAgent(contextKey, command.agent);
457
528
  else
458
- await this.listAgents(contextKey);
459
- return;
460
- case "use":
461
- await this.setDefaultAgent(contextKey, command.agent);
462
- await this.createSession(contextKey, command.agent, command.cwd, true, false);
529
+ await this.openAgentSettings(contextKey);
463
530
  return;
464
531
  case "status":
465
532
  await this.status(contextKey, command.sessionId);
@@ -470,20 +537,19 @@ export class ProxySessionController {
470
537
  case "restart":
471
538
  if (!this.lifecycle)
472
539
  throw new Error("当前运行方式不支持自动重启。");
473
- await this.lifecycle.restart(contextKey);
540
+ await this.lifecycle.restart(contextKey, command.force === true);
474
541
  return;
475
542
  case "model":
476
- await this.model(contextKey, command.model);
543
+ await this.openExecutionSettings(contextKey, "model");
544
+ return;
545
+ case "provider":
546
+ await this.openProviderSettings(contextKey);
477
547
  return;
478
548
  case "thinking":
479
- await this.thinking(contextKey, command.effort);
549
+ await this.openExecutionSettings(contextKey, "thinking");
480
550
  return;
481
551
  case "permissions":
482
- await this.permissions(contextKey, command.mode);
483
- return;
484
- case "modes":
485
- case "mode":
486
- await this.outbound.sendText(contextKey, "当前统一运行时不再暴露 ACP modes;Codex 请使用 /model 和 /permissions。");
552
+ await this.openExecutionSettings(contextKey, "permission");
487
553
  return;
488
554
  case "help":
489
555
  await this.help(contextKey);
@@ -546,7 +612,7 @@ export class ProxySessionController {
546
612
  await this.promptSession(record, contextKey, text, messageId, replyTarget, localImagePaths);
547
613
  }
548
614
  async promptSession(record, contextKey, text, messageId, replyTarget, localImagePaths) {
549
- const configuredRuntime = this.runtimes.forAgent(this.ensureAgent(record.agentName));
615
+ const configuredRuntime = this.runtimes.forAgent(record.agentName);
550
616
  if (!configuredRuntime.getSession(record.localSessionId)) {
551
617
  this.outbound.registerSession(record.localSessionId, contextKey, record.title, record.cwd);
552
618
  await this.outbound.startPendingTurn(record.localSessionId, contextKey, record.title, replyTarget, text);
@@ -760,7 +826,7 @@ export class ProxySessionController {
760
826
  async forkThreadSession(message) {
761
827
  const { anchor, source, snapshot } = this.resolveThreadForkAnchor(message);
762
828
  const agent = this.ensureAgent(source.agentName);
763
- const runtime = this.runtimes.forAgent(agent);
829
+ const runtime = this.runtimes.forAgent(source.agentName);
764
830
  if (runtime.kind !== "codex" || !runtime.forkSession) {
765
831
  throw new Error("当前 Codex 运行时不支持 fork 任务。");
766
832
  }
@@ -779,6 +845,7 @@ export class ProxySessionController {
779
845
  this.store.updateRuntimeSession(localSessionId, {
780
846
  runtimeKind: "codex",
781
847
  title: forkTitle,
848
+ modelProvider: source.modelProvider,
782
849
  model: source.model,
783
850
  reasoningEffort: source.reasoningEffort,
784
851
  permissionMode: source.permissionMode ?? "auto",
@@ -793,6 +860,7 @@ export class ProxySessionController {
793
860
  agentName: source.agentName,
794
861
  cwd: source.cwd,
795
862
  title: forkTitle,
863
+ modelProvider: source.modelProvider,
796
864
  model: source.model,
797
865
  reasoningEffort: source.reasoningEffort,
798
866
  permissionMode: source.permissionMode ?? "auto",
@@ -869,7 +937,7 @@ export class ProxySessionController {
869
937
  if (direct?.status === "closed") {
870
938
  throw new Error(`找不到任务:${taskId}`);
871
939
  }
872
- const global = direct ?? this.store.findSessionByRemoteSessionId(taskId);
940
+ const global = direct ?? this.findStoredSessionByReference(taskId);
873
941
  source = global ? { ...global, contextKey } : undefined;
874
942
  }
875
943
  if (source && (!source.remoteSessionId || !this.isCodexSession(source))) {
@@ -877,27 +945,29 @@ export class ProxySessionController {
877
945
  }
878
946
  let runtime;
879
947
  let agentName;
948
+ let remote;
880
949
  if (source) {
881
950
  agentName = source.agentName;
882
- runtime = this.runtimes.forAgent(this.ensureAgent(agentName));
951
+ runtime = this.runtimes.forAgent(agentName);
883
952
  }
884
953
  else {
885
- const agentEntry = Object.entries(this.config.agents).find(([, agent]) => agent.kind === "codex");
886
- if (!agentEntry)
887
- throw new Error("未配置 Codex Agent。");
888
- [agentName] = agentEntry;
889
- runtime = this.runtimes.get("codex");
954
+ if (!taskId)
955
+ throw new Error("缺少要 fork 的 Codex 任务 ID。");
956
+ const resolved = await this.resolveRemoteCodexSession(taskId);
957
+ agentName = resolved.agentName;
958
+ runtime = resolved.runtime;
959
+ remote = resolved.remote;
890
960
  }
891
961
  if (runtime.kind !== "codex" || !runtime.forkSession) {
892
962
  throw new Error("当前 Codex 运行时不支持 fork 任务。");
893
963
  }
894
- const remoteSessionId = source?.remoteSessionId ?? taskId;
964
+ const remoteSessionId = source?.remoteSessionId ?? remote?.id ?? taskId;
895
965
  if (!remoteSessionId)
896
966
  throw new Error("当前任务尚未创建 Codex 任务 ID,暂时不能 fork。");
897
967
  if (!runtime.readRemoteSession && !source) {
898
968
  throw new Error("当前 Codex 运行时不支持读取指定任务。");
899
969
  }
900
- const remote = runtime.readRemoteSession
970
+ remote ??= runtime.readRemoteSession
901
971
  ? await runtime.readRemoteSession(remoteSessionId)
902
972
  : undefined;
903
973
  const latestTurnId = remote?.lastTurnId ?? source?.lastTurnId;
@@ -921,7 +991,8 @@ export class ProxySessionController {
921
991
  throw new Error("指定的 Codex 任务没有可用的工作目录,暂时不能 fork。");
922
992
  const sourceTitle = remote?.title ?? source?.title ?? remote?.preview;
923
993
  const forkTitle = normalizeTaskTitle(requestedTitle) ?? this.store.nextForkTitle(sourceTitle);
924
- const model = source?.model;
994
+ const modelProvider = remote?.modelProvider ?? source?.modelProvider;
995
+ const model = remote?.model ?? source?.model;
925
996
  const reasoningEffort = source?.reasoningEffort;
926
997
  const permissionMode = source?.permissionMode ?? "auto";
927
998
  return {
@@ -933,6 +1004,7 @@ export class ProxySessionController {
933
1004
  lastTurnId,
934
1005
  cwd,
935
1006
  forkTitle,
1007
+ modelProvider,
936
1008
  model,
937
1009
  reasoningEffort,
938
1010
  permissionMode,
@@ -975,7 +1047,7 @@ export class ProxySessionController {
975
1047
  if (!source.remoteSessionId || !this.isCodexSession(source)) {
976
1048
  throw new Error(`${sourceLabel}不是可 fork 的 Codex 任务。`);
977
1049
  }
978
- const runtime = this.runtimes.forAgent(this.ensureAgent(source.agentName));
1050
+ const runtime = this.runtimes.forAgent(source.agentName);
979
1051
  if (runtime.kind !== "codex" || !runtime.forkSession) {
980
1052
  throw new Error("当前 Codex 运行时不支持 fork 任务。");
981
1053
  }
@@ -996,6 +1068,7 @@ export class ProxySessionController {
996
1068
  lastTurnId,
997
1069
  cwd: source.cwd,
998
1070
  forkTitle,
1071
+ modelProvider: source.modelProvider,
999
1072
  model: source.model,
1000
1073
  reasoningEffort: source.reasoningEffort,
1001
1074
  permissionMode: source.permissionMode ?? "auto",
@@ -1018,6 +1091,7 @@ export class ProxySessionController {
1018
1091
  this.store.updateRuntimeSession(localSessionId, {
1019
1092
  runtimeKind: "codex",
1020
1093
  title: plan.forkTitle,
1094
+ modelProvider: plan.modelProvider,
1021
1095
  model: plan.model,
1022
1096
  reasoningEffort: plan.reasoningEffort,
1023
1097
  permissionMode: plan.permissionMode,
@@ -1030,6 +1104,7 @@ export class ProxySessionController {
1030
1104
  agentName: plan.agentName,
1031
1105
  cwd: plan.cwd,
1032
1106
  title: plan.forkTitle,
1107
+ modelProvider: plan.modelProvider,
1033
1108
  model: plan.model,
1034
1109
  reasoningEffort: plan.reasoningEffort,
1035
1110
  permissionMode: plan.permissionMode,
@@ -1077,26 +1152,25 @@ export class ProxySessionController {
1077
1152
  if (direct?.status === "closed") {
1078
1153
  throw new Error(`找不到任务:${taskId}`);
1079
1154
  }
1080
- const source = direct ?? this.store.findSessionByRemoteSessionId(taskId);
1155
+ const source = direct ?? this.findStoredSessionByReference(taskId);
1081
1156
  if (source && !this.isCodexSession(source)) {
1082
1157
  throw new Error("指定任务不是 Codex 任务,暂时不能按项目创建新任务。");
1083
1158
  }
1084
1159
  let agentName;
1085
1160
  let runtime;
1161
+ let remote;
1086
1162
  if (source) {
1087
1163
  agentName = source.agentName;
1088
- runtime = this.runtimes.forAgent(this.ensureAgent(agentName));
1164
+ runtime = this.runtimes.forAgent(agentName);
1089
1165
  }
1090
1166
  else {
1091
- const agentEntry = Object.entries(this.config.agents).find(([, agent]) => agent.kind === "codex");
1092
- if (!agentEntry)
1093
- throw new Error("未配置 Codex Agent。");
1094
- [agentName] = agentEntry;
1095
- runtime = this.runtimes.get("codex");
1167
+ const resolved = await this.resolveRemoteCodexSession(taskId);
1168
+ agentName = resolved.agentName;
1169
+ runtime = resolved.runtime;
1170
+ remote = resolved.remote;
1096
1171
  }
1097
- const remoteSessionId = source?.remoteSessionId ?? taskId;
1098
- let remote;
1099
- if (runtime.readRemoteSession && remoteSessionId) {
1172
+ const remoteSessionId = source?.remoteSessionId ?? remote?.id ?? taskId;
1173
+ if (!remote && runtime.readRemoteSession && remoteSessionId) {
1100
1174
  try {
1101
1175
  remote = await runtime.readRemoteSession(remoteSessionId);
1102
1176
  }
@@ -1111,6 +1185,7 @@ export class ProxySessionController {
1111
1185
  throw new Error("指定的 Codex 任务没有可用的工作目录,暂时不能按项目创建新任务。");
1112
1186
  }
1113
1187
  const executionSettings = {
1188
+ modelProvider: remote?.modelProvider ?? source?.modelProvider,
1114
1189
  model: remote?.model ?? source?.model,
1115
1190
  reasoningEffort: remote?.reasoningEffort ?? source?.reasoningEffort,
1116
1191
  permissionMode: remote?.permissionMode ?? source?.permissionMode ?? "auto",
@@ -1156,9 +1231,10 @@ export class ProxySessionController {
1156
1231
  ? createProjectlessWorkspace({ prompt: initialTitle }).cwd
1157
1232
  : path.resolve(cwd ?? this.config.defaults.cwd);
1158
1233
  const record = this.store.createSession({ localSessionId, contextKey, agentName, cwd: sessionCwd, status: "starting" });
1159
- if (initialTitle || executionSettings.model || executionSettings.reasoningEffort || executionSettings.permissionMode) {
1234
+ if (initialTitle || executionSettings.modelProvider || executionSettings.model || executionSettings.reasoningEffort || executionSettings.permissionMode) {
1160
1235
  this.store.updateRuntimeSession(localSessionId, {
1161
1236
  title: initialTitle,
1237
+ modelProvider: executionSettings.modelProvider,
1162
1238
  model: executionSettings.model,
1163
1239
  reasoningEffort: executionSettings.reasoningEffort,
1164
1240
  permissionMode: executionSettings.permissionMode,
@@ -1166,7 +1242,7 @@ export class ProxySessionController {
1166
1242
  }
1167
1243
  this.store.setCurrentSession(contextKey, localSessionId);
1168
1244
  this.outbound.registerSession(localSessionId, contextKey, initialTitle, sessionCwd);
1169
- const runtime = this.runtimes.forAgent(agent);
1245
+ const runtime = this.runtimes.forAgent(agentName);
1170
1246
  try {
1171
1247
  if (prepareTurn) {
1172
1248
  await this.outbound.startPendingTurn(localSessionId, contextKey, initialTitle, replyTarget, prompt);
@@ -1176,6 +1252,7 @@ export class ProxySessionController {
1176
1252
  agentName,
1177
1253
  cwd: sessionCwd,
1178
1254
  title: initialTitle,
1255
+ modelProvider: executionSettings.modelProvider,
1179
1256
  model: executionSettings.model,
1180
1257
  reasoningEffort: executionSettings.reasoningEffort,
1181
1258
  permissionMode: executionSettings.permissionMode ?? "auto",
@@ -1196,11 +1273,14 @@ export class ProxySessionController {
1196
1273
  throw error;
1197
1274
  }
1198
1275
  }
1199
- async createFeishuGroup(sourceContextKey, agentName, requestedTitle, userId) {
1276
+ async createFeishuGroup(sourceContextKey, agentName, requestedTitle, userId, requestedProjectCwd, forceProjectless = false) {
1200
1277
  const source = this.currentSession(sourceContextKey);
1201
- const boundProjectCwd = this.currentProjectCwd(sourceContextKey);
1278
+ const boundProjectCwd = forceProjectless
1279
+ ? undefined
1280
+ : requestedProjectCwd ?? this.currentProjectCwd(sourceContextKey);
1202
1281
  const executionSettings = source?.agentName === agentName
1203
1282
  ? {
1283
+ modelProvider: source.modelProvider,
1204
1284
  model: source.model,
1205
1285
  reasoningEffort: source.reasoningEffort,
1206
1286
  permissionMode: source.permissionMode,
@@ -1232,8 +1312,10 @@ export class ProxySessionController {
1232
1312
  "群和新任务已创建。",
1233
1313
  `当前任务:${taskDescription}`,
1234
1314
  `当前 Project 目录:${boundProjectCwd ?? "未绑定(Projectless)"}`,
1315
+ `当前 Provider:${task.modelProvider ?? "Codex 默认"}`,
1235
1316
  `当前模型:${task.model ?? "默认"}`,
1236
1317
  `思考强度:${task.reasoningEffort ?? "自动"}`,
1318
+ `权限类型:${task.permissionMode === "confirm" ? "执行前确认" : "自动执行"}`,
1237
1319
  ].join("\n"));
1238
1320
  await this.outbound.sendText(sourceContextKey, `已创建飞书群:${group.name},并创建新任务 ${taskDescription}。`);
1239
1321
  }
@@ -1275,6 +1357,7 @@ export class ProxySessionController {
1275
1357
  `已从${prepared.sourceDescription}创建分支。`,
1276
1358
  `当前任务:${taskDescription}`,
1277
1359
  `当前 Project 目录:${boundProjectCwd ?? "未绑定(Projectless)"}`,
1360
+ `当前 Provider:${forked.session.modelProvider ?? "Codex 默认"}`,
1278
1361
  `当前模型:${forked.session.model ?? "默认"}`,
1279
1362
  `思考强度:${forked.session.reasoningEffort ?? "自动"}`,
1280
1363
  `权限类型:${forked.session.permissionMode === "confirm" ? "执行前确认" : "自动执行"}`,
@@ -1300,7 +1383,7 @@ export class ProxySessionController {
1300
1383
  }
1301
1384
  async loadSession(record) {
1302
1385
  const agent = this.ensureAgent(record.agentName);
1303
- const runtime = this.runtimes.forAgent(agent);
1386
+ const runtime = this.runtimes.forAgent(record.agentName);
1304
1387
  const existing = runtime.getSession(record.localSessionId);
1305
1388
  if (existing)
1306
1389
  return { record, runtime, session: existing };
@@ -1329,6 +1412,7 @@ export class ProxySessionController {
1329
1412
  agentName: record.agentName,
1330
1413
  cwd: record.cwd,
1331
1414
  title: record.title,
1415
+ modelProvider: record.modelProvider,
1332
1416
  model: record.model,
1333
1417
  reasoningEffort: record.reasoningEffort,
1334
1418
  permissionMode,
@@ -1348,6 +1432,7 @@ export class ProxySessionController {
1348
1432
  agentName: record.agentName,
1349
1433
  cwd: record.cwd,
1350
1434
  title: record.title,
1435
+ modelProvider: record.modelProvider,
1351
1436
  model: record.model,
1352
1437
  reasoningEffort: record.reasoningEffort,
1353
1438
  permissionMode,
@@ -1360,6 +1445,7 @@ export class ProxySessionController {
1360
1445
  agentName: record.agentName,
1361
1446
  cwd: record.cwd,
1362
1447
  title: record.title,
1448
+ modelProvider: record.modelProvider,
1363
1449
  model: record.model,
1364
1450
  reasoningEffort: record.reasoningEffort,
1365
1451
  permissionMode,
@@ -1382,6 +1468,7 @@ export class ProxySessionController {
1382
1468
  runtimeKind: session.runtimeKind,
1383
1469
  remoteSessionId: session.remoteSessionId,
1384
1470
  title: session.title,
1471
+ modelProvider: session.modelProvider,
1385
1472
  model: session.model,
1386
1473
  reasoningEffort: session.reasoningEffort,
1387
1474
  permissionMode: session.permissionMode,
@@ -1407,38 +1494,36 @@ export class ProxySessionController {
1407
1494
  lastTurnStatus: event.type === "turn_completed" ? "completed" : event.type === "turn_cancelled" ? "cancelled" : "failed",
1408
1495
  });
1409
1496
  }
1497
+ let presentationError;
1410
1498
  try {
1411
1499
  await this.outbound.onEvent(event);
1412
- if (event.type === "turn_completed" || event.type === "turn_cancelled" || event.type === "turn_failed") {
1413
- const terminalStatus = event.type === "turn_completed"
1414
- ? "completed"
1415
- : event.type === "turn_cancelled"
1416
- ? "cancelled"
1417
- : "failed";
1418
- const latest = this.store.getSession(event.sessionId);
1419
- if (latest?.lastTurnId === event.turnId && latest.lastTurnStatus === terminalStatus) {
1420
- await this.finalizeTurnMessageReactions(event.turnId, terminalStatus);
1421
- }
1422
- }
1423
1500
  }
1424
- finally {
1425
- if (event.type === "turn_completed" || event.type === "turn_cancelled" || event.type === "turn_failed") {
1426
- const latest = this.store.getSession(event.sessionId);
1427
- const agent = latest ? this.ensureAgent(latest.agentName) : undefined;
1428
- const activeTurnId = agent ? this.runtimes.forAgent(agent).getSession(event.sessionId)?.activeTurnId : undefined;
1429
- if (latest?.lastTurnId === event.turnId && !activeTurnId) {
1430
- this.store.updateSession(event.sessionId, { status: event.type === "turn_failed" ? "failed" : "ready" });
1431
- }
1432
- }
1501
+ catch (error) {
1502
+ presentationError = error;
1433
1503
  }
1434
1504
  if (event.type === "turn_completed" || event.type === "turn_cancelled" || event.type === "turn_failed") {
1505
+ const terminalStatus = event.type === "turn_completed"
1506
+ ? "completed"
1507
+ : event.type === "turn_cancelled"
1508
+ ? "cancelled"
1509
+ : "failed";
1510
+ const reactionSession = this.store.getSession(event.sessionId);
1511
+ if (!presentationError
1512
+ && reactionSession?.lastTurnId === event.turnId
1513
+ && reactionSession.lastTurnStatus === terminalStatus) {
1514
+ await this.finalizeTurnMessageReactions(event.turnId, terminalStatus);
1515
+ }
1435
1516
  const latest = this.store.getSession(event.sessionId);
1436
- const agent = latest ? this.ensureAgent(latest.agentName) : undefined;
1437
- const activeTurnId = agent ? this.runtimes.forAgent(agent).getSession(event.sessionId)?.activeTurnId : undefined;
1438
- if (latest?.lastTurnId !== event.turnId || activeTurnId)
1439
- return;
1440
- await this.scheduleNextQueuedPrompt(event.sessionId);
1517
+ const activeTurnId = latest
1518
+ ? this.runtimes.forAgent(latest.agentName).getSession(event.sessionId)?.activeTurnId
1519
+ : undefined;
1520
+ if (latest?.lastTurnId === event.turnId && !activeTurnId) {
1521
+ this.store.updateSession(event.sessionId, { status: event.type === "turn_failed" ? "failed" : "ready" });
1522
+ await this.scheduleNextQueuedPrompt(event.sessionId);
1523
+ }
1441
1524
  }
1525
+ if (presentationError)
1526
+ throw presentationError;
1442
1527
  }
1443
1528
  scheduleNextQueuedPrompt(sessionId) {
1444
1529
  const previous = this.queuedPromptStarts.get(sessionId) ?? Promise.resolve();
@@ -1533,7 +1618,7 @@ export class ProxySessionController {
1533
1618
  await this.cancelSession(record);
1534
1619
  }
1535
1620
  async cancelSession(record) {
1536
- const runtime = this.runtimes.forAgent(this.ensureAgent(record.agentName));
1621
+ const runtime = this.runtimes.forAgent(record.agentName);
1537
1622
  if (runtime.kind === "codex" && record.remoteSessionId && runtime.interruptRemoteTurn) {
1538
1623
  if (runtime.getGoal && runtime.setGoal) {
1539
1624
  try {
@@ -1590,29 +1675,237 @@ export class ProxySessionController {
1590
1675
  const loaded = await this.loadSession(this.requireSession(action.contextKey, sessionId));
1591
1676
  await loaded.runtime.respondToApproval(sessionId, requestId, decision);
1592
1677
  }
1593
- async model(contextKey, model, options = {}) {
1678
+ async openExecutionSettings(contextKey, activeTab, options = {}) {
1679
+ const context = this.store.getOrCreateUserContext(contextKey, this.config.defaults.agent);
1680
+ const record = options.sessionId
1681
+ ? this.requireSession(contextKey, options.sessionId)
1682
+ : this.currentSession(contextKey);
1683
+ if (!record && activeTab !== "agent")
1684
+ this.requireCurrentSession(contextKey);
1685
+ const agents = Object.entries(this.config.agents).map(([name, agent]) => ({
1686
+ name,
1687
+ title: agent.title,
1688
+ }));
1689
+ let runtimeSettings = {
1690
+ currentPermissionMode: "auto",
1691
+ providers: [],
1692
+ providerSupported: false,
1693
+ models: [],
1694
+ reasoningOptions: [],
1695
+ };
1696
+ if (record) {
1697
+ const loaded = await this.loadSession(record);
1698
+ const models = await loaded.runtime.listModels();
1699
+ const currentModel = models.find((model) => model.id === loaded.session.model)
1700
+ ?? models.find((model) => model.isDefault);
1701
+ const providerSupported = loaded.runtime.kind === "codex" && Boolean(loaded.runtime.listModelProviders);
1702
+ const providers = providerSupported ? await this.modelProviderOptions(loaded) : [];
1703
+ const currentProvider = loaded.session.modelProvider ?? providers.find((provider) => provider.isDefault)?.id;
1704
+ const currentEffort = currentModel?.supportedReasoningEfforts.some((option) => option.value === loaded.session.reasoningEffort)
1705
+ ? loaded.session.reasoningEffort
1706
+ : currentModel?.defaultReasoningEffort ?? loaded.session.reasoningEffort;
1707
+ runtimeSettings = {
1708
+ currentProvider,
1709
+ currentModel: currentModel?.id ?? loaded.session.model,
1710
+ currentEffort,
1711
+ currentPermissionMode: loaded.session.permissionMode,
1712
+ providers,
1713
+ providerSupported,
1714
+ models,
1715
+ reasoningOptions: currentModel?.supportedReasoningEfforts ?? [],
1716
+ };
1717
+ }
1718
+ const card = this.cardRenderer.renderExecutionSettings({
1719
+ ...(record ? { sessionId: record.localSessionId, taskAgent: record.agentName } : {}),
1720
+ contextKey,
1721
+ activeTab,
1722
+ currentAgent: context.defaultAgent,
1723
+ agents,
1724
+ runtimeSettingsAvailable: Boolean(record),
1725
+ ...runtimeSettings,
1726
+ notice: options.notice,
1727
+ });
1728
+ if (options.updateMessageId) {
1729
+ await this.outbound.updateInteractiveCard(contextKey, options.updateMessageId, card);
1730
+ }
1731
+ else {
1732
+ await this.outbound.sendInteractiveCard(contextKey, card);
1733
+ }
1734
+ }
1735
+ async openAgentSettings(contextKey) {
1736
+ const current = this.store.getOrCreateUserContext(contextKey, this.config.defaults.agent).defaultAgent;
1737
+ if (Object.keys(this.config.agents).length <= 1) {
1738
+ await this.outbound.sendText(contextKey, `当前 Agent:${current}\n当前没有其他 Agent 可以切换。`);
1739
+ return;
1740
+ }
1741
+ await this.openExecutionSettings(contextKey, "agent");
1742
+ }
1743
+ async openProviderSettings(contextKey) {
1744
+ const loaded = await this.loadSession(this.requireCurrentSession(contextKey));
1745
+ const providers = loaded.runtime.kind === "codex" && loaded.runtime.listModelProviders
1746
+ ? await this.modelProviderOptions(loaded)
1747
+ : [];
1748
+ const current = loaded.session.modelProvider?.trim()
1749
+ || providers.find((provider) => provider.isDefault)?.id
1750
+ || "运行时默认";
1751
+ if (providers.length <= 1) {
1752
+ await this.outbound.sendText(contextKey, `当前 Provider:${current}\n当前没有其他 Provider 可以切换。`);
1753
+ return;
1754
+ }
1755
+ await this.openExecutionSettings(contextKey, "provider", { sessionId: loaded.record.localSessionId });
1756
+ }
1757
+ async selectProvider(contextKey, modelProvider, options = {}) {
1594
1758
  const record = options.sessionId
1595
1759
  ? this.requireSession(contextKey, options.sessionId)
1596
1760
  : this.requireCurrentSession(contextKey);
1597
1761
  const loaded = await this.loadSession(record);
1762
+ await this.assertModelProvider(loaded, modelProvider);
1598
1763
  const models = await loaded.runtime.listModels();
1599
- if (!model) {
1600
- const currentModel = loaded.session.model ?? models.find((item) => item.isDefault)?.id;
1601
- const card = this.cardRenderer.renderModelSelector({
1764
+ const model = models.find((candidate) => candidate.id === loaded.session.model)
1765
+ ?? models.find((candidate) => candidate.isDefault);
1766
+ if (!model)
1767
+ throw new Error("当前运行时没有可用于 Provider 切换的模型。");
1768
+ const effort = model.supportedReasoningEfforts.some((candidate) => candidate.value === loaded.session.reasoningEffort)
1769
+ ? loaded.session.reasoningEffort
1770
+ : model.defaultReasoningEffort ?? model.supportedReasoningEfforts[0]?.value;
1771
+ if (!effort)
1772
+ throw new Error(`模型 ${model.id} 没有可用于 Provider 切换的思考强度。`);
1773
+ await this.applyProviderSettings(contextKey, {
1774
+ provider: modelProvider,
1775
+ model: model.id,
1776
+ effort,
1777
+ mode: loaded.session.permissionMode,
1778
+ }, options);
1779
+ }
1780
+ async openProviderSelector(contextKey, options = {}) {
1781
+ await this.openExecutionSettings(contextKey, "provider", options);
1782
+ }
1783
+ async openProviderModelSelector(contextKey, modelProvider, permissionMode, sessionId, updateMessageId) {
1784
+ const loaded = await this.loadSession(this.requireSession(contextKey, sessionId));
1785
+ await this.assertModelProvider(loaded, modelProvider);
1786
+ const models = await loaded.runtime.listModels();
1787
+ const card = this.cardRenderer.renderModelSelector({
1788
+ sessionId,
1789
+ contextKey,
1790
+ currentModel: loaded.session.model ?? models.find((model) => model.isDefault)?.id,
1791
+ reasoningEffort: loaded.session.reasoningEffort,
1792
+ models,
1793
+ modelProvider,
1794
+ permissionMode,
1795
+ unifiedSettings: true,
1796
+ });
1797
+ await this.outbound.updateInteractiveCard(contextKey, updateMessageId, card);
1798
+ }
1799
+ async openProviderReasoningSelector(contextKey, modelProvider, model, permissionMode, sessionId, updateMessageId) {
1800
+ const loaded = await this.loadSession(this.requireSession(contextKey, sessionId));
1801
+ await this.assertModelProvider(loaded, modelProvider);
1802
+ const models = await loaded.runtime.listModels();
1803
+ const selected = models.find((candidate) => candidate.id === model);
1804
+ if (!selected)
1805
+ throw new Error(`未知模型:${model}`);
1806
+ const currentEffort = selected.supportedReasoningEfforts.some((option) => option.value === loaded.session.reasoningEffort)
1807
+ ? loaded.session.reasoningEffort
1808
+ : selected.defaultReasoningEffort;
1809
+ const card = this.cardRenderer.renderReasoningSelector({
1810
+ sessionId,
1811
+ contextKey,
1812
+ modelProvider,
1813
+ model,
1814
+ currentEffort,
1815
+ options: selected.supportedReasoningEfforts,
1816
+ permissionMode,
1817
+ unifiedSettings: true,
1818
+ });
1819
+ await this.outbound.updateInteractiveCard(contextKey, updateMessageId, card);
1820
+ }
1821
+ async openProviderPermissionSelector(contextKey, modelProvider, model, effort, permissionMode, sessionId, updateMessageId) {
1822
+ const loaded = await this.loadSession(this.requireSession(contextKey, sessionId));
1823
+ await this.assertProviderModelSettings(loaded, modelProvider, model, effort);
1824
+ await this.outbound.updateInteractiveCard(contextKey, updateMessageId, this.cardRenderer.renderPermissionSelector({
1825
+ sessionId,
1826
+ contextKey,
1827
+ modelProvider,
1828
+ model,
1829
+ reasoningEffort: effort,
1830
+ currentMode: permissionMode,
1831
+ }));
1832
+ }
1833
+ async applyProviderSettings(contextKey, settings, options = {}) {
1834
+ const record = options.sessionId
1835
+ ? this.requireSession(contextKey, options.sessionId)
1836
+ : this.requireCurrentSession(contextKey);
1837
+ const loaded = await this.loadSession(record);
1838
+ await this.assertProviderModelSettings(loaded, settings.provider, settings.model, settings.effort);
1839
+ if (!loaded.runtime.setExecutionSettings) {
1840
+ throw new Error("当前运行时不支持 Provider 设置。");
1841
+ }
1842
+ const session = await loaded.runtime.setExecutionSettings(loaded.record.localSessionId, {
1843
+ modelProvider: settings.provider,
1844
+ model: settings.model,
1845
+ reasoningEffort: settings.effort,
1846
+ permissionMode: settings.mode,
1847
+ });
1848
+ this.store.updateRuntimeSession(loaded.record.localSessionId, {
1849
+ modelProvider: session.modelProvider ?? settings.provider,
1850
+ model: session.model ?? settings.model,
1851
+ reasoningEffort: session.reasoningEffort ?? settings.effort,
1852
+ permissionMode: session.permissionMode,
1853
+ });
1854
+ const notice = [
1855
+ `Provider 已切换为 ${cardCode(session.modelProvider ?? settings.provider)}`,
1856
+ `模型 ${cardCode(session.model ?? settings.model)}`,
1857
+ `思考强度 ${cardCode(session.reasoningEffort ?? settings.effort)}`,
1858
+ `权限 ${cardCode(session.permissionMode)}`,
1859
+ "从下一次请求生效。",
1860
+ ].join(",");
1861
+ if (options.updateMessageId) {
1862
+ await this.openProviderSelector(contextKey, {
1602
1863
  sessionId: loaded.record.localSessionId,
1603
- contextKey,
1604
- currentModel,
1605
- reasoningEffort: loaded.session.reasoningEffort,
1606
- models,
1864
+ updateMessageId: options.updateMessageId,
1865
+ notice,
1607
1866
  });
1608
- if (options.updateMessageId) {
1609
- await this.outbound.updateInteractiveCard(contextKey, options.updateMessageId, card);
1610
- }
1611
- else {
1612
- await this.outbound.sendInteractiveCard(contextKey, card);
1613
- }
1867
+ }
1868
+ else {
1869
+ await this.outbound.sendText(contextKey, notice.replaceAll("`", ""));
1870
+ }
1871
+ }
1872
+ async assertProviderModelSettings(loaded, modelProvider, model, effort) {
1873
+ await this.assertModelProvider(loaded, modelProvider);
1874
+ const selected = (await loaded.runtime.listModels()).find((candidate) => candidate.id === model);
1875
+ if (!selected)
1876
+ throw new Error(`未知模型:${model}`);
1877
+ if (!selected.supportedReasoningEfforts.some((option) => option.value === effort)) {
1878
+ const supported = selected.supportedReasoningEfforts.map((option) => option.value).join("、") || "无";
1879
+ throw new Error(`模型 ${model} 不支持思考强度 ${effort}。支持的强度:${supported}`);
1880
+ }
1881
+ }
1882
+ async assertModelProvider(loaded, modelProvider) {
1883
+ const providers = await this.modelProviderOptions(loaded);
1884
+ if (!providers.some((provider) => provider.id === modelProvider)) {
1885
+ throw new Error(`未知 Provider:${modelProvider}`);
1886
+ }
1887
+ }
1888
+ async modelProviderOptions(loaded) {
1889
+ if (loaded.runtime.kind !== "codex" || !loaded.runtime.listModelProviders) {
1890
+ throw new Error("当前任务不支持 Provider 设置。");
1891
+ }
1892
+ const providers = await loaded.runtime.listModelProviders();
1893
+ const current = loaded.session.modelProvider?.trim();
1894
+ if (current && !providers.some((provider) => provider.id === current)) {
1895
+ providers.unshift({ id: current });
1896
+ }
1897
+ return providers;
1898
+ }
1899
+ async model(contextKey, model, options = {}) {
1900
+ if (!model) {
1901
+ await this.openExecutionSettings(contextKey, "model", options);
1614
1902
  return;
1615
1903
  }
1904
+ const record = options.sessionId
1905
+ ? this.requireSession(contextKey, options.sessionId)
1906
+ : this.requireCurrentSession(contextKey);
1907
+ const loaded = await this.loadSession(record);
1908
+ const models = await loaded.runtime.listModels();
1616
1909
  const selected = models.find((item) => item.id === model);
1617
1910
  if (!selected)
1618
1911
  throw new Error(`未知模型:${model}`);
@@ -1630,14 +1923,11 @@ export class ProxySessionController {
1630
1923
  ? `,思考强度已自动调整为 ${nextEffort}`
1631
1924
  : "";
1632
1925
  if (options.updateMessageId) {
1633
- await this.outbound.updateInteractiveCard(contextKey, options.updateMessageId, this.cardRenderer.renderReasoningSelector({
1926
+ await this.openExecutionSettings(contextKey, "model", {
1634
1927
  sessionId: loaded.record.localSessionId,
1635
- contextKey,
1636
- model,
1637
- currentEffort: nextEffort,
1638
- options: selected.supportedReasoningEfforts,
1639
- notice: `模型已切换为 ${cardCode(model)},请选择思考模式。`,
1640
- }));
1928
+ updateMessageId: options.updateMessageId,
1929
+ notice: `模型已切换为 ${cardCode(model)}${effortMessage},从下一次请求生效。`,
1930
+ });
1641
1931
  return;
1642
1932
  }
1643
1933
  await this.outbound.sendText(contextKey, `模型已切换为 ${model}${effortMessage},从下一次请求生效。`);
@@ -1785,6 +2075,10 @@ export class ProxySessionController {
1785
2075
  });
1786
2076
  }
1787
2077
  async thinking(contextKey, effort, options = {}) {
2078
+ if (!effort) {
2079
+ await this.openExecutionSettings(contextKey, "thinking", options);
2080
+ return;
2081
+ }
1788
2082
  const record = options.sessionId
1789
2083
  ? this.requireSession(contextKey, options.sessionId)
1790
2084
  : this.requireCurrentSession(contextKey);
@@ -1798,22 +2092,6 @@ export class ProxySessionController {
1798
2092
  throw new Error("模型已发生变化,请重新打开 /model。");
1799
2093
  }
1800
2094
  const supported = currentModel.supportedReasoningEfforts;
1801
- if (!effort) {
1802
- const card = this.cardRenderer.renderReasoningSelector({
1803
- sessionId: loaded.record.localSessionId,
1804
- contextKey,
1805
- model: currentModel.id,
1806
- currentEffort: loaded.session.reasoningEffort ?? currentModel.defaultReasoningEffort,
1807
- options: supported,
1808
- });
1809
- if (options.updateMessageId) {
1810
- await this.outbound.updateInteractiveCard(contextKey, options.updateMessageId, card);
1811
- }
1812
- else {
1813
- await this.outbound.sendInteractiveCard(contextKey, card);
1814
- }
1815
- return;
1816
- }
1817
2095
  if (!supported.some((option) => option.value === effort)) {
1818
2096
  const options = supported.map((option) => option.value).join("、") || "无";
1819
2097
  throw new Error(`不支持的思考强度:${effort}。支持的强度:${options}`);
@@ -1821,79 +2099,85 @@ export class ProxySessionController {
1821
2099
  await loaded.runtime.setReasoningEffort(loaded.record.localSessionId, effort);
1822
2100
  this.store.updateRuntimeSession(loaded.record.localSessionId, { reasoningEffort: effort });
1823
2101
  if (options.updateMessageId) {
1824
- await this.outbound.updateInteractiveCard(contextKey, options.updateMessageId, this.cardRenderer.renderReasoningSelector({
2102
+ await this.openExecutionSettings(contextKey, "thinking", {
1825
2103
  sessionId: loaded.record.localSessionId,
1826
- contextKey,
1827
- model: currentModel.id,
1828
- currentEffort: effort,
1829
- options: supported,
1830
- notice: `思考模式已切换为 ${cardCode(effort)},从下一次请求生效。`,
1831
- }));
2104
+ updateMessageId: options.updateMessageId,
2105
+ notice: `思考强度已切换为 ${cardCode(effort)},从下一次请求生效。`,
2106
+ });
1832
2107
  return;
1833
2108
  }
1834
2109
  await this.outbound.sendText(contextKey, `思考强度已切换为 ${effort},从下一次请求生效。`);
1835
2110
  }
1836
- async permissions(contextKey, mode) {
1837
- const record = this.requireCurrentSession(contextKey);
2111
+ async permissions(contextKey, mode, options = {}) {
1838
2112
  if (!mode) {
1839
- await this.outbound.sendText(contextKey, `当前权限模式:${record.permissionMode ?? "auto"}`);
2113
+ await this.openExecutionSettings(contextKey, "permission", options);
1840
2114
  return;
1841
2115
  }
2116
+ const record = options.sessionId
2117
+ ? this.requireSession(contextKey, options.sessionId)
2118
+ : this.requireCurrentSession(contextKey);
1842
2119
  const loaded = await this.loadSession(record);
1843
2120
  await loaded.runtime.setPermissionMode(record.localSessionId, mode);
2121
+ loaded.session.permissionMode = mode;
1844
2122
  this.store.updateRuntimeSession(record.localSessionId, { permissionMode: mode });
2123
+ if (options.updateMessageId) {
2124
+ await this.openExecutionSettings(contextKey, "permission", {
2125
+ sessionId: loaded.record.localSessionId,
2126
+ updateMessageId: options.updateMessageId,
2127
+ notice: mode === "auto"
2128
+ ? "已切换为自动执行模式,从下一次请求生效。"
2129
+ : "已切换为执行前确认模式,从下一次请求生效。",
2130
+ });
2131
+ return;
2132
+ }
1845
2133
  await this.outbound.sendText(contextKey, mode === "auto" ? "已切换为自动执行模式。" : "已切换为执行前确认模式。");
1846
2134
  }
1847
- async listAgents(contextKey) {
1848
- const current = this.store.getOrCreateUserContext(contextKey, this.config.defaults.agent).defaultAgent;
1849
- const lines = Object.entries(this.config.agents).map(([name, agent]) => {
1850
- const selected = name === current;
1851
- return `- ${selected ? "✅ " : ""}${asInlineCode(name)}:${agent.title}${selected ? "(当前)" : ""}`;
1852
- });
1853
- await this.outbound.sendMarkdown(contextKey, [
1854
- `当前 Agent:${asInlineCode(current)}`,
1855
- "可用 Agent:",
1856
- lines.join("\n") || "无",
1857
- ].join("\n"));
1858
- }
1859
2135
  async listSessions(contextKey, searchTerm, options = {}) {
1860
2136
  const context = this.store.getOrCreateUserContext(contextKey, this.config.defaults.agent);
1861
2137
  const normalizedSearch = searchTerm?.trim().toLowerCase();
1862
2138
  const visibleCount = options.visibleCount ?? SESSION_PAGE_SIZE;
1863
2139
  const localSessions = this.store.listSessions(contextKey).filter((session) => session.status !== "closed" && this.isCodexSession(session));
1864
- let remoteSessions = [];
1865
- let remoteHint;
2140
+ const remoteSessions = [];
2141
+ const remoteErrors = [];
1866
2142
  let remoteHasMore = false;
1867
- const runtime = this.runtimes.get("codex");
1868
- if (runtime.listRemoteSessions) {
2143
+ await Promise.all(this.runtimes.entries("codex").map(async ([agentName, runtime]) => {
2144
+ if (!runtime.listRemoteSessions)
2145
+ return;
1869
2146
  try {
1870
2147
  const page = await runtime.listRemoteSessions({ searchTerm, limit: visibleCount });
1871
- remoteSessions = page.sessions;
1872
- remoteHasMore = Boolean(page.nextCursor);
2148
+ remoteSessions.push(...page.sessions.map((session) => ({ agentName, session })));
2149
+ remoteHasMore ||= Boolean(page.nextCursor);
1873
2150
  }
1874
2151
  catch (error) {
1875
- this.logger.warn({ error, contextKey }, "Failed to list Codex sessions.");
1876
- remoteHint = `读取 Codex 任务失败:${error instanceof Error ? error.message : String(error)}`;
2152
+ this.logger.warn({ error, contextKey, agentName }, "Failed to list Codex sessions for an Agent.");
2153
+ remoteErrors.push(`${agentName}: ${error instanceof Error ? error.message : String(error)}`);
1877
2154
  }
1878
- }
2155
+ }));
2156
+ const remoteHint = remoteErrors.length > 0
2157
+ ? `部分 Agent 的任务读取失败:${remoteErrors.join(";")}`
2158
+ : undefined;
1879
2159
  const entries = mergeTaskList(localSessions, remoteSessions, context.currentSessionId)
1880
- .filter((entry) => !normalizedSearch || [entry.id, entry.title, entry.cwd].some((value) => value.toLowerCase().includes(normalizedSearch)))
2160
+ .filter((entry) => !normalizedSearch
2161
+ || [entry.id, entry.title, entry.cwd, entry.agentName]
2162
+ .some((value) => value.toLowerCase().includes(normalizedSearch)))
1881
2163
  .sort((left, right) => Number(right.current) - Number(left.current)
1882
2164
  || Number(right.active) - Number(left.active)
1883
2165
  || right.updatedAt - left.updatedAt);
1884
2166
  const activeCount = entries.filter((entry) => entry.active).length;
1885
2167
  const visibleEntries = entries.slice(0, visibleCount);
1886
2168
  const hasMore = remoteHasMore || entries.length > visibleCount;
1887
- this.lastSessionListings.set(contextKey, visibleEntries.map((entry) => entry.id));
2169
+ this.lastSessionListings.set(contextKey, visibleEntries.map((entry) => entry.reference));
1888
2170
  const cardEntries = visibleEntries.map((entry, index) => {
1889
2171
  const marker = entry.current ? "✅" : entry.active ? "🟢 **活跃**" : "•";
1890
- const showStop = entry.status === "外部执行中" && entry.id !== options.forceSwitchTaskId;
2172
+ const showStop = entry.status === "外部执行中"
2173
+ && entry.reference !== options.forceSwitchTaskId
2174
+ && entry.id !== options.forceSwitchTaskId;
1891
2175
  const actions = entry.current ? [] : [{
1892
2176
  text: showStop ? "Stop" : "Switch",
1893
2177
  type: showStop ? "danger" : "default",
1894
2178
  value: {
1895
2179
  action: showStop ? "session_stop" : "session_switch",
1896
- sessionId: entry.id,
2180
+ sessionId: entry.reference,
1897
2181
  ...(searchTerm ? { searchTerm } : {}),
1898
2182
  visibleCount: String(visibleCount),
1899
2183
  contextKey,
@@ -1903,7 +2187,7 @@ export class ProxySessionController {
1903
2187
  text: "New",
1904
2188
  value: {
1905
2189
  action: "session_new",
1906
- sessionId: entry.id,
2190
+ sessionId: entry.reference,
1907
2191
  ...(searchTerm ? { searchTerm } : {}),
1908
2192
  visibleCount: String(visibleCount),
1909
2193
  contextKey,
@@ -1913,7 +2197,7 @@ export class ProxySessionController {
1913
2197
  text: "NewGroup",
1914
2198
  value: {
1915
2199
  action: "session_new_group",
1916
- sessionId: entry.id,
2200
+ sessionId: entry.reference,
1917
2201
  ...(searchTerm ? { searchTerm } : {}),
1918
2202
  visibleCount: String(visibleCount),
1919
2203
  contextKey,
@@ -1923,7 +2207,7 @@ export class ProxySessionController {
1923
2207
  text: "Fork",
1924
2208
  value: {
1925
2209
  action: "session_fork",
1926
- sessionId: entry.id,
2210
+ sessionId: entry.reference,
1927
2211
  ...(searchTerm ? { searchTerm } : {}),
1928
2212
  visibleCount: String(visibleCount),
1929
2213
  contextKey,
@@ -1933,7 +2217,7 @@ export class ProxySessionController {
1933
2217
  text: "ForkGroup",
1934
2218
  value: {
1935
2219
  action: "session_fork_group",
1936
- sessionId: entry.id,
2220
+ sessionId: entry.reference,
1937
2221
  ...(searchTerm ? { searchTerm } : {}),
1938
2222
  visibleCount: String(visibleCount),
1939
2223
  contextKey,
@@ -1943,14 +2227,14 @@ export class ProxySessionController {
1943
2227
  text: "Status",
1944
2228
  value: {
1945
2229
  action: "session_status",
1946
- sessionId: entry.id,
2230
+ sessionId: entry.reference,
1947
2231
  contextKey,
1948
2232
  },
1949
2233
  });
1950
2234
  return {
1951
2235
  lines: [
1952
2236
  `**${index + 1}.** ${marker} **${cardText(entry.title)}** ${cardText(entry.id)}`,
1953
- `${entry.status} · ${entry.updatedLabel} · ${cardText(entry.cwd || "目录未知")}`,
2237
+ `${entry.status} · ${entry.updatedLabel} · ${cardText(entry.agentName)} · ${cardText(entry.cwd || "目录未知")}`,
1954
2238
  ],
1955
2239
  actions,
1956
2240
  };
@@ -2003,6 +2287,45 @@ export class ProxySessionController {
2003
2287
  forceSwitchTaskId,
2004
2288
  });
2005
2289
  }
2290
+ findStoredSessionByReference(reference, contextKey) {
2291
+ const scoped = parseRemoteSessionReference(reference);
2292
+ return scoped
2293
+ ? this.store.findSessionByRemoteSessionId(scoped.remoteSessionId, contextKey, scoped.agentName)
2294
+ : this.store.findSessionByRemoteSessionId(reference, contextKey);
2295
+ }
2296
+ async resolveRemoteCodexSession(reference) {
2297
+ const scoped = parseRemoteSessionReference(reference);
2298
+ const candidates = scoped
2299
+ ? [[scoped.agentName, this.runtimes.forAgent(scoped.agentName)]]
2300
+ : this.runtimes.entries("codex");
2301
+ if (candidates.length === 0)
2302
+ throw new Error("未配置 Codex Agent。");
2303
+ const reads = await Promise.allSettled(candidates.map(async ([agentName, runtime]) => {
2304
+ if (runtime.kind !== "codex" || !runtime.readRemoteSession) {
2305
+ throw new Error(`Agent ${agentName} 不支持读取远端任务。`);
2306
+ }
2307
+ return {
2308
+ agentName,
2309
+ runtime,
2310
+ remote: await runtime.readRemoteSession(scoped?.remoteSessionId ?? reference),
2311
+ };
2312
+ }));
2313
+ const matches = reads
2314
+ .filter((result) => result.status === "fulfilled")
2315
+ .map((result) => result.value);
2316
+ if (matches.length === 1)
2317
+ return matches[0];
2318
+ if (matches.length > 1) {
2319
+ throw new Error(`多个 Agent 中存在相同任务 ID:${reference}。请先使用 /sessions,再通过序号选择任务。`);
2320
+ }
2321
+ const details = reads
2322
+ .map((result, index) => result.status === "rejected"
2323
+ ? `${candidates[index]?.[0] ?? "unknown"}: ${runtimeErrorMessage(result.reason)}`
2324
+ : undefined)
2325
+ .filter((detail) => Boolean(detail))
2326
+ .join(";");
2327
+ throw new Error(`找不到 Codex 任务:${scoped?.remoteSessionId ?? reference}${details ? `(${details})` : ""}`);
2328
+ }
2006
2329
  async switchSession(contextKey, reference) {
2007
2330
  this.store.getOrCreateUserContext(contextKey, this.config.defaults.agent);
2008
2331
  const taskId = this.resolveSessionReference(contextKey, reference);
@@ -2010,9 +2333,9 @@ export class ProxySessionController {
2010
2333
  if (direct?.status === "closed") {
2011
2334
  throw new Error(`找不到任务:${taskId}`);
2012
2335
  }
2013
- const existing = direct ?? this.store.findSessionByRemoteSessionId(taskId);
2336
+ const existing = direct ?? this.findStoredSessionByReference(taskId);
2014
2337
  if (existing) {
2015
- const runtime = this.runtimes.forAgent(this.ensureAgent(existing.agentName));
2338
+ const runtime = this.runtimes.forAgent(existing.agentName);
2016
2339
  await this.assertSessionTurnOwnership(existing, runtime);
2017
2340
  this.store.attachSessionToContext(contextKey, existing.localSessionId);
2018
2341
  this.store.setCurrentSession(contextKey, existing.localSessionId);
@@ -2020,19 +2343,12 @@ export class ProxySessionController {
2020
2343
  await this.outbound.sendText(contextKey, `已切换到任务:${existing.title ?? existing.remoteSessionId ?? taskId}`);
2021
2344
  return;
2022
2345
  }
2023
- const runtime = this.runtimes.get("codex");
2024
- if (!runtime.readRemoteSession)
2025
- throw new Error("当前 Codex 运行时不支持读取任务。");
2026
- const remote = await runtime.readRemoteSession(taskId);
2346
+ const { agentName, remote } = await this.resolveRemoteCodexSession(taskId);
2027
2347
  if (remote.status === "active" || remote.lastTurnStatus === "inProgress") {
2028
2348
  throw new Error(`这个任务正在外部 Codex 中执行,当前不会切换。可使用 /status ${taskId} 查看进度。`);
2029
2349
  }
2030
2350
  if (!remote.cwd)
2031
2351
  throw new Error("这个 Codex 任务没有可用的工作目录,暂时无法切换。");
2032
- const agentEntry = Object.entries(this.config.agents).find(([, agent]) => agent.kind === "codex");
2033
- if (!agentEntry)
2034
- throw new Error("未配置 Codex Agent。");
2035
- const [agentName] = agentEntry;
2036
2352
  const localSessionId = createId("sess");
2037
2353
  this.store.createSession({
2038
2354
  localSessionId,
@@ -2061,16 +2377,15 @@ export class ProxySessionController {
2061
2377
  await this.cancelSession({ ...direct, contextKey });
2062
2378
  return;
2063
2379
  }
2064
- const existing = this.store.findSessionByRemoteSessionId(taskId);
2380
+ const existing = this.findStoredSessionByReference(taskId);
2065
2381
  if (existing) {
2066
2382
  await this.cancelSession({ ...existing, contextKey });
2067
2383
  return;
2068
2384
  }
2069
- const runtime = this.runtimes.get("codex");
2385
+ const { runtime, remote } = await this.resolveRemoteCodexSession(taskId);
2070
2386
  if (!runtime.readRemoteSession || !runtime.interruptRemoteTurn) {
2071
2387
  throw new Error("当前 Codex 运行时不支持停止外部任务。");
2072
2388
  }
2073
- const remote = await runtime.readRemoteSession(taskId);
2074
2389
  const turnId = remote.status === "active" || remote.lastTurnStatus === "inProgress"
2075
2390
  ? remote.lastTurnId
2076
2391
  : undefined;
@@ -2147,11 +2462,18 @@ export class ProxySessionController {
2147
2462
  }
2148
2463
  return undefined;
2149
2464
  }
2150
- async setDefaultAgent(contextKey, agentName) {
2465
+ async setDefaultAgent(contextKey, agentName, options = {}) {
2151
2466
  this.ensureAgent(agentName);
2152
2467
  this.store.getOrCreateUserContext(contextKey, this.config.defaults.agent);
2153
2468
  this.store.setDefaultAgent(contextKey, agentName);
2154
- await this.outbound.sendText(contextKey, `默认 agent 已切换为:${agentName}`);
2469
+ if (Object.keys(this.config.agents).length <= 1) {
2470
+ await this.outbound.sendText(contextKey, `当前 Agent:${agentName}\n当前没有其他 Agent 可以切换。`);
2471
+ return;
2472
+ }
2473
+ await this.openExecutionSettings(contextKey, "agent", {
2474
+ ...options,
2475
+ notice: `默认 Agent 已切换为 ${cardCode(agentName)},从下一次新建任务生效。`,
2476
+ });
2155
2477
  }
2156
2478
  async status(contextKey, sessionId, options = {}) {
2157
2479
  const context = this.store.getOrCreateUserContext(contextKey, this.config.defaults.agent);
@@ -2161,7 +2483,7 @@ export class ProxySessionController {
2161
2483
  const direct = this.store.getSession(targetSessionId);
2162
2484
  current = direct
2163
2485
  ? { ...direct, contextKey }
2164
- : this.store.findSessionByRemoteSessionId(targetSessionId, contextKey);
2486
+ : this.findStoredSessionByReference(targetSessionId, contextKey);
2165
2487
  if (!current) {
2166
2488
  await this.statusForCodexTask(contextKey, targetSessionId, options);
2167
2489
  return;
@@ -2172,7 +2494,7 @@ export class ProxySessionController {
2172
2494
  }
2173
2495
  if (current && current.status === "running") {
2174
2496
  try {
2175
- const runtime = this.runtimes.forAgent(this.ensureAgent(current.agentName));
2497
+ const runtime = this.runtimes.forAgent(current.agentName);
2176
2498
  if (runtime.getSession(current.localSessionId)) {
2177
2499
  await runtime.synchronizeSession(current.localSessionId);
2178
2500
  current = this.store.getSession(current.localSessionId) ?? current;
@@ -2186,7 +2508,7 @@ export class ProxySessionController {
2186
2508
  let remote;
2187
2509
  let goal;
2188
2510
  if (current?.remoteSessionId) {
2189
- const runtime = this.runtimes.forAgent(this.ensureAgent(current.agentName));
2511
+ const runtime = this.runtimes.forAgent(current.agentName);
2190
2512
  if (runtime.readRemoteSession) {
2191
2513
  try {
2192
2514
  remote = await runtime.readRemoteSession(current.remoteSessionId);
@@ -2216,7 +2538,7 @@ export class ProxySessionController {
2216
2538
  }
2217
2539
  else {
2218
2540
  const agent = this.ensureAgent(current.agentName);
2219
- const runtimeSession = this.runtimes.forAgent(agent).getSession(current.localSessionId);
2541
+ const runtimeSession = this.runtimes.forAgent(current.agentName).getSession(current.localSessionId);
2220
2542
  activeTurnId = runtimeSession?.activeTurnId;
2221
2543
  const remoteActive = isRemoteSessionActive(remote);
2222
2544
  activeTurnId = remoteActive ? remote?.lastTurnId ?? activeTurnId : activeTurnId;
@@ -2228,7 +2550,7 @@ export class ProxySessionController {
2228
2550
  const resultLabel = remoteActive
2229
2551
  ? "执行中"
2230
2552
  : remoteTurnStatusLabel(remote?.lastTurnStatus ?? current.lastTurnStatus);
2231
- taskLines.push(`**标题**:${cardCode(current.title ?? "未命名任务")}`, `**工作目录**:${cardCode(current.cwd)}`, `**模型 / 思考强度**:${cardCode(current.model ?? "默认")} / ${cardCode(current.reasoningEffort ?? "自动")}`, `**状态 / 最近结果**:${statusLabel} / ${resultLabel}`, `**Agent**:${cardCode(agent.title)}`, `**权限 / 任务范围**:${current.permissionMode === "confirm" ? "执行前确认" : "自动执行"} / ${detectProjectlessWorkspace(current.cwd) ? "未指定项目" : "指定项目"}`, `**Codex 任务 ID**:${cardCode(current.remoteSessionId ?? "尚未创建")}`, `**当前执行 / 排队消息**:${activeTurnId ? cardCode(activeTurnId) : "无"} / ${queued} 条`, `**创建时间 / 最近活动**:${formatStatusTime(current.createdAt)} / ${formatStatusTime(current.updatedAt)}`);
2553
+ taskLines.push(`**标题**:${cardCode(current.title ?? "未命名任务")}`, `**工作目录**:${cardCode(current.cwd)}`, `**Provider / 模型 / 思考强度**:${cardCode(current.modelProvider ?? "Codex 默认")} / ${cardCode(current.model ?? "默认")} / ${cardCode(current.reasoningEffort ?? "自动")}`, `**状态 / 最近结果**:${statusLabel} / ${resultLabel}`, `**Agent**:${cardCode(agent.title)}`, `**权限 / 任务范围**:${current.permissionMode === "confirm" ? "执行前确认" : "自动执行"} / ${detectProjectlessWorkspace(current.cwd) ? "未指定项目" : "指定项目"}`, `**Codex 任务 ID**:${cardCode(current.remoteSessionId ?? "尚未创建")}`, `**当前执行 / 排队消息**:${activeTurnId ? cardCode(activeTurnId) : "无"} / ${queued} 条`, `**创建时间 / 最近活动**:${formatStatusTime(current.createdAt)} / ${formatStatusTime(current.updatedAt)}`);
2232
2554
  }
2233
2555
  const sections = [
2234
2556
  { title: targetSessionId ? "指定任务" : "当前任务", lines: taskLines },
@@ -2257,12 +2579,15 @@ export class ProxySessionController {
2257
2579
  const title = targetSessionId && current
2258
2580
  ? `Codex 状态:${truncateText((current.title ?? current.remoteSessionId ?? current.localSessionId).replace(/\s+/g, " "), 40)}`
2259
2581
  : "Codex 状态";
2260
- const taskId = current?.remoteSessionId ?? current?.localSessionId;
2582
+ const taskId = current?.remoteSessionId
2583
+ ? remoteSessionReference(current.agentName, current.remoteSessionId)
2584
+ : current?.localSessionId;
2261
2585
  const isCurrent = Boolean(current && current.localSessionId === context.currentSessionId);
2262
2586
  const remoteActive = isRemoteSessionActive(remote);
2263
2587
  const botOwnsActiveTurn = Boolean(localCurrent && remote && isBotOwnedActiveTurn(localCurrent, remote));
2264
2588
  const active = Boolean(activeTurnId || current?.status === "running" || remoteActive);
2265
- const forceSwitch = Boolean(taskId && options.forceSwitchTaskId === taskId);
2589
+ const forceSwitch = Boolean(taskId && (options.forceSwitchTaskId === taskId
2590
+ || options.forceSwitchTaskId === current?.remoteSessionId));
2266
2591
  const taskActions = !taskId ? []
2267
2592
  : remoteActive && !botOwnsActiveTurn && !forceSwitch
2268
2593
  ? [statusCardAction("Stop", "danger", "session_stop", taskId, contextKey)]
@@ -2281,11 +2606,9 @@ export class ProxySessionController {
2281
2606
  else
2282
2607
  await this.outbound.sendInteractiveCard(contextKey, card);
2283
2608
  }
2284
- async statusForCodexTask(contextKey, remoteSessionId, options = {}) {
2285
- const runtime = this.runtimes.get("codex");
2286
- if (!runtime.readRemoteSession)
2287
- throw new Error("当前 Codex 运行时不支持读取外部任务状态。");
2288
- const remote = await runtime.readRemoteSession(remoteSessionId);
2609
+ async statusForCodexTask(contextKey, reference, options = {}) {
2610
+ const { agentName, remote } = await this.resolveRemoteCodexSession(reference);
2611
+ const actionReference = remoteSessionReference(agentName, remote.id);
2289
2612
  const sections = [
2290
2613
  {
2291
2614
  title: "指定任务",
@@ -2293,6 +2616,7 @@ export class ProxySessionController {
2293
2616
  `**标题**:${cardCode(remote.title ?? remote.preview ?? "未命名任务")}`,
2294
2617
  `**工作目录**:${cardCode(remote.cwd || "目录未知")}`,
2295
2618
  `**状态 / 当前任务**:${remoteSessionDetailStatus(remote)} / 未切换`,
2619
+ `**Agent**:${cardCode(agentName)}`,
2296
2620
  `**Codex 任务 ID**:${cardCode(remote.id)}`,
2297
2621
  `**最近回合**:${cardCode(remote.lastTurnId ?? "无")} ${remoteTurnStatusLabel(remote.lastTurnStatus)}`,
2298
2622
  `**创建时间 / 最近活动**:${formatRemoteTime(remote.createdAt)} / ${formatRemoteTime(latestRemoteTimestamp(remote.recencyAt, remote.updatedAt))}`,
@@ -2312,10 +2636,12 @@ export class ProxySessionController {
2312
2636
  },
2313
2637
  ];
2314
2638
  const title = `Codex 状态:${truncateText((remote.title ?? remote.preview ?? remote.id).replace(/\s+/g, " "), 40)}`;
2315
- const showStop = isRemoteSessionActive(remote) && options.forceSwitchTaskId !== remote.id;
2639
+ const showStop = isRemoteSessionActive(remote)
2640
+ && options.forceSwitchTaskId !== actionReference
2641
+ && options.forceSwitchTaskId !== remote.id;
2316
2642
  const actions = [
2317
- statusRefreshAction(remote.id, contextKey),
2318
- statusCardAction(showStop ? "Stop" : "Switch", showStop ? "danger" : "default", showStop ? "session_stop" : "session_switch", remote.id, contextKey),
2643
+ statusRefreshAction(actionReference, contextKey),
2644
+ statusCardAction(showStop ? "Stop" : "Switch", showStop ? "danger" : "default", showStop ? "session_stop" : "session_switch", actionReference, contextKey),
2319
2645
  ];
2320
2646
  const card = this.cardRenderer.renderSectionsCard(title, sections, actions);
2321
2647
  if (options.updateMessageId)
@@ -2338,7 +2664,7 @@ export class ProxySessionController {
2338
2664
  title: "任务管理",
2339
2665
  lines: [
2340
2666
  "**/new [title] [--dir &#60;cwd&#62; | --nodir]** 使用默认 Agent 创建任务;--nodir 强制创建 Projectless 任务",
2341
- "**/newgroup [title]** 创建飞书群和新任务;继承当前项目与执行设置",
2667
+ "**/newgroup [title] [--dir &#60;cwd&#62; | --nodir]** 创建飞书群和新任务;参数与 /new 相同",
2342
2668
  "**/forkgroup [title]** 从当前任务最新已完成轮次创建分支并绑定到新群",
2343
2669
  "**/fork [序号或任务 ID]** 从当前或指定任务创建分支;运行中使用最近已完成轮次",
2344
2670
  "**/title &#60;新标题&#62;** 修改当前任务的标题",
@@ -2353,22 +2679,23 @@ export class ProxySessionController {
2353
2679
  "**/stop** 停止当前执行",
2354
2680
  "**/queue &#60;prompt&#62;**(兼容 **/nosteer**) 不追加到当前轮次,按顺序排队为后续轮次",
2355
2681
  "**/goal [目标]** 查看或创建长任务 Goal;支持 pause、resume、edit、clear",
2356
- "**/model [name]** 查看或切换模型",
2357
- "**/thinking [level]** 查看或设置思考强度",
2358
- "**/permissions [auto|confirm]** 查看或切换确认模式",
2682
+ "**/provider** 选择 AI 服务提供商;没有其他候选时仅显示当前 Provider",
2683
+ "**/model** 选择当前任务使用的模型",
2684
+ "**/thinking** 设置模型的思考强度",
2685
+ "**/permissions** 设置执行工具前是否需要确认",
2359
2686
  ],
2360
2687
  },
2361
2688
  {
2362
2689
  title: "Agent",
2363
2690
  lines: [
2364
- "**/agent [name]** 查看或设置新任务的默认 Agent",
2691
+ "**/agent [name]** 选择新任务使用的默认 Agent;仅配置一个时直接显示当前 Agent",
2365
2692
  ],
2366
2693
  },
2367
2694
  {
2368
2695
  title: "系统",
2369
2696
  lines: [
2370
2697
  "**/status [序号或任务 ID]** 查看当前或指定任务的详细状态",
2371
- "**/restart** 重启 Agent Bot 并恢复会话",
2698
+ "**/restart [--force]** 默认安全重启;--force 立即重启",
2372
2699
  "**/help** 显示本帮助",
2373
2700
  ],
2374
2701
  },
@@ -2400,6 +2727,17 @@ export class ProxySessionController {
2400
2727
  ? createProjectlessWorkspace().cwd
2401
2728
  : current.cwd;
2402
2729
  }
2730
+ inheritedExecutionSettings(contextKey, agentName) {
2731
+ const current = this.currentSession(contextKey);
2732
+ if (!current || current.agentName !== agentName)
2733
+ return {};
2734
+ return {
2735
+ modelProvider: current.modelProvider,
2736
+ model: current.model,
2737
+ reasoningEffort: current.reasoningEffort,
2738
+ permissionMode: current.permissionMode,
2739
+ };
2740
+ }
2403
2741
  currentProjectCwd(contextKey) {
2404
2742
  const current = this.currentSession(contextKey);
2405
2743
  return current && !detectProjectlessWorkspace(current.cwd) ? current.cwd : undefined;
@@ -2454,10 +2792,10 @@ function statusRefreshAction(sessionId, contextKey) {
2454
2792
  function mergeTaskList(localSessions, remoteSessions, currentLocalSessionId) {
2455
2793
  const localByRemoteId = new Map(localSessions
2456
2794
  .filter((session) => session.runtimeKind === "codex" && session.remoteSessionId)
2457
- .map((session) => [session.remoteSessionId, session]));
2795
+ .map((session) => [agentRemoteKey(session.agentName, session.remoteSessionId), session]));
2458
2796
  const representedLocalIds = new Set();
2459
- const entries = remoteSessions.map((remote) => {
2460
- const local = localByRemoteId.get(remote.id);
2797
+ const entries = remoteSessions.map(({ agentName, session: remote }) => {
2798
+ const local = localByRemoteId.get(agentRemoteKey(agentName, remote.id));
2461
2799
  if (local)
2462
2800
  representedLocalIds.add(local.localSessionId);
2463
2801
  const active = remote.status === "active" || remote.lastTurnStatus === "inProgress";
@@ -2466,7 +2804,9 @@ function mergeTaskList(localSessions, remoteSessions, currentLocalSessionId) {
2466
2804
  : remoteSessionStatusLabel(remote.status);
2467
2805
  const recencyAt = remote.recencyAt ?? remote.updatedAt;
2468
2806
  return {
2807
+ reference: remoteSessionReference(agentName, remote.id),
2469
2808
  id: remote.id,
2809
+ agentName,
2470
2810
  title: remote.title ?? remote.preview ?? local?.title ?? "未命名任务",
2471
2811
  cwd: remote.cwd || local?.cwd || "",
2472
2812
  status,
@@ -2481,7 +2821,11 @@ function mergeTaskList(localSessions, remoteSessions, currentLocalSessionId) {
2481
2821
  continue;
2482
2822
  const updatedAt = Date.parse(local.updatedAt);
2483
2823
  entries.push({
2824
+ reference: local.remoteSessionId
2825
+ ? remoteSessionReference(local.agentName, local.remoteSessionId)
2826
+ : local.localSessionId,
2484
2827
  id: local.remoteSessionId ?? local.localSessionId,
2828
+ agentName: local.agentName,
2485
2829
  title: local.title ?? "未命名任务",
2486
2830
  cwd: local.cwd,
2487
2831
  status: sessionStatusLabel(local.status),
@@ -2493,6 +2837,31 @@ function mergeTaskList(localSessions, remoteSessions, currentLocalSessionId) {
2493
2837
  }
2494
2838
  return entries;
2495
2839
  }
2840
+ function agentRemoteKey(agentName, remoteSessionId) {
2841
+ return `${agentName}\u0000${remoteSessionId}`;
2842
+ }
2843
+ function remoteSessionReference(agentName, remoteSessionId) {
2844
+ return `${REMOTE_SESSION_REFERENCE_PREFIX}${encodeURIComponent(agentName)}:${encodeURIComponent(remoteSessionId)}`;
2845
+ }
2846
+ function parseRemoteSessionReference(reference) {
2847
+ if (!reference.startsWith(REMOTE_SESSION_REFERENCE_PREFIX))
2848
+ return undefined;
2849
+ const value = reference.slice(REMOTE_SESSION_REFERENCE_PREFIX.length);
2850
+ const separator = value.indexOf(":");
2851
+ if (separator <= 0 || separator === value.length - 1)
2852
+ return undefined;
2853
+ try {
2854
+ const agentName = decodeURIComponent(value.slice(0, separator));
2855
+ const remoteSessionId = decodeURIComponent(value.slice(separator + 1));
2856
+ return agentName && remoteSessionId ? { agentName, remoteSessionId } : undefined;
2857
+ }
2858
+ catch {
2859
+ return undefined;
2860
+ }
2861
+ }
2862
+ function runtimeErrorMessage(error) {
2863
+ return error instanceof Error ? error.message : String(error);
2864
+ }
2496
2865
  function isMissingRolloutError(error) {
2497
2866
  const message = error instanceof Error ? error.message : String(error);
2498
2867
  return /no rollout found|rollout[^\n]*(?:not found|missing)|thread\/resume failed/i.test(message);
@@ -2671,6 +3040,21 @@ function cardText(value) {
2671
3040
  function cardCode(value) {
2672
3041
  return `\`${value.replaceAll("`", "'")}\``;
2673
3042
  }
3043
+ function permissionModeValue(value) {
3044
+ if (value === "auto" || value === "confirm")
3045
+ return value;
3046
+ throw new Error("权限模式只能是 auto 或 confirm。");
3047
+ }
3048
+ function executionSettingsTabValue(value) {
3049
+ if (value === "agent" || value === "provider" || value === "model" || value === "thinking" || value === "permission")
3050
+ return value;
3051
+ throw new Error("设置卡片的 tab 无效,请重新发送设置命令。");
3052
+ }
3053
+ function requiredCardMessageId(value) {
3054
+ if (value)
3055
+ return value;
3056
+ throw new Error("无法更新设置卡片,请重新发送设置命令。");
3057
+ }
2674
3058
  function remoteSessionStatusLabel(status) {
2675
3059
  const labels = {
2676
3060
  active: "执行中",
@@ -2707,6 +3091,10 @@ function mergeRemoteTaskStatus(record, remote) {
2707
3091
  ...record,
2708
3092
  title: remote.title ?? record.title,
2709
3093
  cwd: remote.cwd || record.cwd,
3094
+ modelProvider: remote.modelProvider ?? record.modelProvider,
3095
+ model: remote.model ?? record.model,
3096
+ reasoningEffort: remote.reasoningEffort ?? record.reasoningEffort,
3097
+ permissionMode: remote.permissionMode ?? record.permissionMode,
2710
3098
  status: isRemoteSessionActive(remote)
2711
3099
  ? "running"
2712
3100
  : remote.lastTurnStatus
@@ -2758,12 +3146,8 @@ function isQueueIndependentCommand(command) {
2758
3146
  return true;
2759
3147
  if (command.type === "agent")
2760
3148
  return command.agent === undefined;
2761
- if (command.type === "model")
2762
- return command.model === undefined;
2763
- if (command.type === "thinking")
2764
- return command.effort === undefined;
2765
- if (command.type === "permissions")
2766
- return command.mode === undefined;
3149
+ if (["model", "provider", "thinking", "permissions"].includes(command.type))
3150
+ return true;
2767
3151
  return false;
2768
3152
  }
2769
3153
  function parseSessionVisibleCount(value) {
@@ -2771,7 +3155,7 @@ function parseSessionVisibleCount(value) {
2771
3155
  return Number.isSafeInteger(parsed) && parsed >= SESSION_PAGE_SIZE ? parsed : SESSION_PAGE_SIZE;
2772
3156
  }
2773
3157
  function isPromptCommand(command) {
2774
- return command.type === "ask" || command.type === "prompt" || command.type === "nosteer";
3158
+ return command.type === "prompt" || command.type === "nosteer";
2775
3159
  }
2776
3160
  function runtimePrompt(text, localImagePaths) {
2777
3161
  return localImagePaths?.length ? { text, localImagePaths } : text;
@@ -2783,8 +3167,8 @@ function formatGroupDateSuffix(date) {
2783
3167
  }
2784
3168
  function formatNewGroupName(agentName, projectCwd, taskTitle, date, includeTimestamp) {
2785
3169
  const prefix = `[${agentName}] `;
2786
- const project = formatGroupProjectDirectory(projectCwd);
2787
- const titlePrefix = `${prefix}${project} `;
3170
+ const projectPrefix = projectCwd ? `${formatGroupProjectDirectory(projectCwd)} ` : "";
3171
+ const titlePrefix = `${prefix}${projectPrefix}`;
2788
3172
  const timestampSuffix = includeTimestamp ? ` (${formatGroupDateSuffix(date)})` : "";
2789
3173
  const availableTitleLength = FEISHU_GROUP_NAME_MAX_LENGTH
2790
3174
  - Array.from(titlePrefix).length
@@ -2795,7 +3179,7 @@ function formatNewGroupName(agentName, projectCwd, taskTitle, date, includeTimes
2795
3179
  const FEISHU_GROUP_NAME_MAX_LENGTH = 60;
2796
3180
  const GROUP_PROJECT_DIRECTORY_MAX_LENGTH = 15;
2797
3181
  function formatGroupProjectDirectory(projectCwd) {
2798
- const value = projectCwd ? abbreviateHomeDirectory(projectCwd) : "Projectless";
3182
+ const value = abbreviateHomeDirectory(projectCwd);
2799
3183
  if (Array.from(value).length <= GROUP_PROJECT_DIRECTORY_MAX_LENGTH)
2800
3184
  return `[${value}]`;
2801
3185
  const levels = value