@keyou007/agent-bot 0.1.9 → 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 (119) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/README.md +22 -10
  3. package/README.zh.md +22 -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/cli/FeishuAppConfiguration.js +84 -22
  8. package/dist/cli/FeishuAppConfiguration.js.map +1 -1
  9. package/dist/cli/FeishuAppRegistration.js +12 -9
  10. package/dist/cli/FeishuAppRegistration.js.map +1 -1
  11. package/dist/cli/Initializer.js +7 -6
  12. package/dist/cli/Initializer.js.map +1 -1
  13. package/dist/cli/LocalControlClient.js +3 -2
  14. package/dist/cli/LocalControlClient.js.map +1 -1
  15. package/dist/cli/LocalControlServer.js +2 -1
  16. package/dist/cli/LocalControlServer.js.map +1 -1
  17. package/dist/cli/OptionalAuthorizationSkip.js +27 -6
  18. package/dist/cli/OptionalAuthorizationSkip.js.map +1 -1
  19. package/dist/cli/ServerStarter.js +12 -3
  20. package/dist/cli/ServerStarter.js.map +1 -1
  21. package/dist/cli/SkillRegistry.js +5 -4
  22. package/dist/cli/SkillRegistry.js.map +1 -1
  23. package/dist/cli/VerificationOutput.js +2 -1
  24. package/dist/cli/VerificationOutput.js.map +1 -1
  25. package/dist/cli/help.js +48 -1
  26. package/dist/cli/help.js.map +1 -1
  27. package/dist/cli/i18n.js +27 -0
  28. package/dist/cli/i18n.js.map +1 -0
  29. package/dist/cli/initOptions.js +5 -4
  30. package/dist/cli/initOptions.js.map +1 -1
  31. package/dist/cli/packageVersion.js +2 -1
  32. package/dist/cli/packageVersion.js.map +1 -1
  33. package/dist/cli/profile.js +4 -3
  34. package/dist/cli/profile.js.map +1 -1
  35. package/dist/cli/serverStatus.js +15 -14
  36. package/dist/cli/serverStatus.js.map +1 -1
  37. package/dist/cli/taskChatRoute.js +3 -2
  38. package/dist/cli/taskChatRoute.js.map +1 -1
  39. package/dist/cli.js +165 -107
  40. package/dist/cli.js.map +1 -1
  41. package/dist/codex/AppServerConnection.js +15 -1
  42. package/dist/codex/AppServerConnection.js.map +1 -1
  43. package/dist/codex/CodexLocalActivityDetector.js +4 -1
  44. package/dist/codex/CodexLocalActivityDetector.js.map +1 -1
  45. package/dist/codex/CodexProcessManager.js +1 -0
  46. package/dist/codex/CodexProcessManager.js.map +1 -1
  47. package/dist/codex/CodexRuntime.js +77 -2
  48. package/dist/codex/CodexRuntime.js.map +1 -1
  49. package/dist/commands/CommandRouter.js +108 -47
  50. package/dist/commands/CommandRouter.js.map +1 -1
  51. package/dist/console/ConsoleTurnPresenter.js +1 -1
  52. package/dist/console/ConsoleTurnPresenter.js.map +1 -1
  53. package/dist/feishu/CardRenderer.js +345 -11
  54. package/dist/feishu/CardRenderer.js.map +1 -1
  55. package/dist/feishu/FeishuMessageClient.js +51 -5
  56. package/dist/feishu/FeishuMessageClient.js.map +1 -1
  57. package/dist/feishu/FeishuTurnPresenter.js +8 -3
  58. package/dist/feishu/FeishuTurnPresenter.js.map +1 -1
  59. package/dist/index.js +18 -17
  60. package/dist/index.js.map +1 -1
  61. package/dist/proxy/ProxySessionController.js +595 -205
  62. package/dist/proxy/ProxySessionController.js.map +1 -1
  63. package/dist/runtime/AcpRuntimeAdapter.js +1 -0
  64. package/dist/runtime/AcpRuntimeAdapter.js.map +1 -1
  65. package/dist/runtime/AgentRuntimeRegistry.js +7 -4
  66. package/dist/runtime/AgentRuntimeRegistry.js.map +1 -1
  67. package/dist/runtime/createAgentRuntimeRegistry.js +22 -0
  68. package/dist/runtime/createAgentRuntimeRegistry.js.map +1 -0
  69. package/dist/startup/SessionMetadataHydrator.js +2 -2
  70. package/dist/startup/SessionMetadataHydrator.js.map +1 -1
  71. package/dist/startup/StartupNotifier.js +1 -0
  72. package/dist/startup/StartupNotifier.js.map +1 -1
  73. package/dist/state/StateStore.js +21 -15
  74. package/dist/state/StateStore.js.map +1 -1
  75. package/dist/supervision/SafeRestartScheduler.js +5 -0
  76. package/dist/supervision/SafeRestartScheduler.js.map +1 -1
  77. package/docs/technical-reference.md +27 -6
  78. package/docs/technical-reference.zh.md +27 -6
  79. package/npm-shrinkwrap.json +2 -2
  80. package/package.json +2 -1
  81. package/skills/agent-bot/SKILL.md +19 -9
  82. package/src/acp/AcpSessionManager.ts +73 -54
  83. package/src/cli/FeishuAppConfiguration.ts +130 -18
  84. package/src/cli/FeishuAppRegistration.ts +24 -9
  85. package/src/cli/Initializer.ts +25 -6
  86. package/src/cli/LocalControlClient.ts +9 -2
  87. package/src/cli/LocalControlServer.ts +2 -1
  88. package/src/cli/OptionalAuthorizationSkip.ts +51 -6
  89. package/src/cli/ServerStarter.ts +20 -3
  90. package/src/cli/SkillRegistry.ts +17 -4
  91. package/src/cli/VerificationOutput.ts +4 -1
  92. package/src/cli/help.ts +50 -1
  93. package/src/cli/i18n.ts +38 -0
  94. package/src/cli/initOptions.ts +17 -4
  95. package/src/cli/packageVersion.ts +5 -1
  96. package/src/cli/profile.ts +13 -3
  97. package/src/cli/serverStatus.ts +19 -14
  98. package/src/cli/taskChatRoute.ts +9 -2
  99. package/src/cli.ts +290 -103
  100. package/src/codex/AppServerConnection.ts +21 -1
  101. package/src/codex/CodexLocalActivityDetector.ts +8 -1
  102. package/src/codex/CodexProcessManager.ts +1 -0
  103. package/src/codex/CodexRuntime.ts +91 -2
  104. package/src/commands/CommandRouter.ts +123 -46
  105. package/src/commands/commandTypes.ts +6 -11
  106. package/src/console/ConsoleTurnPresenter.ts +1 -1
  107. package/src/feishu/CardRenderer.ts +418 -11
  108. package/src/feishu/FeishuMessageClient.ts +66 -3
  109. package/src/feishu/FeishuTurnPresenter.ts +7 -2
  110. package/src/index.ts +21 -22
  111. package/src/proxy/ProxySessionController.ts +712 -208
  112. package/src/runtime/AcpRuntimeAdapter.ts +1 -0
  113. package/src/runtime/AgentRuntimeRegistry.ts +7 -6
  114. package/src/runtime/createAgentRuntimeRegistry.ts +25 -0
  115. package/src/runtime/types.ts +18 -0
  116. package/src/startup/SessionMetadataHydrator.ts +2 -2
  117. package/src/startup/StartupNotifier.ts +1 -0
  118. package/src/state/StateStore.ts +29 -15
  119. 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,10 @@ export class ProxySessionController {
1275
1357
  `已从${prepared.sourceDescription}创建分支。`,
1276
1358
  `当前任务:${taskDescription}`,
1277
1359
  `当前 Project 目录:${boundProjectCwd ?? "未绑定(Projectless)"}`,
1360
+ `当前 Provider:${forked.session.modelProvider ?? "Codex 默认"}`,
1361
+ `当前模型:${forked.session.model ?? "默认"}`,
1362
+ `思考强度:${forked.session.reasoningEffort ?? "自动"}`,
1363
+ `权限类型:${forked.session.permissionMode === "confirm" ? "执行前确认" : "自动执行"}`,
1278
1364
  ].join("\n"));
1279
1365
  await this.outbound.sendText(sourceContextKey, `已将${sourceSummary} Fork 到飞书群:${group.name};新群当前任务为 ${taskDescription}。`);
1280
1366
  }
@@ -1297,7 +1383,7 @@ export class ProxySessionController {
1297
1383
  }
1298
1384
  async loadSession(record) {
1299
1385
  const agent = this.ensureAgent(record.agentName);
1300
- const runtime = this.runtimes.forAgent(agent);
1386
+ const runtime = this.runtimes.forAgent(record.agentName);
1301
1387
  const existing = runtime.getSession(record.localSessionId);
1302
1388
  if (existing)
1303
1389
  return { record, runtime, session: existing };
@@ -1326,6 +1412,7 @@ export class ProxySessionController {
1326
1412
  agentName: record.agentName,
1327
1413
  cwd: record.cwd,
1328
1414
  title: record.title,
1415
+ modelProvider: record.modelProvider,
1329
1416
  model: record.model,
1330
1417
  reasoningEffort: record.reasoningEffort,
1331
1418
  permissionMode,
@@ -1345,6 +1432,7 @@ export class ProxySessionController {
1345
1432
  agentName: record.agentName,
1346
1433
  cwd: record.cwd,
1347
1434
  title: record.title,
1435
+ modelProvider: record.modelProvider,
1348
1436
  model: record.model,
1349
1437
  reasoningEffort: record.reasoningEffort,
1350
1438
  permissionMode,
@@ -1357,6 +1445,7 @@ export class ProxySessionController {
1357
1445
  agentName: record.agentName,
1358
1446
  cwd: record.cwd,
1359
1447
  title: record.title,
1448
+ modelProvider: record.modelProvider,
1360
1449
  model: record.model,
1361
1450
  reasoningEffort: record.reasoningEffort,
1362
1451
  permissionMode,
@@ -1379,6 +1468,7 @@ export class ProxySessionController {
1379
1468
  runtimeKind: session.runtimeKind,
1380
1469
  remoteSessionId: session.remoteSessionId,
1381
1470
  title: session.title,
1471
+ modelProvider: session.modelProvider,
1382
1472
  model: session.model,
1383
1473
  reasoningEffort: session.reasoningEffort,
1384
1474
  permissionMode: session.permissionMode,
@@ -1404,38 +1494,36 @@ export class ProxySessionController {
1404
1494
  lastTurnStatus: event.type === "turn_completed" ? "completed" : event.type === "turn_cancelled" ? "cancelled" : "failed",
1405
1495
  });
1406
1496
  }
1497
+ let presentationError;
1407
1498
  try {
1408
1499
  await this.outbound.onEvent(event);
1409
- if (event.type === "turn_completed" || event.type === "turn_cancelled" || event.type === "turn_failed") {
1410
- const terminalStatus = event.type === "turn_completed"
1411
- ? "completed"
1412
- : event.type === "turn_cancelled"
1413
- ? "cancelled"
1414
- : "failed";
1415
- const latest = this.store.getSession(event.sessionId);
1416
- if (latest?.lastTurnId === event.turnId && latest.lastTurnStatus === terminalStatus) {
1417
- await this.finalizeTurnMessageReactions(event.turnId, terminalStatus);
1418
- }
1419
- }
1420
1500
  }
1421
- finally {
1422
- if (event.type === "turn_completed" || event.type === "turn_cancelled" || event.type === "turn_failed") {
1423
- const latest = this.store.getSession(event.sessionId);
1424
- const agent = latest ? this.ensureAgent(latest.agentName) : undefined;
1425
- const activeTurnId = agent ? this.runtimes.forAgent(agent).getSession(event.sessionId)?.activeTurnId : undefined;
1426
- if (latest?.lastTurnId === event.turnId && !activeTurnId) {
1427
- this.store.updateSession(event.sessionId, { status: event.type === "turn_failed" ? "failed" : "ready" });
1428
- }
1429
- }
1501
+ catch (error) {
1502
+ presentationError = error;
1430
1503
  }
1431
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
+ }
1432
1516
  const latest = this.store.getSession(event.sessionId);
1433
- const agent = latest ? this.ensureAgent(latest.agentName) : undefined;
1434
- const activeTurnId = agent ? this.runtimes.forAgent(agent).getSession(event.sessionId)?.activeTurnId : undefined;
1435
- if (latest?.lastTurnId !== event.turnId || activeTurnId)
1436
- return;
1437
- 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
+ }
1438
1524
  }
1525
+ if (presentationError)
1526
+ throw presentationError;
1439
1527
  }
1440
1528
  scheduleNextQueuedPrompt(sessionId) {
1441
1529
  const previous = this.queuedPromptStarts.get(sessionId) ?? Promise.resolve();
@@ -1530,7 +1618,7 @@ export class ProxySessionController {
1530
1618
  await this.cancelSession(record);
1531
1619
  }
1532
1620
  async cancelSession(record) {
1533
- const runtime = this.runtimes.forAgent(this.ensureAgent(record.agentName));
1621
+ const runtime = this.runtimes.forAgent(record.agentName);
1534
1622
  if (runtime.kind === "codex" && record.remoteSessionId && runtime.interruptRemoteTurn) {
1535
1623
  if (runtime.getGoal && runtime.setGoal) {
1536
1624
  try {
@@ -1587,29 +1675,237 @@ export class ProxySessionController {
1587
1675
  const loaded = await this.loadSession(this.requireSession(action.contextKey, sessionId));
1588
1676
  await loaded.runtime.respondToApproval(sessionId, requestId, decision);
1589
1677
  }
1590
- 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 = {}) {
1591
1758
  const record = options.sessionId
1592
1759
  ? this.requireSession(contextKey, options.sessionId)
1593
1760
  : this.requireCurrentSession(contextKey);
1594
1761
  const loaded = await this.loadSession(record);
1762
+ await this.assertModelProvider(loaded, modelProvider);
1595
1763
  const models = await loaded.runtime.listModels();
1596
- if (!model) {
1597
- const currentModel = loaded.session.model ?? models.find((item) => item.isDefault)?.id;
1598
- 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, {
1599
1863
  sessionId: loaded.record.localSessionId,
1600
- contextKey,
1601
- currentModel,
1602
- reasoningEffort: loaded.session.reasoningEffort,
1603
- models,
1864
+ updateMessageId: options.updateMessageId,
1865
+ notice,
1604
1866
  });
1605
- if (options.updateMessageId) {
1606
- await this.outbound.updateInteractiveCard(contextKey, options.updateMessageId, card);
1607
- }
1608
- else {
1609
- await this.outbound.sendInteractiveCard(contextKey, card);
1610
- }
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);
1611
1902
  return;
1612
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();
1613
1909
  const selected = models.find((item) => item.id === model);
1614
1910
  if (!selected)
1615
1911
  throw new Error(`未知模型:${model}`);
@@ -1627,14 +1923,11 @@ export class ProxySessionController {
1627
1923
  ? `,思考强度已自动调整为 ${nextEffort}`
1628
1924
  : "";
1629
1925
  if (options.updateMessageId) {
1630
- await this.outbound.updateInteractiveCard(contextKey, options.updateMessageId, this.cardRenderer.renderReasoningSelector({
1926
+ await this.openExecutionSettings(contextKey, "model", {
1631
1927
  sessionId: loaded.record.localSessionId,
1632
- contextKey,
1633
- model,
1634
- currentEffort: nextEffort,
1635
- options: selected.supportedReasoningEfforts,
1636
- notice: `模型已切换为 ${cardCode(model)},请选择思考模式。`,
1637
- }));
1928
+ updateMessageId: options.updateMessageId,
1929
+ notice: `模型已切换为 ${cardCode(model)}${effortMessage},从下一次请求生效。`,
1930
+ });
1638
1931
  return;
1639
1932
  }
1640
1933
  await this.outbound.sendText(contextKey, `模型已切换为 ${model}${effortMessage},从下一次请求生效。`);
@@ -1782,6 +2075,10 @@ export class ProxySessionController {
1782
2075
  });
1783
2076
  }
1784
2077
  async thinking(contextKey, effort, options = {}) {
2078
+ if (!effort) {
2079
+ await this.openExecutionSettings(contextKey, "thinking", options);
2080
+ return;
2081
+ }
1785
2082
  const record = options.sessionId
1786
2083
  ? this.requireSession(contextKey, options.sessionId)
1787
2084
  : this.requireCurrentSession(contextKey);
@@ -1795,22 +2092,6 @@ export class ProxySessionController {
1795
2092
  throw new Error("模型已发生变化,请重新打开 /model。");
1796
2093
  }
1797
2094
  const supported = currentModel.supportedReasoningEfforts;
1798
- if (!effort) {
1799
- const card = this.cardRenderer.renderReasoningSelector({
1800
- sessionId: loaded.record.localSessionId,
1801
- contextKey,
1802
- model: currentModel.id,
1803
- currentEffort: loaded.session.reasoningEffort ?? currentModel.defaultReasoningEffort,
1804
- options: supported,
1805
- });
1806
- if (options.updateMessageId) {
1807
- await this.outbound.updateInteractiveCard(contextKey, options.updateMessageId, card);
1808
- }
1809
- else {
1810
- await this.outbound.sendInteractiveCard(contextKey, card);
1811
- }
1812
- return;
1813
- }
1814
2095
  if (!supported.some((option) => option.value === effort)) {
1815
2096
  const options = supported.map((option) => option.value).join("、") || "无";
1816
2097
  throw new Error(`不支持的思考强度:${effort}。支持的强度:${options}`);
@@ -1818,79 +2099,85 @@ export class ProxySessionController {
1818
2099
  await loaded.runtime.setReasoningEffort(loaded.record.localSessionId, effort);
1819
2100
  this.store.updateRuntimeSession(loaded.record.localSessionId, { reasoningEffort: effort });
1820
2101
  if (options.updateMessageId) {
1821
- await this.outbound.updateInteractiveCard(contextKey, options.updateMessageId, this.cardRenderer.renderReasoningSelector({
2102
+ await this.openExecutionSettings(contextKey, "thinking", {
1822
2103
  sessionId: loaded.record.localSessionId,
1823
- contextKey,
1824
- model: currentModel.id,
1825
- currentEffort: effort,
1826
- options: supported,
1827
- notice: `思考模式已切换为 ${cardCode(effort)},从下一次请求生效。`,
1828
- }));
2104
+ updateMessageId: options.updateMessageId,
2105
+ notice: `思考强度已切换为 ${cardCode(effort)},从下一次请求生效。`,
2106
+ });
1829
2107
  return;
1830
2108
  }
1831
2109
  await this.outbound.sendText(contextKey, `思考强度已切换为 ${effort},从下一次请求生效。`);
1832
2110
  }
1833
- async permissions(contextKey, mode) {
1834
- const record = this.requireCurrentSession(contextKey);
2111
+ async permissions(contextKey, mode, options = {}) {
1835
2112
  if (!mode) {
1836
- await this.outbound.sendText(contextKey, `当前权限模式:${record.permissionMode ?? "auto"}`);
2113
+ await this.openExecutionSettings(contextKey, "permission", options);
1837
2114
  return;
1838
2115
  }
2116
+ const record = options.sessionId
2117
+ ? this.requireSession(contextKey, options.sessionId)
2118
+ : this.requireCurrentSession(contextKey);
1839
2119
  const loaded = await this.loadSession(record);
1840
2120
  await loaded.runtime.setPermissionMode(record.localSessionId, mode);
2121
+ loaded.session.permissionMode = mode;
1841
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
+ }
1842
2133
  await this.outbound.sendText(contextKey, mode === "auto" ? "已切换为自动执行模式。" : "已切换为执行前确认模式。");
1843
2134
  }
1844
- async listAgents(contextKey) {
1845
- const current = this.store.getOrCreateUserContext(contextKey, this.config.defaults.agent).defaultAgent;
1846
- const lines = Object.entries(this.config.agents).map(([name, agent]) => {
1847
- const selected = name === current;
1848
- return `- ${selected ? "✅ " : ""}${asInlineCode(name)}:${agent.title}${selected ? "(当前)" : ""}`;
1849
- });
1850
- await this.outbound.sendMarkdown(contextKey, [
1851
- `当前 Agent:${asInlineCode(current)}`,
1852
- "可用 Agent:",
1853
- lines.join("\n") || "无",
1854
- ].join("\n"));
1855
- }
1856
2135
  async listSessions(contextKey, searchTerm, options = {}) {
1857
2136
  const context = this.store.getOrCreateUserContext(contextKey, this.config.defaults.agent);
1858
2137
  const normalizedSearch = searchTerm?.trim().toLowerCase();
1859
2138
  const visibleCount = options.visibleCount ?? SESSION_PAGE_SIZE;
1860
2139
  const localSessions = this.store.listSessions(contextKey).filter((session) => session.status !== "closed" && this.isCodexSession(session));
1861
- let remoteSessions = [];
1862
- let remoteHint;
2140
+ const remoteSessions = [];
2141
+ const remoteErrors = [];
1863
2142
  let remoteHasMore = false;
1864
- const runtime = this.runtimes.get("codex");
1865
- if (runtime.listRemoteSessions) {
2143
+ await Promise.all(this.runtimes.entries("codex").map(async ([agentName, runtime]) => {
2144
+ if (!runtime.listRemoteSessions)
2145
+ return;
1866
2146
  try {
1867
2147
  const page = await runtime.listRemoteSessions({ searchTerm, limit: visibleCount });
1868
- remoteSessions = page.sessions;
1869
- remoteHasMore = Boolean(page.nextCursor);
2148
+ remoteSessions.push(...page.sessions.map((session) => ({ agentName, session })));
2149
+ remoteHasMore ||= Boolean(page.nextCursor);
1870
2150
  }
1871
2151
  catch (error) {
1872
- this.logger.warn({ error, contextKey }, "Failed to list Codex sessions.");
1873
- 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)}`);
1874
2154
  }
1875
- }
2155
+ }));
2156
+ const remoteHint = remoteErrors.length > 0
2157
+ ? `部分 Agent 的任务读取失败:${remoteErrors.join(";")}`
2158
+ : undefined;
1876
2159
  const entries = mergeTaskList(localSessions, remoteSessions, context.currentSessionId)
1877
- .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)))
1878
2163
  .sort((left, right) => Number(right.current) - Number(left.current)
1879
2164
  || Number(right.active) - Number(left.active)
1880
2165
  || right.updatedAt - left.updatedAt);
1881
2166
  const activeCount = entries.filter((entry) => entry.active).length;
1882
2167
  const visibleEntries = entries.slice(0, visibleCount);
1883
2168
  const hasMore = remoteHasMore || entries.length > visibleCount;
1884
- this.lastSessionListings.set(contextKey, visibleEntries.map((entry) => entry.id));
2169
+ this.lastSessionListings.set(contextKey, visibleEntries.map((entry) => entry.reference));
1885
2170
  const cardEntries = visibleEntries.map((entry, index) => {
1886
2171
  const marker = entry.current ? "✅" : entry.active ? "🟢 **活跃**" : "•";
1887
- const showStop = entry.status === "外部执行中" && entry.id !== options.forceSwitchTaskId;
2172
+ const showStop = entry.status === "外部执行中"
2173
+ && entry.reference !== options.forceSwitchTaskId
2174
+ && entry.id !== options.forceSwitchTaskId;
1888
2175
  const actions = entry.current ? [] : [{
1889
2176
  text: showStop ? "Stop" : "Switch",
1890
2177
  type: showStop ? "danger" : "default",
1891
2178
  value: {
1892
2179
  action: showStop ? "session_stop" : "session_switch",
1893
- sessionId: entry.id,
2180
+ sessionId: entry.reference,
1894
2181
  ...(searchTerm ? { searchTerm } : {}),
1895
2182
  visibleCount: String(visibleCount),
1896
2183
  contextKey,
@@ -1900,7 +2187,7 @@ export class ProxySessionController {
1900
2187
  text: "New",
1901
2188
  value: {
1902
2189
  action: "session_new",
1903
- sessionId: entry.id,
2190
+ sessionId: entry.reference,
1904
2191
  ...(searchTerm ? { searchTerm } : {}),
1905
2192
  visibleCount: String(visibleCount),
1906
2193
  contextKey,
@@ -1910,7 +2197,7 @@ export class ProxySessionController {
1910
2197
  text: "NewGroup",
1911
2198
  value: {
1912
2199
  action: "session_new_group",
1913
- sessionId: entry.id,
2200
+ sessionId: entry.reference,
1914
2201
  ...(searchTerm ? { searchTerm } : {}),
1915
2202
  visibleCount: String(visibleCount),
1916
2203
  contextKey,
@@ -1920,7 +2207,7 @@ export class ProxySessionController {
1920
2207
  text: "Fork",
1921
2208
  value: {
1922
2209
  action: "session_fork",
1923
- sessionId: entry.id,
2210
+ sessionId: entry.reference,
1924
2211
  ...(searchTerm ? { searchTerm } : {}),
1925
2212
  visibleCount: String(visibleCount),
1926
2213
  contextKey,
@@ -1930,7 +2217,7 @@ export class ProxySessionController {
1930
2217
  text: "ForkGroup",
1931
2218
  value: {
1932
2219
  action: "session_fork_group",
1933
- sessionId: entry.id,
2220
+ sessionId: entry.reference,
1934
2221
  ...(searchTerm ? { searchTerm } : {}),
1935
2222
  visibleCount: String(visibleCount),
1936
2223
  contextKey,
@@ -1940,14 +2227,14 @@ export class ProxySessionController {
1940
2227
  text: "Status",
1941
2228
  value: {
1942
2229
  action: "session_status",
1943
- sessionId: entry.id,
2230
+ sessionId: entry.reference,
1944
2231
  contextKey,
1945
2232
  },
1946
2233
  });
1947
2234
  return {
1948
2235
  lines: [
1949
2236
  `**${index + 1}.** ${marker} **${cardText(entry.title)}** ${cardText(entry.id)}`,
1950
- `${entry.status} · ${entry.updatedLabel} · ${cardText(entry.cwd || "目录未知")}`,
2237
+ `${entry.status} · ${entry.updatedLabel} · ${cardText(entry.agentName)} · ${cardText(entry.cwd || "目录未知")}`,
1951
2238
  ],
1952
2239
  actions,
1953
2240
  };
@@ -2000,6 +2287,45 @@ export class ProxySessionController {
2000
2287
  forceSwitchTaskId,
2001
2288
  });
2002
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
+ }
2003
2329
  async switchSession(contextKey, reference) {
2004
2330
  this.store.getOrCreateUserContext(contextKey, this.config.defaults.agent);
2005
2331
  const taskId = this.resolveSessionReference(contextKey, reference);
@@ -2007,9 +2333,9 @@ export class ProxySessionController {
2007
2333
  if (direct?.status === "closed") {
2008
2334
  throw new Error(`找不到任务:${taskId}`);
2009
2335
  }
2010
- const existing = direct ?? this.store.findSessionByRemoteSessionId(taskId);
2336
+ const existing = direct ?? this.findStoredSessionByReference(taskId);
2011
2337
  if (existing) {
2012
- const runtime = this.runtimes.forAgent(this.ensureAgent(existing.agentName));
2338
+ const runtime = this.runtimes.forAgent(existing.agentName);
2013
2339
  await this.assertSessionTurnOwnership(existing, runtime);
2014
2340
  this.store.attachSessionToContext(contextKey, existing.localSessionId);
2015
2341
  this.store.setCurrentSession(contextKey, existing.localSessionId);
@@ -2017,19 +2343,12 @@ export class ProxySessionController {
2017
2343
  await this.outbound.sendText(contextKey, `已切换到任务:${existing.title ?? existing.remoteSessionId ?? taskId}`);
2018
2344
  return;
2019
2345
  }
2020
- const runtime = this.runtimes.get("codex");
2021
- if (!runtime.readRemoteSession)
2022
- throw new Error("当前 Codex 运行时不支持读取任务。");
2023
- const remote = await runtime.readRemoteSession(taskId);
2346
+ const { agentName, remote } = await this.resolveRemoteCodexSession(taskId);
2024
2347
  if (remote.status === "active" || remote.lastTurnStatus === "inProgress") {
2025
2348
  throw new Error(`这个任务正在外部 Codex 中执行,当前不会切换。可使用 /status ${taskId} 查看进度。`);
2026
2349
  }
2027
2350
  if (!remote.cwd)
2028
2351
  throw new Error("这个 Codex 任务没有可用的工作目录,暂时无法切换。");
2029
- const agentEntry = Object.entries(this.config.agents).find(([, agent]) => agent.kind === "codex");
2030
- if (!agentEntry)
2031
- throw new Error("未配置 Codex Agent。");
2032
- const [agentName] = agentEntry;
2033
2352
  const localSessionId = createId("sess");
2034
2353
  this.store.createSession({
2035
2354
  localSessionId,
@@ -2058,16 +2377,15 @@ export class ProxySessionController {
2058
2377
  await this.cancelSession({ ...direct, contextKey });
2059
2378
  return;
2060
2379
  }
2061
- const existing = this.store.findSessionByRemoteSessionId(taskId);
2380
+ const existing = this.findStoredSessionByReference(taskId);
2062
2381
  if (existing) {
2063
2382
  await this.cancelSession({ ...existing, contextKey });
2064
2383
  return;
2065
2384
  }
2066
- const runtime = this.runtimes.get("codex");
2385
+ const { runtime, remote } = await this.resolveRemoteCodexSession(taskId);
2067
2386
  if (!runtime.readRemoteSession || !runtime.interruptRemoteTurn) {
2068
2387
  throw new Error("当前 Codex 运行时不支持停止外部任务。");
2069
2388
  }
2070
- const remote = await runtime.readRemoteSession(taskId);
2071
2389
  const turnId = remote.status === "active" || remote.lastTurnStatus === "inProgress"
2072
2390
  ? remote.lastTurnId
2073
2391
  : undefined;
@@ -2144,11 +2462,18 @@ export class ProxySessionController {
2144
2462
  }
2145
2463
  return undefined;
2146
2464
  }
2147
- async setDefaultAgent(contextKey, agentName) {
2465
+ async setDefaultAgent(contextKey, agentName, options = {}) {
2148
2466
  this.ensureAgent(agentName);
2149
2467
  this.store.getOrCreateUserContext(contextKey, this.config.defaults.agent);
2150
2468
  this.store.setDefaultAgent(contextKey, agentName);
2151
- 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
+ });
2152
2477
  }
2153
2478
  async status(contextKey, sessionId, options = {}) {
2154
2479
  const context = this.store.getOrCreateUserContext(contextKey, this.config.defaults.agent);
@@ -2158,7 +2483,7 @@ export class ProxySessionController {
2158
2483
  const direct = this.store.getSession(targetSessionId);
2159
2484
  current = direct
2160
2485
  ? { ...direct, contextKey }
2161
- : this.store.findSessionByRemoteSessionId(targetSessionId, contextKey);
2486
+ : this.findStoredSessionByReference(targetSessionId, contextKey);
2162
2487
  if (!current) {
2163
2488
  await this.statusForCodexTask(contextKey, targetSessionId, options);
2164
2489
  return;
@@ -2169,7 +2494,7 @@ export class ProxySessionController {
2169
2494
  }
2170
2495
  if (current && current.status === "running") {
2171
2496
  try {
2172
- const runtime = this.runtimes.forAgent(this.ensureAgent(current.agentName));
2497
+ const runtime = this.runtimes.forAgent(current.agentName);
2173
2498
  if (runtime.getSession(current.localSessionId)) {
2174
2499
  await runtime.synchronizeSession(current.localSessionId);
2175
2500
  current = this.store.getSession(current.localSessionId) ?? current;
@@ -2183,7 +2508,7 @@ export class ProxySessionController {
2183
2508
  let remote;
2184
2509
  let goal;
2185
2510
  if (current?.remoteSessionId) {
2186
- const runtime = this.runtimes.forAgent(this.ensureAgent(current.agentName));
2511
+ const runtime = this.runtimes.forAgent(current.agentName);
2187
2512
  if (runtime.readRemoteSession) {
2188
2513
  try {
2189
2514
  remote = await runtime.readRemoteSession(current.remoteSessionId);
@@ -2213,7 +2538,7 @@ export class ProxySessionController {
2213
2538
  }
2214
2539
  else {
2215
2540
  const agent = this.ensureAgent(current.agentName);
2216
- const runtimeSession = this.runtimes.forAgent(agent).getSession(current.localSessionId);
2541
+ const runtimeSession = this.runtimes.forAgent(current.agentName).getSession(current.localSessionId);
2217
2542
  activeTurnId = runtimeSession?.activeTurnId;
2218
2543
  const remoteActive = isRemoteSessionActive(remote);
2219
2544
  activeTurnId = remoteActive ? remote?.lastTurnId ?? activeTurnId : activeTurnId;
@@ -2225,7 +2550,7 @@ export class ProxySessionController {
2225
2550
  const resultLabel = remoteActive
2226
2551
  ? "执行中"
2227
2552
  : remoteTurnStatusLabel(remote?.lastTurnStatus ?? current.lastTurnStatus);
2228
- 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)}`);
2229
2554
  }
2230
2555
  const sections = [
2231
2556
  { title: targetSessionId ? "指定任务" : "当前任务", lines: taskLines },
@@ -2254,12 +2579,15 @@ export class ProxySessionController {
2254
2579
  const title = targetSessionId && current
2255
2580
  ? `Codex 状态:${truncateText((current.title ?? current.remoteSessionId ?? current.localSessionId).replace(/\s+/g, " "), 40)}`
2256
2581
  : "Codex 状态";
2257
- const taskId = current?.remoteSessionId ?? current?.localSessionId;
2582
+ const taskId = current?.remoteSessionId
2583
+ ? remoteSessionReference(current.agentName, current.remoteSessionId)
2584
+ : current?.localSessionId;
2258
2585
  const isCurrent = Boolean(current && current.localSessionId === context.currentSessionId);
2259
2586
  const remoteActive = isRemoteSessionActive(remote);
2260
2587
  const botOwnsActiveTurn = Boolean(localCurrent && remote && isBotOwnedActiveTurn(localCurrent, remote));
2261
2588
  const active = Boolean(activeTurnId || current?.status === "running" || remoteActive);
2262
- const forceSwitch = Boolean(taskId && options.forceSwitchTaskId === taskId);
2589
+ const forceSwitch = Boolean(taskId && (options.forceSwitchTaskId === taskId
2590
+ || options.forceSwitchTaskId === current?.remoteSessionId));
2263
2591
  const taskActions = !taskId ? []
2264
2592
  : remoteActive && !botOwnsActiveTurn && !forceSwitch
2265
2593
  ? [statusCardAction("Stop", "danger", "session_stop", taskId, contextKey)]
@@ -2278,11 +2606,9 @@ export class ProxySessionController {
2278
2606
  else
2279
2607
  await this.outbound.sendInteractiveCard(contextKey, card);
2280
2608
  }
2281
- async statusForCodexTask(contextKey, remoteSessionId, options = {}) {
2282
- const runtime = this.runtimes.get("codex");
2283
- if (!runtime.readRemoteSession)
2284
- throw new Error("当前 Codex 运行时不支持读取外部任务状态。");
2285
- 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);
2286
2612
  const sections = [
2287
2613
  {
2288
2614
  title: "指定任务",
@@ -2290,6 +2616,7 @@ export class ProxySessionController {
2290
2616
  `**标题**:${cardCode(remote.title ?? remote.preview ?? "未命名任务")}`,
2291
2617
  `**工作目录**:${cardCode(remote.cwd || "目录未知")}`,
2292
2618
  `**状态 / 当前任务**:${remoteSessionDetailStatus(remote)} / 未切换`,
2619
+ `**Agent**:${cardCode(agentName)}`,
2293
2620
  `**Codex 任务 ID**:${cardCode(remote.id)}`,
2294
2621
  `**最近回合**:${cardCode(remote.lastTurnId ?? "无")} ${remoteTurnStatusLabel(remote.lastTurnStatus)}`,
2295
2622
  `**创建时间 / 最近活动**:${formatRemoteTime(remote.createdAt)} / ${formatRemoteTime(latestRemoteTimestamp(remote.recencyAt, remote.updatedAt))}`,
@@ -2309,10 +2636,12 @@ export class ProxySessionController {
2309
2636
  },
2310
2637
  ];
2311
2638
  const title = `Codex 状态:${truncateText((remote.title ?? remote.preview ?? remote.id).replace(/\s+/g, " "), 40)}`;
2312
- const showStop = isRemoteSessionActive(remote) && options.forceSwitchTaskId !== remote.id;
2639
+ const showStop = isRemoteSessionActive(remote)
2640
+ && options.forceSwitchTaskId !== actionReference
2641
+ && options.forceSwitchTaskId !== remote.id;
2313
2642
  const actions = [
2314
- statusRefreshAction(remote.id, contextKey),
2315
- 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),
2316
2645
  ];
2317
2646
  const card = this.cardRenderer.renderSectionsCard(title, sections, actions);
2318
2647
  if (options.updateMessageId)
@@ -2325,6 +2654,9 @@ export class ProxySessionController {
2325
2654
  {
2326
2655
  lines: [
2327
2656
  "直接发送消息即可继续当前任务;执行中发送的新消息会追加到本次任务。",
2657
+ "> 命令前缀示例:/sess 等同于 /sessions。",
2658
+ "> 命令缩写示例:/fg 等同于 /forkgroup。",
2659
+ "前缀或缩写匹配多个命令时,需要输入更长的形式。",
2328
2660
  "**[参数]** 可选 **&#60;参数&#62;** 必填",
2329
2661
  ],
2330
2662
  },
@@ -2332,7 +2664,7 @@ export class ProxySessionController {
2332
2664
  title: "任务管理",
2333
2665
  lines: [
2334
2666
  "**/new [title] [--dir &#60;cwd&#62; | --nodir]** 使用默认 Agent 创建任务;--nodir 强制创建 Projectless 任务",
2335
- "**/newgroup [title]** 创建飞书群和新任务;继承当前项目与执行设置",
2667
+ "**/newgroup [title] [--dir &#60;cwd&#62; | --nodir]** 创建飞书群和新任务;参数与 /new 相同",
2336
2668
  "**/forkgroup [title]** 从当前任务最新已完成轮次创建分支并绑定到新群",
2337
2669
  "**/fork [序号或任务 ID]** 从当前或指定任务创建分支;运行中使用最近已完成轮次",
2338
2670
  "**/title &#60;新标题&#62;** 修改当前任务的标题",
@@ -2347,22 +2679,23 @@ export class ProxySessionController {
2347
2679
  "**/stop** 停止当前执行",
2348
2680
  "**/queue &#60;prompt&#62;**(兼容 **/nosteer**) 不追加到当前轮次,按顺序排队为后续轮次",
2349
2681
  "**/goal [目标]** 查看或创建长任务 Goal;支持 pause、resume、edit、clear",
2350
- "**/model [name]** 查看或切换模型",
2351
- "**/thinking [level]** 查看或设置思考强度",
2352
- "**/permissions [auto|confirm]** 查看或切换确认模式",
2682
+ "**/provider** 选择 AI 服务提供商;没有其他候选时仅显示当前 Provider",
2683
+ "**/model** 选择当前任务使用的模型",
2684
+ "**/thinking** 设置模型的思考强度",
2685
+ "**/permissions** 设置执行工具前是否需要确认",
2353
2686
  ],
2354
2687
  },
2355
2688
  {
2356
2689
  title: "Agent",
2357
2690
  lines: [
2358
- "**/agent [name]** 查看或设置新任务的默认 Agent",
2691
+ "**/agent [name]** 选择新任务使用的默认 Agent;仅配置一个时直接显示当前 Agent",
2359
2692
  ],
2360
2693
  },
2361
2694
  {
2362
2695
  title: "系统",
2363
2696
  lines: [
2364
2697
  "**/status [序号或任务 ID]** 查看当前或指定任务的详细状态",
2365
- "**/restart** 重启 Agent Bot 并恢复会话",
2698
+ "**/restart [--force]** 默认安全重启;--force 立即重启",
2366
2699
  "**/help** 显示本帮助",
2367
2700
  ],
2368
2701
  },
@@ -2394,6 +2727,17 @@ export class ProxySessionController {
2394
2727
  ? createProjectlessWorkspace().cwd
2395
2728
  : current.cwd;
2396
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
+ }
2397
2741
  currentProjectCwd(contextKey) {
2398
2742
  const current = this.currentSession(contextKey);
2399
2743
  return current && !detectProjectlessWorkspace(current.cwd) ? current.cwd : undefined;
@@ -2448,10 +2792,10 @@ function statusRefreshAction(sessionId, contextKey) {
2448
2792
  function mergeTaskList(localSessions, remoteSessions, currentLocalSessionId) {
2449
2793
  const localByRemoteId = new Map(localSessions
2450
2794
  .filter((session) => session.runtimeKind === "codex" && session.remoteSessionId)
2451
- .map((session) => [session.remoteSessionId, session]));
2795
+ .map((session) => [agentRemoteKey(session.agentName, session.remoteSessionId), session]));
2452
2796
  const representedLocalIds = new Set();
2453
- const entries = remoteSessions.map((remote) => {
2454
- const local = localByRemoteId.get(remote.id);
2797
+ const entries = remoteSessions.map(({ agentName, session: remote }) => {
2798
+ const local = localByRemoteId.get(agentRemoteKey(agentName, remote.id));
2455
2799
  if (local)
2456
2800
  representedLocalIds.add(local.localSessionId);
2457
2801
  const active = remote.status === "active" || remote.lastTurnStatus === "inProgress";
@@ -2460,7 +2804,9 @@ function mergeTaskList(localSessions, remoteSessions, currentLocalSessionId) {
2460
2804
  : remoteSessionStatusLabel(remote.status);
2461
2805
  const recencyAt = remote.recencyAt ?? remote.updatedAt;
2462
2806
  return {
2807
+ reference: remoteSessionReference(agentName, remote.id),
2463
2808
  id: remote.id,
2809
+ agentName,
2464
2810
  title: remote.title ?? remote.preview ?? local?.title ?? "未命名任务",
2465
2811
  cwd: remote.cwd || local?.cwd || "",
2466
2812
  status,
@@ -2475,7 +2821,11 @@ function mergeTaskList(localSessions, remoteSessions, currentLocalSessionId) {
2475
2821
  continue;
2476
2822
  const updatedAt = Date.parse(local.updatedAt);
2477
2823
  entries.push({
2824
+ reference: local.remoteSessionId
2825
+ ? remoteSessionReference(local.agentName, local.remoteSessionId)
2826
+ : local.localSessionId,
2478
2827
  id: local.remoteSessionId ?? local.localSessionId,
2828
+ agentName: local.agentName,
2479
2829
  title: local.title ?? "未命名任务",
2480
2830
  cwd: local.cwd,
2481
2831
  status: sessionStatusLabel(local.status),
@@ -2487,6 +2837,31 @@ function mergeTaskList(localSessions, remoteSessions, currentLocalSessionId) {
2487
2837
  }
2488
2838
  return entries;
2489
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
+ }
2490
2865
  function isMissingRolloutError(error) {
2491
2866
  const message = error instanceof Error ? error.message : String(error);
2492
2867
  return /no rollout found|rollout[^\n]*(?:not found|missing)|thread\/resume failed/i.test(message);
@@ -2665,6 +3040,21 @@ function cardText(value) {
2665
3040
  function cardCode(value) {
2666
3041
  return `\`${value.replaceAll("`", "'")}\``;
2667
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
+ }
2668
3058
  function remoteSessionStatusLabel(status) {
2669
3059
  const labels = {
2670
3060
  active: "执行中",
@@ -2701,6 +3091,10 @@ function mergeRemoteTaskStatus(record, remote) {
2701
3091
  ...record,
2702
3092
  title: remote.title ?? record.title,
2703
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,
2704
3098
  status: isRemoteSessionActive(remote)
2705
3099
  ? "running"
2706
3100
  : remote.lastTurnStatus
@@ -2752,12 +3146,8 @@ function isQueueIndependentCommand(command) {
2752
3146
  return true;
2753
3147
  if (command.type === "agent")
2754
3148
  return command.agent === undefined;
2755
- if (command.type === "model")
2756
- return command.model === undefined;
2757
- if (command.type === "thinking")
2758
- return command.effort === undefined;
2759
- if (command.type === "permissions")
2760
- return command.mode === undefined;
3149
+ if (["model", "provider", "thinking", "permissions"].includes(command.type))
3150
+ return true;
2761
3151
  return false;
2762
3152
  }
2763
3153
  function parseSessionVisibleCount(value) {
@@ -2765,7 +3155,7 @@ function parseSessionVisibleCount(value) {
2765
3155
  return Number.isSafeInteger(parsed) && parsed >= SESSION_PAGE_SIZE ? parsed : SESSION_PAGE_SIZE;
2766
3156
  }
2767
3157
  function isPromptCommand(command) {
2768
- return command.type === "ask" || command.type === "prompt" || command.type === "nosteer";
3158
+ return command.type === "prompt" || command.type === "nosteer";
2769
3159
  }
2770
3160
  function runtimePrompt(text, localImagePaths) {
2771
3161
  return localImagePaths?.length ? { text, localImagePaths } : text;
@@ -2777,8 +3167,8 @@ function formatGroupDateSuffix(date) {
2777
3167
  }
2778
3168
  function formatNewGroupName(agentName, projectCwd, taskTitle, date, includeTimestamp) {
2779
3169
  const prefix = `[${agentName}] `;
2780
- const project = formatGroupProjectDirectory(projectCwd);
2781
- const titlePrefix = `${prefix}${project} `;
3170
+ const projectPrefix = projectCwd ? `${formatGroupProjectDirectory(projectCwd)} ` : "";
3171
+ const titlePrefix = `${prefix}${projectPrefix}`;
2782
3172
  const timestampSuffix = includeTimestamp ? ` (${formatGroupDateSuffix(date)})` : "";
2783
3173
  const availableTitleLength = FEISHU_GROUP_NAME_MAX_LENGTH
2784
3174
  - Array.from(titlePrefix).length
@@ -2789,7 +3179,7 @@ function formatNewGroupName(agentName, projectCwd, taskTitle, date, includeTimes
2789
3179
  const FEISHU_GROUP_NAME_MAX_LENGTH = 60;
2790
3180
  const GROUP_PROJECT_DIRECTORY_MAX_LENGTH = 15;
2791
3181
  function formatGroupProjectDirectory(projectCwd) {
2792
- const value = projectCwd ? abbreviateHomeDirectory(projectCwd) : "Projectless";
3182
+ const value = abbreviateHomeDirectory(projectCwd);
2793
3183
  if (Array.from(value).length <= GROUP_PROJECT_DIRECTORY_MAX_LENGTH)
2794
3184
  return `[${value}]`;
2795
3185
  const levels = value