@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,539 @@
1
+ import type { AgentEvent } from "../runtime/types.js";
2
+ import {
3
+ appendSteerMessage as appendSteerMessageToState,
4
+ createTurnViewState,
5
+ reduceTurnEvent,
6
+ } from "../presentation/TurnStateReducer.js";
7
+ import { splitMarkdown } from "../presentation/splitMarkdown.js";
8
+ import type { TurnViewState } from "../presentation/turnViewTypes.js";
9
+ import { CardRenderer } from "./CardRenderer.js";
10
+ import { CardUpdateScheduler } from "./CardUpdateScheduler.js";
11
+ import type { FeishuOutbound, MessageReplyTarget } from "./types.js";
12
+ import { createId } from "../utils/id.js";
13
+ import { createHash } from "node:crypto";
14
+
15
+ const THINKING_CARD_RUNNING_REACTION = "OnIt";
16
+ const THINKING_CARD_FAILED_REACTION = "ERROR";
17
+ const THINKING_CARD_CANCELLED_REACTION = "CrossMark";
18
+
19
+ interface TurnDeliveryRecord {
20
+ progressMessageId?: string;
21
+ finalDelivered: boolean;
22
+ finalMessageIds: string[];
23
+ }
24
+
25
+ export interface TurnPresentationStore {
26
+ saveTurnSnapshot(turnId: string, localSessionId: string, snapshot: unknown, contextKey?: string): void;
27
+ promotePendingTurn(
28
+ pendingTurnId: string,
29
+ turnId: string,
30
+ localSessionId: string,
31
+ snapshot: unknown,
32
+ contextKey?: string,
33
+ ): void;
34
+ getTurnSnapshot(turnId: string): unknown;
35
+ getTurnContextKey?(turnId: string): string | undefined;
36
+ saveTurnDelivery(turnId: string, patch: { progressMessageId?: string; lastCardHash?: string }): void;
37
+ saveFinalDeliveryProgress(turnId: string, messageIds: string[]): void;
38
+ markFinalDelivered(turnId: string, messageIds: string[]): void;
39
+ getTurnDelivery(turnId: string): TurnDeliveryRecord | undefined;
40
+ }
41
+
42
+ export interface FeishuTurnPresenterOptions {
43
+ normalIntervalMs?: number;
44
+ criticalGapMs?: number;
45
+ finalChunkLength?: number;
46
+ onError?: (error: unknown) => void;
47
+ finalRetryBackoffMs?: number[];
48
+ }
49
+
50
+ interface TurnEntry {
51
+ contextKey: string;
52
+ state: TurnViewState;
53
+ historySnapshot?: TurnViewState;
54
+ initializing: Promise<void>;
55
+ messageId?: string;
56
+ reactionId?: string;
57
+ reactionEmoji?: string;
58
+ scheduler?: CardUpdateScheduler<TurnViewState>;
59
+ finalizing?: Promise<void>;
60
+ lastFinalizedStatus?: TurnViewState["status"];
61
+ }
62
+
63
+ export class FeishuTurnPresenter {
64
+ private readonly sessionContexts = new Map<string, string>();
65
+ private readonly sessionTitles = new Map<string, string>();
66
+ private readonly sessionCwds = new Map<string, string>();
67
+ private readonly entries = new Map<string, TurnEntry>();
68
+ private readonly pendingEntries = new Map<string, TurnEntry>();
69
+ private readonly renderer: CardRenderer;
70
+
71
+ constructor(
72
+ private readonly outbound: FeishuOutbound,
73
+ private readonly store: TurnPresentationStore,
74
+ renderer?: CardRenderer,
75
+ private readonly options: FeishuTurnPresenterOptions = {},
76
+ ) {
77
+ this.renderer = renderer ?? new CardRenderer();
78
+ }
79
+
80
+ registerSession(sessionId: string, contextKey: string, taskTitle?: string, projectCwd?: string): void {
81
+ this.sessionContexts.set(sessionId, contextKey);
82
+ if (taskTitle) this.sessionTitles.set(sessionId, taskTitle);
83
+ if (projectCwd) this.sessionCwds.set(sessionId, projectCwd);
84
+ }
85
+
86
+ updateSessionTitle(sessionId: string, taskTitle: string): void {
87
+ this.sessionTitles.set(sessionId, taskTitle);
88
+ for (const entry of this.entries.values()) {
89
+ if (entry.state.sessionId !== sessionId || entry.state.taskTitle === taskTitle) continue;
90
+ entry.state = { ...entry.state, taskTitle };
91
+ this.store.saveTurnSnapshot(entry.state.turnId, sessionId, entry.state, entry.contextKey);
92
+ if (!entry.historySnapshot) entry.scheduler?.update(entry.state, "critical");
93
+ }
94
+ }
95
+
96
+ unregisterSession(sessionId: string): void {
97
+ this.sessionContexts.delete(sessionId);
98
+ this.sessionTitles.delete(sessionId);
99
+ this.sessionCwds.delete(sessionId);
100
+ }
101
+
102
+ async startPendingTurn(
103
+ sessionId: string,
104
+ contextKey: string,
105
+ taskTitle?: string,
106
+ replyTarget?: MessageReplyTarget,
107
+ prompt?: string,
108
+ ): Promise<void> {
109
+ const existing = this.pendingEntries.get(sessionId);
110
+ if (existing) {
111
+ await existing.initializing;
112
+ if (prompt && existing.state.prompt !== prompt) {
113
+ existing.state = { ...existing.state, prompt };
114
+ this.store.saveTurnSnapshot(existing.state.turnId, sessionId, existing.state, existing.contextKey);
115
+ if (!existing.historySnapshot) existing.scheduler?.update(existing.state, "critical");
116
+ }
117
+ return;
118
+ }
119
+ this.sessionContexts.set(sessionId, contextKey);
120
+ if (taskTitle) this.sessionTitles.set(sessionId, taskTitle);
121
+ const state = createTurnViewState(
122
+ sessionId,
123
+ createId("pending"),
124
+ Date.now(),
125
+ taskTitle ?? this.sessionTitles.get(sessionId),
126
+ replyTarget,
127
+ this.sessionCwds.get(sessionId),
128
+ prompt,
129
+ );
130
+ const entry = { contextKey, state, initializing: Promise.resolve() } as TurnEntry;
131
+ this.entries.set(state.turnId, entry);
132
+ this.pendingEntries.set(sessionId, entry);
133
+ this.store.saveTurnSnapshot(state.turnId, sessionId, state, contextKey);
134
+ entry.initializing = this.initializeEntry(entry);
135
+ try {
136
+ await entry.initializing;
137
+ } catch (error) {
138
+ if (this.pendingEntries.get(sessionId) === entry) this.pendingEntries.delete(sessionId);
139
+ this.entries.delete(state.turnId);
140
+ throw error;
141
+ }
142
+ }
143
+
144
+ async failPendingTurn(sessionId: string, message: string): Promise<void> {
145
+ const entry = this.pendingEntries.get(sessionId);
146
+ if (!entry) return;
147
+ this.pendingEntries.delete(sessionId);
148
+ entry.state = { ...entry.state, status: "failed", error: message, completedAt: Date.now() };
149
+ this.store.saveTurnSnapshot(entry.state.turnId, sessionId, entry.state, entry.contextKey);
150
+ await entry.initializing;
151
+ try {
152
+ await entry.scheduler?.flush(entry.state);
153
+ } catch (error) {
154
+ this.options.onError?.(error);
155
+ }
156
+ await this.syncThinkingCardReaction(entry);
157
+ }
158
+
159
+ async appendSteerMessage(
160
+ sessionId: string,
161
+ turnId: string,
162
+ text: string,
163
+ messageId?: string,
164
+ ): Promise<void> {
165
+ const activityId = `steer:${messageId ?? createId("message")}`;
166
+ let entry = this.entries.get(turnId);
167
+ if (!entry) {
168
+ const saved = this.store.getTurnSnapshot(turnId);
169
+ const contextKey = this.store.getTurnContextKey?.(turnId) ?? this.sessionContexts.get(sessionId);
170
+ if (!contextKey) return;
171
+ const initial = isTurnViewState(saved) && saved.sessionId === sessionId
172
+ ? saved
173
+ : {
174
+ ...createTurnViewState(
175
+ sessionId,
176
+ turnId,
177
+ Date.now(),
178
+ this.sessionTitles.get(sessionId),
179
+ undefined,
180
+ this.sessionCwds.get(sessionId),
181
+ ),
182
+ status: "running" as const,
183
+ };
184
+ const state = appendSteerMessageToState(initial, activityId, text);
185
+ entry = { contextKey, state, initializing: Promise.resolve() } as TurnEntry;
186
+ this.entries.set(turnId, entry);
187
+ this.store.saveTurnSnapshot(turnId, sessionId, state, contextKey);
188
+ entry.initializing = this.initializeEntry(entry);
189
+ await entry.initializing;
190
+ return;
191
+ }
192
+
193
+ await entry.initializing;
194
+ entry.state = appendSteerMessageToState(entry.state, activityId, text);
195
+ this.store.saveTurnSnapshot(turnId, sessionId, entry.state, entry.contextKey);
196
+ if (!entry.historySnapshot) entry.scheduler?.update(entry.state, "critical");
197
+ }
198
+
199
+ async onEvent(event: AgentEvent): Promise<void> {
200
+ let existing = this.entries.get(event.turnId);
201
+ let eventApplied = false;
202
+ if (!existing && event.type === "turn_started") {
203
+ const pending = this.pendingEntries.get(event.sessionId);
204
+ if (pending) {
205
+ const pendingTurnId = pending.state.turnId;
206
+ const state = reduceTurnEvent(
207
+ createTurnViewState(
208
+ event.sessionId,
209
+ event.turnId,
210
+ event.startedAt,
211
+ pending.state.taskTitle,
212
+ pending.state.replyTarget,
213
+ pending.state.projectCwd,
214
+ pending.state.prompt,
215
+ ),
216
+ event,
217
+ );
218
+ this.store.promotePendingTurn(
219
+ pendingTurnId,
220
+ event.turnId,
221
+ event.sessionId,
222
+ state,
223
+ pending.contextKey,
224
+ );
225
+ this.pendingEntries.delete(event.sessionId);
226
+ this.entries.delete(pendingTurnId);
227
+ pending.state = state;
228
+ this.entries.set(event.turnId, pending);
229
+ existing = pending;
230
+ eventApplied = true;
231
+ }
232
+ }
233
+ const entry = existing ?? this.createEntry(event);
234
+ if (!entry) return;
235
+
236
+ if (existing && !eventApplied) {
237
+ entry.state = reduceTurnEvent(entry.state, event);
238
+ this.store.saveTurnSnapshot(event.turnId, event.sessionId, entry.state, entry.contextKey);
239
+ }
240
+
241
+ await entry.initializing;
242
+ if (!entry.scheduler) return;
243
+
244
+ if (isTerminalEvent(event)) {
245
+ if (entry.finalizing) await entry.finalizing;
246
+ const terminalState = entry.state;
247
+ if (entry.lastFinalizedStatus === terminalState.status) return;
248
+ const finalizing = this.finalize(entry, terminalState);
249
+ entry.finalizing = finalizing;
250
+ try {
251
+ await finalizing;
252
+ entry.lastFinalizedStatus = terminalState.status;
253
+ } finally {
254
+ if (entry.finalizing === finalizing) entry.finalizing = undefined;
255
+ }
256
+ return;
257
+ }
258
+
259
+ if (!entry.historySnapshot) entry.scheduler.update(entry.state, eventPriority(event));
260
+ }
261
+
262
+ async showDetails(contextKey: string, turnId: string): Promise<void> {
263
+ const snapshot = this.store.getTurnSnapshot(turnId);
264
+ if (!isTurnViewState(snapshot)) {
265
+ await this.outbound.sendText(contextKey, "未找到这次执行的详情。可能已被清理。");
266
+ return;
267
+ }
268
+ await this.outbound.sendInteractiveCard(contextKey, this.renderer.renderTurnDetails(snapshot));
269
+ }
270
+
271
+ async showActivityPage(
272
+ contextKey: string,
273
+ turnId: string,
274
+ page: number | "latest",
275
+ messageId?: string,
276
+ ): Promise<void> {
277
+ const entry = this.entries.get(turnId);
278
+ if (entry) await entry.initializing;
279
+ const ownsProgressCard = entry
280
+ && entry.contextKey === contextKey
281
+ && (!messageId || !entry.messageId || messageId === entry.messageId);
282
+
283
+ if (ownsProgressCard) {
284
+ const targetMessageId = messageId ?? entry.messageId;
285
+ if (page === "latest") {
286
+ entry.historySnapshot = undefined;
287
+ if (entry.scheduler) {
288
+ entry.scheduler.invalidateRenderedCard();
289
+ await entry.scheduler.flush(entry.state);
290
+ } else if (targetMessageId) {
291
+ await this.outbound.updateInteractiveCard(targetMessageId, this.renderer.renderTurn(entry.state));
292
+ } else {
293
+ await this.outbound.sendInteractiveCard(contextKey, this.renderer.renderTurn(entry.state));
294
+ }
295
+ return;
296
+ }
297
+
298
+ if (!entry.historySnapshot) {
299
+ entry.historySnapshot = entry.state;
300
+ await entry.scheduler?.flush();
301
+ }
302
+ const card = this.renderer.renderActivityHistory(entry.historySnapshot, page);
303
+ if (targetMessageId) await this.outbound.updateInteractiveCard(targetMessageId, card);
304
+ else await this.outbound.sendInteractiveCard(contextKey, card);
305
+ return;
306
+ }
307
+
308
+ const snapshot = this.store.getTurnSnapshot(turnId);
309
+ if (!isTurnViewState(snapshot)) throw new Error("未找到这次执行的活动历史。");
310
+ const card = page === "latest"
311
+ ? this.renderer.renderTurn(snapshot)
312
+ : this.renderer.renderActivityHistory(snapshot, page);
313
+ if (messageId) await this.outbound.updateInteractiveCard(messageId, card);
314
+ else await this.outbound.sendInteractiveCard(contextKey, card);
315
+ }
316
+
317
+ async resumeDelivery(_sessionId: string, contextKey: string, turnId: string): Promise<void> {
318
+ const delivery = this.store.getTurnDelivery(turnId);
319
+ if (delivery?.finalDelivered) return;
320
+ const snapshot = this.store.getTurnSnapshot(turnId);
321
+ if (!isTurnViewState(snapshot) || snapshot.status !== "completed" || !snapshot.finalResponse) return;
322
+ await this.deliverFinal(contextKey, snapshot);
323
+ }
324
+
325
+ async flushAll(): Promise<void> {
326
+ await Promise.all(
327
+ [...this.entries.values()].map(async (entry) => {
328
+ await entry.initializing;
329
+ if (!entry.historySnapshot) await entry.scheduler?.flush();
330
+ }),
331
+ );
332
+ }
333
+
334
+ private createEntry(event: AgentEvent): TurnEntry | undefined {
335
+ const saved = this.store.getTurnSnapshot(event.turnId);
336
+ const contextKey = this.store.getTurnContextKey?.(event.turnId) ?? this.sessionContexts.get(event.sessionId);
337
+ if (!contextKey) return undefined;
338
+ const startedAt = event.type === "turn_started" ? event.startedAt : Date.now();
339
+ const initial = isTurnViewState(saved) && saved.sessionId === event.sessionId
340
+ ? saved
341
+ : createTurnViewState(
342
+ event.sessionId,
343
+ event.turnId,
344
+ startedAt,
345
+ this.sessionTitles.get(event.sessionId),
346
+ undefined,
347
+ this.sessionCwds.get(event.sessionId),
348
+ undefined,
349
+ );
350
+ const state = reduceTurnEvent(
351
+ initial,
352
+ event,
353
+ );
354
+ const entry = { contextKey, state, initializing: Promise.resolve() } as TurnEntry;
355
+ this.entries.set(event.turnId, entry);
356
+ this.store.saveTurnSnapshot(event.turnId, event.sessionId, state, contextKey);
357
+ entry.initializing = this.initializeEntry(entry);
358
+ return entry;
359
+ }
360
+
361
+ private async initializeEntry(entry: TurnEntry): Promise<void> {
362
+ const sentState = entry.state;
363
+ const card = this.renderer.renderTurn(sentState);
364
+ const delivery = this.store.getTurnDelivery(sentState.turnId);
365
+ const persistedMessageId = delivery?.progressMessageId;
366
+ if (delivery?.finalDelivered && !persistedMessageId) return;
367
+ let messageId = persistedMessageId;
368
+ if (messageId) {
369
+ await this.outbound.updateInteractiveCard(messageId, card);
370
+ } else {
371
+ messageId = sentState.replyTarget && this.outbound.replyInteractiveCard
372
+ ? await this.outbound.replyInteractiveCard(
373
+ entry.contextKey,
374
+ sentState.replyTarget,
375
+ card,
376
+ progressMessageKey(sentState.turnId),
377
+ )
378
+ : await this.outbound.sendInteractiveCard(entry.contextKey, card);
379
+ }
380
+ entry.messageId = messageId;
381
+ if (!messageId) return;
382
+ this.store.saveTurnDelivery(entry.state.turnId, { progressMessageId: messageId });
383
+ entry.scheduler = new CardUpdateScheduler<TurnViewState>({
384
+ render: (state) => this.renderer.renderTurn(state),
385
+ write: (card) => this.outbound.updateInteractiveCard(messageId, card),
386
+ normalIntervalMs: this.options.normalIntervalMs,
387
+ criticalGapMs: this.options.criticalGapMs,
388
+ onError: this.options.onError,
389
+ });
390
+ entry.scheduler.seed(sentState);
391
+ if (entry.state !== sentState) entry.scheduler.update(entry.state, "critical");
392
+ await this.syncThinkingCardReaction(entry);
393
+ }
394
+
395
+ private async finalize(entry: TurnEntry, state: TurnViewState): Promise<void> {
396
+ if (!entry.historySnapshot) {
397
+ try {
398
+ await entry.scheduler?.flush(state);
399
+ } catch (error) {
400
+ this.options.onError?.(error);
401
+ }
402
+ }
403
+ await this.syncThinkingCardReaction(entry);
404
+ if (state.status !== "completed" || !state.finalResponse) return;
405
+ await this.deliverFinal(entry.contextKey, state);
406
+ }
407
+
408
+ private async syncThinkingCardReaction(entry: TurnEntry): Promise<void> {
409
+ const messageId = entry.messageId;
410
+ if (!messageId) return;
411
+ const desiredEmoji = thinkingCardReactionForStatus(entry.state.status);
412
+ if (desiredEmoji === entry.reactionEmoji) return;
413
+
414
+ if (!desiredEmoji) {
415
+ if (!entry.reactionId || !this.outbound.deleteReaction) return;
416
+ try {
417
+ await this.outbound.deleteReaction(messageId, entry.reactionId);
418
+ entry.reactionId = undefined;
419
+ entry.reactionEmoji = undefined;
420
+ } catch (error) {
421
+ this.options.onError?.(error);
422
+ }
423
+ return;
424
+ }
425
+
426
+ if (!this.outbound.addReaction) return;
427
+ try {
428
+ const replacementId = await this.outbound.addReaction(messageId, desiredEmoji);
429
+ if (!replacementId) return;
430
+ const previousId = entry.reactionId;
431
+ entry.reactionId = replacementId;
432
+ entry.reactionEmoji = desiredEmoji;
433
+ if (previousId && this.outbound.deleteReaction) {
434
+ try {
435
+ await this.outbound.deleteReaction(messageId, previousId);
436
+ } catch (error) {
437
+ this.options.onError?.(error);
438
+ }
439
+ }
440
+ } catch (error) {
441
+ this.options.onError?.(error);
442
+ }
443
+ }
444
+
445
+ private async deliverFinal(contextKey: string, state: TurnViewState): Promise<void> {
446
+ const delivery = this.store.getTurnDelivery(state.turnId);
447
+ if (delivery?.finalDelivered || !state.finalResponse) return;
448
+ const chunks = splitMarkdown(state.finalResponse, this.options.finalChunkLength ?? 4_000);
449
+ const messageIds = [...(delivery?.finalMessageIds ?? [])];
450
+ for (let index = messageIds.length; index < chunks.length; index += 1) {
451
+ const chunk = chunks[index];
452
+ if (chunk === undefined) continue;
453
+ const messageId = await this.sendFinalChunk(
454
+ contextKey,
455
+ state.replyTarget,
456
+ chunk,
457
+ finalMessageKey(state.turnId, index),
458
+ );
459
+ messageIds.push(messageId ?? `delivered-chunk-${index}`);
460
+ this.store.saveFinalDeliveryProgress(state.turnId, messageIds);
461
+ }
462
+ this.store.markFinalDelivered(state.turnId, messageIds);
463
+ }
464
+
465
+ private async sendFinalChunk(
466
+ contextKey: string,
467
+ replyTarget: MessageReplyTarget | undefined,
468
+ chunk: string,
469
+ idempotencyKey: string,
470
+ ): Promise<string | undefined> {
471
+ const backoffs = this.options.finalRetryBackoffMs ?? [2_000, 4_000, 8_000];
472
+ for (let attempt = 0; ; attempt += 1) {
473
+ try {
474
+ if (replyTarget && this.outbound.replyMarkdown) {
475
+ return await this.outbound.replyMarkdown(contextKey, replyTarget, chunk, idempotencyKey);
476
+ }
477
+ return await this.outbound.sendMarkdown(contextKey, chunk, idempotencyKey);
478
+ } catch (error) {
479
+ if (!isRetryableError(error) || attempt >= backoffs.length) throw error;
480
+ await delay(backoffs[attempt] ?? 8_000);
481
+ }
482
+ }
483
+ }
484
+ }
485
+
486
+ function eventPriority(event: AgentEvent): "normal" | "critical" {
487
+ if (event.type === "turn_started" || event.type === "approval_requested" || event.type === "approval_resolved" || event.type === "tool_started") {
488
+ return "critical";
489
+ }
490
+ return "normal";
491
+ }
492
+
493
+ function isTerminalEvent(event: AgentEvent): boolean {
494
+ return event.type === "turn_completed" || event.type === "turn_cancelled" || event.type === "turn_failed";
495
+ }
496
+
497
+ function thinkingCardReactionForStatus(status: TurnViewState["status"]): string | undefined {
498
+ if (status === "completed") return undefined;
499
+ if (status === "failed") return THINKING_CARD_FAILED_REACTION;
500
+ if (status === "cancelled") return THINKING_CARD_CANCELLED_REACTION;
501
+ return THINKING_CARD_RUNNING_REACTION;
502
+ }
503
+
504
+ function isTurnViewState(value: unknown): value is TurnViewState {
505
+ if (typeof value !== "object" || value === null) return false;
506
+ const state = value as Partial<TurnViewState>;
507
+ return (
508
+ typeof state.sessionId === "string" &&
509
+ typeof state.turnId === "string" &&
510
+ typeof state.status === "string" &&
511
+ typeof state.startedAt === "number" &&
512
+ Array.isArray(state.plan) &&
513
+ Array.isArray(state.completedTools) &&
514
+ Array.isArray(state.failedTools) &&
515
+ Array.isArray(state.fileSummary)
516
+ );
517
+ }
518
+
519
+ function isRetryableError(error: unknown): boolean {
520
+ return (
521
+ typeof error === "object" &&
522
+ error !== null &&
523
+ (("isRetryable" in error && error.isRetryable === true) || ("isRateLimit" in error && error.isRateLimit === true))
524
+ );
525
+ }
526
+
527
+ function delay(milliseconds: number): Promise<void> {
528
+ return new Promise((resolve) => setTimeout(resolve, milliseconds));
529
+ }
530
+
531
+ function finalMessageKey(turnId: string, index: number): string {
532
+ const digest = createHash("sha256").update(`${turnId}:${index}`).digest("hex").slice(0, 32);
533
+ return `codex-final-${digest}`;
534
+ }
535
+
536
+ function progressMessageKey(turnId: string): string {
537
+ const digest = createHash("sha256").update(`progress:${turnId}`).digest("hex").slice(0, 32);
538
+ return `codex-progress-${digest}`;
539
+ }
@@ -0,0 +1,102 @@
1
+ import { createHash } from "node:crypto";
2
+ import { readFileSync } from "node:fs";
3
+ import path from "node:path";
4
+ import { Resvg } from "@resvg/resvg-js";
5
+
6
+ const AVATAR_SIZE = 512;
7
+
8
+ interface AvatarPalette {
9
+ primary: string;
10
+ accent: string;
11
+ light: string;
12
+ dark: string;
13
+ }
14
+
15
+ export function resolveGroupAvatarProjectName(projectCwd: string | undefined, fallbackTitle: string): string {
16
+ if (!projectCwd) return fallbackTitle;
17
+ try {
18
+ const manifest = JSON.parse(readFileSync(path.join(projectCwd, "package.json"), "utf8")) as { name?: unknown };
19
+ if (typeof manifest.name === "string" && manifest.name.trim()) {
20
+ return manifest.name.trim().split("/").at(-1) ?? manifest.name.trim();
21
+ }
22
+ } catch {
23
+ // Non-Node projects and unreadable manifests fall back to the directory name.
24
+ }
25
+ return path.basename(path.resolve(projectCwd)) || fallbackTitle;
26
+ }
27
+
28
+ export function groupAvatarLabel(projectName: string): string {
29
+ const normalized = projectName.trim();
30
+ const characters = Array.from(normalized);
31
+ const firstCharacter = characters[0];
32
+ if (firstCharacter && isCjk(firstCharacter.codePointAt(0) ?? 0)) return characters.slice(0, 4).join("");
33
+ return normalized.match(/[\p{L}\p{N}]+/u)?.[0] || "?";
34
+ }
35
+
36
+ export function generateGroupAvatarPng(projectName: string, projectCwd?: string): Uint8Array {
37
+ const seed = projectCwd ? path.resolve(projectCwd) : projectName;
38
+ const hash = createHash("sha256").update(seed).digest();
39
+ const svg = identiconAvatarSvg(groupAvatarLabel(projectName), choosePalette(hash[0] ?? 0), hash);
40
+ return new Resvg(svg, {
41
+ fitTo: { mode: "width", value: AVATAR_SIZE },
42
+ font: { loadSystemFonts: true },
43
+ }).render().asPng();
44
+ }
45
+
46
+ function identiconAvatarSvg(label: string, colors: AvatarPalette, bytes: Buffer): string {
47
+ const dots: string[] = [];
48
+ const cell = 62;
49
+ const start = 101;
50
+ for (let row = 0; row < 5; row += 1) {
51
+ for (let column = 0; column < 3; column += 1) {
52
+ if (((bytes[1 + row * 3 + column] ?? 0) & 1) === 0) continue;
53
+ for (const mirrored of new Set([column, 4 - column])) {
54
+ const color = (row + column) % 3 === 0 ? colors.accent : colors.primary;
55
+ dots.push(`<circle cx="${start + mirrored * cell + cell / 2}" cy="${start + row * cell + cell / 2}" r="25" fill="${color}"/>`);
56
+ }
57
+ }
58
+ }
59
+ return `<?xml version="1.0" encoding="UTF-8"?>
60
+ <svg xmlns="http://www.w3.org/2000/svg" width="${AVATAR_SIZE}" height="${AVATAR_SIZE}" viewBox="0 0 ${AVATAR_SIZE} ${AVATAR_SIZE}">
61
+ <circle cx="256" cy="256" r="240" fill="${colors.dark}"/>
62
+ ${dots.join("\n ")}
63
+ <rect x="54" y="184" width="404" height="144" rx="52" fill="${colors.dark}"/>
64
+ ${centeredText(label, colors.light)}
65
+ </svg>`;
66
+ }
67
+
68
+ function centeredText(text: string, color: string): string {
69
+ const visualWidth = Math.max(1, Array.from(text).reduce(
70
+ (width, character) => width + (isCjk(character.codePointAt(0) ?? 0) ? 1 : 0.62),
71
+ 0,
72
+ ));
73
+ const fontSize = Math.max(34, Math.min(144, 400 / visualWidth));
74
+ return `<text x="256" y="274" text-anchor="middle" dominant-baseline="middle" fill="${color}" font-family="Inter, Segoe UI, PingFang SC, Noto Sans CJK SC, sans-serif" font-size="${fontSize.toFixed(1)}" font-weight="700">${escapeXml(text)}</text>`;
75
+ }
76
+
77
+ function choosePalette(seed: number): AvatarPalette {
78
+ const palettes: AvatarPalette[] = [
79
+ { primary: "#246BFD", accent: "#20D5E8", light: "#FFFFFF", dark: "#10223C" },
80
+ { primary: "#7657FF", accent: "#E75BFF", light: "#FFFFFF", dark: "#24153F" },
81
+ { primary: "#00A884", accent: "#5CE1B9", light: "#FFFFFF", dark: "#123B36" },
82
+ { primary: "#F26B38", accent: "#FFB14A", light: "#FFFFFF", dark: "#432418" },
83
+ ];
84
+ return palettes[seed % palettes.length]!;
85
+ }
86
+
87
+ function isCjk(codePoint: number): boolean {
88
+ return (
89
+ (codePoint >= 0x3400 && codePoint <= 0x4dbf)
90
+ || (codePoint >= 0x4e00 && codePoint <= 0x9fff)
91
+ || (codePoint >= 0xf900 && codePoint <= 0xfaff)
92
+ );
93
+ }
94
+
95
+ function escapeXml(value: string): string {
96
+ return value
97
+ .replaceAll("&", "&amp;")
98
+ .replaceAll("<", "&lt;")
99
+ .replaceAll(">", "&gt;")
100
+ .replaceAll('"', "&quot;")
101
+ .replaceAll("'", "&apos;");
102
+ }
@@ -0,0 +1,13 @@
1
+ export const LOCAL_CARD_IMAGE_PATH = "__acp_local_image_path";
2
+
3
+ export function localCardImage(filePath: string, label: string): Record<string, unknown> {
4
+ return {
5
+ tag: "img",
6
+ img_key: "",
7
+ [LOCAL_CARD_IMAGE_PATH]: filePath,
8
+ alt: { tag: "plain_text", content: label },
9
+ title: { tag: "plain_text", content: label },
10
+ mode: "fit_horizontal",
11
+ preview: true,
12
+ };
13
+ }