@keyou007/agent-bot 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (212) hide show
  1. package/.env.example +8 -0
  2. package/CHANGELOG.md +11 -0
  3. package/LICENSE +675 -0
  4. package/README.md +187 -0
  5. package/README.zh.md +187 -0
  6. package/SECURITY.md +13 -0
  7. package/config.example.yaml +56 -0
  8. package/dist/acp/AcpJsonRpcConnection.js +155 -0
  9. package/dist/acp/AcpJsonRpcConnection.js.map +1 -0
  10. package/dist/acp/AcpProcessManager.js +65 -0
  11. package/dist/acp/AcpProcessManager.js.map +1 -0
  12. package/dist/acp/AcpSessionManager.js +164 -0
  13. package/dist/acp/AcpSessionManager.js.map +1 -0
  14. package/dist/acp/acpTypes.js +10 -0
  15. package/dist/acp/acpTypes.js.map +1 -0
  16. package/dist/cli/FeishuAppConfiguration.js +349 -0
  17. package/dist/cli/FeishuAppConfiguration.js.map +1 -0
  18. package/dist/cli/FeishuAppRegistration.js +160 -0
  19. package/dist/cli/FeishuAppRegistration.js.map +1 -0
  20. package/dist/cli/Initializer.js +112 -0
  21. package/dist/cli/Initializer.js.map +1 -0
  22. package/dist/cli/LocalControlClient.js +47 -0
  23. package/dist/cli/LocalControlClient.js.map +1 -0
  24. package/dist/cli/LocalControlServer.js +66 -0
  25. package/dist/cli/LocalControlServer.js.map +1 -0
  26. package/dist/cli/SkillRegistry.js +126 -0
  27. package/dist/cli/SkillRegistry.js.map +1 -0
  28. package/dist/cli/controlProtocol.js +10 -0
  29. package/dist/cli/controlProtocol.js.map +1 -0
  30. package/dist/cli/taskChatRoute.js +23 -0
  31. package/dist/cli/taskChatRoute.js.map +1 -0
  32. package/dist/cli.js +616 -0
  33. package/dist/cli.js.map +1 -0
  34. package/dist/codex/AppServerConnection.js +131 -0
  35. package/dist/codex/AppServerConnection.js.map +1 -0
  36. package/dist/codex/CodexEventMapper.js +296 -0
  37. package/dist/codex/CodexEventMapper.js.map +1 -0
  38. package/dist/codex/CodexLocalActivityDetector.js +124 -0
  39. package/dist/codex/CodexLocalActivityDetector.js.map +1 -0
  40. package/dist/codex/CodexProcessManager.js +64 -0
  41. package/dist/codex/CodexProcessManager.js.map +1 -0
  42. package/dist/codex/CodexRuntime.js +825 -0
  43. package/dist/codex/CodexRuntime.js.map +1 -0
  44. package/dist/codex/ProjectlessWorkspace.js +74 -0
  45. package/dist/codex/ProjectlessWorkspace.js.map +1 -0
  46. package/dist/codex/protocol.js +10 -0
  47. package/dist/codex/protocol.js.map +1 -0
  48. package/dist/commands/CommandRouter.js +176 -0
  49. package/dist/commands/CommandRouter.js.map +1 -0
  50. package/dist/commands/commandTypes.js +2 -0
  51. package/dist/commands/commandTypes.js.map +1 -0
  52. package/dist/config/loadConfig.js +107 -0
  53. package/dist/config/loadConfig.js.map +1 -0
  54. package/dist/config/paths.js +32 -0
  55. package/dist/config/paths.js.map +1 -0
  56. package/dist/config/schema.js +38 -0
  57. package/dist/config/schema.js.map +1 -0
  58. package/dist/console/ConsoleConnector.js +39 -0
  59. package/dist/console/ConsoleConnector.js.map +1 -0
  60. package/dist/console/ConsoleTurnPresenter.js +60 -0
  61. package/dist/console/ConsoleTurnPresenter.js.map +1 -0
  62. package/dist/feishu/CardRenderer.js +972 -0
  63. package/dist/feishu/CardRenderer.js.map +1 -0
  64. package/dist/feishu/CardUpdateScheduler.js +148 -0
  65. package/dist/feishu/CardUpdateScheduler.js.map +1 -0
  66. package/dist/feishu/ConsoleFeishuClient.js +18 -0
  67. package/dist/feishu/ConsoleFeishuClient.js.map +1 -0
  68. package/dist/feishu/FeishuConnector.js +257 -0
  69. package/dist/feishu/FeishuConnector.js.map +1 -0
  70. package/dist/feishu/FeishuMarkdown.js +37 -0
  71. package/dist/feishu/FeishuMarkdown.js.map +1 -0
  72. package/dist/feishu/FeishuMessageClient.js +459 -0
  73. package/dist/feishu/FeishuMessageClient.js.map +1 -0
  74. package/dist/feishu/FeishuTurnPresenter.js +425 -0
  75. package/dist/feishu/FeishuTurnPresenter.js.map +1 -0
  76. package/dist/feishu/GroupAvatarGenerator.js +86 -0
  77. package/dist/feishu/GroupAvatarGenerator.js.map +1 -0
  78. package/dist/feishu/LocalCardImage.js +13 -0
  79. package/dist/feishu/LocalCardImage.js.map +1 -0
  80. package/dist/feishu/LocalImageMarkdown.js +89 -0
  81. package/dist/feishu/LocalImageMarkdown.js.map +1 -0
  82. package/dist/feishu/contextKey.js +12 -0
  83. package/dist/feishu/contextKey.js.map +1 -0
  84. package/dist/feishu/transport.js +14 -0
  85. package/dist/feishu/transport.js.map +1 -0
  86. package/dist/feishu/types.js +2 -0
  87. package/dist/feishu/types.js.map +1 -0
  88. package/dist/index.js +213 -0
  89. package/dist/index.js.map +1 -0
  90. package/dist/logging/logger.js +22 -0
  91. package/dist/logging/logger.js.map +1 -0
  92. package/dist/presentation/OutboundRouter.js +127 -0
  93. package/dist/presentation/OutboundRouter.js.map +1 -0
  94. package/dist/presentation/TurnStateReducer.js +307 -0
  95. package/dist/presentation/TurnStateReducer.js.map +1 -0
  96. package/dist/presentation/splitMarkdown.js +341 -0
  97. package/dist/presentation/splitMarkdown.js.map +1 -0
  98. package/dist/presentation/turnViewTypes.js +2 -0
  99. package/dist/presentation/turnViewTypes.js.map +1 -0
  100. package/dist/proxy/ProxySessionController.js +2819 -0
  101. package/dist/proxy/ProxySessionController.js.map +1 -0
  102. package/dist/runtime/AcpRuntimeAdapter.js +271 -0
  103. package/dist/runtime/AcpRuntimeAdapter.js.map +1 -0
  104. package/dist/runtime/AgentRuntimeRegistry.js +18 -0
  105. package/dist/runtime/AgentRuntimeRegistry.js.map +1 -0
  106. package/dist/runtime/agentEnvironment.js +11 -0
  107. package/dist/runtime/agentEnvironment.js.map +1 -0
  108. package/dist/runtime/types.js +2 -0
  109. package/dist/runtime/types.js.map +1 -0
  110. package/dist/startup/SessionMetadataHydrator.js +46 -0
  111. package/dist/startup/SessionMetadataHydrator.js.map +1 -0
  112. package/dist/startup/StartupNotifier.js +78 -0
  113. package/dist/startup/StartupNotifier.js.map +1 -0
  114. package/dist/startup/startFeishu.js +6 -0
  115. package/dist/startup/startFeishu.js.map +1 -0
  116. package/dist/state/StateStore.js +893 -0
  117. package/dist/state/StateStore.js.map +1 -0
  118. package/dist/state/migrations.js +129 -0
  119. package/dist/state/migrations.js.map +1 -0
  120. package/dist/supervision/SafeRestartNotifier.js +116 -0
  121. package/dist/supervision/SafeRestartNotifier.js.map +1 -0
  122. package/dist/supervision/SafeRestartScheduler.js +123 -0
  123. package/dist/supervision/SafeRestartScheduler.js.map +1 -0
  124. package/dist/supervision/replacementSupervisor.js +8 -0
  125. package/dist/supervision/replacementSupervisor.js.map +1 -0
  126. package/dist/supervision/restartPolicy.js +16 -0
  127. package/dist/supervision/restartPolicy.js.map +1 -0
  128. package/dist/supervisor.js +94 -0
  129. package/dist/supervisor.js.map +1 -0
  130. package/dist/utils/executeShellCommand.js +76 -0
  131. package/dist/utils/executeShellCommand.js.map +1 -0
  132. package/dist/utils/id.js +5 -0
  133. package/dist/utils/id.js.map +1 -0
  134. package/dist/utils/markdown.js +29 -0
  135. package/dist/utils/markdown.js.map +1 -0
  136. package/dist/utils/spawnCommand.js +21 -0
  137. package/dist/utils/spawnCommand.js.map +1 -0
  138. package/dist/utils/taskTitle.js +10 -0
  139. package/dist/utils/taskTitle.js.map +1 -0
  140. package/docs/technical-reference.md +294 -0
  141. package/docs/technical-reference.zh.md +294 -0
  142. package/npm-shrinkwrap.json +4525 -0
  143. package/package.json +87 -0
  144. package/skills/agent-bot/SKILL.md +141 -0
  145. package/skills/agent-bot/agents/openai.yaml +4 -0
  146. package/src/acp/AcpJsonRpcConnection.ts +212 -0
  147. package/src/acp/AcpProcessManager.ts +87 -0
  148. package/src/acp/AcpSessionManager.ts +227 -0
  149. package/src/acp/acpTypes.ts +99 -0
  150. package/src/cli/FeishuAppConfiguration.ts +473 -0
  151. package/src/cli/FeishuAppRegistration.ts +207 -0
  152. package/src/cli/Initializer.ts +159 -0
  153. package/src/cli/LocalControlClient.ts +48 -0
  154. package/src/cli/LocalControlServer.ts +67 -0
  155. package/src/cli/SkillRegistry.ts +151 -0
  156. package/src/cli/controlProtocol.ts +34 -0
  157. package/src/cli/taskChatRoute.ts +31 -0
  158. package/src/cli.ts +630 -0
  159. package/src/codex/AppServerConnection.ts +159 -0
  160. package/src/codex/CodexEventMapper.ts +351 -0
  161. package/src/codex/CodexLocalActivityDetector.ts +148 -0
  162. package/src/codex/CodexProcessManager.ts +63 -0
  163. package/src/codex/CodexRuntime.ts +1025 -0
  164. package/src/codex/ProjectlessWorkspace.ts +90 -0
  165. package/src/codex/protocol.ts +37 -0
  166. package/src/commands/CommandRouter.ts +179 -0
  167. package/src/commands/commandTypes.ts +28 -0
  168. package/src/config/loadConfig.ts +115 -0
  169. package/src/config/paths.ts +37 -0
  170. package/src/config/schema.ts +46 -0
  171. package/src/console/ConsoleConnector.ts +40 -0
  172. package/src/console/ConsoleTurnPresenter.ts +64 -0
  173. package/src/feishu/CardRenderer.ts +1157 -0
  174. package/src/feishu/CardUpdateScheduler.ts +165 -0
  175. package/src/feishu/ConsoleFeishuClient.ts +22 -0
  176. package/src/feishu/FeishuConnector.ts +277 -0
  177. package/src/feishu/FeishuMarkdown.ts +47 -0
  178. package/src/feishu/FeishuMessageClient.ts +649 -0
  179. package/src/feishu/FeishuTurnPresenter.ts +539 -0
  180. package/src/feishu/GroupAvatarGenerator.ts +102 -0
  181. package/src/feishu/LocalCardImage.ts +13 -0
  182. package/src/feishu/LocalImageMarkdown.ts +92 -0
  183. package/src/feishu/contextKey.ts +14 -0
  184. package/src/feishu/transport.ts +24 -0
  185. package/src/feishu/types.ts +83 -0
  186. package/src/index.ts +228 -0
  187. package/src/logging/logger.ts +28 -0
  188. package/src/presentation/OutboundRouter.ts +205 -0
  189. package/src/presentation/TurnStateReducer.ts +341 -0
  190. package/src/presentation/splitMarkdown.ts +358 -0
  191. package/src/presentation/turnViewTypes.ts +54 -0
  192. package/src/proxy/ProxySessionController.ts +3311 -0
  193. package/src/runtime/AcpRuntimeAdapter.ts +324 -0
  194. package/src/runtime/AgentRuntimeRegistry.ts +20 -0
  195. package/src/runtime/agentEnvironment.ts +14 -0
  196. package/src/runtime/types.ts +215 -0
  197. package/src/startup/SessionMetadataHydrator.ts +46 -0
  198. package/src/startup/StartupNotifier.ts +92 -0
  199. package/src/startup/startFeishu.ts +19 -0
  200. package/src/state/StateStore.ts +1249 -0
  201. package/src/state/migrations.ts +128 -0
  202. package/src/supervision/SafeRestartNotifier.ts +122 -0
  203. package/src/supervision/SafeRestartScheduler.ts +149 -0
  204. package/src/supervision/replacementSupervisor.ts +10 -0
  205. package/src/supervision/restartPolicy.ts +19 -0
  206. package/src/supervisor.ts +106 -0
  207. package/src/utils/executeShellCommand.ts +97 -0
  208. package/src/utils/id.ts +5 -0
  209. package/src/utils/markdown.ts +25 -0
  210. package/src/utils/spawnCommand.ts +35 -0
  211. package/src/utils/taskTitle.ts +9 -0
  212. package/tsconfig.json +19 -0
@@ -0,0 +1,2819 @@
1
+ import path from "node:path";
2
+ import os from "node:os";
3
+ import { createProjectlessWorkspace, detectProjectlessWorkspace } from "../codex/ProjectlessWorkspace.js";
4
+ import { CommandRouter } from "../commands/CommandRouter.js";
5
+ import { baseChatContextKey, isThreadContextKey } from "../feishu/contextKey.js";
6
+ import { CardRenderer } from "../feishu/CardRenderer.js";
7
+ import { generateGroupAvatarPng, resolveGroupAvatarProjectName } from "../feishu/GroupAvatarGenerator.js";
8
+ import { createId } from "../utils/id.js";
9
+ import { asInlineCode, codeBlock, truncateMiddle, truncateText } from "../utils/markdown.js";
10
+ import { normalizeTaskTitle } from "../utils/taskTitle.js";
11
+ import { executeShellCommand, } from "../utils/executeShellCommand.js";
12
+ const MESSAGE_RECEIVED_REACTION = "OnIt";
13
+ const MESSAGE_COMPLETED_REACTION = "DONE";
14
+ const MESSAGE_FAILED_REACTION = "ERROR";
15
+ const MESSAGE_CANCELLED_REACTION = "CrossMark";
16
+ const SESSION_PAGE_SIZE = 5;
17
+ export class ProxySessionController {
18
+ config;
19
+ store;
20
+ runtimes;
21
+ outbound;
22
+ logger;
23
+ lifecycle;
24
+ shellCommandExecutor;
25
+ router = new CommandRouter();
26
+ cardRenderer = new CardRenderer();
27
+ messageQueues = new Map();
28
+ sessionLoads = new Map();
29
+ queuedPromptStarts = new Map();
30
+ queuedPromptCards = new Map();
31
+ queuedPromptCardWrites = new Map();
32
+ lastSessionListings = new Map();
33
+ threadInitializations = new Map();
34
+ unsubscribe = [];
35
+ constructor(config, store, runtimes, outbound, logger, lifecycle, shellCommandExecutor = executeShellCommand) {
36
+ this.config = config;
37
+ this.store = store;
38
+ this.runtimes = runtimes;
39
+ this.outbound = outbound;
40
+ this.logger = logger;
41
+ this.lifecycle = lifecycle;
42
+ this.shellCommandExecutor = shellCommandExecutor;
43
+ const interruptedAcpSessions = this.store.reconcileInterruptedAcpSessions(Object.entries(this.config.agents)
44
+ .filter(([, agent]) => agent.kind === "acp")
45
+ .map(([name]) => name));
46
+ if (interruptedAcpSessions.length > 0) {
47
+ this.logger.warn({ sessionIds: interruptedAcpSessions.map((session) => session.localSessionId) }, "Marked interrupted ACP sessions as failed after process restart.");
48
+ }
49
+ for (const kind of ["acp", "codex"]) {
50
+ this.unsubscribe.push(this.runtimes.get(kind).onEvent((event) => {
51
+ void this.handleRuntimeEvent(event).catch((error) => {
52
+ this.logger.warn({ error, event }, "Failed to present runtime event.");
53
+ });
54
+ }));
55
+ }
56
+ this.restorePersistedSessionRoutes();
57
+ this.restorePersistedQueuedPrompts();
58
+ void this.restorePersistedMessageReactions().catch((error) => {
59
+ this.logger.warn({ error }, "Failed to restore persisted message reaction statuses.");
60
+ });
61
+ }
62
+ async onMessage(message) {
63
+ if (message.chatId && message.chatType) {
64
+ this.store.recordChatContext(baseChatContextKey(message.contextKey), message.chatType);
65
+ }
66
+ if (!this.store.claimInboundEvent(message.messageId, "message"))
67
+ return;
68
+ try {
69
+ const reactionId = await this.outbound.addReaction(message.contextKey, message.messageId, MESSAGE_RECEIVED_REACTION);
70
+ if (reactionId) {
71
+ this.store.saveMessageReaction(message.messageId, message.contextKey, reactionId, MESSAGE_RECEIVED_REACTION);
72
+ }
73
+ }
74
+ catch (error) {
75
+ this.logger.warn({ error, messageId: message.messageId, contextKey: message.contextKey }, "Failed to acknowledge the incoming Feishu message with a reaction.");
76
+ }
77
+ const replyTarget = message.replyInThread
78
+ ? { messageId: message.messageId, replyInThread: true }
79
+ : undefined;
80
+ const imageCount = message.images?.length ?? 0;
81
+ this.store.audit(message.contextKey, "incoming_message", {
82
+ messageId: message.messageId,
83
+ text: message.text,
84
+ ...(imageCount > 0 ? { imageCount } : {}),
85
+ });
86
+ if (imageCount > 0 && message.chatId && message.chatType) {
87
+ this.store.markChatActive(baseChatContextKey(message.contextKey));
88
+ }
89
+ let localImagePaths;
90
+ if (imageCount > 0) {
91
+ try {
92
+ localImagePaths = await Promise.all(message.images.map((image) => this.outbound.downloadImage(message.contextKey, message.messageId, image.imageKey)));
93
+ }
94
+ catch (error) {
95
+ await this.finalizeStandaloneMessageReaction(message.messageId, "failed");
96
+ await this.outbound.withReplyTarget(message.contextKey, replyTarget, () => this.sendError(message.contextKey, error));
97
+ return;
98
+ }
99
+ }
100
+ let command;
101
+ try {
102
+ command = imageCount > 0 && !message.text.trimStart().startsWith("/")
103
+ ? { type: "prompt", text: message.text.trim() || "请分析这张图片。" }
104
+ : this.router.parse(message.text);
105
+ }
106
+ catch (error) {
107
+ await this.finalizeStandaloneMessageReaction(message.messageId, "failed");
108
+ await this.outbound.withReplyTarget(message.contextKey, replyTarget, () => this.sendError(message.contextKey, error));
109
+ return;
110
+ }
111
+ if (message.chatId
112
+ && message.chatType
113
+ && (command.type === "shell"
114
+ || (command.type === "prompt" && !message.text.trimStart().startsWith("/")))) {
115
+ this.store.markChatActive(baseChatContextKey(message.contextKey));
116
+ }
117
+ // Operational and read-only commands must remain available even if a prompt operation is slow.
118
+ if (isQueueIndependentCommand(command)) {
119
+ await this.outbound.withReplyTarget(message.contextKey, replyTarget, async () => {
120
+ try {
121
+ if (command.type !== "forkgroup")
122
+ await this.ensureThreadFork(message);
123
+ await this.execute(message.contextKey, command, message.messageId, replyTarget, localImagePaths, message.userId, message);
124
+ if (!isPromptCommand(command))
125
+ await this.finalizeStandaloneMessageReaction(message.messageId, "completed");
126
+ }
127
+ catch (error) {
128
+ await this.finalizeStandaloneMessageReaction(message.messageId, "failed");
129
+ await this.sendError(message.contextKey, error);
130
+ }
131
+ });
132
+ return;
133
+ }
134
+ const previous = this.messageQueues.get(message.contextKey) ?? Promise.resolve();
135
+ const next = previous.catch(() => undefined).then(() => this.outbound.withReplyTarget(message.contextKey, replyTarget, async () => {
136
+ try {
137
+ if (command.type !== "forkgroup")
138
+ await this.ensureThreadFork(message);
139
+ await this.execute(message.contextKey, command, message.messageId, replyTarget, localImagePaths, message.userId, message);
140
+ if (!isPromptCommand(command))
141
+ await this.finalizeStandaloneMessageReaction(message.messageId, "completed");
142
+ }
143
+ catch (error) {
144
+ await this.finalizeStandaloneMessageReaction(message.messageId, "failed");
145
+ await this.sendError(message.contextKey, error);
146
+ }
147
+ }));
148
+ this.messageQueues.set(message.contextKey, next);
149
+ await next;
150
+ if (this.messageQueues.get(message.contextKey) === next)
151
+ this.messageQueues.delete(message.contextKey);
152
+ }
153
+ async onCardAction(action) {
154
+ if (!this.store.claimInboundEvent(action.actionId, "card_action"))
155
+ return;
156
+ const contextKey = this.cardActionContextKey(action);
157
+ const scopedAction = contextKey === action.contextKey ? action : { ...action, contextKey };
158
+ const replyTarget = isThreadContextKey(contextKey) && action.messageId
159
+ ? { messageId: action.messageId, replyInThread: true }
160
+ : undefined;
161
+ await this.outbound.withReplyTarget(contextKey, replyTarget, async () => {
162
+ try {
163
+ const kind = String(scopedAction.value.action ?? "");
164
+ if (kind === "turn_details") {
165
+ await this.outbound.showDetails(contextKey, String(scopedAction.value.turnId ?? ""));
166
+ }
167
+ else if (kind === "activity_history") {
168
+ const requestedPage = String(scopedAction.value.page ?? "0");
169
+ const numericPage = Number(requestedPage);
170
+ await this.outbound.showActivityPage(contextKey, String(scopedAction.value.turnId ?? ""), requestedPage === "latest" ? "latest" : Number.isFinite(numericPage) ? numericPage : 0, scopedAction.messageId);
171
+ }
172
+ else if (kind === "turn_cancel") {
173
+ const sessionId = String(scopedAction.value.sessionId ?? "");
174
+ await this.cancelSession(this.requireSession(contextKey, sessionId));
175
+ }
176
+ else if (kind === "queued_prompt_cancel") {
177
+ await this.cancelQueuedPrompt(scopedAction);
178
+ }
179
+ else if (kind === "session_more") {
180
+ await this.refreshSessionsCardFromAction(scopedAction, undefined, true);
181
+ }
182
+ else if (kind === "session_switch") {
183
+ const sessionId = String(scopedAction.value.sessionId ?? "");
184
+ await this.switchSession(contextKey, sessionId);
185
+ if (scopedAction.value.cardView === "status")
186
+ await this.refreshStatusCardFromAction(scopedAction);
187
+ else
188
+ await this.refreshSessionsCardFromAction(scopedAction);
189
+ }
190
+ else if (kind === "session_fork") {
191
+ const sessionId = String(scopedAction.value.sessionId ?? "");
192
+ await this.forkSessionReference(contextKey, sessionId);
193
+ await this.refreshSessionsCardFromAction(scopedAction);
194
+ }
195
+ else if (kind === "session_new") {
196
+ const sessionId = String(scopedAction.value.sessionId ?? "");
197
+ await this.createProjectSessionFromReference(contextKey, sessionId);
198
+ await this.refreshSessionsCardFromAction(scopedAction);
199
+ }
200
+ else if (kind === "session_stop") {
201
+ const sessionId = String(scopedAction.value.sessionId ?? "");
202
+ await this.stopSessionReference(contextKey, sessionId);
203
+ if (scopedAction.value.cardView === "status")
204
+ await this.refreshStatusCardFromAction(scopedAction, sessionId);
205
+ else
206
+ await this.refreshSessionsCardFromAction(scopedAction, sessionId);
207
+ }
208
+ else if (kind === "session_status") {
209
+ await this.status(contextKey, String(scopedAction.value.sessionId ?? ""));
210
+ }
211
+ else if (kind === "session_status_refresh") {
212
+ await this.refreshStatusCardFromAction(scopedAction);
213
+ }
214
+ else if (kind === "model_select") {
215
+ const sessionId = String(scopedAction.value.sessionId ?? "");
216
+ const model = String(scopedAction.value.model ?? "");
217
+ await this.model(contextKey, model, {
218
+ sessionId,
219
+ updateMessageId: scopedAction.messageId,
220
+ });
221
+ }
222
+ else if (kind === "model_open") {
223
+ const sessionId = String(scopedAction.value.sessionId ?? "");
224
+ await this.model(contextKey, undefined, {
225
+ sessionId,
226
+ updateMessageId: scopedAction.messageId,
227
+ });
228
+ }
229
+ else if (kind === "reasoning_select") {
230
+ const sessionId = String(scopedAction.value.sessionId ?? "");
231
+ const model = String(scopedAction.value.model ?? "");
232
+ const effort = String(scopedAction.value.effort ?? "");
233
+ await this.thinking(contextKey, effort, {
234
+ sessionId,
235
+ updateMessageId: scopedAction.messageId,
236
+ expectedModel: model,
237
+ });
238
+ }
239
+ else if (kind === "approval") {
240
+ await this.resolveApproval(scopedAction);
241
+ }
242
+ }
243
+ catch (error) {
244
+ await this.sendError(contextKey, error);
245
+ }
246
+ });
247
+ }
248
+ async onChatUpdated(event) {
249
+ const contextKey = `chat_id:${event.chatId}`;
250
+ const previous = this.messageQueues.get(contextKey) ?? Promise.resolve();
251
+ const next = previous.catch(() => undefined).then(() => this.syncTaskTitleFromGroupName(contextKey, event));
252
+ this.messageQueues.set(contextKey, next);
253
+ try {
254
+ await next;
255
+ }
256
+ finally {
257
+ if (this.messageQueues.get(contextKey) === next)
258
+ this.messageQueues.delete(contextKey);
259
+ }
260
+ }
261
+ close() {
262
+ for (const unsubscribe of this.unsubscribe)
263
+ unsubscribe();
264
+ this.unsubscribe.length = 0;
265
+ this.lastSessionListings.clear();
266
+ this.queuedPromptCards.clear();
267
+ this.queuedPromptCardWrites.clear();
268
+ }
269
+ async controlStopTask(localSessionId) {
270
+ const record = this.store.getSession(localSessionId);
271
+ if (!record || record.status === "closed")
272
+ throw new Error(`找不到任务:${localSessionId}`);
273
+ await this.cancelSession(record);
274
+ return `已请求停止任务:${record.title ?? record.remoteSessionId ?? record.localSessionId}`;
275
+ }
276
+ async controlGetTaskStatus(localSessionId) {
277
+ const record = this.store.getSession(localSessionId);
278
+ if (!record)
279
+ throw new Error(`找不到任务:${localSessionId}`);
280
+ let remote;
281
+ if (record.remoteSessionId) {
282
+ const runtime = this.runtimes.forAgent(this.ensureAgent(record.agentName));
283
+ if (runtime.readRemoteSession) {
284
+ try {
285
+ remote = await runtime.readRemoteSession(record.remoteSessionId);
286
+ }
287
+ catch (error) {
288
+ this.logger.warn({ error, sessionId: record.localSessionId }, "Failed to inspect Codex task status for CLI.");
289
+ }
290
+ }
291
+ }
292
+ const session = mergeRemoteTaskStatus(record, remote);
293
+ const snapshot = session.lastTurnId
294
+ ? turnViewSnapshot(this.store.getTurnSnapshot(session.lastTurnId))
295
+ : undefined;
296
+ return { session, snapshot, remote };
297
+ }
298
+ async controlSetTaskTitle(localSessionId, title) {
299
+ const normalizedTitle = normalizeTaskTitle(title);
300
+ if (!normalizedTitle)
301
+ throw new Error("任务标题不能为空。");
302
+ const record = this.store.getSession(localSessionId);
303
+ if (!record || record.status === "closed")
304
+ throw new Error(`找不到任务:${localSessionId}`);
305
+ const loaded = await this.loadSession(record);
306
+ if (loaded.runtime.setTitle)
307
+ await loaded.runtime.setTitle(record.localSessionId, normalizedTitle);
308
+ else
309
+ loaded.session.title = normalizedTitle;
310
+ this.store.updateRuntimeSession(record.localSessionId, { title: normalizedTitle });
311
+ this.outbound.updateSessionTitle(record.localSessionId, normalizedTitle);
312
+ this.store.audit(record.contextKey, "session_title_changed", {
313
+ localSessionId: record.localSessionId,
314
+ title: normalizedTitle,
315
+ source: "cli",
316
+ });
317
+ return `任务标题已修改为:${normalizedTitle}`;
318
+ }
319
+ async controlSendTaskPrompt(localSessionId, text) {
320
+ const promptText = text.trim();
321
+ if (!promptText)
322
+ throw new Error("Prompt 不能为空。");
323
+ const record = this.store.getSession(localSessionId);
324
+ if (!record || record.status === "closed")
325
+ throw new Error(`找不到任务:${localSessionId}`);
326
+ const runtime = this.runtimes.forAgent(this.ensureAgent(record.agentName));
327
+ const activeTurnId = runtime.getSession(localSessionId)?.activeTurnId;
328
+ const routedContextKey = this.outbound.getSessionContextKey(localSessionId);
329
+ const responseContextKey = (activeTurnId ? this.store.getTurnContextKey(activeTurnId) ?? routedContextKey : undefined)
330
+ ?? (record.lastTurnId ? this.store.getTurnContextKey(record.lastTurnId) : undefined)
331
+ ?? routedContextKey
332
+ ?? record.contextKey;
333
+ const lastSnapshot = record.lastTurnId
334
+ ? turnViewSnapshot(this.store.getTurnSnapshot(record.lastTurnId))
335
+ : undefined;
336
+ const activeSnapshot = activeTurnId
337
+ ? turnViewSnapshot(this.store.getTurnSnapshot(activeTurnId))
338
+ : undefined;
339
+ const routedReplyTarget = this.outbound.getSessionReplyTarget(localSessionId);
340
+ const existingReplyTarget = activeSnapshot?.replyTarget
341
+ ?? (activeTurnId ? routedReplyTarget : undefined)
342
+ ?? lastSnapshot?.replyTarget
343
+ ?? routedReplyTarget;
344
+ if (isThreadContextKey(responseContextKey) && !existingReplyTarget) {
345
+ throw new Error("无法确定目标任务的话题回复位置,未发送 Prompt。");
346
+ }
347
+ const scopedRecord = this.store.getSessionForContext(localSessionId, responseContextKey)
348
+ ?? { ...record, contextKey: responseContextKey };
349
+ const previous = this.messageQueues.get(responseContextKey) ?? Promise.resolve();
350
+ const next = previous.catch(() => undefined).then(async () => {
351
+ this.outbound.registerSession(scopedRecord.localSessionId, responseContextKey, scopedRecord.title, scopedRecord.cwd);
352
+ const promptMessageId = await this.outbound.withReplyTarget(responseContextKey, existingReplyTarget, () => this.outbound.sendText(responseContextKey, promptText));
353
+ const turnReplyTarget = isThreadContextKey(responseContextKey)
354
+ ? promptMessageId
355
+ ? { messageId: promptMessageId, replyInThread: true }
356
+ : existingReplyTarget
357
+ : undefined;
358
+ await this.promptSession(scopedRecord, responseContextKey, promptText, undefined, turnReplyTarget);
359
+ this.store.audit(responseContextKey, "task_prompt_sent", {
360
+ localSessionId,
361
+ source: "cli",
362
+ });
363
+ });
364
+ this.messageQueues.set(responseContextKey, next);
365
+ try {
366
+ await next;
367
+ }
368
+ finally {
369
+ if (this.messageQueues.get(responseContextKey) === next)
370
+ this.messageQueues.delete(responseContextKey);
371
+ }
372
+ return `已通过机器人向原会话发送 Prompt,并提交给任务:${record.title ?? record.remoteSessionId ?? record.localSessionId}`;
373
+ }
374
+ cardActionContextKey(action) {
375
+ const explicit = typeof action.value.contextKey === "string" ? action.value.contextKey : undefined;
376
+ if (explicit && baseChatContextKey(explicit) === baseChatContextKey(action.contextKey))
377
+ return explicit;
378
+ const sessionReference = typeof action.value.sessionId === "string" ? action.value.sessionId : undefined;
379
+ const session = sessionReference
380
+ ? this.store.getSession(sessionReference) ?? this.store.findSessionByRemoteSessionId(sessionReference)
381
+ : undefined;
382
+ if (session && baseChatContextKey(session.contextKey) === baseChatContextKey(action.contextKey)) {
383
+ return session.contextKey;
384
+ }
385
+ const turnId = typeof action.value.turnId === "string" ? action.value.turnId : undefined;
386
+ const snapshot = turnId ? turnViewSnapshot(this.store.getTurnSnapshot(turnId)) : undefined;
387
+ const turnSession = snapshot ? this.store.getSession(snapshot.sessionId) : undefined;
388
+ return turnSession && baseChatContextKey(turnSession.contextKey) === baseChatContextKey(action.contextKey)
389
+ ? turnSession.contextKey
390
+ : action.contextKey;
391
+ }
392
+ async execute(contextKey, command, messageId, replyTarget, localImagePaths, userId, incomingMessage) {
393
+ const context = this.store.getOrCreateUserContext(contextKey, this.config.defaults.agent);
394
+ switch (command.type) {
395
+ case "shell":
396
+ await this.runShellCommand(contextKey, command.command);
397
+ return;
398
+ case "new":
399
+ if (command.projectless && this.ensureAgent(context.defaultAgent).kind !== "codex") {
400
+ throw new Error("/new --nodir 仅支持 Codex Agent。");
401
+ }
402
+ await this.createSession(contextKey, context.defaultAgent, command.projectless ? undefined : command.cwd ?? this.inheritedNewTaskCwd(contextKey), true, false, undefined, undefined, command.title);
403
+ return;
404
+ case "newgroup":
405
+ await this.createFeishuGroup(contextKey, context.defaultAgent, command.title, userId);
406
+ return;
407
+ case "forkgroup":
408
+ await this.forkCurrentSessionToFeishuGroup(contextKey, command.title, userId, incomingMessage);
409
+ return;
410
+ case "fork":
411
+ await this.forkSessionReference(contextKey, command.sessionId);
412
+ return;
413
+ case "title":
414
+ await this.setTitle(contextKey, command.title);
415
+ return;
416
+ case "ask":
417
+ case "prompt":
418
+ await this.prompt(contextKey, command.text, messageId, replyTarget, localImagePaths);
419
+ return;
420
+ case "nosteer":
421
+ await this.enqueueNoSteerPrompt(contextKey, command.text, messageId, replyTarget);
422
+ return;
423
+ case "sessions":
424
+ await this.listSessions(contextKey, command.searchTerm);
425
+ return;
426
+ case "switch":
427
+ await this.switchSession(contextKey, command.sessionId);
428
+ return;
429
+ case "agent":
430
+ if (command.agent)
431
+ await this.setDefaultAgent(contextKey, command.agent);
432
+ else
433
+ await this.listAgents(contextKey);
434
+ return;
435
+ case "use":
436
+ await this.setDefaultAgent(contextKey, command.agent);
437
+ await this.createSession(contextKey, command.agent, command.cwd, true, false);
438
+ return;
439
+ case "status":
440
+ await this.status(contextKey, command.sessionId);
441
+ return;
442
+ case "goal":
443
+ await this.goal(contextKey, command);
444
+ return;
445
+ case "restart":
446
+ if (!this.lifecycle)
447
+ throw new Error("当前运行方式不支持自动重启。");
448
+ await this.lifecycle.restart(contextKey);
449
+ return;
450
+ case "model":
451
+ await this.model(contextKey, command.model);
452
+ return;
453
+ case "thinking":
454
+ await this.thinking(contextKey, command.effort);
455
+ return;
456
+ case "permissions":
457
+ await this.permissions(contextKey, command.mode);
458
+ return;
459
+ case "modes":
460
+ case "mode":
461
+ await this.outbound.sendText(contextKey, "当前统一运行时不再暴露 ACP modes;Codex 请使用 /model 和 /permissions。");
462
+ return;
463
+ case "help":
464
+ await this.help(contextKey);
465
+ return;
466
+ case "stop":
467
+ await this.cancel(contextKey);
468
+ return;
469
+ }
470
+ }
471
+ restorePersistedSessionRoutes() {
472
+ for (const context of this.store.listUserContexts()) {
473
+ if (!context.currentSessionId)
474
+ continue;
475
+ const session = this.store.getSessionForContext(context.currentSessionId, context.contextKey);
476
+ if (!session || session.status === "closed")
477
+ continue;
478
+ const turnContextKey = session.lastTurnId
479
+ ? this.store.getTurnContextKey(session.lastTurnId) ?? context.contextKey
480
+ : context.contextKey;
481
+ this.outbound.registerSession(session.localSessionId, turnContextKey, session.title, session.cwd);
482
+ if (!session.lastTurnId)
483
+ continue;
484
+ void this.outbound.resumeDelivery(session.localSessionId, turnContextKey, session.lastTurnId).catch((error) => {
485
+ this.logger.warn({ error, sessionId: session.localSessionId }, "Failed to restore persisted turn delivery.");
486
+ });
487
+ }
488
+ }
489
+ restorePersistedQueuedPrompts() {
490
+ for (const sessionId of this.store.listQueuedPromptSessionIds()) {
491
+ void this.scheduleNextQueuedPrompt(sessionId).catch((error) => {
492
+ this.logger.warn({ error, sessionId }, "Failed to resume a persisted prompt queue.");
493
+ });
494
+ }
495
+ }
496
+ async restorePersistedMessageReactions() {
497
+ const pending = this.store.listPendingMessageReactions();
498
+ const terminalTurns = new Map();
499
+ for (const reaction of pending) {
500
+ if (!reaction.turnId || !reaction.localSessionId)
501
+ continue;
502
+ const session = this.store.getSession(reaction.localSessionId);
503
+ if (session?.lastTurnId !== reaction.turnId)
504
+ continue;
505
+ if (session.lastTurnStatus === "completed" || session.lastTurnStatus === "failed" || session.lastTurnStatus === "cancelled") {
506
+ terminalTurns.set(reaction.turnId, session.lastTurnStatus);
507
+ }
508
+ }
509
+ for (const [turnId, status] of terminalTurns) {
510
+ await this.finalizeTurnMessageReactions(turnId, status);
511
+ }
512
+ }
513
+ async prompt(contextKey, text, messageId, replyTarget, localImagePaths) {
514
+ if (!text.trim())
515
+ throw new Error("请输入要交给 Codex 的内容。");
516
+ let record = this.currentSession(contextKey);
517
+ if (!record) {
518
+ const context = this.store.getOrCreateUserContext(contextKey, this.config.defaults.agent);
519
+ record = await this.createSession(contextKey, context.defaultAgent, this.inheritedNewTaskCwd(contextKey), false, true, text, replyTarget);
520
+ }
521
+ await this.promptSession(record, contextKey, text, messageId, replyTarget, localImagePaths);
522
+ }
523
+ async promptSession(record, contextKey, text, messageId, replyTarget, localImagePaths) {
524
+ const configuredRuntime = this.runtimes.forAgent(this.ensureAgent(record.agentName));
525
+ if (!configuredRuntime.getSession(record.localSessionId)) {
526
+ this.outbound.registerSession(record.localSessionId, contextKey, record.title, record.cwd);
527
+ await this.outbound.startPendingTurn(record.localSessionId, contextKey, record.title, replyTarget, text);
528
+ }
529
+ let loaded;
530
+ try {
531
+ loaded = await this.loadSession(record);
532
+ }
533
+ catch (error) {
534
+ await this.outbound.failPendingTurn(record.localSessionId, error instanceof Error ? error.message : String(error));
535
+ throw error;
536
+ }
537
+ let remoteActivity;
538
+ try {
539
+ remoteActivity = await this.assertSessionTurnOwnership(loaded.record, loaded.runtime);
540
+ }
541
+ catch (error) {
542
+ await this.outbound.failPendingTurn(record.localSessionId, error instanceof Error ? error.message : String(error));
543
+ throw error;
544
+ }
545
+ if (needsFullSessionSynchronization(loaded.record, loaded.session, remoteActivity)) {
546
+ try {
547
+ loaded.session = await loaded.runtime.synchronizeSession(record.localSessionId);
548
+ }
549
+ catch (error) {
550
+ this.logger.warn({ error, sessionId: record.localSessionId }, "Failed to synchronize session before prompt.");
551
+ }
552
+ }
553
+ const activeTurnId = loaded.session.activeTurnId;
554
+ if (activeTurnId) {
555
+ const activeContextKey = this.store.getTurnContextKey(activeTurnId);
556
+ if (activeContextKey && activeContextKey !== contextKey) {
557
+ this.persistQueuedPrompt(record.localSessionId, contextKey, text, {
558
+ localImagePaths,
559
+ messageId,
560
+ replyTarget,
561
+ });
562
+ return;
563
+ }
564
+ try {
565
+ await loaded.runtime.steerTurn(record.localSessionId, activeTurnId, runtimePrompt(text, localImagePaths));
566
+ await this.presentSteerMessage(record.localSessionId, activeTurnId, text, messageId);
567
+ if (messageId)
568
+ await this.bindMessageReactionToTurn(messageId, record.localSessionId, activeTurnId);
569
+ return;
570
+ }
571
+ catch (error) {
572
+ this.logger.debug({ error, sessionId: record.localSessionId, activeTurnId }, "Steering failed; reconciling the Codex thread.");
573
+ let current = loaded.runtime.getSession(record.localSessionId);
574
+ try {
575
+ current = await loaded.runtime.synchronizeSession(record.localSessionId);
576
+ }
577
+ catch (syncError) {
578
+ this.logger.warn({ error: syncError, sessionId: record.localSessionId }, "Failed to synchronize after steering failure.");
579
+ }
580
+ if (!current?.activeTurnId) {
581
+ const turnId = await this.startTurn(loaded, text, replyTarget, localImagePaths);
582
+ if (messageId)
583
+ await this.bindMessageReactionToTurn(messageId, record.localSessionId, turnId);
584
+ return;
585
+ }
586
+ if (current.activeTurnId !== activeTurnId) {
587
+ try {
588
+ await loaded.runtime.steerTurn(record.localSessionId, current.activeTurnId, runtimePrompt(text, localImagePaths));
589
+ await this.presentSteerMessage(record.localSessionId, current.activeTurnId, text, messageId);
590
+ if (messageId)
591
+ await this.bindMessageReactionToTurn(messageId, record.localSessionId, current.activeTurnId);
592
+ return;
593
+ }
594
+ catch (retryError) {
595
+ this.logger.warn({ error: retryError, sessionId: record.localSessionId, activeTurnId: current.activeTurnId }, "Failed to steer the reconciled Codex turn; queueing prompt.");
596
+ }
597
+ }
598
+ this.persistQueuedPrompt(record.localSessionId, contextKey, text, {
599
+ localImagePaths,
600
+ messageId,
601
+ replyTarget,
602
+ });
603
+ return;
604
+ }
605
+ }
606
+ const turnId = await this.startTurn(loaded, text, replyTarget, localImagePaths);
607
+ if (messageId)
608
+ await this.bindMessageReactionToTurn(messageId, record.localSessionId, turnId);
609
+ }
610
+ async presentSteerMessage(localSessionId, turnId, text, messageId) {
611
+ try {
612
+ await this.outbound.appendSteerMessage(localSessionId, turnId, text, messageId);
613
+ }
614
+ catch (error) {
615
+ this.logger.warn({ error, sessionId: localSessionId, turnId, messageId }, "Failed to insert a steer message into the thinking card.");
616
+ }
617
+ }
618
+ async enqueueNoSteerPrompt(contextKey, text, messageId, replyTarget) {
619
+ const record = this.requireCurrentSession(contextKey);
620
+ const queued = this.persistQueuedPrompt(record.localSessionId, contextKey, text, { messageId, replyTarget });
621
+ this.store.audit(contextKey, "queued_prompt_added", {
622
+ promptId: queued.promptId,
623
+ localSessionId: record.localSessionId,
624
+ });
625
+ await this.presentPromptQueueCard(record.localSessionId, contextKey);
626
+ await this.scheduleNextQueuedPrompt(record.localSessionId);
627
+ }
628
+ persistQueuedPrompt(localSessionId, contextKey, text, options = {}) {
629
+ return this.store.enqueuePrompt({
630
+ promptId: createId("prompt"),
631
+ localSessionId,
632
+ contextKey,
633
+ text,
634
+ localImagePaths: options.localImagePaths,
635
+ messageId: options.messageId,
636
+ replyMessageId: options.replyTarget?.messageId,
637
+ });
638
+ }
639
+ async cancelQueuedPrompt(action) {
640
+ const promptId = String(action.value.promptId ?? "");
641
+ const sessionId = String(action.value.sessionId ?? "");
642
+ if (!promptId || !sessionId)
643
+ throw new Error("无效的排队 Prompt 取消请求。");
644
+ this.requireSession(action.contextKey, sessionId);
645
+ if (action.messageId)
646
+ this.rememberPromptQueueCard(sessionId, action.contextKey, action.messageId);
647
+ const cancelled = this.store.cancelQueuedPrompt(promptId, sessionId);
648
+ if (cancelled?.messageId)
649
+ await this.finalizeStandaloneMessageReaction(cancelled.messageId, "cancelled");
650
+ if (cancelled) {
651
+ this.store.audit(action.contextKey, "queued_prompt_cancelled", {
652
+ promptId,
653
+ localSessionId: sessionId,
654
+ });
655
+ }
656
+ await this.refreshPromptQueueCards(sessionId);
657
+ }
658
+ async presentPromptQueueCard(localSessionId, contextKey) {
659
+ await this.serializePromptQueueCardWrite(localSessionId, async () => {
660
+ const card = this.renderPromptQueueCard(localSessionId, contextKey);
661
+ const existing = this.queuedPromptCards.get(localSessionId)?.get(contextKey);
662
+ if (existing) {
663
+ try {
664
+ await this.outbound.updateInteractiveCard(contextKey, existing, card);
665
+ return;
666
+ }
667
+ catch (error) {
668
+ this.logger.warn({ error, localSessionId, contextKey, messageId: existing }, "Failed to update prompt queue card; sending a replacement.");
669
+ }
670
+ }
671
+ const messageId = await this.outbound.sendInteractiveCard(contextKey, card);
672
+ if (messageId)
673
+ this.rememberPromptQueueCard(localSessionId, contextKey, messageId);
674
+ });
675
+ }
676
+ async refreshPromptQueueCards(localSessionId) {
677
+ await this.serializePromptQueueCardWrite(localSessionId, async () => {
678
+ const cards = this.queuedPromptCards.get(localSessionId);
679
+ if (!cards)
680
+ return;
681
+ await Promise.all([...cards].map(async ([contextKey, messageId]) => {
682
+ try {
683
+ await this.outbound.updateInteractiveCard(contextKey, messageId, this.renderPromptQueueCard(localSessionId, contextKey));
684
+ }
685
+ catch (error) {
686
+ this.logger.warn({ error, localSessionId, contextKey, messageId }, "Failed to refresh prompt queue card.");
687
+ }
688
+ }));
689
+ });
690
+ }
691
+ renderPromptQueueCard(localSessionId, contextKey) {
692
+ return this.cardRenderer.renderPromptQueue({
693
+ sessionId: localSessionId,
694
+ contextKey,
695
+ prompts: this.store.listQueuedPrompts(localSessionId).map((prompt) => ({
696
+ id: prompt.promptId,
697
+ text: prompt.text,
698
+ })),
699
+ });
700
+ }
701
+ rememberPromptQueueCard(localSessionId, contextKey, messageId) {
702
+ const cards = this.queuedPromptCards.get(localSessionId) ?? new Map();
703
+ cards.set(contextKey, messageId);
704
+ this.queuedPromptCards.set(localSessionId, cards);
705
+ }
706
+ serializePromptQueueCardWrite(localSessionId, write) {
707
+ const previous = this.queuedPromptCardWrites.get(localSessionId) ?? Promise.resolve();
708
+ const next = previous.catch(() => undefined).then(write);
709
+ this.queuedPromptCardWrites.set(localSessionId, next);
710
+ return next.finally(() => {
711
+ if (this.queuedPromptCardWrites.get(localSessionId) === next)
712
+ this.queuedPromptCardWrites.delete(localSessionId);
713
+ });
714
+ }
715
+ async ensureThreadFork(message) {
716
+ if (!message.threadContext || !message.threadId || !message.chatId)
717
+ return;
718
+ const current = this.store.getUserContext(message.contextKey)?.currentSessionId;
719
+ if (current && this.store.getSession(current))
720
+ return;
721
+ const existing = this.threadInitializations.get(message.contextKey);
722
+ if (existing)
723
+ return existing;
724
+ const initialization = this.forkThreadSession(message);
725
+ this.threadInitializations.set(message.contextKey, initialization);
726
+ try {
727
+ await initialization;
728
+ }
729
+ finally {
730
+ if (this.threadInitializations.get(message.contextKey) === initialization) {
731
+ this.threadInitializations.delete(message.contextKey);
732
+ }
733
+ }
734
+ }
735
+ async forkThreadSession(message) {
736
+ const { anchor, source, snapshot } = this.resolveThreadForkAnchor(message);
737
+ const agent = this.ensureAgent(source.agentName);
738
+ const runtime = this.runtimes.forAgent(agent);
739
+ if (runtime.kind !== "codex" || !runtime.forkSession) {
740
+ throw new Error("当前 Codex 运行时不支持 fork 任务。");
741
+ }
742
+ const context = this.store.getOrCreateUserContext(message.contextKey, source.agentName);
743
+ if (context.currentSessionId && this.store.getSession(context.currentSessionId))
744
+ return;
745
+ const forkTitle = this.store.nextForkTitle(source.title);
746
+ const localSessionId = createId("sess");
747
+ const record = this.store.createSession({
748
+ localSessionId,
749
+ contextKey: message.contextKey,
750
+ agentName: source.agentName,
751
+ cwd: source.cwd,
752
+ status: "starting",
753
+ });
754
+ this.store.updateRuntimeSession(localSessionId, {
755
+ runtimeKind: "codex",
756
+ title: forkTitle,
757
+ model: source.model,
758
+ reasoningEffort: source.reasoningEffort,
759
+ permissionMode: source.permissionMode ?? "auto",
760
+ });
761
+ this.store.setCurrentSession(message.contextKey, localSessionId);
762
+ this.outbound.registerSession(localSessionId, message.contextKey, forkTitle, source.cwd);
763
+ try {
764
+ const forked = await runtime.forkSession({
765
+ localSessionId,
766
+ remoteSessionId: source.remoteSessionId,
767
+ lastTurnId: anchor.turnId,
768
+ agentName: source.agentName,
769
+ cwd: source.cwd,
770
+ title: forkTitle,
771
+ model: source.model,
772
+ reasoningEffort: source.reasoningEffort,
773
+ permissionMode: source.permissionMode ?? "auto",
774
+ });
775
+ this.persistRuntimeSession(record, forked, "ready");
776
+ this.store.updateRuntimeSession(localSessionId, {
777
+ lastTurnId: anchor.turnId,
778
+ lastTurnStatus: forkedTurnStatus(snapshot?.status),
779
+ });
780
+ this.store.audit(message.contextKey, "thread_forked", {
781
+ threadId: message.threadId,
782
+ sourceMessageId: message.rootMessageId ?? message.parentMessageId,
783
+ sourceLocalSessionId: source.localSessionId,
784
+ sourceRemoteSessionId: source.remoteSessionId,
785
+ sourceTurnId: anchor.turnId,
786
+ forkedLocalSessionId: localSessionId,
787
+ forkedRemoteSessionId: forked.remoteSessionId,
788
+ });
789
+ }
790
+ catch (error) {
791
+ this.store.updateSession(localSessionId, { status: "failed" });
792
+ this.store.setCurrentSession(message.contextKey, undefined);
793
+ this.outbound.unregisterSession(localSessionId);
794
+ throw error;
795
+ }
796
+ }
797
+ resolveThreadForkAnchor(message) {
798
+ if (!message.threadContext || !message.threadId || !message.chatId) {
799
+ throw new Error("当前消息不属于可识别的飞书话题。");
800
+ }
801
+ const anchorMessageIds = [message.rootMessageId, message.parentMessageId]
802
+ .filter((messageId) => Boolean(messageId && messageId !== message.messageId));
803
+ const anchor = [...new Set(anchorMessageIds)]
804
+ .map((messageId) => this.store.findTurnAnchorByMessageId(messageId))
805
+ .find((candidate) => candidate !== undefined);
806
+ if (!anchor) {
807
+ throw new Error("无法确定这个话题对应的 Codex 轮次,因此没有创建分支任务。请从该轮的用户消息、思考卡片或最终回答创建话题。");
808
+ }
809
+ const source = anchor.contextKey
810
+ ? this.store.getSessionForContext(anchor.localSessionId, anchor.contextKey)
811
+ : this.store.getSession(anchor.localSessionId);
812
+ if (!source || !source.remoteSessionId || !this.isCodexSession(source)) {
813
+ throw new Error("这个话题的来源不是可 fork 的 Codex 任务。");
814
+ }
815
+ if (baseChatContextKey(anchor.contextKey ?? source.contextKey) !== `chat_id:${message.chatId}`) {
816
+ throw new Error("话题来源任务不属于当前会话,已拒绝创建分支。");
817
+ }
818
+ const snapshot = turnViewSnapshot(this.store.getTurnSnapshot(anchor.turnId));
819
+ if (isTurnStillRunning(snapshot?.status)
820
+ || (source.lastTurnId === anchor.turnId && source.lastTurnStatus === "running")) {
821
+ throw new Error("话题对应的轮次仍在执行,Codex 暂时不能从这一轮 fork。请等待该轮完成后再在话题中发送消息。");
822
+ }
823
+ return { anchor, source, snapshot };
824
+ }
825
+ async forkSessionReference(contextKey, reference) {
826
+ const plan = await this.prepareForkSession(contextKey, reference);
827
+ const forked = await this.forkSessionIntoContext(contextKey, plan);
828
+ const forkSourceLabel = plan.forkedFromHistoricalTurn
829
+ ? `${plan.sourceLabel}最近已完成轮次`
830
+ : plan.sourceLabel;
831
+ await this.outbound.sendText(contextKey, `已从${forkSourceLabel}创建分支并切换到新任务:${forked.session.title
832
+ ? `${forked.session.title}(${forked.session.remoteSessionId})`
833
+ : forked.session.remoteSessionId}`);
834
+ }
835
+ async prepareForkSession(contextKey, reference, requestedTitle) {
836
+ const sourceLabel = reference === undefined ? "当前任务" : "指定任务";
837
+ const taskId = reference === undefined ? undefined : this.resolveSessionReference(contextKey, reference);
838
+ let source;
839
+ if (taskId === undefined) {
840
+ source = this.requireCurrentSession(contextKey);
841
+ }
842
+ else {
843
+ const direct = this.store.getSession(taskId);
844
+ if (direct?.status === "closed") {
845
+ throw new Error(`找不到任务:${taskId}`);
846
+ }
847
+ const global = direct ?? this.store.findSessionByRemoteSessionId(taskId);
848
+ source = global ? { ...global, contextKey } : undefined;
849
+ }
850
+ if (source && (!source.remoteSessionId || !this.isCodexSession(source))) {
851
+ throw new Error(`${sourceLabel}不是可 fork 的 Codex 任务。`);
852
+ }
853
+ let runtime;
854
+ let agentName;
855
+ if (source) {
856
+ agentName = source.agentName;
857
+ runtime = this.runtimes.forAgent(this.ensureAgent(agentName));
858
+ }
859
+ else {
860
+ const agentEntry = Object.entries(this.config.agents).find(([, agent]) => agent.kind === "codex");
861
+ if (!agentEntry)
862
+ throw new Error("未配置 Codex Agent。");
863
+ [agentName] = agentEntry;
864
+ runtime = this.runtimes.get("codex");
865
+ }
866
+ if (runtime.kind !== "codex" || !runtime.forkSession) {
867
+ throw new Error("当前 Codex 运行时不支持 fork 任务。");
868
+ }
869
+ const remoteSessionId = source?.remoteSessionId ?? taskId;
870
+ if (!remoteSessionId)
871
+ throw new Error("当前任务尚未创建 Codex 任务 ID,暂时不能 fork。");
872
+ if (!runtime.readRemoteSession && !source) {
873
+ throw new Error("当前 Codex 运行时不支持读取指定任务。");
874
+ }
875
+ const remote = runtime.readRemoteSession
876
+ ? await runtime.readRemoteSession(remoteSessionId)
877
+ : undefined;
878
+ const latestTurnId = remote?.lastTurnId ?? source?.lastTurnId;
879
+ const latestSnapshot = turnViewSnapshot(latestTurnId ? this.store.getTurnSnapshot(latestTurnId) : undefined);
880
+ const isRunning = remote
881
+ ? isRemoteSessionActive(remote)
882
+ : Boolean((source && runtime.getSession(source.localSessionId)?.activeTurnId)
883
+ || source?.lastTurnStatus === "running"
884
+ || isTurnStillRunning(latestSnapshot?.status));
885
+ const lastTurnId = isRunning ? remote?.lastCompletedTurnId : latestTurnId;
886
+ if (!lastTurnId) {
887
+ if (isRunning) {
888
+ throw new Error(`${sourceLabel}正在执行,且还没有已完成轮次可供 fork。请等待当前轮次完成后重试。`);
889
+ }
890
+ throw new Error(`${sourceLabel}还没有可供 fork 的轮次。请先完成至少一轮对话。`);
891
+ }
892
+ const snapshot = turnViewSnapshot(this.store.getTurnSnapshot(lastTurnId));
893
+ const forkedFromHistoricalTurn = isRunning && lastTurnId !== latestTurnId;
894
+ const cwd = remote?.cwd || source?.cwd;
895
+ if (!cwd)
896
+ throw new Error("指定的 Codex 任务没有可用的工作目录,暂时不能 fork。");
897
+ const sourceTitle = remote?.title ?? source?.title ?? remote?.preview;
898
+ const forkTitle = normalizeTaskTitle(requestedTitle) ?? this.store.nextForkTitle(sourceTitle);
899
+ const model = source?.model;
900
+ const reasoningEffort = source?.reasoningEffort;
901
+ const permissionMode = source?.permissionMode ?? "auto";
902
+ return {
903
+ source,
904
+ sourceLabel,
905
+ runtime,
906
+ agentName,
907
+ remoteSessionId,
908
+ lastTurnId,
909
+ cwd,
910
+ forkTitle,
911
+ model,
912
+ reasoningEffort,
913
+ permissionMode,
914
+ lastTurnStatus: forkedFromHistoricalTurn
915
+ ? "completed"
916
+ : mapRemoteTurnStatus(remote?.lastTurnStatus)
917
+ ?? forkedTurnStatus(snapshot?.status)
918
+ ?? source?.lastTurnStatus,
919
+ sourceWasRunning: isRunning,
920
+ forkedFromHistoricalTurn,
921
+ };
922
+ }
923
+ async prepareForkGroupSession(contextKey, requestedTitle, incomingMessage) {
924
+ if (!incomingMessage?.threadContext) {
925
+ const plan = await this.prepareForkSession(contextKey, undefined, requestedTitle);
926
+ return {
927
+ plan,
928
+ sourceDescription: plan.forkedFromHistoricalTurn
929
+ ? "当前任务最近已完成轮次"
930
+ : "当前任务最新轮次",
931
+ };
932
+ }
933
+ const resolved = this.resolveThreadForkAnchor(incomingMessage);
934
+ const topicSession = this.currentSession(contextKey);
935
+ const topicTurnId = topicSession
936
+ ? this.store.findLatestCompletedTurnId(topicSession.localSessionId, contextKey)
937
+ : undefined;
938
+ if (topicSession && topicTurnId) {
939
+ return {
940
+ plan: this.prepareForkSessionFromTurn(topicSession, topicTurnId, requestedTitle, "当前话题任务"),
941
+ sourceDescription: "当前话题任务最近已完成轮次",
942
+ };
943
+ }
944
+ return {
945
+ plan: this.prepareForkSessionFromTurn(resolved.source, resolved.anchor.turnId, requestedTitle, "话题原始轮次"),
946
+ sourceDescription: "话题原始轮次",
947
+ };
948
+ }
949
+ prepareForkSessionFromTurn(source, lastTurnId, requestedTitle, sourceLabel) {
950
+ if (!source.remoteSessionId || !this.isCodexSession(source)) {
951
+ throw new Error(`${sourceLabel}不是可 fork 的 Codex 任务。`);
952
+ }
953
+ const runtime = this.runtimes.forAgent(this.ensureAgent(source.agentName));
954
+ if (runtime.kind !== "codex" || !runtime.forkSession) {
955
+ throw new Error("当前 Codex 运行时不支持 fork 任务。");
956
+ }
957
+ const snapshot = turnViewSnapshot(this.store.getTurnSnapshot(lastTurnId));
958
+ if (isTurnStillRunning(snapshot?.status)) {
959
+ throw new Error(`${sourceLabel}仍在执行,Codex 暂时不能从这一轮 fork。`);
960
+ }
961
+ const sourceWasRunning = Boolean(runtime.getSession(source.localSessionId)?.activeTurnId
962
+ || source.lastTurnStatus === "running"
963
+ || source.status === "running");
964
+ const forkTitle = normalizeTaskTitle(requestedTitle) ?? this.store.nextForkTitle(source.title);
965
+ return {
966
+ source,
967
+ sourceLabel,
968
+ runtime,
969
+ agentName: source.agentName,
970
+ remoteSessionId: source.remoteSessionId,
971
+ lastTurnId,
972
+ cwd: source.cwd,
973
+ forkTitle,
974
+ model: source.model,
975
+ reasoningEffort: source.reasoningEffort,
976
+ permissionMode: source.permissionMode ?? "auto",
977
+ lastTurnStatus: forkedTurnStatus(snapshot?.status)
978
+ ?? (source.lastTurnId === lastTurnId ? source.lastTurnStatus : undefined)
979
+ ?? "completed",
980
+ sourceWasRunning,
981
+ forkedFromHistoricalTurn: sourceWasRunning && source.lastTurnId !== lastTurnId,
982
+ };
983
+ }
984
+ async forkSessionIntoContext(contextKey, plan) {
985
+ const localSessionId = createId("sess");
986
+ const record = this.store.createSession({
987
+ localSessionId,
988
+ contextKey,
989
+ agentName: plan.agentName,
990
+ cwd: plan.cwd,
991
+ status: "starting",
992
+ });
993
+ this.store.updateRuntimeSession(localSessionId, {
994
+ runtimeKind: "codex",
995
+ title: plan.forkTitle,
996
+ model: plan.model,
997
+ reasoningEffort: plan.reasoningEffort,
998
+ permissionMode: plan.permissionMode,
999
+ });
1000
+ try {
1001
+ const forked = await plan.runtime.forkSession({
1002
+ localSessionId,
1003
+ remoteSessionId: plan.remoteSessionId,
1004
+ lastTurnId: plan.lastTurnId,
1005
+ agentName: plan.agentName,
1006
+ cwd: plan.cwd,
1007
+ title: plan.forkTitle,
1008
+ model: plan.model,
1009
+ reasoningEffort: plan.reasoningEffort,
1010
+ permissionMode: plan.permissionMode,
1011
+ });
1012
+ this.persistRuntimeSession(record, forked, "ready");
1013
+ this.store.updateRuntimeSession(localSessionId, {
1014
+ lastTurnId: plan.lastTurnId,
1015
+ lastTurnStatus: plan.lastTurnStatus,
1016
+ });
1017
+ this.store.setCurrentSession(contextKey, localSessionId);
1018
+ this.outbound.registerSession(localSessionId, contextKey, forked.title ?? plan.forkTitle, plan.cwd);
1019
+ this.store.audit(contextKey, "session_forked", {
1020
+ sourceLocalSessionId: plan.source?.localSessionId,
1021
+ sourceRemoteSessionId: plan.remoteSessionId,
1022
+ sourceTurnId: plan.lastTurnId,
1023
+ sourceWasRunning: plan.sourceWasRunning,
1024
+ forkedLocalSessionId: localSessionId,
1025
+ forkedRemoteSessionId: forked.remoteSessionId,
1026
+ });
1027
+ return {
1028
+ record: this.store.getSession(localSessionId) ?? record,
1029
+ session: forked,
1030
+ };
1031
+ }
1032
+ catch (error) {
1033
+ this.store.updateSession(localSessionId, { status: "failed" });
1034
+ throw error;
1035
+ }
1036
+ }
1037
+ async createProjectSessionFromReference(contextKey, reference) {
1038
+ const taskId = this.resolveSessionReference(contextKey, reference);
1039
+ const direct = this.store.getSession(taskId);
1040
+ if (direct?.status === "closed") {
1041
+ throw new Error(`找不到任务:${taskId}`);
1042
+ }
1043
+ const source = direct ?? this.store.findSessionByRemoteSessionId(taskId);
1044
+ if (source && !this.isCodexSession(source)) {
1045
+ throw new Error("指定任务不是 Codex 任务,暂时不能按项目创建新任务。");
1046
+ }
1047
+ let agentName;
1048
+ let runtime;
1049
+ if (source) {
1050
+ agentName = source.agentName;
1051
+ runtime = this.runtimes.forAgent(this.ensureAgent(agentName));
1052
+ }
1053
+ else {
1054
+ const agentEntry = Object.entries(this.config.agents).find(([, agent]) => agent.kind === "codex");
1055
+ if (!agentEntry)
1056
+ throw new Error("未配置 Codex Agent。");
1057
+ [agentName] = agentEntry;
1058
+ runtime = this.runtimes.get("codex");
1059
+ }
1060
+ const remoteSessionId = source?.remoteSessionId ?? taskId;
1061
+ let remote;
1062
+ if (runtime.readRemoteSession && remoteSessionId) {
1063
+ try {
1064
+ remote = await runtime.readRemoteSession(remoteSessionId);
1065
+ }
1066
+ catch (error) {
1067
+ if (!source)
1068
+ throw error;
1069
+ this.logger.warn({ error, contextKey, taskId: remoteSessionId }, "Failed to refresh the source task before creating a project task; using the local project path.");
1070
+ }
1071
+ }
1072
+ const cwd = remote?.cwd || source?.cwd;
1073
+ if (!cwd) {
1074
+ throw new Error("指定的 Codex 任务没有可用的工作目录,暂时不能按项目创建新任务。");
1075
+ }
1076
+ const executionSettings = {
1077
+ model: remote?.model ?? source?.model,
1078
+ reasoningEffort: remote?.reasoningEffort ?? source?.reasoningEffort,
1079
+ permissionMode: remote?.permissionMode ?? source?.permissionMode ?? "auto",
1080
+ };
1081
+ const created = await this.createSession(contextKey, agentName, cwd, true, false, undefined, undefined, undefined, executionSettings);
1082
+ this.store.audit(contextKey, "project_session_created", {
1083
+ sourceLocalSessionId: source?.localSessionId,
1084
+ sourceRemoteSessionId: remoteSessionId,
1085
+ createdLocalSessionId: created.localSessionId,
1086
+ createdRemoteSessionId: created.remoteSessionId,
1087
+ cwd,
1088
+ ...executionSettings,
1089
+ });
1090
+ }
1091
+ async startTurn(loaded, text, replyTarget, localImagePaths) {
1092
+ const currentRecord = this.store.getSession(loaded.record.localSessionId);
1093
+ const title = currentRecord?.title ?? normalizeTaskTitle(text);
1094
+ if (!currentRecord?.title && title)
1095
+ this.store.updateRuntimeSession(loaded.record.localSessionId, { title });
1096
+ if (title)
1097
+ this.outbound.updateSessionTitle(loaded.record.localSessionId, title);
1098
+ await this.outbound.startPendingTurn(loaded.record.localSessionId, loaded.record.contextKey, title, replyTarget, text);
1099
+ let turnId;
1100
+ try {
1101
+ turnId = await loaded.runtime.startTurn(loaded.record.localSessionId, runtimePrompt(text, localImagePaths));
1102
+ }
1103
+ catch (error) {
1104
+ await this.outbound.failPendingTurn(loaded.record.localSessionId, error instanceof Error ? error.message : String(error));
1105
+ throw error;
1106
+ }
1107
+ const latest = this.store.getSession(loaded.record.localSessionId);
1108
+ const alreadyTerminal = latest?.lastTurnId === turnId
1109
+ && ["completed", "cancelled", "failed"].includes(latest.lastTurnStatus ?? "");
1110
+ if (!alreadyTerminal) {
1111
+ this.store.updateSession(loaded.record.localSessionId, { status: "running" });
1112
+ this.store.updateRuntimeSession(loaded.record.localSessionId, { lastTurnId: turnId, lastTurnStatus: "running" });
1113
+ }
1114
+ return turnId;
1115
+ }
1116
+ async createSession(contextKey, agentName, cwd, announce, prepareTurn, prompt, replyTarget, requestedTitle, executionSettings = {}) {
1117
+ const agent = this.ensureAgent(agentName);
1118
+ const localSessionId = createId("sess");
1119
+ const initialTitle = normalizeTaskTitle(requestedTitle ?? prompt ?? "");
1120
+ const sessionCwd = cwd === undefined && agent.kind === "codex"
1121
+ ? createProjectlessWorkspace({ prompt: initialTitle }).cwd
1122
+ : path.resolve(cwd ?? this.config.defaults.cwd);
1123
+ const record = this.store.createSession({ localSessionId, contextKey, agentName, cwd: sessionCwd, status: "starting" });
1124
+ if (initialTitle || executionSettings.model || executionSettings.reasoningEffort || executionSettings.permissionMode) {
1125
+ this.store.updateRuntimeSession(localSessionId, {
1126
+ title: initialTitle,
1127
+ model: executionSettings.model,
1128
+ reasoningEffort: executionSettings.reasoningEffort,
1129
+ permissionMode: executionSettings.permissionMode,
1130
+ });
1131
+ }
1132
+ this.store.setCurrentSession(contextKey, localSessionId);
1133
+ this.outbound.registerSession(localSessionId, contextKey, initialTitle, sessionCwd);
1134
+ const runtime = this.runtimes.forAgent(agent);
1135
+ try {
1136
+ if (prepareTurn) {
1137
+ await this.outbound.startPendingTurn(localSessionId, contextKey, initialTitle, replyTarget, prompt);
1138
+ }
1139
+ const session = await runtime.createSession({
1140
+ localSessionId,
1141
+ agentName,
1142
+ cwd: sessionCwd,
1143
+ title: initialTitle,
1144
+ model: executionSettings.model,
1145
+ reasoningEffort: executionSettings.reasoningEffort,
1146
+ permissionMode: executionSettings.permissionMode ?? "auto",
1147
+ });
1148
+ this.persistRuntimeSession(record, session, session.activeTurnId ? "running" : "ready");
1149
+ const saved = this.store.getSession(localSessionId) ?? record;
1150
+ if (announce) {
1151
+ const task = initialTitle ? `${initialTitle}(${session.remoteSessionId})` : session.remoteSessionId;
1152
+ await this.outbound.sendText(contextKey, `已创建 ${agent.title} 任务:${task}`);
1153
+ }
1154
+ return saved;
1155
+ }
1156
+ catch (error) {
1157
+ this.store.updateSession(localSessionId, { status: "failed" });
1158
+ if (prepareTurn) {
1159
+ await this.outbound.failPendingTurn(localSessionId, error instanceof Error ? error.message : String(error));
1160
+ }
1161
+ throw error;
1162
+ }
1163
+ }
1164
+ async createFeishuGroup(sourceContextKey, agentName, requestedTitle, userId) {
1165
+ const source = this.currentSession(sourceContextKey);
1166
+ const boundProjectCwd = this.currentProjectCwd(sourceContextKey);
1167
+ const explicitTitle = normalizeTaskTitle(requestedTitle);
1168
+ const taskTitle = explicitTitle ?? "新任务";
1169
+ const executionSettings = source?.agentName === agentName
1170
+ ? {
1171
+ model: source.model,
1172
+ reasoningEffort: source.reasoningEffort,
1173
+ permissionMode: source.permissionMode,
1174
+ }
1175
+ : {};
1176
+ const group = await this.createFeishuGroupContext(sourceContextKey, agentName, taskTitle, userId, boundProjectCwd, "/newgroup", !explicitTitle);
1177
+ let task;
1178
+ try {
1179
+ task = await this.createSession(group.contextKey, agentName, boundProjectCwd, false, false, undefined, undefined, taskTitle, executionSettings);
1180
+ }
1181
+ catch (error) {
1182
+ await this.outbound.sendText(group.contextKey, `群已创建,但新任务创建失败:${error instanceof Error ? error.message : String(error)}`);
1183
+ throw error;
1184
+ }
1185
+ const taskDescription = task.title
1186
+ ? `${task.title}(${task.remoteSessionId})`
1187
+ : task.remoteSessionId ?? task.localSessionId;
1188
+ await this.outbound.sendText(group.contextKey, [
1189
+ "群和新任务已创建。",
1190
+ `当前任务:${taskDescription}`,
1191
+ `当前 Project 目录:${boundProjectCwd ?? "未绑定(Projectless)"}`,
1192
+ `当前模型:${task.model ?? "默认"}`,
1193
+ `思考强度:${task.reasoningEffort ?? "自动"}`,
1194
+ ].join("\n"));
1195
+ await this.outbound.sendText(sourceContextKey, `已创建飞书群:${group.name},并创建新任务 ${taskDescription}。`);
1196
+ }
1197
+ async forkCurrentSessionToFeishuGroup(sourceContextKey, requestedTitle, userId, incomingMessage) {
1198
+ if (!userId?.startsWith("ou_")) {
1199
+ throw new Error("/forkgroup 只能由具有 open_id 的飞书用户消息触发。");
1200
+ }
1201
+ const prepared = await this.prepareForkGroupSession(sourceContextKey, requestedTitle, incomingMessage);
1202
+ const { plan } = prepared;
1203
+ const boundProjectCwd = detectProjectlessWorkspace(plan.cwd) ? undefined : plan.cwd;
1204
+ const group = await this.createFeishuGroupContext(sourceContextKey, plan.agentName, plan.forkTitle, userId, boundProjectCwd, "/forkgroup", false);
1205
+ let forked;
1206
+ try {
1207
+ forked = await this.forkSessionIntoContext(group.contextKey, plan);
1208
+ }
1209
+ catch (error) {
1210
+ await this.outbound.sendText(group.contextKey, `群已创建,但 Fork 任务失败:${error instanceof Error ? error.message : String(error)}`);
1211
+ throw error;
1212
+ }
1213
+ const taskDescription = forked.session.title
1214
+ ? `${forked.session.title}(${forked.session.remoteSessionId})`
1215
+ : forked.session.remoteSessionId;
1216
+ await this.outbound.sendText(group.contextKey, [
1217
+ `已从${prepared.sourceDescription}创建分支。`,
1218
+ `当前任务:${taskDescription}`,
1219
+ `当前 Project 目录:${boundProjectCwd ?? "未绑定(Projectless)"}`,
1220
+ ].join("\n"));
1221
+ await this.outbound.sendText(sourceContextKey, `已将${incomingMessage?.threadContext ? prepared.sourceDescription : "当前任务"} Fork 到飞书群:${group.name};新群当前任务为 ${taskDescription}。`);
1222
+ }
1223
+ async createFeishuGroupContext(sourceContextKey, agentName, taskTitle, userId, boundProjectCwd, commandName, includeTimestamp) {
1224
+ if (!userId?.startsWith("ou_")) {
1225
+ throw new Error(`${commandName} 只能由具有 open_id 的飞书用户消息触发。`);
1226
+ }
1227
+ const groupName = formatNewGroupName(agentName, boundProjectCwd, taskTitle, new Date(), includeTimestamp);
1228
+ const group = await this.outbound.createGroup(sourceContextKey, {
1229
+ name: groupName,
1230
+ userOpenId: userId,
1231
+ avatarPng: generateGroupAvatarPng(resolveGroupAvatarProjectName(boundProjectCwd, taskTitle), boundProjectCwd),
1232
+ });
1233
+ const groupContextKey = `chat_id:${group.chatId}`;
1234
+ this.store.recordChatContext(groupContextKey, "group");
1235
+ this.store.getOrCreateUserContext(groupContextKey, agentName);
1236
+ if (boundProjectCwd)
1237
+ this.store.setBoundProjectCwd(groupContextKey, boundProjectCwd);
1238
+ return { contextKey: groupContextKey, name: group.name };
1239
+ }
1240
+ async loadSession(record) {
1241
+ const agent = this.ensureAgent(record.agentName);
1242
+ const runtime = this.runtimes.forAgent(agent);
1243
+ const existing = runtime.getSession(record.localSessionId);
1244
+ if (existing)
1245
+ return { record, runtime, session: existing };
1246
+ const pending = this.sessionLoads.get(record.localSessionId);
1247
+ if (pending)
1248
+ return pending;
1249
+ this.outbound.registerSession(record.localSessionId, record.contextKey, record.title, record.cwd);
1250
+ const loading = (async () => {
1251
+ if (record.lastTurnId) {
1252
+ try {
1253
+ await this.outbound.resumeDelivery(record.localSessionId, record.contextKey, record.lastTurnId);
1254
+ }
1255
+ catch (error) {
1256
+ this.logger.warn({ error, sessionId: record.localSessionId, turnId: record.lastTurnId }, "Failed to restore persisted turn delivery before loading session.");
1257
+ }
1258
+ }
1259
+ const permissionMode = record.permissionMode ?? "auto";
1260
+ if (record.remoteSessionId)
1261
+ await this.assertSessionTurnOwnership(record, runtime);
1262
+ let session;
1263
+ if (record.remoteSessionId) {
1264
+ try {
1265
+ session = await runtime.resumeSession({
1266
+ localSessionId: record.localSessionId,
1267
+ remoteSessionId: record.remoteSessionId,
1268
+ agentName: record.agentName,
1269
+ cwd: record.cwd,
1270
+ title: record.title,
1271
+ model: record.model,
1272
+ reasoningEffort: record.reasoningEffort,
1273
+ permissionMode,
1274
+ activeTurnId: agent.kind === "codex" && record.status === "running" && record.lastTurnStatus === "running"
1275
+ ? record.lastTurnId
1276
+ : undefined,
1277
+ lastTurnId: record.lastTurnId,
1278
+ lastTurnStatus: record.lastTurnStatus,
1279
+ });
1280
+ }
1281
+ catch (error) {
1282
+ if (!(agent.kind === "codex" && !record.lastTurnId && isMissingRolloutError(error)))
1283
+ throw error;
1284
+ this.logger.warn({ error, sessionId: record.localSessionId }, "Codex task has no rollout; creating a replacement task.");
1285
+ session = await runtime.createSession({
1286
+ localSessionId: record.localSessionId,
1287
+ agentName: record.agentName,
1288
+ cwd: record.cwd,
1289
+ title: record.title,
1290
+ model: record.model,
1291
+ reasoningEffort: record.reasoningEffort,
1292
+ permissionMode,
1293
+ });
1294
+ }
1295
+ }
1296
+ else {
1297
+ session = await runtime.createSession({
1298
+ localSessionId: record.localSessionId,
1299
+ agentName: record.agentName,
1300
+ cwd: record.cwd,
1301
+ title: record.title,
1302
+ model: record.model,
1303
+ reasoningEffort: record.reasoningEffort,
1304
+ permissionMode,
1305
+ });
1306
+ }
1307
+ this.persistRuntimeSession(record, session, session.activeTurnId ? "running" : "ready");
1308
+ const saved = this.store.getSession(record.localSessionId) ?? record;
1309
+ return { record: saved, runtime, session };
1310
+ })();
1311
+ this.sessionLoads.set(record.localSessionId, loading);
1312
+ try {
1313
+ return await loading;
1314
+ }
1315
+ finally {
1316
+ this.sessionLoads.delete(record.localSessionId);
1317
+ }
1318
+ }
1319
+ persistRuntimeSession(record, session, status) {
1320
+ this.store.updateRuntimeSession(record.localSessionId, {
1321
+ runtimeKind: session.runtimeKind,
1322
+ remoteSessionId: session.remoteSessionId,
1323
+ title: session.title,
1324
+ model: session.model,
1325
+ reasoningEffort: session.reasoningEffort,
1326
+ permissionMode: session.permissionMode,
1327
+ });
1328
+ this.store.updateSession(record.localSessionId, {
1329
+ acpSessionId: session.runtimeKind === "acp" ? session.remoteSessionId : undefined,
1330
+ status,
1331
+ });
1332
+ }
1333
+ async handleRuntimeEvent(event) {
1334
+ if (event.type === "session_metadata_updated") {
1335
+ this.store.updateRuntimeSession(event.sessionId, { title: event.title });
1336
+ this.outbound.updateSessionTitle(event.sessionId, event.title);
1337
+ return;
1338
+ }
1339
+ if (event.type === "turn_started") {
1340
+ this.store.updateSession(event.sessionId, { status: "running" });
1341
+ this.store.updateRuntimeSession(event.sessionId, { lastTurnId: event.turnId, lastTurnStatus: "running" });
1342
+ }
1343
+ else if (event.type === "turn_completed" || event.type === "turn_cancelled" || event.type === "turn_failed") {
1344
+ this.store.updateRuntimeSession(event.sessionId, {
1345
+ lastTurnId: event.turnId,
1346
+ lastTurnStatus: event.type === "turn_completed" ? "completed" : event.type === "turn_cancelled" ? "cancelled" : "failed",
1347
+ });
1348
+ }
1349
+ try {
1350
+ await this.outbound.onEvent(event);
1351
+ if (event.type === "turn_completed" || event.type === "turn_cancelled" || event.type === "turn_failed") {
1352
+ const terminalStatus = event.type === "turn_completed"
1353
+ ? "completed"
1354
+ : event.type === "turn_cancelled"
1355
+ ? "cancelled"
1356
+ : "failed";
1357
+ const latest = this.store.getSession(event.sessionId);
1358
+ if (latest?.lastTurnId === event.turnId && latest.lastTurnStatus === terminalStatus) {
1359
+ await this.finalizeTurnMessageReactions(event.turnId, terminalStatus);
1360
+ }
1361
+ }
1362
+ }
1363
+ finally {
1364
+ if (event.type === "turn_completed" || event.type === "turn_cancelled" || event.type === "turn_failed") {
1365
+ const latest = this.store.getSession(event.sessionId);
1366
+ const agent = latest ? this.ensureAgent(latest.agentName) : undefined;
1367
+ const activeTurnId = agent ? this.runtimes.forAgent(agent).getSession(event.sessionId)?.activeTurnId : undefined;
1368
+ if (latest?.lastTurnId === event.turnId && !activeTurnId) {
1369
+ this.store.updateSession(event.sessionId, { status: event.type === "turn_failed" ? "failed" : "ready" });
1370
+ }
1371
+ }
1372
+ }
1373
+ if (event.type === "turn_completed" || event.type === "turn_cancelled" || event.type === "turn_failed") {
1374
+ const latest = this.store.getSession(event.sessionId);
1375
+ const agent = latest ? this.ensureAgent(latest.agentName) : undefined;
1376
+ const activeTurnId = agent ? this.runtimes.forAgent(agent).getSession(event.sessionId)?.activeTurnId : undefined;
1377
+ if (latest?.lastTurnId !== event.turnId || activeTurnId)
1378
+ return;
1379
+ await this.scheduleNextQueuedPrompt(event.sessionId);
1380
+ }
1381
+ }
1382
+ scheduleNextQueuedPrompt(sessionId) {
1383
+ const previous = this.queuedPromptStarts.get(sessionId) ?? Promise.resolve();
1384
+ const next = previous.catch(() => undefined).then(() => this.startNextQueuedPromptIfIdle(sessionId));
1385
+ this.queuedPromptStarts.set(sessionId, next);
1386
+ return next.finally(() => {
1387
+ if (this.queuedPromptStarts.get(sessionId) === next)
1388
+ this.queuedPromptStarts.delete(sessionId);
1389
+ });
1390
+ }
1391
+ async startNextQueuedPromptIfIdle(sessionId) {
1392
+ if (this.store.countQueuedPrompts(sessionId) === 0)
1393
+ return;
1394
+ const baseRecord = this.store.getSession(sessionId);
1395
+ if (!baseRecord || baseRecord.status === "closed")
1396
+ return;
1397
+ let prompt;
1398
+ try {
1399
+ let loaded = await this.loadSession(baseRecord);
1400
+ const remoteActivity = await this.assertSessionTurnOwnership(loaded.record, loaded.runtime);
1401
+ if (needsFullSessionSynchronization(loaded.record, loaded.session, remoteActivity)) {
1402
+ loaded = { ...loaded, session: await loaded.runtime.synchronizeSession(sessionId) };
1403
+ }
1404
+ if (loaded.session.activeTurnId)
1405
+ return;
1406
+ prompt = this.store.takeNextQueuedPrompt(sessionId);
1407
+ if (!prompt)
1408
+ return;
1409
+ const record = this.store.getSessionForContext(sessionId, prompt.contextKey) ?? baseRecord;
1410
+ loaded = { ...loaded, record };
1411
+ await this.refreshPromptQueueCards(sessionId);
1412
+ const turnId = await this.startTurn(loaded, prompt.text, prompt.replyMessageId ? { messageId: prompt.replyMessageId, replyInThread: true } : undefined, prompt.localImagePaths);
1413
+ if (prompt.messageId)
1414
+ await this.bindMessageReactionToTurn(prompt.messageId, sessionId, turnId);
1415
+ }
1416
+ catch (error) {
1417
+ this.logger.warn({ error, sessionId }, "Failed to start queued prompt.");
1418
+ if (prompt?.messageId)
1419
+ await this.finalizeStandaloneMessageReaction(prompt.messageId, "failed");
1420
+ if (prompt)
1421
+ await this.sendError(prompt.contextKey, error);
1422
+ if (prompt && this.store.countQueuedPrompts(sessionId) > 0) {
1423
+ queueMicrotask(() => void this.scheduleNextQueuedPrompt(sessionId));
1424
+ }
1425
+ }
1426
+ }
1427
+ async finalizeStandaloneMessageReaction(messageId, status) {
1428
+ const reaction = this.store.claimMessageReaction(messageId);
1429
+ if (reaction)
1430
+ await this.replaceMessageReaction(reaction, status);
1431
+ }
1432
+ async bindMessageReactionToTurn(messageId, sessionId, turnId) {
1433
+ this.store.bindMessageToTurn(messageId, sessionId, turnId);
1434
+ this.store.bindMessageReaction(messageId, sessionId, turnId);
1435
+ const session = this.store.getSession(sessionId);
1436
+ if (session?.lastTurnId !== turnId)
1437
+ return;
1438
+ if (session.lastTurnStatus === "completed" || session.lastTurnStatus === "failed" || session.lastTurnStatus === "cancelled") {
1439
+ await this.finalizeTurnMessageReactions(turnId, session.lastTurnStatus);
1440
+ }
1441
+ }
1442
+ async finalizeTurnMessageReactions(turnId, status) {
1443
+ for (const reaction of this.store.claimMessageReactionsForTurn(turnId)) {
1444
+ await this.replaceMessageReaction(reaction, status);
1445
+ }
1446
+ }
1447
+ async replaceMessageReaction(reaction, status) {
1448
+ const emojiType = status === "completed"
1449
+ ? MESSAGE_COMPLETED_REACTION
1450
+ : status === "cancelled"
1451
+ ? MESSAGE_CANCELLED_REACTION
1452
+ : MESSAGE_FAILED_REACTION;
1453
+ try {
1454
+ const replacementId = await this.outbound.addReaction(reaction.contextKey, reaction.messageId, emojiType);
1455
+ if (!replacementId)
1456
+ throw new Error("Feishu did not return the replacement reaction ID.");
1457
+ try {
1458
+ await this.outbound.deleteReaction(reaction.contextKey, reaction.messageId, reaction.reactionId);
1459
+ }
1460
+ catch (error) {
1461
+ this.logger.warn({ error, messageId: reaction.messageId, reactionId: reaction.reactionId }, "Added the terminal reaction but failed to remove the previous reaction.");
1462
+ }
1463
+ this.store.finishMessageReaction(reaction.messageId, replacementId, emojiType, status);
1464
+ }
1465
+ catch (error) {
1466
+ this.store.releaseMessageReaction(reaction.messageId);
1467
+ this.logger.warn({ error, messageId: reaction.messageId, status }, "Failed to update the Feishu message reaction for a completed task state.");
1468
+ }
1469
+ }
1470
+ async cancel(contextKey) {
1471
+ const record = this.requireCurrentSession(contextKey);
1472
+ await this.cancelSession(record);
1473
+ }
1474
+ async cancelSession(record) {
1475
+ const runtime = this.runtimes.forAgent(this.ensureAgent(record.agentName));
1476
+ if (runtime.kind === "codex" && record.remoteSessionId && runtime.interruptRemoteTurn) {
1477
+ if (runtime.getGoal && runtime.setGoal) {
1478
+ try {
1479
+ if (!runtime.getSession(record.localSessionId))
1480
+ await this.loadSession(record);
1481
+ const goal = await runtime.getGoal(record.localSessionId);
1482
+ if (goal?.status === "active")
1483
+ await runtime.setGoal(record.localSessionId, { status: "paused" });
1484
+ }
1485
+ catch (error) {
1486
+ this.logger.warn({ error, sessionId: record.localSessionId }, "Failed to pause the active Codex goal before interrupting its turn.");
1487
+ }
1488
+ }
1489
+ let turnId = runtime.getSession(record.localSessionId)?.activeTurnId;
1490
+ if (runtime.readRemoteSession) {
1491
+ try {
1492
+ const remote = await runtime.readRemoteSession(record.remoteSessionId);
1493
+ turnId = remote.status === "active" || remote.lastTurnStatus === "inProgress"
1494
+ ? remote.lastTurnId
1495
+ : undefined;
1496
+ }
1497
+ catch (error) {
1498
+ this.logger.warn({ error, sessionId: record.localSessionId, remoteSessionId: record.remoteSessionId }, "Failed to inspect the current Codex turn before interrupting; using the locally tracked turn.");
1499
+ }
1500
+ }
1501
+ if (!turnId) {
1502
+ await this.outbound.sendText(record.contextKey, "当前没有正在执行的任务。");
1503
+ return;
1504
+ }
1505
+ await runtime.interruptRemoteTurn(record.remoteSessionId, turnId);
1506
+ this.store.audit(record.contextKey, "turn_interrupt_sent", {
1507
+ localSessionId: record.localSessionId,
1508
+ remoteSessionId: record.remoteSessionId,
1509
+ turnId,
1510
+ });
1511
+ await this.outbound.sendText(record.contextKey, `已向 Codex 发送 Interrupt 请求:${turnId}`);
1512
+ return;
1513
+ }
1514
+ const loaded = await this.loadSession(record);
1515
+ const turnId = loaded.session.activeTurnId;
1516
+ if (!turnId) {
1517
+ await this.outbound.sendText(record.contextKey, "当前没有正在执行的任务。");
1518
+ return;
1519
+ }
1520
+ await loaded.runtime.cancelTurn(record.localSessionId, turnId);
1521
+ }
1522
+ async resolveApproval(action) {
1523
+ const sessionId = String(action.value.sessionId ?? "");
1524
+ const requestId = String(action.value.requestId ?? "");
1525
+ const decision = String(action.value.decision ?? "");
1526
+ if (!["accept", "acceptForSession", "decline", "cancel"].includes(decision)) {
1527
+ throw new Error("无效的确认选项。");
1528
+ }
1529
+ const loaded = await this.loadSession(this.requireSession(action.contextKey, sessionId));
1530
+ await loaded.runtime.respondToApproval(sessionId, requestId, decision);
1531
+ }
1532
+ async model(contextKey, model, options = {}) {
1533
+ const record = options.sessionId
1534
+ ? this.requireSession(contextKey, options.sessionId)
1535
+ : this.requireCurrentSession(contextKey);
1536
+ const loaded = await this.loadSession(record);
1537
+ const models = await loaded.runtime.listModels();
1538
+ if (!model) {
1539
+ const currentModel = loaded.session.model ?? models.find((item) => item.isDefault)?.id;
1540
+ const card = this.cardRenderer.renderModelSelector({
1541
+ sessionId: loaded.record.localSessionId,
1542
+ contextKey,
1543
+ currentModel,
1544
+ reasoningEffort: loaded.session.reasoningEffort,
1545
+ models,
1546
+ });
1547
+ if (options.updateMessageId) {
1548
+ await this.outbound.updateInteractiveCard(contextKey, options.updateMessageId, card);
1549
+ }
1550
+ else {
1551
+ await this.outbound.sendInteractiveCard(contextKey, card);
1552
+ }
1553
+ return;
1554
+ }
1555
+ const selected = models.find((item) => item.id === model);
1556
+ if (!selected)
1557
+ throw new Error(`未知模型:${model}`);
1558
+ const currentEffort = loaded.session.reasoningEffort;
1559
+ const compatible = currentEffort
1560
+ ? selected.supportedReasoningEfforts.some((option) => option.value === currentEffort)
1561
+ : false;
1562
+ const nextEffort = compatible ? currentEffort : selected.defaultReasoningEffort;
1563
+ await loaded.runtime.setModel(loaded.record.localSessionId, model);
1564
+ if (nextEffort && nextEffort !== currentEffort) {
1565
+ await loaded.runtime.setReasoningEffort(loaded.record.localSessionId, nextEffort);
1566
+ }
1567
+ this.store.updateRuntimeSession(loaded.record.localSessionId, { model, reasoningEffort: nextEffort });
1568
+ const effortMessage = nextEffort && nextEffort !== currentEffort
1569
+ ? `,思考强度已自动调整为 ${nextEffort}`
1570
+ : "";
1571
+ if (options.updateMessageId) {
1572
+ await this.outbound.updateInteractiveCard(contextKey, options.updateMessageId, this.cardRenderer.renderReasoningSelector({
1573
+ sessionId: loaded.record.localSessionId,
1574
+ contextKey,
1575
+ model,
1576
+ currentEffort: nextEffort,
1577
+ options: selected.supportedReasoningEfforts,
1578
+ notice: `模型已切换为 ${cardCode(model)},请选择思考模式。`,
1579
+ }));
1580
+ return;
1581
+ }
1582
+ await this.outbound.sendText(contextKey, `模型已切换为 ${model}${effortMessage},从下一次请求生效。`);
1583
+ }
1584
+ async goal(contextKey, command) {
1585
+ const objective = command.action === "set" || command.action === "edit" ? command.objective : undefined;
1586
+ if (objective !== undefined)
1587
+ validateGoalObjective(objective);
1588
+ let record = this.currentSession(contextKey);
1589
+ if (!record) {
1590
+ if (command.action !== "set") {
1591
+ throw new Error("当前没有任务。请使用 /goal <目标> 创建 Goal,或先发送消息创建任务。");
1592
+ }
1593
+ const context = this.store.getOrCreateUserContext(contextKey, this.config.defaults.agent);
1594
+ const agent = this.ensureAgent(context.defaultAgent);
1595
+ if (agent.kind !== "codex")
1596
+ throw new Error("Goal 模式仅支持 Codex App Server 任务。");
1597
+ record = await this.createSession(contextKey, context.defaultAgent, this.inheritedNewTaskCwd(contextKey), false, false, objective);
1598
+ }
1599
+ const loaded = await this.loadSession(record);
1600
+ if (loaded.runtime.kind !== "codex" || !loaded.runtime.getGoal || !loaded.runtime.setGoal || !loaded.runtime.clearGoal) {
1601
+ throw new Error("当前任务不支持 Codex Goal 模式。");
1602
+ }
1603
+ if (command.action === "show") {
1604
+ await this.sendGoalCard(contextKey, await loaded.runtime.getGoal(record.localSessionId));
1605
+ return;
1606
+ }
1607
+ if (command.action === "clear") {
1608
+ const cleared = await loaded.runtime.clearGoal(record.localSessionId);
1609
+ if (!cleared) {
1610
+ await this.outbound.sendText(contextKey, "当前任务没有 Goal。");
1611
+ return;
1612
+ }
1613
+ await this.sendGoalCard(contextKey, undefined, "Goal 已清除;当前正在执行的轮次不会被中断。", record);
1614
+ return;
1615
+ }
1616
+ const current = await loaded.runtime.getGoal(record.localSessionId);
1617
+ if (command.action === "pause" || command.action === "resume") {
1618
+ if (!current)
1619
+ throw new Error("当前任务没有 Goal。使用 /goal <目标> 创建一个 Goal。");
1620
+ const status = command.action === "pause" ? "paused" : "active";
1621
+ const goal = await loaded.runtime.setGoal(record.localSessionId, { status });
1622
+ await this.sendGoalCard(contextKey, goal, command.action === "pause"
1623
+ ? "Goal 已暂停;当前轮次可以完成,但不会继续自动执行。"
1624
+ : "Goal 已恢复,Codex 会继续自动执行。", record);
1625
+ return;
1626
+ }
1627
+ if (objective === undefined)
1628
+ throw new Error("无效的 Goal 命令。");
1629
+ if (command.action === "set" && current && current.status !== "complete") {
1630
+ throw new Error("当前已有未完成的 Goal。使用 /goal edit <新目标> 修改,或先使用 /goal clear 清除。");
1631
+ }
1632
+ if (command.action === "edit" && !current) {
1633
+ throw new Error("当前任务没有可修改的 Goal。使用 /goal <目标> 创建一个 Goal。");
1634
+ }
1635
+ const goal = await loaded.runtime.setGoal(record.localSessionId, {
1636
+ objective,
1637
+ status: command.action === "edit" ? current.status : "active",
1638
+ ...(command.action === "edit" ? { tokenBudget: current.tokenBudget } : {}),
1639
+ });
1640
+ await this.sendGoalCard(contextKey, goal, command.action === "edit" ? "Goal 已更新。" : "Goal 已启动,Codex 会持续执行直到完成、暂停或遇到阻塞。", record);
1641
+ }
1642
+ async sendGoalCard(contextKey, goal, notice, record = this.currentSession(contextKey)) {
1643
+ const sections = [];
1644
+ if (notice)
1645
+ sections.push({ lines: [cardText(notice)] });
1646
+ sections.push(goal
1647
+ ? { title: "当前 Goal", lines: goalDetailLines(goal) }
1648
+ : { title: "当前 Goal", lines: ["未设置。使用 **/goal &#60;目标&#62;** 创建一个长任务。"] });
1649
+ if (record) {
1650
+ sections.push({
1651
+ title: "任务",
1652
+ lines: [
1653
+ `**标题**:${cardText(record.title ?? "未命名任务")}`,
1654
+ `**Codex 任务 ID**:${cardText(record.remoteSessionId ?? "尚未创建")}`,
1655
+ ],
1656
+ });
1657
+ }
1658
+ sections.push({
1659
+ title: "命令",
1660
+ lines: [
1661
+ "**/goal** 查看  **/goal pause** 暂停  **/goal resume** 恢复",
1662
+ "**/goal edit &#60;新目标&#62;** 修改  **/goal clear** 清除",
1663
+ ],
1664
+ });
1665
+ await this.outbound.sendInteractiveCard(contextKey, this.cardRenderer.renderSectionsCard("Codex Goal", sections));
1666
+ }
1667
+ async runShellCommand(contextKey, command) {
1668
+ const cwd = this.currentSession(contextKey)?.cwd ?? this.config.defaults.cwd;
1669
+ const result = await this.shellCommandExecutor(command, cwd);
1670
+ const outputParts = [
1671
+ result.stdout.trimEnd(),
1672
+ result.stderr.trimEnd() ? `[stderr]\n${result.stderr.trimEnd()}` : "",
1673
+ ].filter(Boolean);
1674
+ const output = truncateMiddle(outputParts.join("\n"), 6_000);
1675
+ const status = result.timedOut
1676
+ ? "已超时(120s)"
1677
+ : `退出码 ${result.exitCode ?? "未知"}`;
1678
+ await this.outbound.sendMarkdown(contextKey, [
1679
+ codeBlock(`$ ${command.trim()}\n${output || "(无输出)"}`, "text"),
1680
+ `${asInlineCode(cwd)} · ${status}${result.outputTruncated ? " · 输出已截断" : ""}`,
1681
+ ].join("\n"));
1682
+ }
1683
+ async setTitle(contextKey, title) {
1684
+ const normalizedTitle = normalizeTaskTitle(title);
1685
+ if (!normalizedTitle)
1686
+ throw new Error("任务标题不能为空。");
1687
+ const loaded = await this.loadSession(this.requireCurrentSession(contextKey));
1688
+ if (loaded.runtime.setTitle)
1689
+ await loaded.runtime.setTitle(loaded.record.localSessionId, normalizedTitle);
1690
+ else
1691
+ loaded.session.title = normalizedTitle;
1692
+ this.store.updateRuntimeSession(loaded.record.localSessionId, { title: normalizedTitle });
1693
+ this.outbound.updateSessionTitle(loaded.record.localSessionId, normalizedTitle);
1694
+ await this.outbound.sendText(contextKey, `已将当前任务标题修改为:${normalizedTitle}`);
1695
+ }
1696
+ async syncTaskTitleFromGroupName(contextKey, event) {
1697
+ const parsed = parseAgentGroupName(event.afterName);
1698
+ if (!parsed)
1699
+ return;
1700
+ const context = this.store.getUserContext(contextKey);
1701
+ if (!context?.currentSessionId)
1702
+ return;
1703
+ const record = this.store.getSessionForContext(context.currentSessionId, contextKey);
1704
+ if (!record || record.status === "closed")
1705
+ return;
1706
+ if (record.agentName.toLowerCase() !== parsed.agentName.toLowerCase())
1707
+ return;
1708
+ const title = normalizeTaskTitle(parsed.title);
1709
+ if (!title || title === record.title)
1710
+ return;
1711
+ const loaded = await this.loadSession(record);
1712
+ if (loaded.runtime.setTitle)
1713
+ await loaded.runtime.setTitle(record.localSessionId, title);
1714
+ else
1715
+ loaded.session.title = title;
1716
+ this.store.updateRuntimeSession(record.localSessionId, { title });
1717
+ this.outbound.updateSessionTitle(record.localSessionId, title);
1718
+ this.store.audit(contextKey, "session_title_changed", {
1719
+ localSessionId: record.localSessionId,
1720
+ title,
1721
+ source: "group_name",
1722
+ beforeName: event.beforeName,
1723
+ afterName: event.afterName,
1724
+ });
1725
+ }
1726
+ async thinking(contextKey, effort, options = {}) {
1727
+ const record = options.sessionId
1728
+ ? this.requireSession(contextKey, options.sessionId)
1729
+ : this.requireCurrentSession(contextKey);
1730
+ const loaded = await this.loadSession(record);
1731
+ const models = await loaded.runtime.listModels();
1732
+ const currentModel = models.find((item) => item.id === loaded.session.model)
1733
+ ?? models.find((item) => item.isDefault);
1734
+ if (!currentModel)
1735
+ throw new Error("当前运行时没有可配置思考强度的模型。");
1736
+ if (options.expectedModel && currentModel.id !== options.expectedModel) {
1737
+ throw new Error("模型已发生变化,请重新打开 /model。");
1738
+ }
1739
+ const supported = currentModel.supportedReasoningEfforts;
1740
+ if (!effort) {
1741
+ const card = this.cardRenderer.renderReasoningSelector({
1742
+ sessionId: loaded.record.localSessionId,
1743
+ contextKey,
1744
+ model: currentModel.id,
1745
+ currentEffort: loaded.session.reasoningEffort ?? currentModel.defaultReasoningEffort,
1746
+ options: supported,
1747
+ });
1748
+ if (options.updateMessageId) {
1749
+ await this.outbound.updateInteractiveCard(contextKey, options.updateMessageId, card);
1750
+ }
1751
+ else {
1752
+ await this.outbound.sendInteractiveCard(contextKey, card);
1753
+ }
1754
+ return;
1755
+ }
1756
+ if (!supported.some((option) => option.value === effort)) {
1757
+ const options = supported.map((option) => option.value).join("、") || "无";
1758
+ throw new Error(`不支持的思考强度:${effort}。支持的强度:${options}`);
1759
+ }
1760
+ await loaded.runtime.setReasoningEffort(loaded.record.localSessionId, effort);
1761
+ this.store.updateRuntimeSession(loaded.record.localSessionId, { reasoningEffort: effort });
1762
+ if (options.updateMessageId) {
1763
+ await this.outbound.updateInteractiveCard(contextKey, options.updateMessageId, this.cardRenderer.renderReasoningSelector({
1764
+ sessionId: loaded.record.localSessionId,
1765
+ contextKey,
1766
+ model: currentModel.id,
1767
+ currentEffort: effort,
1768
+ options: supported,
1769
+ notice: `思考模式已切换为 ${cardCode(effort)},从下一次请求生效。`,
1770
+ }));
1771
+ return;
1772
+ }
1773
+ await this.outbound.sendText(contextKey, `思考强度已切换为 ${effort},从下一次请求生效。`);
1774
+ }
1775
+ async permissions(contextKey, mode) {
1776
+ const record = this.requireCurrentSession(contextKey);
1777
+ if (!mode) {
1778
+ await this.outbound.sendText(contextKey, `当前权限模式:${record.permissionMode ?? "auto"}`);
1779
+ return;
1780
+ }
1781
+ const loaded = await this.loadSession(record);
1782
+ await loaded.runtime.setPermissionMode(record.localSessionId, mode);
1783
+ this.store.updateRuntimeSession(record.localSessionId, { permissionMode: mode });
1784
+ await this.outbound.sendText(contextKey, mode === "auto" ? "已切换为自动执行模式。" : "已切换为执行前确认模式。");
1785
+ }
1786
+ async listAgents(contextKey) {
1787
+ const current = this.store.getOrCreateUserContext(contextKey, this.config.defaults.agent).defaultAgent;
1788
+ const lines = Object.entries(this.config.agents).map(([name, agent]) => {
1789
+ const selected = name === current;
1790
+ return `- ${selected ? "✅ " : ""}${asInlineCode(name)}:${agent.title}${selected ? "(当前)" : ""}`;
1791
+ });
1792
+ await this.outbound.sendMarkdown(contextKey, [
1793
+ `当前 Agent:${asInlineCode(current)}`,
1794
+ "可用 Agent:",
1795
+ lines.join("\n") || "无",
1796
+ ].join("\n"));
1797
+ }
1798
+ async listSessions(contextKey, searchTerm, options = {}) {
1799
+ const context = this.store.getOrCreateUserContext(contextKey, this.config.defaults.agent);
1800
+ const normalizedSearch = searchTerm?.trim().toLowerCase();
1801
+ const visibleCount = options.visibleCount ?? SESSION_PAGE_SIZE;
1802
+ const localSessions = this.store.listSessions(contextKey).filter((session) => session.status !== "closed" && this.isCodexSession(session));
1803
+ let remoteSessions = [];
1804
+ let remoteHint;
1805
+ let remoteHasMore = false;
1806
+ const runtime = this.runtimes.get("codex");
1807
+ if (runtime.listRemoteSessions) {
1808
+ try {
1809
+ const page = await runtime.listRemoteSessions({ searchTerm, limit: visibleCount });
1810
+ remoteSessions = page.sessions;
1811
+ remoteHasMore = Boolean(page.nextCursor);
1812
+ }
1813
+ catch (error) {
1814
+ this.logger.warn({ error, contextKey }, "Failed to list Codex sessions.");
1815
+ remoteHint = `读取 Codex 任务失败:${error instanceof Error ? error.message : String(error)}`;
1816
+ }
1817
+ }
1818
+ const entries = mergeTaskList(localSessions, remoteSessions, context.currentSessionId)
1819
+ .filter((entry) => !normalizedSearch || [entry.id, entry.title, entry.cwd].some((value) => value.toLowerCase().includes(normalizedSearch)))
1820
+ .sort((left, right) => Number(right.current) - Number(left.current)
1821
+ || Number(right.active) - Number(left.active)
1822
+ || right.updatedAt - left.updatedAt);
1823
+ const activeCount = entries.filter((entry) => entry.active).length;
1824
+ const visibleEntries = entries.slice(0, visibleCount);
1825
+ const hasMore = remoteHasMore || entries.length > visibleCount;
1826
+ this.lastSessionListings.set(contextKey, visibleEntries.map((entry) => entry.id));
1827
+ const cardEntries = visibleEntries.map((entry, index) => {
1828
+ const marker = entry.current ? "✅" : entry.active ? "🟢 **活跃**" : "•";
1829
+ const showStop = entry.status === "外部执行中" && entry.id !== options.forceSwitchTaskId;
1830
+ const actions = entry.current ? [] : [{
1831
+ text: showStop ? "Stop" : "Switch",
1832
+ type: showStop ? "danger" : "default",
1833
+ value: {
1834
+ action: showStop ? "session_stop" : "session_switch",
1835
+ sessionId: entry.id,
1836
+ ...(searchTerm ? { searchTerm } : {}),
1837
+ visibleCount: String(visibleCount),
1838
+ contextKey,
1839
+ },
1840
+ }];
1841
+ actions.push({
1842
+ text: "New",
1843
+ value: {
1844
+ action: "session_new",
1845
+ sessionId: entry.id,
1846
+ ...(searchTerm ? { searchTerm } : {}),
1847
+ visibleCount: String(visibleCount),
1848
+ contextKey,
1849
+ },
1850
+ });
1851
+ actions.push({
1852
+ text: "Fork",
1853
+ value: {
1854
+ action: "session_fork",
1855
+ sessionId: entry.id,
1856
+ ...(searchTerm ? { searchTerm } : {}),
1857
+ visibleCount: String(visibleCount),
1858
+ contextKey,
1859
+ },
1860
+ });
1861
+ actions.push({
1862
+ text: "Status",
1863
+ value: {
1864
+ action: "session_status",
1865
+ sessionId: entry.id,
1866
+ contextKey,
1867
+ },
1868
+ });
1869
+ return {
1870
+ lines: [
1871
+ `**${index + 1}.** ${marker} **${cardText(entry.title)}** ${cardText(entry.id)}`,
1872
+ `${entry.status} · ${entry.updatedLabel} · ${cardText(entry.cwd || "目录未知")}`,
1873
+ ],
1874
+ actions,
1875
+ };
1876
+ });
1877
+ const card = this.cardRenderer.renderTaskListCard(searchTerm ? `Codex 任务:${searchTerm}` : "Codex 任务", activeCount > 0 ? `任务(${activeCount} 个活跃)` : "任务", cardEntries, [
1878
+ ...(remoteHint ? [remoteHint] : []),
1879
+ "点击 **New** 在对应任务的项目中创建新任务;点击 **Switch** 快速切换;点击 **Fork** 从任务最新已完成轮次创建分支;外部正在运行的任务显示 **Stop**,点击后发送 Interrupt 并变为 **Switch**。",
1880
+ "也可发送 **/switch [序号或任务 ID]**;不带参数切回上一个任务。外部正在执行的回合不会被接管。",
1881
+ "发送 **/fork [序号或任务 ID]**,可从当前或指定任务创建分支;任务运行中时使用最近已完成轮次。",
1882
+ "点击 **Status**,或发送 **/status [序号或任务 ID]**,查看当前或指定任务状态。",
1883
+ ], hasMore ? {
1884
+ text: "More",
1885
+ type: "primary",
1886
+ value: {
1887
+ action: "session_more",
1888
+ ...(searchTerm ? { searchTerm } : {}),
1889
+ visibleCount: String(visibleCount),
1890
+ contextKey,
1891
+ },
1892
+ } : undefined);
1893
+ if (options.updateMessageId) {
1894
+ await this.outbound.updateInteractiveCard(contextKey, options.updateMessageId, card);
1895
+ }
1896
+ else {
1897
+ await this.outbound.sendInteractiveCard(contextKey, card);
1898
+ }
1899
+ }
1900
+ async refreshSessionsCardFromAction(action, forceSwitchTaskId, loadMore = false) {
1901
+ if (!action.messageId)
1902
+ return;
1903
+ const searchTerm = typeof action.value.searchTerm === "string" && action.value.searchTerm.trim()
1904
+ ? action.value.searchTerm
1905
+ : undefined;
1906
+ const currentVisibleCount = parseSessionVisibleCount(action.value.visibleCount);
1907
+ await this.listSessions(action.contextKey, searchTerm, {
1908
+ updateMessageId: action.messageId,
1909
+ forceSwitchTaskId,
1910
+ visibleCount: currentVisibleCount + (loadMore ? SESSION_PAGE_SIZE : 0),
1911
+ });
1912
+ }
1913
+ async refreshStatusCardFromAction(action, forceSwitchTaskId) {
1914
+ if (!action.messageId)
1915
+ return;
1916
+ const sessionId = typeof action.value.sessionId === "string" && action.value.sessionId.trim()
1917
+ ? action.value.sessionId
1918
+ : undefined;
1919
+ await this.status(action.contextKey, sessionId, {
1920
+ updateMessageId: action.messageId,
1921
+ forceSwitchTaskId,
1922
+ });
1923
+ }
1924
+ async switchSession(contextKey, reference) {
1925
+ this.store.getOrCreateUserContext(contextKey, this.config.defaults.agent);
1926
+ const taskId = this.resolveSessionReference(contextKey, reference);
1927
+ const direct = this.store.getSession(taskId);
1928
+ if (direct?.status === "closed") {
1929
+ throw new Error(`找不到任务:${taskId}`);
1930
+ }
1931
+ const existing = direct ?? this.store.findSessionByRemoteSessionId(taskId);
1932
+ if (existing) {
1933
+ const runtime = this.runtimes.forAgent(this.ensureAgent(existing.agentName));
1934
+ await this.assertSessionTurnOwnership(existing, runtime);
1935
+ this.store.attachSessionToContext(contextKey, existing.localSessionId);
1936
+ this.store.setCurrentSession(contextKey, existing.localSessionId);
1937
+ this.outbound.registerSession(existing.localSessionId, contextKey, existing.title, existing.cwd);
1938
+ await this.outbound.sendText(contextKey, `已切换到任务:${existing.title ?? existing.remoteSessionId ?? taskId}`);
1939
+ return;
1940
+ }
1941
+ const runtime = this.runtimes.get("codex");
1942
+ if (!runtime.readRemoteSession)
1943
+ throw new Error("当前 Codex 运行时不支持读取任务。");
1944
+ const remote = await runtime.readRemoteSession(taskId);
1945
+ if (remote.status === "active" || remote.lastTurnStatus === "inProgress") {
1946
+ throw new Error(`这个任务正在外部 Codex 中执行,当前不会切换。可使用 /status ${taskId} 查看进度。`);
1947
+ }
1948
+ if (!remote.cwd)
1949
+ throw new Error("这个 Codex 任务没有可用的工作目录,暂时无法切换。");
1950
+ const agentEntry = Object.entries(this.config.agents).find(([, agent]) => agent.kind === "codex");
1951
+ if (!agentEntry)
1952
+ throw new Error("未配置 Codex Agent。");
1953
+ const [agentName] = agentEntry;
1954
+ const localSessionId = createId("sess");
1955
+ this.store.createSession({
1956
+ localSessionId,
1957
+ contextKey,
1958
+ agentName,
1959
+ cwd: remote.cwd,
1960
+ status: "ready",
1961
+ });
1962
+ this.store.updateRuntimeSession(localSessionId, {
1963
+ runtimeKind: "codex",
1964
+ remoteSessionId: remote.id,
1965
+ title: remote.title ?? remote.preview,
1966
+ permissionMode: "auto",
1967
+ lastTurnId: remote.lastTurnId,
1968
+ lastTurnStatus: mapRemoteTurnStatus(remote.lastTurnStatus),
1969
+ });
1970
+ this.store.setCurrentSession(contextKey, localSessionId);
1971
+ this.outbound.registerSession(localSessionId, contextKey, remote.title ?? remote.preview, remote.cwd);
1972
+ await this.outbound.sendText(contextKey, `已切换到任务:${remote.title ?? remote.preview ?? remote.id}。历史消息不会重新发送。`);
1973
+ }
1974
+ async stopSessionReference(contextKey, taskId) {
1975
+ const direct = this.store.getSession(taskId);
1976
+ if (direct) {
1977
+ if (direct.status === "closed")
1978
+ throw new Error(`找不到任务:${taskId}`);
1979
+ await this.cancelSession({ ...direct, contextKey });
1980
+ return;
1981
+ }
1982
+ const existing = this.store.findSessionByRemoteSessionId(taskId);
1983
+ if (existing) {
1984
+ await this.cancelSession({ ...existing, contextKey });
1985
+ return;
1986
+ }
1987
+ const runtime = this.runtimes.get("codex");
1988
+ if (!runtime.readRemoteSession || !runtime.interruptRemoteTurn) {
1989
+ throw new Error("当前 Codex 运行时不支持停止外部任务。");
1990
+ }
1991
+ const remote = await runtime.readRemoteSession(taskId);
1992
+ const turnId = remote.status === "active" || remote.lastTurnStatus === "inProgress"
1993
+ ? remote.lastTurnId
1994
+ : undefined;
1995
+ if (!turnId) {
1996
+ await this.outbound.sendText(contextKey, "当前没有正在执行的任务。");
1997
+ return;
1998
+ }
1999
+ await runtime.interruptRemoteTurn(remote.id, turnId);
2000
+ this.store.audit(contextKey, "turn_interrupt_sent", {
2001
+ remoteSessionId: remote.id,
2002
+ turnId,
2003
+ source: "sessions_card",
2004
+ });
2005
+ await this.outbound.sendText(contextKey, `已向 Codex 发送 Interrupt 请求:${turnId}`);
2006
+ }
2007
+ resolveSessionReference(contextKey, reference) {
2008
+ if (reference === undefined) {
2009
+ const previous = this.store.getOrCreateUserContext(contextKey, this.config.defaults.agent).previousSessionId;
2010
+ if (!previous)
2011
+ throw new Error("没有可切换的上一个任务。请先使用 /new 创建任务,或使用 /switch <序号或任务 ID> 切换任务。");
2012
+ return previous;
2013
+ }
2014
+ if (!/^\d+$/.test(reference))
2015
+ return reference;
2016
+ const position = Number(reference);
2017
+ const listing = this.lastSessionListings.get(contextKey);
2018
+ if (!listing)
2019
+ throw new Error("请先发送 /sessions 获取任务列表,再使用任务序号。");
2020
+ if (!Number.isSafeInteger(position) || position < 1 || position > listing.length) {
2021
+ throw new Error(`任务序号超出范围:${reference}。当前列表共有 ${listing.length} 项,请重新发送 /sessions。`);
2022
+ }
2023
+ return listing[position - 1];
2024
+ }
2025
+ async assertSessionTurnOwnership(record, runtime) {
2026
+ if (runtime.kind !== "codex" || !record.remoteSessionId || !runtime.readRemoteSession)
2027
+ return;
2028
+ if (runtime.inspectRemoteSessionActivity) {
2029
+ let activity;
2030
+ try {
2031
+ activity = await runtime.inspectRemoteSessionActivity(record.remoteSessionId);
2032
+ }
2033
+ catch (error) {
2034
+ if (!record.lastTurnId && isUnmaterializedCodexThreadError(error))
2035
+ return { active: false };
2036
+ throw error;
2037
+ }
2038
+ const localActiveTurnId = runtime.getSession(record.localSessionId)?.activeTurnId;
2039
+ const runtimeOwnsActiveTurn = Boolean(localActiveTurnId)
2040
+ && (!activity.activeTurnId || activity.activeTurnId === localActiveTurnId);
2041
+ const persistedTurnMatches = record.status === "running"
2042
+ && record.lastTurnStatus === "running"
2043
+ && Boolean(record.lastTurnId)
2044
+ && activity.activeTurnId === record.lastTurnId;
2045
+ const botOwnsActiveTurn = runtimeOwnsActiveTurn || persistedTurnMatches;
2046
+ if (activity.active && !botOwnsActiveTurn) {
2047
+ throw new Error("这个任务正在外部 Codex 中执行。Agent Bot 不会接管或追加消息,请等待外部执行完成。");
2048
+ }
2049
+ return activity;
2050
+ }
2051
+ let remote;
2052
+ try {
2053
+ remote = await runtime.readRemoteSession(record.remoteSessionId);
2054
+ }
2055
+ catch (error) {
2056
+ // Codex does not materialize a new thread until its first user message.
2057
+ // Such a thread has no turn to take over, so allow turn/start to create it.
2058
+ if (!record.lastTurnId && isUnmaterializedCodexThreadError(error))
2059
+ return;
2060
+ throw error;
2061
+ }
2062
+ const botOwnsActiveTurn = isBotOwnedActiveTurn(record, remote);
2063
+ if ((remote.status === "active" || remote.lastTurnStatus === "inProgress") && !botOwnsActiveTurn) {
2064
+ throw new Error("这个任务正在外部 Codex 中执行。Agent Bot 不会接管或追加消息,请等待外部执行完成。");
2065
+ }
2066
+ return undefined;
2067
+ }
2068
+ async setDefaultAgent(contextKey, agentName) {
2069
+ this.ensureAgent(agentName);
2070
+ this.store.getOrCreateUserContext(contextKey, this.config.defaults.agent);
2071
+ this.store.setDefaultAgent(contextKey, agentName);
2072
+ await this.outbound.sendText(contextKey, `默认 agent 已切换为:${agentName}`);
2073
+ }
2074
+ async status(contextKey, sessionId, options = {}) {
2075
+ const context = this.store.getOrCreateUserContext(contextKey, this.config.defaults.agent);
2076
+ const targetSessionId = sessionId === undefined ? undefined : this.resolveSessionReference(contextKey, sessionId);
2077
+ let current;
2078
+ if (targetSessionId) {
2079
+ const direct = this.store.getSession(targetSessionId);
2080
+ current = direct
2081
+ ? { ...direct, contextKey }
2082
+ : this.store.findSessionByRemoteSessionId(targetSessionId, contextKey);
2083
+ if (!current) {
2084
+ await this.statusForCodexTask(contextKey, targetSessionId, options);
2085
+ return;
2086
+ }
2087
+ }
2088
+ else {
2089
+ current = context.currentSessionId ? this.store.getSession(context.currentSessionId) : undefined;
2090
+ }
2091
+ if (current && current.status === "running") {
2092
+ try {
2093
+ const runtime = this.runtimes.forAgent(this.ensureAgent(current.agentName));
2094
+ if (runtime.getSession(current.localSessionId)) {
2095
+ await runtime.synchronizeSession(current.localSessionId);
2096
+ current = this.store.getSession(current.localSessionId) ?? current;
2097
+ }
2098
+ }
2099
+ catch (error) {
2100
+ this.logger.warn({ error, sessionId: current.localSessionId }, "Failed to synchronize task status.");
2101
+ }
2102
+ }
2103
+ const localCurrent = current;
2104
+ let remote;
2105
+ let goal;
2106
+ if (current?.remoteSessionId) {
2107
+ const runtime = this.runtimes.forAgent(this.ensureAgent(current.agentName));
2108
+ if (runtime.readRemoteSession) {
2109
+ try {
2110
+ remote = await runtime.readRemoteSession(current.remoteSessionId);
2111
+ }
2112
+ catch (error) {
2113
+ this.logger.warn({ error, sessionId: current.localSessionId }, "Failed to inspect Codex task status.");
2114
+ }
2115
+ }
2116
+ if (runtime.kind === "codex" && runtime.getGoal) {
2117
+ try {
2118
+ const loaded = runtime.getSession(current.localSessionId) ?? (await this.loadSession(current)).session;
2119
+ goal = await runtime.getGoal(loaded.localSessionId);
2120
+ }
2121
+ catch (error) {
2122
+ this.logger.warn({ error, sessionId: current.localSessionId }, "Failed to inspect Codex goal status.");
2123
+ }
2124
+ }
2125
+ }
2126
+ if (current)
2127
+ current = mergeRemoteTaskStatus(current, remote);
2128
+ const taskLines = [];
2129
+ let snapshot;
2130
+ let activeTurnId;
2131
+ let queued = 0;
2132
+ if (!current) {
2133
+ taskLines.push("无。直接发送消息即可创建一个未指定项目的 Codex 任务。");
2134
+ }
2135
+ else {
2136
+ const agent = this.ensureAgent(current.agentName);
2137
+ const runtimeSession = this.runtimes.forAgent(agent).getSession(current.localSessionId);
2138
+ activeTurnId = runtimeSession?.activeTurnId;
2139
+ const remoteActive = isRemoteSessionActive(remote);
2140
+ activeTurnId = remoteActive ? remote?.lastTurnId ?? activeTurnId : activeTurnId;
2141
+ queued = this.store.countQueuedPrompts(current.localSessionId);
2142
+ snapshot = turnViewSnapshot(current.lastTurnId ? this.store.getTurnSnapshot(current.lastTurnId) : undefined);
2143
+ const statusLabel = remoteActive && remote && localCurrent && !isBotOwnedActiveTurn(localCurrent, remote)
2144
+ ? "外部执行中"
2145
+ : sessionStatusLabel(current.status, activeTurnId);
2146
+ const resultLabel = remoteActive
2147
+ ? "执行中"
2148
+ : remoteTurnStatusLabel(remote?.lastTurnStatus ?? current.lastTurnStatus);
2149
+ 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)}`);
2150
+ }
2151
+ const sections = [
2152
+ { title: targetSessionId ? "指定任务" : "当前任务", lines: taskLines },
2153
+ ...(current ? [{
2154
+ title: "Goal",
2155
+ lines: goal ? goalDetailLines(goal) : ["未设置。"],
2156
+ }] : []),
2157
+ ...(current ? [{
2158
+ title: "最终结果",
2159
+ lines: finalResultLines(snapshot, remote),
2160
+ }] : []),
2161
+ ...(current ? [{
2162
+ title: "执行详情",
2163
+ lines: executionDetailLines(localCurrent ?? current, snapshot, remote, activeTurnId, queued),
2164
+ collapsible: true,
2165
+ elementId: "status_execution_details",
2166
+ }] : []),
2167
+ {
2168
+ title: "Agent Bot",
2169
+ lines: [
2170
+ `**默认 Agent / 保活**:${cardCode(context.defaultAgent)} / ${this.lifecycle?.supervised ? "已启用" : "未启用"}`,
2171
+ "**交互方式**:普通消息继续当前任务;/new 创建新任务;/help 查看命令。",
2172
+ ],
2173
+ },
2174
+ ];
2175
+ const title = targetSessionId && current
2176
+ ? `Codex 状态:${truncateText((current.title ?? current.remoteSessionId ?? current.localSessionId).replace(/\s+/g, " "), 40)}`
2177
+ : "Codex 状态";
2178
+ const taskId = current?.remoteSessionId ?? current?.localSessionId;
2179
+ const isCurrent = Boolean(current && current.localSessionId === context.currentSessionId);
2180
+ const remoteActive = isRemoteSessionActive(remote);
2181
+ const botOwnsActiveTurn = Boolean(localCurrent && remote && isBotOwnedActiveTurn(localCurrent, remote));
2182
+ const active = Boolean(activeTurnId || current?.status === "running" || remoteActive);
2183
+ const forceSwitch = Boolean(taskId && options.forceSwitchTaskId === taskId);
2184
+ const taskActions = !taskId ? []
2185
+ : remoteActive && !botOwnsActiveTurn && !forceSwitch
2186
+ ? [statusCardAction("Stop", "danger", "session_stop", taskId, contextKey)]
2187
+ : isCurrent && active && !forceSwitch
2188
+ ? [statusCardAction("Stop", "danger", "session_stop", taskId, contextKey)]
2189
+ : !isCurrent
2190
+ ? [statusCardAction("Switch", "default", "session_switch", taskId, contextKey)]
2191
+ : [];
2192
+ const actions = [
2193
+ statusRefreshAction(taskId, contextKey),
2194
+ ...taskActions,
2195
+ ];
2196
+ const card = this.cardRenderer.renderSectionsCard(title, sections, actions);
2197
+ if (options.updateMessageId)
2198
+ await this.outbound.updateInteractiveCard(contextKey, options.updateMessageId, card);
2199
+ else
2200
+ await this.outbound.sendInteractiveCard(contextKey, card);
2201
+ }
2202
+ async statusForCodexTask(contextKey, remoteSessionId, options = {}) {
2203
+ const runtime = this.runtimes.get("codex");
2204
+ if (!runtime.readRemoteSession)
2205
+ throw new Error("当前 Codex 运行时不支持读取外部任务状态。");
2206
+ const remote = await runtime.readRemoteSession(remoteSessionId);
2207
+ const sections = [
2208
+ {
2209
+ title: "指定任务",
2210
+ lines: [
2211
+ `**标题**:${cardCode(remote.title ?? remote.preview ?? "未命名任务")}`,
2212
+ `**工作目录**:${cardCode(remote.cwd || "目录未知")}`,
2213
+ `**状态 / 当前任务**:${remoteSessionDetailStatus(remote)} / 未切换`,
2214
+ `**Codex 任务 ID**:${cardCode(remote.id)}`,
2215
+ `**最近回合**:${cardCode(remote.lastTurnId ?? "无")} ${remoteTurnStatusLabel(remote.lastTurnStatus)}`,
2216
+ `**创建时间 / 最近活动**:${formatRemoteTime(remote.createdAt)} / ${formatRemoteTime(latestRemoteTimestamp(remote.recencyAt, remote.updatedAt))}`,
2217
+ ],
2218
+ },
2219
+ { title: "最终结果", lines: finalResultLines(undefined, remote) },
2220
+ {
2221
+ title: "执行详情",
2222
+ lines: [
2223
+ `**当前 / 最后步骤**:${statusExcerpt(remote.lastActivity ?? remoteStatusStep(remote), 500)}`,
2224
+ isRemoteSessionActive(remote)
2225
+ ? "外部 Codex 正在执行;Agent Bot 只读取状态,不会接管。"
2226
+ : `发送 **/switch ${cardText(remote.id)}** 切换到此任务。`,
2227
+ ],
2228
+ collapsible: true,
2229
+ elementId: "status_execution_details",
2230
+ },
2231
+ ];
2232
+ const title = `Codex 状态:${truncateText((remote.title ?? remote.preview ?? remote.id).replace(/\s+/g, " "), 40)}`;
2233
+ const showStop = isRemoteSessionActive(remote) && options.forceSwitchTaskId !== remote.id;
2234
+ const actions = [
2235
+ statusRefreshAction(remote.id, contextKey),
2236
+ statusCardAction(showStop ? "Stop" : "Switch", showStop ? "danger" : "default", showStop ? "session_stop" : "session_switch", remote.id, contextKey),
2237
+ ];
2238
+ const card = this.cardRenderer.renderSectionsCard(title, sections, actions);
2239
+ if (options.updateMessageId)
2240
+ await this.outbound.updateInteractiveCard(contextKey, options.updateMessageId, card);
2241
+ else
2242
+ await this.outbound.sendInteractiveCard(contextKey, card);
2243
+ }
2244
+ async help(contextKey) {
2245
+ const sections = [
2246
+ {
2247
+ lines: [
2248
+ "直接发送消息即可继续当前任务;执行中发送的新消息会追加到本次任务。",
2249
+ "**[参数]** 可选 **&#60;参数&#62;** 必填",
2250
+ ],
2251
+ },
2252
+ {
2253
+ title: "任务管理",
2254
+ lines: [
2255
+ "**/new [title] [--dir &#60;cwd&#62; | --nodir]** 使用默认 Agent 创建任务;--nodir 强制创建 Projectless 任务",
2256
+ "**/newgroup [title]** 创建飞书群和新任务;继承当前项目与执行设置",
2257
+ "**/forkgroup [title]** 从当前任务最新已完成轮次创建分支并绑定到新群",
2258
+ "**/fork [序号或任务 ID]** 从当前或指定任务创建分支;运行中使用最近已完成轮次",
2259
+ "**/title &#60;新标题&#62;** 修改当前任务的标题",
2260
+ "**/sessions [关键词]** 查找本机任务",
2261
+ "**/switch [序号或任务 ID]** 不填参数切回上一个任务",
2262
+ ],
2263
+ },
2264
+ {
2265
+ title: "执行设置",
2266
+ lines: [
2267
+ "**! &#60;命令&#62;** 在当前任务目录直接执行本地命令",
2268
+ "**/stop** 停止当前执行",
2269
+ "**/queue &#60;prompt&#62;**(兼容 **/nosteer**) 不追加到当前轮次,按顺序排队为后续轮次",
2270
+ "**/goal [目标]** 查看或创建长任务 Goal;支持 pause、resume、edit、clear",
2271
+ "**/model [name]** 查看或切换模型",
2272
+ "**/thinking [level]** 查看或设置思考强度",
2273
+ "**/permissions [auto|confirm]** 查看或切换确认模式",
2274
+ ],
2275
+ },
2276
+ {
2277
+ title: "Agent",
2278
+ lines: [
2279
+ "**/agent [name]** 查看或设置新任务的默认 Agent",
2280
+ ],
2281
+ },
2282
+ {
2283
+ title: "系统",
2284
+ lines: [
2285
+ "**/status [序号或任务 ID]** 查看当前或指定任务的详细状态",
2286
+ "**/restart** 重启 Agent Bot 并恢复会话",
2287
+ "**/help** 显示本帮助",
2288
+ ],
2289
+ },
2290
+ ];
2291
+ await this.outbound.sendInteractiveCard(contextKey, this.cardRenderer.renderSectionsCard("Agent Bot 使用帮助", sections));
2292
+ }
2293
+ ensureAgent(agentName) {
2294
+ const agent = this.config.agents[agentName];
2295
+ if (!agent)
2296
+ throw new Error(`未知 agent:${agentName}`);
2297
+ return agent;
2298
+ }
2299
+ isCodexSession(session) {
2300
+ if (session.runtimeKind)
2301
+ return session.runtimeKind === "codex";
2302
+ return this.config.agents[session.agentName]?.kind === "codex";
2303
+ }
2304
+ currentSession(contextKey) {
2305
+ const context = this.store.getOrCreateUserContext(contextKey, this.config.defaults.agent);
2306
+ return context.currentSessionId
2307
+ ? this.store.getSessionForContext(context.currentSessionId, contextKey)
2308
+ : undefined;
2309
+ }
2310
+ inheritedNewTaskCwd(contextKey) {
2311
+ const current = this.currentSession(contextKey);
2312
+ if (!current)
2313
+ return this.store.getUserContext(contextKey)?.boundProjectCwd;
2314
+ return detectProjectlessWorkspace(current.cwd)
2315
+ ? createProjectlessWorkspace().cwd
2316
+ : current.cwd;
2317
+ }
2318
+ currentProjectCwd(contextKey) {
2319
+ const current = this.currentSession(contextKey);
2320
+ return current && !detectProjectlessWorkspace(current.cwd) ? current.cwd : undefined;
2321
+ }
2322
+ requireCurrentSession(contextKey) {
2323
+ const record = this.currentSession(contextKey);
2324
+ if (!record)
2325
+ throw new Error("当前没有任务,直接发送一条消息即可自动创建。");
2326
+ return record;
2327
+ }
2328
+ requireSession(contextKey, sessionId) {
2329
+ const record = this.store.getSessionForContext(sessionId, contextKey);
2330
+ if (!record)
2331
+ throw new Error(`找不到任务:${sessionId}`);
2332
+ return record;
2333
+ }
2334
+ async sendError(contextKey, error) {
2335
+ this.logger.warn({ error, contextKey }, "Request failed.");
2336
+ await this.outbound.sendText(contextKey, error instanceof Error ? error.message : String(error));
2337
+ }
2338
+ }
2339
+ function sessionStatusLabel(status, activeTurnId) {
2340
+ if (activeTurnId || status === "running")
2341
+ return "执行中";
2342
+ const labels = {
2343
+ starting: "正在启动",
2344
+ ready: "就绪",
2345
+ running: "执行中",
2346
+ closed: "已关闭",
2347
+ failed: "异常",
2348
+ };
2349
+ return labels[status];
2350
+ }
2351
+ function statusCardAction(text, type, action, sessionId, contextKey) {
2352
+ return {
2353
+ text,
2354
+ type,
2355
+ value: { action, sessionId, cardView: "status", contextKey },
2356
+ };
2357
+ }
2358
+ function statusRefreshAction(sessionId, contextKey) {
2359
+ return {
2360
+ text: "Refresh",
2361
+ value: {
2362
+ action: "session_status_refresh",
2363
+ ...(sessionId ? { sessionId } : {}),
2364
+ cardView: "status",
2365
+ contextKey,
2366
+ },
2367
+ };
2368
+ }
2369
+ function mergeTaskList(localSessions, remoteSessions, currentLocalSessionId) {
2370
+ const localByRemoteId = new Map(localSessions
2371
+ .filter((session) => session.runtimeKind === "codex" && session.remoteSessionId)
2372
+ .map((session) => [session.remoteSessionId, session]));
2373
+ const representedLocalIds = new Set();
2374
+ const entries = remoteSessions.map((remote) => {
2375
+ const local = localByRemoteId.get(remote.id);
2376
+ if (local)
2377
+ representedLocalIds.add(local.localSessionId);
2378
+ const active = remote.status === "active" || remote.lastTurnStatus === "inProgress";
2379
+ const status = remote.status === "active" || remote.lastTurnStatus === "inProgress"
2380
+ ? local && isBotOwnedActiveTurn(local, remote) ? "执行中" : "外部执行中"
2381
+ : remoteSessionStatusLabel(remote.status);
2382
+ const recencyAt = remote.recencyAt ?? remote.updatedAt;
2383
+ return {
2384
+ id: remote.id,
2385
+ title: remote.title ?? remote.preview ?? local?.title ?? "未命名任务",
2386
+ cwd: remote.cwd || local?.cwd || "",
2387
+ status,
2388
+ active,
2389
+ current: Boolean(local && local.localSessionId === currentLocalSessionId),
2390
+ updatedAt: (recencyAt ?? 0) * 1_000,
2391
+ updatedLabel: formatRemoteTime(recencyAt),
2392
+ };
2393
+ });
2394
+ for (const local of localSessions) {
2395
+ if (representedLocalIds.has(local.localSessionId))
2396
+ continue;
2397
+ const updatedAt = Date.parse(local.updatedAt);
2398
+ entries.push({
2399
+ id: local.remoteSessionId ?? local.localSessionId,
2400
+ title: local.title ?? "未命名任务",
2401
+ cwd: local.cwd,
2402
+ status: sessionStatusLabel(local.status),
2403
+ active: local.status === "running",
2404
+ current: local.localSessionId === currentLocalSessionId,
2405
+ updatedAt: Number.isNaN(updatedAt) ? 0 : updatedAt,
2406
+ updatedLabel: formatStatusTime(local.updatedAt),
2407
+ });
2408
+ }
2409
+ return entries;
2410
+ }
2411
+ function isMissingRolloutError(error) {
2412
+ const message = error instanceof Error ? error.message : String(error);
2413
+ return /no rollout found|rollout[^\n]*(?:not found|missing)|thread\/resume failed/i.test(message);
2414
+ }
2415
+ function isUnmaterializedCodexThreadError(error) {
2416
+ const message = error instanceof Error ? error.message : String(error);
2417
+ return /thread\/read failed:[^\n]*(?:not materialized|not loaded|includeTurns is unavailable before first user message)/i.test(message);
2418
+ }
2419
+ function turnViewSnapshot(value) {
2420
+ if (!value || typeof value !== "object")
2421
+ return undefined;
2422
+ const candidate = value;
2423
+ return typeof candidate.turnId === "string" && typeof candidate.status === "string"
2424
+ ? candidate
2425
+ : undefined;
2426
+ }
2427
+ function executionDetailLines(record, snapshot, remote, activeTurnId, queued) {
2428
+ const remoteActive = isRemoteSessionActive(remote);
2429
+ const turnId = remoteActive
2430
+ ? remote?.lastTurnId ?? activeTurnId
2431
+ : activeTurnId ?? remote?.lastTurnId ?? snapshot?.turnId ?? record.lastTurnId;
2432
+ if (!turnId)
2433
+ return ["尚无执行记录。"];
2434
+ const relevantSnapshot = snapshot?.turnId === turnId ? snapshot : undefined;
2435
+ const externallyActive = Boolean(remoteActive && remote && !isBotOwnedActiveTurn(record, remote));
2436
+ const lines = [
2437
+ `**回合 ID**:${cardText(turnId)}`,
2438
+ `**执行状态**:${externallyActive ? "外部执行中" : relevantSnapshot ? turnViewStatusLabel(relevantSnapshot.status) : remoteTurnStatusLabel(remote?.lastTurnStatus ?? remoteStatusToTurnStatus(remote?.status) ?? record.lastTurnStatus)}`,
2439
+ `**当前 / 最后步骤**:${statusExcerpt(currentOrLastStep(relevantSnapshot, remote), 600)}`,
2440
+ ];
2441
+ const remoteCountsApply = remote?.lastTurnId === turnId && remote.lastTurnToolCount !== undefined;
2442
+ if (relevantSnapshot || remoteCountsApply) {
2443
+ const lastTool = [...(relevantSnapshot?.activities ?? [])]
2444
+ .reverse()
2445
+ .find((activity) => activity.kind === "tool")?.tool;
2446
+ const tool = relevantSnapshot?.activeTool ?? lastTool;
2447
+ const completedTools = remoteCountsApply
2448
+ ? remote.lastTurnCompletedToolCount ?? 0
2449
+ : relevantSnapshot?.completedToolCount ?? relevantSnapshot?.completedTools?.length ?? 0;
2450
+ const failedTools = remoteCountsApply
2451
+ ? remote.lastTurnFailedToolCount ?? 0
2452
+ : relevantSnapshot?.failedToolCount ?? relevantSnapshot?.failedTools?.length ?? 0;
2453
+ const runningTools = remoteCountsApply
2454
+ ? remote.lastTurnRunningToolCount ?? 0
2455
+ : relevantSnapshot?.activeTool ? 1 : 0;
2456
+ lines.push(`**工具执行**:完成 ${completedTools},失败 ${failedTools}${runningTools > 0 ? `,当前 ${runningTools}` : ""}`);
2457
+ if (tool)
2458
+ lines.push(`**当前 / 最后工具**:${statusExcerpt(tool.title, 400)}(${toolStatusLabel(tool.status)})`);
2459
+ if (relevantSnapshot?.durationMs !== undefined)
2460
+ lines.push(`**耗时**:${formatDuration(relevantSnapshot.durationMs)}`);
2461
+ }
2462
+ if (queued > 0)
2463
+ lines.push(`**排队消息**:${queued} 条`);
2464
+ return lines;
2465
+ }
2466
+ function finalResultLines(snapshot, remote) {
2467
+ const relevantSnapshot = !remote?.lastTurnId || snapshot?.turnId === remote.lastTurnId ? snapshot : undefined;
2468
+ if (relevantSnapshot?.status === "running" || relevantSnapshot?.status === "tool_running" || relevantSnapshot?.status === "waiting_for_approval"
2469
+ || isRemoteSessionActive(remote)) {
2470
+ return ["任务仍在执行,尚无最终结果。"];
2471
+ }
2472
+ const result = remote?.finalResponse?.trim() || relevantSnapshot?.finalResponse?.trim();
2473
+ if (result)
2474
+ return [statusExcerpt(result, 2_800)];
2475
+ const error = remote?.lastError?.trim() || relevantSnapshot?.error?.trim();
2476
+ if (error)
2477
+ return [`❌ ${statusExcerpt(error, 2_400)}`];
2478
+ if (relevantSnapshot?.status === "cancelled" || remote?.lastTurnStatus === "interrupted") {
2479
+ return ["任务已停止,未产生最终回答。"];
2480
+ }
2481
+ if (relevantSnapshot?.status === "failed" || remote?.lastTurnStatus === "failed") {
2482
+ return ["任务执行失败,未记录最终回答。"];
2483
+ }
2484
+ return ["没有保存到可展示的最终结果。"];
2485
+ }
2486
+ function currentOrLastStep(snapshot, remote) {
2487
+ const snapshotActive = snapshot?.status === "running" || snapshot?.status === "tool_running" || snapshot?.status === "waiting_for_approval";
2488
+ if (isRemoteSessionActive(remote) && !snapshotActive)
2489
+ return remote?.lastActivity ?? remoteStatusStep(remote);
2490
+ if (snapshot?.approval)
2491
+ return `等待确认:${snapshot.approval.title}`;
2492
+ if (snapshot?.activeTool)
2493
+ return `正在执行:${snapshot.activeTool.title}`;
2494
+ const activePlan = snapshot?.plan?.find((step) => step.status === "in_progress");
2495
+ if (activePlan)
2496
+ return activePlan.text;
2497
+ const activity = [...(snapshot?.activities ?? [])].reverse().find((item) => item.kind === "reasoning" ? Boolean(item.text.trim()) : true);
2498
+ if (activity?.kind === "reasoning")
2499
+ return activity.text;
2500
+ if (activity?.kind === "tool")
2501
+ return `${toolStatusLabel(activity.tool.status)}:${activity.tool.title}`;
2502
+ if (snapshot?.progressText)
2503
+ return snapshot.progressText;
2504
+ if (remote?.lastActivity)
2505
+ return remote.lastActivity;
2506
+ return remoteStatusStep(remote);
2507
+ }
2508
+ function remoteStatusStep(remote) {
2509
+ if (!remote)
2510
+ return "未记录执行步骤";
2511
+ if (isRemoteSessionActive(remote))
2512
+ return "外部任务正在执行,实时步骤尚未同步到本进程";
2513
+ if (remote.lastTurnStatus === "completed")
2514
+ return "最近回合已完成";
2515
+ if (remote.lastTurnStatus === "interrupted")
2516
+ return "最近回合已停止";
2517
+ if (remote.lastTurnStatus === "failed")
2518
+ return "最近回合执行失败";
2519
+ return "尚无执行记录";
2520
+ }
2521
+ function remoteSessionDetailStatus(remote) {
2522
+ if (isRemoteSessionActive(remote))
2523
+ return "🟢 外部执行中";
2524
+ return remoteSessionStatusLabel(remote.status);
2525
+ }
2526
+ function isRemoteSessionActive(remote) {
2527
+ return remote?.status === "active" || remote?.lastTurnStatus === "inProgress";
2528
+ }
2529
+ function remoteTurnStatusLabel(status) {
2530
+ const labels = {
2531
+ inProgress: "执行中",
2532
+ completed: "已完成",
2533
+ interrupted: "已停止",
2534
+ failed: "失败",
2535
+ running: "执行中",
2536
+ cancelled: "已停止",
2537
+ };
2538
+ return status ? labels[status] ?? status : "尚无执行记录";
2539
+ }
2540
+ function remoteStatusToTurnStatus(status) {
2541
+ if (status === "active")
2542
+ return "inProgress";
2543
+ if (status === "error")
2544
+ return "failed";
2545
+ return undefined;
2546
+ }
2547
+ function turnViewStatusLabel(status) {
2548
+ const labels = {
2549
+ starting: "正在启动",
2550
+ running: "执行中",
2551
+ tool_running: "工具执行中",
2552
+ waiting_for_approval: "等待确认",
2553
+ completed: "已完成",
2554
+ cancelled: "已停止",
2555
+ failed: "失败",
2556
+ };
2557
+ return labels[status];
2558
+ }
2559
+ function toolStatusLabel(status) {
2560
+ return status === "running" ? "执行中" : status === "completed" ? "已完成" : "失败";
2561
+ }
2562
+ function statusExcerpt(value, maxLength) {
2563
+ const clean = value.replace(/\u001B\[[0-?]*[ -/]*[@-~]/g, "").trim();
2564
+ return cardText(truncateMiddle(clean || "未记录", maxLength));
2565
+ }
2566
+ function formatDuration(milliseconds) {
2567
+ return milliseconds < 1_000 ? `${milliseconds}ms` : `${(milliseconds / 1_000).toFixed(1)}s`;
2568
+ }
2569
+ function formatStatusTime(value) {
2570
+ const date = new Date(value);
2571
+ if (Number.isNaN(date.getTime()))
2572
+ return value;
2573
+ return new Intl.DateTimeFormat("zh-CN", {
2574
+ year: "numeric",
2575
+ month: "2-digit",
2576
+ day: "2-digit",
2577
+ hour: "2-digit",
2578
+ minute: "2-digit",
2579
+ second: "2-digit",
2580
+ hourCycle: "h23",
2581
+ }).format(date);
2582
+ }
2583
+ function cardText(value) {
2584
+ return value.replaceAll("&", "&amp;").replaceAll("`", "&#96;").replaceAll("<", "&#60;").replaceAll(">", "&#62;");
2585
+ }
2586
+ function cardCode(value) {
2587
+ return `\`${value.replaceAll("`", "'")}\``;
2588
+ }
2589
+ function remoteSessionStatusLabel(status) {
2590
+ const labels = {
2591
+ active: "执行中",
2592
+ idle: "空闲",
2593
+ not_loaded: "未加载",
2594
+ error: "异常",
2595
+ };
2596
+ return labels[status];
2597
+ }
2598
+ function formatRemoteTime(value) {
2599
+ if (value === undefined)
2600
+ return "时间未知";
2601
+ return formatStatusTime(new Date(normalizeRemoteTimestamp(value)).toISOString());
2602
+ }
2603
+ function mapRemoteTurnStatus(status) {
2604
+ if (status === "interrupted")
2605
+ return "cancelled";
2606
+ if (status === "inProgress")
2607
+ return "running";
2608
+ return status;
2609
+ }
2610
+ function mergeRemoteTaskStatus(record, remote) {
2611
+ if (!remote)
2612
+ return record;
2613
+ const remoteTurnStatus = mapRemoteTurnStatus(remote.lastTurnStatus);
2614
+ const createdAt = remote.createdAt === undefined
2615
+ ? record.createdAt
2616
+ : new Date(normalizeRemoteTimestamp(remote.createdAt)).toISOString();
2617
+ const remoteActivityAt = latestRemoteTimestamp(remote.recencyAt, remote.updatedAt);
2618
+ const updatedAt = remoteActivityAt === undefined
2619
+ ? record.updatedAt
2620
+ : new Date(remoteActivityAt).toISOString();
2621
+ return {
2622
+ ...record,
2623
+ title: remote.title ?? record.title,
2624
+ cwd: remote.cwd || record.cwd,
2625
+ status: isRemoteSessionActive(remote)
2626
+ ? "running"
2627
+ : remote.lastTurnStatus
2628
+ ? "ready"
2629
+ : record.status,
2630
+ lastTurnId: remote.lastTurnId ?? record.lastTurnId,
2631
+ lastTurnStatus: remoteTurnStatus ?? record.lastTurnStatus,
2632
+ createdAt,
2633
+ updatedAt,
2634
+ };
2635
+ }
2636
+ function normalizeRemoteTimestamp(value) {
2637
+ return value >= 10_000_000_000 ? value : value * 1_000;
2638
+ }
2639
+ function latestRemoteTimestamp(...values) {
2640
+ const timestamps = values
2641
+ .filter((value) => value !== undefined)
2642
+ .map(normalizeRemoteTimestamp);
2643
+ return timestamps.length > 0 ? Math.max(...timestamps) : undefined;
2644
+ }
2645
+ function isTurnStillRunning(status) {
2646
+ return status === "starting"
2647
+ || status === "running"
2648
+ || status === "tool_running"
2649
+ || status === "waiting_for_approval";
2650
+ }
2651
+ function forkedTurnStatus(status) {
2652
+ if (status === "cancelled")
2653
+ return "cancelled";
2654
+ if (status === "failed")
2655
+ return "failed";
2656
+ if (status === "completed")
2657
+ return "completed";
2658
+ return undefined;
2659
+ }
2660
+ function needsFullSessionSynchronization(record, session, remoteActivity) {
2661
+ if (!remoteActivity)
2662
+ return Boolean(record.lastTurnId || session.activeTurnId);
2663
+ return Boolean(session.activeTurnId && !remoteActivity.active);
2664
+ }
2665
+ function isBotOwnedActiveTurn(record, remote) {
2666
+ return record.status === "running"
2667
+ && record.lastTurnStatus === "running"
2668
+ && Boolean(record.lastTurnId)
2669
+ && record.lastTurnId === remote.lastTurnId;
2670
+ }
2671
+ function isQueueIndependentCommand(command) {
2672
+ if (["stop", "status", "restart", "help", "sessions", "goal", "nosteer"].includes(command.type))
2673
+ return true;
2674
+ if (command.type === "agent")
2675
+ return command.agent === undefined;
2676
+ if (command.type === "model")
2677
+ return command.model === undefined;
2678
+ if (command.type === "thinking")
2679
+ return command.effort === undefined;
2680
+ if (command.type === "permissions")
2681
+ return command.mode === undefined;
2682
+ return false;
2683
+ }
2684
+ function parseSessionVisibleCount(value) {
2685
+ const parsed = typeof value === "string" || typeof value === "number" ? Number(value) : Number.NaN;
2686
+ return Number.isSafeInteger(parsed) && parsed >= SESSION_PAGE_SIZE ? parsed : SESSION_PAGE_SIZE;
2687
+ }
2688
+ function isPromptCommand(command) {
2689
+ return command.type === "ask" || command.type === "prompt" || command.type === "nosteer";
2690
+ }
2691
+ function runtimePrompt(text, localImagePaths) {
2692
+ return localImagePaths?.length ? { text, localImagePaths } : text;
2693
+ }
2694
+ function formatGroupDateSuffix(date) {
2695
+ const month = String(date.getMonth() + 1).padStart(2, "0");
2696
+ const day = String(date.getDate()).padStart(2, "0");
2697
+ return `${month}-${day}`;
2698
+ }
2699
+ function formatNewGroupName(agentName, projectCwd, taskTitle, date, includeTimestamp) {
2700
+ const prefix = `[${agentName}] `;
2701
+ const project = formatGroupProjectDirectory(projectCwd);
2702
+ const titlePrefix = `${prefix}${project} `;
2703
+ const timestampSuffix = includeTimestamp ? ` (${formatGroupDateSuffix(date)})` : "";
2704
+ const availableTitleLength = FEISHU_GROUP_NAME_MAX_LENGTH
2705
+ - Array.from(titlePrefix).length
2706
+ - Array.from(timestampSuffix).length;
2707
+ const title = truncateTail(taskTitle, availableTitleLength);
2708
+ return truncateTail(`${titlePrefix}${title}${timestampSuffix}`, FEISHU_GROUP_NAME_MAX_LENGTH);
2709
+ }
2710
+ const FEISHU_GROUP_NAME_MAX_LENGTH = 60;
2711
+ const GROUP_PROJECT_DIRECTORY_MAX_LENGTH = 15;
2712
+ function formatGroupProjectDirectory(projectCwd) {
2713
+ const value = projectCwd ? abbreviateHomeDirectory(projectCwd) : "Projectless";
2714
+ if (Array.from(value).length <= GROUP_PROJECT_DIRECTORY_MAX_LENGTH)
2715
+ return `[${value}]`;
2716
+ const levels = value
2717
+ .replace(/[\\/]+$/, "")
2718
+ .split(/[\\/]+/)
2719
+ .filter(Boolean)
2720
+ .slice(-2);
2721
+ return `[${fitTrailingPathLevels(levels, GROUP_PROJECT_DIRECTORY_MAX_LENGTH, path.sep)}]`;
2722
+ }
2723
+ function abbreviateHomeDirectory(value) {
2724
+ const relative = path.relative(os.homedir(), value);
2725
+ if (relative === "")
2726
+ return "~";
2727
+ if (path.isAbsolute(relative) || relative === ".." || relative.startsWith(`..${path.sep}`))
2728
+ return value;
2729
+ return `~${path.sep}${relative}`;
2730
+ }
2731
+ function fitTrailingPathLevels(levels, maxLength, separator) {
2732
+ if (levels.length === 0)
2733
+ return "";
2734
+ if (levels.length === 1)
2735
+ return truncatePathLevel(levels[0], maxLength);
2736
+ const parent = levels[0];
2737
+ const leaf = levels[1];
2738
+ const joined = `${parent}${separator}${leaf}`;
2739
+ if (Array.from(joined).length <= maxLength)
2740
+ return joined;
2741
+ if (Array.from(leaf).length <= maxLength)
2742
+ return leaf;
2743
+ return truncatePathLevel(leaf, maxLength);
2744
+ }
2745
+ function truncatePathLevel(value, maxLength) {
2746
+ return truncateTail(value, maxLength);
2747
+ }
2748
+ function truncateTail(value, maxLength) {
2749
+ const characters = Array.from(value);
2750
+ if (characters.length <= maxLength)
2751
+ return value;
2752
+ if (maxLength <= 0)
2753
+ return "";
2754
+ if (maxLength <= 3)
2755
+ return ".".repeat(maxLength);
2756
+ return `${characters.slice(0, maxLength - 3).join("")}...`;
2757
+ }
2758
+ function parseAgentGroupName(value) {
2759
+ const match = /^\[([^[\]]+)\]\s+(.+)$/.exec(value.trim());
2760
+ const agentName = match?.[1]?.trim();
2761
+ const title = match?.[2]?.trim();
2762
+ return agentName && title ? { agentName, title } : undefined;
2763
+ }
2764
+ function validateGoalObjective(objective) {
2765
+ const length = Array.from(objective.trim()).length;
2766
+ if (length === 0)
2767
+ throw new Error("Goal 不能为空。");
2768
+ if (length > 4_000) {
2769
+ throw new Error("Goal 最多 4000 个字符。请把详细说明写入文件,并在 Goal 中引用该文件。");
2770
+ }
2771
+ }
2772
+ function goalDetailLines(goal) {
2773
+ return [
2774
+ `**状态**:${goalStatusLabel(goal.status)}`,
2775
+ `**目标**:${statusExcerpt(goal.objective, 2_800)}`,
2776
+ `**消耗**:${formatGoalTokenCount(goal.tokensUsed)} tokens / ${formatGoalElapsed(goal.timeUsedSeconds)}`,
2777
+ ...(goal.tokenBudget === null || goal.tokenBudget === undefined
2778
+ ? []
2779
+ : [`**Token 预算**:${formatGoalTokenCount(goal.tokenBudget)}`]),
2780
+ `**更新时间**:${formatUnixTime(goal.updatedAt)}`,
2781
+ ];
2782
+ }
2783
+ function goalStatusLabel(status) {
2784
+ const labels = {
2785
+ active: "执行中",
2786
+ paused: "已暂停",
2787
+ blocked: "已阻塞",
2788
+ usageLimited: "额度受限",
2789
+ budgetLimited: "Token 预算受限",
2790
+ complete: "已完成",
2791
+ };
2792
+ return labels[status];
2793
+ }
2794
+ function formatGoalTokenCount(tokens) {
2795
+ const rounded = Math.max(0, Math.round(tokens));
2796
+ if (rounded < 10_000)
2797
+ return new Intl.NumberFormat("zh-CN").format(rounded);
2798
+ return new Intl.NumberFormat("en-US", {
2799
+ notation: "compact",
2800
+ compactDisplay: "short",
2801
+ maximumSignificantDigits: 3,
2802
+ }).format(rounded);
2803
+ }
2804
+ function formatGoalElapsed(seconds) {
2805
+ const totalSeconds = Math.max(0, Math.round(seconds));
2806
+ const second = totalSeconds % 60;
2807
+ const totalMinutes = Math.floor(totalSeconds / 60);
2808
+ const minute = totalMinutes % 60;
2809
+ const hours = Math.floor(totalMinutes / 60);
2810
+ if (hours > 0)
2811
+ return `${String(hours).padStart(2, "0")}:${String(minute).padStart(2, "0")}:${String(second).padStart(2, "0")}`;
2812
+ if (totalMinutes > 0)
2813
+ return `${String(totalMinutes).padStart(2, "0")}:${String(second).padStart(2, "0")}`;
2814
+ return `${totalSeconds}s`;
2815
+ }
2816
+ function formatUnixTime(seconds) {
2817
+ return formatStatusTime(new Date(seconds * 1_000).toISOString());
2818
+ }
2819
+ //# sourceMappingURL=ProxySessionController.js.map