@kobinflow/canvas-agent 0.6.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 (50) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +159 -0
  3. package/agent-instructions.md +13 -0
  4. package/dist/agent/claude.d.ts +3 -0
  5. package/dist/agent/claude.js +54 -0
  6. package/dist/agent/codex-client.d.ts +139 -0
  7. package/dist/agent/codex-client.js +937 -0
  8. package/dist/agent/codex-event-history.d.ts +21 -0
  9. package/dist/agent/codex-event-history.js +163 -0
  10. package/dist/agent/codex-history.d.ts +46 -0
  11. package/dist/agent/codex-history.js +822 -0
  12. package/dist/agent/codex-protocol.d.ts +335 -0
  13. package/dist/agent/codex-protocol.js +1 -0
  14. package/dist/agent/codex.d.ts +178 -0
  15. package/dist/agent/codex.js +509 -0
  16. package/dist/agent/message-metadata.d.ts +51 -0
  17. package/dist/agent/message-metadata.js +378 -0
  18. package/dist/agent/types.d.ts +45 -0
  19. package/dist/agent/types.js +1 -0
  20. package/dist/canvas/operations.d.ts +13 -0
  21. package/dist/canvas/operations.js +167 -0
  22. package/dist/canvas/schemas.d.ts +989 -0
  23. package/dist/canvas/schemas.js +155 -0
  24. package/dist/canvas/session.d.ts +163 -0
  25. package/dist/canvas/session.js +523 -0
  26. package/dist/canvas/tools.d.ts +214 -0
  27. package/dist/canvas/tools.js +27 -0
  28. package/dist/canvas/types.d.ts +34 -0
  29. package/dist/canvas/types.js +1 -0
  30. package/dist/config.d.ts +30 -0
  31. package/dist/config.js +86 -0
  32. package/dist/index.d.ts +2 -0
  33. package/dist/index.js +7 -0
  34. package/dist/server/http.d.ts +2 -0
  35. package/dist/server/http.js +576 -0
  36. package/dist/server/mcp.d.ts +2 -0
  37. package/dist/server/mcp.js +27 -0
  38. package/dist/skills/store.d.ts +54 -0
  39. package/dist/skills/store.js +426 -0
  40. package/dist/utils/agent-runtime.d.ts +8 -0
  41. package/dist/utils/agent-runtime.js +33 -0
  42. package/dist/utils/date.d.ts +2 -0
  43. package/dist/utils/date.js +7 -0
  44. package/dist/utils/logger.d.ts +17 -0
  45. package/dist/utils/logger.js +81 -0
  46. package/dist/utils/value.d.ts +5 -0
  47. package/dist/utils/value.js +8 -0
  48. package/dist/version-check.d.ts +2 -0
  49. package/dist/version-check.js +65 -0
  50. package/package.json +55 -0
@@ -0,0 +1,822 @@
1
+ import { field } from "../utils/value.js";
2
+ const emptySupplementalHistory = { items: [], turns: [] };
3
+ /** 将 Codex 线程转换为列表展示所需的摘要。 */
4
+ export function summarizeCodexThread(thread) {
5
+ const status = field(thread, "status");
6
+ return {
7
+ id: String(field(thread, "id") || ""),
8
+ sessionId: String(field(thread, "sessionId") || ""),
9
+ preview: displayUserText(String(field(thread, "preview") || "")),
10
+ name: stringOrNull(field(thread, "name")),
11
+ cwd: String(field(thread, "cwd") || ""),
12
+ status: typeof status === "string" ? status : String(field(status, "type") || ""),
13
+ source: field(thread, "source"),
14
+ threadSource: field(thread, "threadSource"),
15
+ createdAt: Number(field(thread, "createdAt") || 0),
16
+ updatedAt: Number(field(thread, "updatedAt") || 0),
17
+ };
18
+ }
19
+ /** 返回已经终结、可由持久化历史作为权威内容的 turn。 */
20
+ export function settledTurnIds(thread, supplementalHistory = emptySupplementalHistory) {
21
+ return mergeHistoryTurns(thread, supplementalHistory).flatMap((turn) => {
22
+ const id = String(field(turn, "id") || "");
23
+ return id && isSettledTurn(turn) ? [id] : [];
24
+ });
25
+ }
26
+ /** 将 Codex turn items 转换为网页聊天历史。 */
27
+ export function threadMessages(thread, planUpdates = [], supplementalHistory = emptySupplementalHistory) {
28
+ const threadId = String(field(thread, "id") || "");
29
+ if (!threadId)
30
+ return [];
31
+ const turns = mergeHistoryTurns(thread, supplementalHistory);
32
+ const plansByTurn = new Map(planUpdates.map((item) => [item.turnId, item]));
33
+ const supplementalByTurn = new Map();
34
+ supplementalHistory.items.filter((item) => item.threadId === threadId).forEach((item) => supplementalByTurn.set(item.turnId, [...(supplementalByTurn.get(item.turnId) || []), item]));
35
+ const messages = [];
36
+ turns.forEach((turn) => {
37
+ const turnId = String(field(turn, "id") || "");
38
+ if (!turnId || !isSettledTurn(turn))
39
+ return;
40
+ const turnError = String(field(field(turn, "error"), "message") || "").trim();
41
+ const items = mergeHistoryItems(arrayValue(field(turn, "items")), supplementalByTurn.get(turnId) || []);
42
+ const push = (message) => messages.push({ ...message, id: historyMessageId(threadId, turnId, message.id), itemId: message.id, threadId, turnId });
43
+ const planMessage = structuredPlanMessage(plansByTurn.get(turnId) || { threadId, turnId, explanation: stringOrNull(field(turn, "explanation")), plan: arrayValue(field(turn, "plan")), turnStatus: String(field(turn, "status") || "") }, threadId);
44
+ const reasoningItems = items.flatMap((item) => {
45
+ if (field(item, "type") !== "reasoning")
46
+ return [];
47
+ const id = String(field(item, "id") || "");
48
+ const text = readableText(field(item, "summary"));
49
+ return id && text ? [{ id, text, status: itemStatus(item, itemErrorMessage(item)) }] : [];
50
+ });
51
+ const reasoningText = reasoningItems.map((item) => item.text).join("\n\n");
52
+ const reasoningStatus = aggregateItemStatus(reasoningItems.map((item) => item.status));
53
+ const itemErrors = items.flatMap((item) => field(item, "type") === "error" ? [readableText(field(item, "message"))] : []).filter(Boolean);
54
+ const itemError = itemErrors[itemErrors.length - 1] || "";
55
+ let userAdded = false;
56
+ let planAdded = false;
57
+ let reasoningAdded = false;
58
+ const explicitUserText = displayUserText(String(field(turn, "messageText") || ""));
59
+ const fallbackUserText = explicitUserText || displayUserText(String(field(turn, "input") || ""));
60
+ if (fallbackUserText && (explicitUserText || !items.some((item) => field(item, "type") === "userMessage"))) {
61
+ push({ id: "synthetic:user", role: "user", text: fallbackUserText });
62
+ userAdded = true;
63
+ if (planMessage) {
64
+ messages.push(planMessage);
65
+ planAdded = true;
66
+ }
67
+ }
68
+ items.forEach((item) => {
69
+ const type = String(field(item, "type") || "");
70
+ const id = String(field(item, "id") || "");
71
+ if (!id)
72
+ return;
73
+ if (type === "userMessage") {
74
+ const text = displayUserText(userInputText(field(item, "content")));
75
+ if (text && !userAdded) {
76
+ push({ id: "synthetic:user", role: "user", text });
77
+ userAdded = true;
78
+ }
79
+ if (planMessage && !planAdded) {
80
+ messages.push(planMessage);
81
+ planAdded = true;
82
+ }
83
+ }
84
+ if (type === "agentMessage") {
85
+ const text = String(field(item, "text") || "").trim();
86
+ if (text)
87
+ push({ id, role: "assistant", title: "Codex", text });
88
+ }
89
+ if (type === "mcpToolCall") {
90
+ const tool = String(field(item, "tool") || "工具调用");
91
+ const error = String(field(field(item, "error"), "message") || "");
92
+ const input = toolArguments(field(item, "arguments"));
93
+ push({ id, role: "tool", title: toolName(tool), text: error || toolHistorySummary(tool, item, input), detail: toolHistoryDetail(tool, item, input, error) });
94
+ }
95
+ if (type === "commandExecution") {
96
+ const command = String(field(item, "command") || "").trim();
97
+ const detail = commandDetail(item);
98
+ push({ id, role: "tool", title: "执行命令", text: command || (detail.status === "failed" ? "命令执行失败" : "命令已完成"), detail });
99
+ }
100
+ if (type === "fileChange") {
101
+ const changes = arrayValue(field(item, "changes"));
102
+ const error = itemErrorMessage(item);
103
+ push({ id, role: "tool", title: "修改文件", text: error || fileChangeSummary(changes), detail: { kind: "file", status: itemStatus(item, error), files: changes.map((change) => ({ path: String(field(change, "path") || "未知文件"), action: changeKind(field(change, "kind")) })), ...(error ? { output: error } : {}) } });
104
+ }
105
+ if (type === "reasoning" && reasoningText && !reasoningAdded) {
106
+ const sourceItemIds = reasoningItems.map((item) => item.id);
107
+ push({ id: "synthetic:reasoning", role: "tool", title: "思考摘要", text: reasoningText, activityItems: Object.fromEntries(reasoningItems.map((item) => [item.id, item.text])), detail: { kind: "reasoning", status: reasoningStatus, sourceItemIds } });
108
+ reasoningAdded = true;
109
+ }
110
+ if (type === "plan") {
111
+ const text = String(field(item, "text") || "").trim();
112
+ const error = itemErrorMessage(item);
113
+ if (text || error)
114
+ push({ id, role: "tool", title: "执行计划", text: error || text, detail: { kind: "plan", status: itemStatus(item, error), ...(error ? { output: error } : {}) } });
115
+ }
116
+ if (type === "webSearch") {
117
+ const error = itemErrorMessage(item);
118
+ push({ id, role: "tool", title: "搜索资料", text: error || webSearchSummary(item), detail: { kind: "search", status: itemStatus(item, error), rows: webSearchRows(item), ...(error ? { output: error } : {}) } });
119
+ }
120
+ if (type === "imageView") {
121
+ const error = itemErrorMessage(item);
122
+ push({ id, role: "tool", title: "查看图片", text: error || String(field(item, "path") || "已查看图片"), detail: { kind: "image", status: itemStatus(item, error), ...(error ? { output: error } : {}) } });
123
+ }
124
+ if (type === "imageGeneration") {
125
+ const error = String(field(field(item, "error"), "message") || "");
126
+ const normalizedStatus = itemStatus(item, error);
127
+ push({ id, role: "tool", title: "内置生图", text: error || (normalizedStatus === "failed" ? "图片生成失败" : "图片生成完成"), detail: { kind: "image", status: normalizedStatus, savedPath: field(item, "savedPath"), ...(error ? { output: error } : {}) } });
128
+ }
129
+ if (type === "contextCompaction") {
130
+ const error = itemErrorMessage(item);
131
+ push({ id, role: "tool", title: "整理上下文", text: error || "已整理当前对话,继续处理任务", detail: { kind: "context", status: itemStatus(item, error), ...(error ? { output: error } : {}) } });
132
+ }
133
+ if (type === "dynamicToolCall") {
134
+ const tool = String(field(item, "tool") || "");
135
+ const title = toolName(tool);
136
+ const error = itemErrorMessage(item);
137
+ push({ id, role: "tool", title, text: error || readableText(field(item, "contentItems")), detail: toolHistoryDetail(tool, item, toolArguments(field(item, "arguments")), error) });
138
+ }
139
+ if (type === "collabToolCall" || type === "collabAgentToolCall") {
140
+ const error = String(field(field(item, "error"), "message") || "");
141
+ const normalizedStatus = itemStatus(item, error);
142
+ push({ id, role: "tool", title: "协作处理", text: error || (normalizedStatus === "failed" ? "协作任务失败" : "已完成协作任务"), detail: { kind: "tool", status: normalizedStatus, ...(error ? { output: error } : {}) } });
143
+ }
144
+ });
145
+ if (planMessage && !planAdded)
146
+ messages.push(planMessage);
147
+ if (itemError || turnError) {
148
+ const error = userFacingCodexError(turnError || itemError);
149
+ push({ id: "synthetic:error", role: "error", title: error.title, text: error.text });
150
+ }
151
+ });
152
+ return latestCompleteTurns(messages.filter((item) => item.text || item.role === "tool"), 120);
153
+ }
154
+ /** 合并标准 turn 与本地终态事件,标准历史已有字段保持优先。 */
155
+ function mergeHistoryTurns(thread, supplementalHistory) {
156
+ const threadId = String(field(thread, "id") || "");
157
+ const standardTurns = arrayValue(field(thread, "turns"));
158
+ const supplementalTurns = supplementalHistory.turns.filter((entry) => entry.threadId === threadId && entry.turnId);
159
+ const supplementalById = new Map(supplementalTurns.map((entry) => [entry.turnId, entry.turn]));
160
+ const standardIds = new Set(standardTurns.map((turn) => String(field(turn, "id") || "")).filter(Boolean));
161
+ return [
162
+ ...standardTurns.map((turn) => {
163
+ const turnId = String(field(turn, "id") || "");
164
+ const supplemental = supplementalById.get(turnId);
165
+ return supplemental ? mergeHistoryTurn(turn, supplemental) : turn;
166
+ }),
167
+ ...supplementalTurns.filter((entry) => !standardIds.has(entry.turnId)).map((entry) => entry.turn),
168
+ ];
169
+ }
170
+ /** 终态不可被标准历史中尚未物化完成的临时状态降级。 */
171
+ function mergeHistoryTurn(standard, supplemental) {
172
+ const merged = mergeHistoryItem(standard, supplemental);
173
+ const standardStatus = String(field(standard, "status") || "");
174
+ const supplementalStatus = String(field(supplemental, "status") || "");
175
+ if (!isSettledStatus(standardStatus) && isSettledStatus(supplementalStatus))
176
+ merged.status = supplementalStatus;
177
+ return merged;
178
+ }
179
+ /** 以 Codex 标准历史为准,只补入同一 turn 中被持久线程投影省略的实时条目。 */
180
+ function mergeHistoryItems(items, supplementalItems) {
181
+ const supplemental = supplementalItems
182
+ .filter((entry) => entry.itemId)
183
+ .map((entry, index) => ({ ...entry, item: normalizeSupplementalItem(entry.item), fallbackIndex: index }))
184
+ .sort(compareSupplementalEntries);
185
+ items = removeCorruptedHistoryDuplicates(items, supplemental);
186
+ items = reconcileHistoryItemIds(items, supplemental);
187
+ const standardIds = new Set(items.map((item) => String(field(item, "id") || "")).filter(Boolean));
188
+ const supplementalById = new Map(supplemental.map((entry) => [entry.itemId, entry]));
189
+ const mergedStandard = items.map((item) => {
190
+ const id = String(field(item, "id") || "");
191
+ const supplementalEntry = id ? supplementalById.get(id) : undefined;
192
+ return supplementalEntry ? mergeHistoryItem(item, supplementalEntry.item) : item;
193
+ });
194
+ const sequenced = supplemental.filter((entry) => entry.sequence !== undefined);
195
+ let merged = sequenced.length ? mergeSequencedItems(mergedStandard, sequenced) : mergedStandard;
196
+ supplemental.forEach((entry, index) => {
197
+ if (standardIds.has(entry.itemId) || entry.sequence !== undefined)
198
+ return;
199
+ const nextStandardId = supplemental.slice(index + 1).find((candidate) => standardIds.has(candidate.itemId))?.itemId;
200
+ const insertAt = nextStandardId ? merged.findIndex((candidate) => String(field(candidate, "id") || "") === nextStandardId) : -1;
201
+ if (insertAt >= 0)
202
+ merged.splice(insertAt, 0, entry.item);
203
+ else
204
+ merged.push(entry.item);
205
+ });
206
+ return merged;
207
+ }
208
+ /** thread/read 会重写部分 item id;按类型、内容和出现顺序恢复实时事件的稳定身份。 */
209
+ function reconcileHistoryItemIds(items, supplementalItems) {
210
+ const supplementalById = new Map(supplementalItems.map((entry) => [entry.itemId, entry]));
211
+ const reservedIds = new Set(items.map((item) => String(field(item, "id") || "")).filter((id) => supplementalById.has(id)));
212
+ const candidates = new Map();
213
+ supplementalItems.forEach((entry) => {
214
+ if (reservedIds.has(entry.itemId))
215
+ return;
216
+ const identity = historyItemIdentity(entry.item);
217
+ if (identity)
218
+ candidates.set(identity, [...(candidates.get(identity) || []), entry]);
219
+ });
220
+ const replacements = new Map();
221
+ const usedSupplementalIds = new Set(reservedIds);
222
+ items.forEach((item, index) => {
223
+ const id = String(field(item, "id") || "");
224
+ if (!id || supplementalById.has(id))
225
+ return;
226
+ const identity = historyItemIdentity(item);
227
+ const candidate = identity ? candidates.get(identity)?.shift() : undefined;
228
+ if (!candidate || usedSupplementalIds.has(candidate.itemId))
229
+ return;
230
+ replacements.set(index, candidate.itemId);
231
+ usedSupplementalIds.add(candidate.itemId);
232
+ });
233
+ const remainingStandard = new Map();
234
+ items.forEach((item, index) => {
235
+ const id = String(field(item, "id") || "");
236
+ if (!id || supplementalById.has(id) || replacements.has(index))
237
+ return;
238
+ const type = historyItemType(item);
239
+ if (type)
240
+ remainingStandard.set(type, [...(remainingStandard.get(type) || []), index]);
241
+ });
242
+ const remainingSupplemental = new Map();
243
+ supplementalItems.forEach((entry) => {
244
+ if (usedSupplementalIds.has(entry.itemId))
245
+ return;
246
+ const type = historyItemType(entry.item);
247
+ if (type)
248
+ remainingSupplemental.set(type, [...(remainingSupplemental.get(type) || []), entry]);
249
+ });
250
+ remainingStandard.forEach((standardIndexes, type) => {
251
+ const supplemental = remainingSupplemental.get(type) || [];
252
+ if (standardIndexes.length === supplemental.length) {
253
+ standardIndexes.forEach((index, position) => {
254
+ const entry = supplemental[position];
255
+ replacements.set(index, entry.itemId);
256
+ usedSupplementalIds.add(entry.itemId);
257
+ });
258
+ return;
259
+ }
260
+ if (supplemental.length !== 1)
261
+ return;
262
+ const corrupted = standardIndexes.filter((index) => hasReplacementCharacter(items[index]));
263
+ if (corrupted.length === 1) {
264
+ replacements.set(corrupted[0], supplemental[0].itemId);
265
+ usedSupplementalIds.add(supplemental[0].itemId);
266
+ }
267
+ });
268
+ return items.map((item, index) => {
269
+ const itemId = replacements.get(index);
270
+ if (!itemId || !item || typeof item !== "object" || Array.isArray(item))
271
+ return item;
272
+ return { ...item, id: itemId };
273
+ });
274
+ }
275
+ function historyItemType(item) {
276
+ const type = String(field(normalizeSupplementalItem(item && typeof item === "object" && !Array.isArray(item) ? item : {}), "type") || "");
277
+ return type;
278
+ }
279
+ function hasReplacementCharacter(item) {
280
+ const value = item && typeof item === "object" && !Array.isArray(item) ? item : {};
281
+ return [value.text, value.summary, value.message, value.aggregatedOutput].some((fieldValue) => readableText(fieldValue).includes("\uFFFD"));
282
+ }
283
+ /** thread/read 偶尔同时返回损坏临时条目和已物化条目,只保留补充历史已确认的稳定副本。 */
284
+ function removeCorruptedHistoryDuplicates(items, supplementalItems) {
285
+ const standardIds = new Set(items.map((item) => String(field(item, "id") || "")).filter(Boolean));
286
+ const stableItems = supplementalItems.filter((entry) => standardIds.has(entry.itemId) && !hasReplacementCharacter(entry.item));
287
+ return items.filter((item) => {
288
+ if (!hasReplacementCharacter(item))
289
+ return true;
290
+ const identity = comparableHistoryText(item);
291
+ if (identity.length < 8)
292
+ return true;
293
+ return !stableItems.some((entry) => historyItemType(entry.item) === historyItemType(item) && comparableHistoryText(entry.item) === identity);
294
+ });
295
+ }
296
+ function comparableHistoryText(item) {
297
+ const value = item && typeof item === "object" && !Array.isArray(item) ? item : {};
298
+ const type = historyItemType(value);
299
+ const text = type === "agentMessage" ? value.text : type === "reasoning" ? value.summary : type === "plan" ? value.text : "";
300
+ return readableText(text).normalize("NFKC").toLocaleLowerCase().replace(/[\uFFFD\p{P}\p{S}\p{Z}]+/gu, "");
301
+ }
302
+ /** 只使用消息本身的稳定语义字段,不使用状态、结果或临时投影 id。 */
303
+ function historyItemIdentity(item) {
304
+ const normalized = normalizeSupplementalItem(item && typeof item === "object" && !Array.isArray(item) ? item : {});
305
+ const type = String(field(normalized, "type") || "");
306
+ let value;
307
+ if (type === "agentMessage")
308
+ value = String(field(normalized, "text") || "").trim();
309
+ else if (type === "reasoning")
310
+ value = readableText(field(normalized, "summary"));
311
+ else if (type === "plan")
312
+ value = String(field(normalized, "text") || "").trim();
313
+ else if (type === "mcpToolCall" || type === "dynamicToolCall")
314
+ value = [field(normalized, "tool"), toolArguments(field(normalized, "arguments"))];
315
+ else if (type === "commandExecution")
316
+ value = String(field(normalized, "command") || "").trim();
317
+ else if (type === "fileChange")
318
+ value = arrayValue(field(normalized, "changes")).map((change) => [field(change, "path"), field(change, "kind")]);
319
+ else if (type === "collabToolCall" || type === "collabAgentToolCall")
320
+ value = [field(normalized, "tool"), field(normalized, "prompt"), field(normalized, "receiverThreadId"), field(normalized, "receiverAgentId")];
321
+ else if (type === "webSearch")
322
+ value = [field(normalized, "query"), field(field(normalized, "action"), "type"), field(field(normalized, "action"), "query"), field(field(normalized, "action"), "url"), field(field(normalized, "action"), "pattern")];
323
+ else if (type === "imageView")
324
+ value = field(normalized, "path");
325
+ else if (type === "imageGeneration")
326
+ value = field(normalized, "prompt") || field(normalized, "savedPath");
327
+ else if (type === "contextCompaction")
328
+ value = true;
329
+ else
330
+ return "";
331
+ return `${type}\0${JSON.stringify(stableHistoryValue(value))}`;
332
+ }
333
+ function stableHistoryValue(value) {
334
+ if (Array.isArray(value))
335
+ return value.map(stableHistoryValue);
336
+ if (value && typeof value === "object")
337
+ return Object.fromEntries(Object.entries(value).sort(([left], [right]) => left.localeCompare(right)).map(([key, item]) => [key, stableHistoryValue(item)]));
338
+ return value;
339
+ }
340
+ function mergeSequencedItems(standardItems, supplementalItems) {
341
+ const byId = new Map(supplementalItems.map((entry) => [entry.itemId, entry]));
342
+ const emitted = new Set();
343
+ const merged = [];
344
+ standardItems.forEach((item) => {
345
+ const id = String(field(item, "id") || "");
346
+ const sequence = id ? byId.get(id)?.sequence : undefined;
347
+ if (sequence !== undefined) {
348
+ supplementalItems.forEach((entry) => {
349
+ if (entry.sequence !== undefined && entry.sequence < sequence && !emitted.has(entry.itemId)) {
350
+ merged.push(findStandardItem(standardItems, entry.itemId) || entry.item);
351
+ emitted.add(entry.itemId);
352
+ }
353
+ });
354
+ if (emitted.has(id))
355
+ return;
356
+ if (id)
357
+ emitted.add(id);
358
+ merged.push(item);
359
+ return;
360
+ }
361
+ merged.push(item);
362
+ });
363
+ supplementalItems.forEach((entry) => {
364
+ if (!emitted.has(entry.itemId))
365
+ merged.push(entry.item);
366
+ });
367
+ return merged;
368
+ }
369
+ function findStandardItem(items, itemId) {
370
+ return items.find((item) => String(field(item, "id") || "") === itemId);
371
+ }
372
+ function compareSupplementalEntries(left, right) {
373
+ if (left.sequence !== undefined && right.sequence !== undefined && left.sequence !== right.sequence)
374
+ return left.sequence - right.sequence;
375
+ if (left.sequence !== undefined)
376
+ return -1;
377
+ if (right.sequence !== undefined)
378
+ return 1;
379
+ return left.fallbackIndex - right.fallbackIndex;
380
+ }
381
+ /** 标准历史字段优先,补充事件只填充标准历史没有返回的字段。 */
382
+ function mergeHistoryItem(standard, supplemental) {
383
+ if (!standard || typeof standard !== "object" || Array.isArray(standard))
384
+ return supplemental;
385
+ const merged = { ...supplemental };
386
+ Object.entries(standard).forEach(([key, value]) => {
387
+ const supplementalValue = merged[key];
388
+ if (typeof value === "string" && typeof supplementalValue === "string" && value.includes("\uFFFD") && !supplementalValue.includes("\uFFFD"))
389
+ return;
390
+ if (value !== undefined)
391
+ merged[key] = value;
392
+ });
393
+ return merged;
394
+ }
395
+ /** 将实时事件的 snake_case 类型还原为 thread/read 使用的 camelCase 类型。 */
396
+ function normalizeSupplementalItem(item) {
397
+ const type = String(field(item, "type") || "");
398
+ const normalizedType = supplementalItemTypes[type] || type;
399
+ return normalizedType === type ? item : { ...item, type: normalizedType };
400
+ }
401
+ const supplementalItemTypes = {
402
+ agent_message: "agentMessage",
403
+ mcp_tool_call: "mcpToolCall",
404
+ command_execution: "commandExecution",
405
+ file_change: "fileChange",
406
+ dynamic_tool_call: "dynamicToolCall",
407
+ collab_tool_call: "collabToolCall",
408
+ web_search: "webSearch",
409
+ image_view: "imageView",
410
+ image_generation: "imageGeneration",
411
+ context_compaction: "contextCompaction",
412
+ };
413
+ /** 只按完整 turn 截取最近历史,避免列表从某轮中间开始。 */
414
+ function latestCompleteTurns(messages, limit) {
415
+ const turns = [];
416
+ messages.forEach((message) => {
417
+ const current = turns.at(-1);
418
+ if (!current || current[0].turnId !== message.turnId)
419
+ turns.push([message]);
420
+ else
421
+ current.push(message);
422
+ });
423
+ const selected = [];
424
+ let count = 0;
425
+ for (let index = turns.length - 1; index >= 0; index -= 1) {
426
+ const turn = turns[index];
427
+ if (selected.length && count + turn.length > limit)
428
+ break;
429
+ selected.unshift(turn);
430
+ count += turn.length;
431
+ }
432
+ return selected.flat();
433
+ }
434
+ function isSettledTurn(turn) {
435
+ return isSettledStatus(String(field(turn, "status") || ""));
436
+ }
437
+ function isSettledStatus(status) {
438
+ return ["completed", "failed", "interrupted", "cancelled", "canceled"].includes(status);
439
+ }
440
+ /** 将结构化任务计划转换为聊天进度卡片。 */
441
+ function structuredPlanMessage(update, threadId) {
442
+ const tasks = arrayValue(update.plan).flatMap((item) => {
443
+ const step = String(field(item, "step") || "").trim();
444
+ return step ? [{ step, status: String(field(item, "status") || "pending") }] : [];
445
+ });
446
+ if (!tasks.length)
447
+ return null;
448
+ const completed = tasks.filter((item) => item.status === "completed").length;
449
+ return {
450
+ id: historyMessageId(threadId, update.turnId, "synthetic:plan"),
451
+ itemId: "synthetic:plan",
452
+ threadId,
453
+ turnId: update.turnId,
454
+ role: "tool",
455
+ title: "任务进度",
456
+ text: `已完成 ${completed}/${tasks.length} 项`,
457
+ detail: { kind: "todo", status: planStatus(tasks, update.turnStatus), tasks, explanation: update.explanation || "" },
458
+ };
459
+ }
460
+ /** 生成跨线程和 turn 唯一的聊天消息 ID。 */
461
+ function historyMessageId(threadId, turnId, itemId) {
462
+ return `${threadId}:${turnId}:${itemId}`;
463
+ }
464
+ /** 根据步骤和 turn 状态生成任务卡片状态。 */
465
+ function planStatus(tasks, turnStatus) {
466
+ if (turnStatus === "failed")
467
+ return "failed";
468
+ if (turnStatus === "interrupted")
469
+ return "interrupted";
470
+ if (tasks.every((item) => item.status === "completed"))
471
+ return "completed";
472
+ return turnStatus === "completed" ? "finished" : "inProgress";
473
+ }
474
+ /** 将常见 Codex 错误转换为普通用户可理解的提示。 */
475
+ function userFacingCodexError(message) {
476
+ if (/selected model is at capacity/i.test(message))
477
+ return { title: "模型暂时繁忙", text: "当前选择的模型请求量过大,暂时无法处理。请稍后重试,或切换其他模型后再试。" };
478
+ return { title: "任务失败", text: message || "Codex 未能完成本次任务,请稍后重试。" };
479
+ }
480
+ /** 提取用户输入条目中的文本与附件占位信息。 */
481
+ function userInputText(content) {
482
+ const items = arrayValue(content);
483
+ const text = items
484
+ .map((item) => {
485
+ const type = String(field(item, "type") || "");
486
+ if (type === "text")
487
+ return String(field(item, "text") || "");
488
+ if (type === "image" || type === "localImage")
489
+ return "图片附件";
490
+ if (type === "mention")
491
+ return `@${String(field(item, "name") || "文件")}`;
492
+ return "";
493
+ })
494
+ .filter(Boolean)
495
+ .join("\n");
496
+ return text.trim();
497
+ }
498
+ /** 移除用户消息中由旧流程拼接的 Agent 前置提示词。 */
499
+ function displayUserText(text) {
500
+ const value = text.trim();
501
+ const marker = "用户请求:";
502
+ const index = value.lastIndexOf(marker);
503
+ const prompt = index >= 0 ? value.slice(index + marker.length) : value;
504
+ return prompt.split("\n\n本轮可用图片附件(顺序与图片输入一致):", 1)[0].trim();
505
+ }
506
+ /** 将未知值转换为数组。 */
507
+ function arrayValue(value) {
508
+ return Array.isArray(value) ? value : [];
509
+ }
510
+ /** 将非空字符串保留为字符串,否则返回 null。 */
511
+ function stringOrNull(value) {
512
+ return typeof value === "string" && value.trim() ? value : null;
513
+ }
514
+ /** 生成命令执行的用户可读详情。 */
515
+ function commandDetail(item) {
516
+ const error = String(field(field(item, "error"), "message") || "");
517
+ const status = String(field(item, "status") || "completed");
518
+ const exitCode = field(item, "exitCode");
519
+ const failed = Boolean(error) || field(item, "success") === false || status === "failed" || status === "error" || (typeof exitCode === "number" && exitCode !== 0);
520
+ const rows = [
521
+ textRow("工作目录", field(item, "cwd")),
522
+ textRow("退出状态", exitCode),
523
+ durationRow(field(item, "durationMs")),
524
+ ].filter(Boolean);
525
+ return { kind: "command", status: failed ? "failed" : status, rows, output: error || String(field(item, "aggregatedOutput") || "").trim() };
526
+ }
527
+ /** 生成 MCP 工具的用户可读详情。 */
528
+ function toolHistoryDetail(tool, item, input, error) {
529
+ return { kind: "tool", status: itemStatus(item, error), rows: toolInputRows(tool, input), ...(error ? { output: error } : {}) };
530
+ }
531
+ function itemErrorMessage(item) {
532
+ return String(field(field(item, "error"), "message") || "");
533
+ }
534
+ function itemStatus(item, error = "") {
535
+ const status = String(field(item, "status") || "completed");
536
+ return error || field(item, "success") === false || status === "failed" || status === "error" ? "failed" : status;
537
+ }
538
+ function aggregateItemStatus(statuses) {
539
+ if (statuses.includes("failed"))
540
+ return "failed";
541
+ if (statuses.includes("interrupted"))
542
+ return "interrupted";
543
+ if (statuses.some((status) => ["inProgress", "in_progress", "running", "started", "pending"].includes(status)))
544
+ return "inProgress";
545
+ return "completed";
546
+ }
547
+ /** 生成 MCP 工具在对话中的结果摘要。 */
548
+ function toolHistorySummary(tool, item, input) {
549
+ const result = parseToolResult(field(item, "result"));
550
+ if (tool === "site_navigate")
551
+ return `已打开${routeName(String(field(input, "path") || "/"))}`;
552
+ if (tool === "canvas_list_projects")
553
+ return `共 ${numberValue(field(result, "total"))} 个画布`;
554
+ if (tool === "canvas_get_state") {
555
+ const nodes = arrayValue(field(result, "nodes"));
556
+ const connections = arrayValue(field(result, "connections"));
557
+ return Array.isArray(field(result, "nodes")) || Array.isArray(field(result, "connections")) ? canvasContentSummary(nodes, connections.length) : "已读取当前画布内容";
558
+ }
559
+ if (tool === "canvas_get_selection")
560
+ return "已读取当前选中内容";
561
+ if (tool === "prompts_search")
562
+ return `找到 ${numberValue(field(result, "total"))} 条提示词`;
563
+ if (tool === "assets_list")
564
+ return `共 ${numberValue(field(result, "total"))} 个资产`;
565
+ if (tool === "assets_add")
566
+ return "已加入我的素材";
567
+ if (tool === "generation_get_status") {
568
+ const summary = field(result, "summary");
569
+ return `共 ${numberValue(field(result, "total"))} 个任务,排队 ${numberValue(field(summary, "queued"))},运行中 ${numberValue(field(summary, "running"))},成功 ${numberValue(field(summary, "succeeded"))},失败 ${numberValue(field(summary, "failed"))}`;
570
+ }
571
+ if (tool === "workbench_image_generate" || tool === "workbench_video_generate")
572
+ return String(field(result, "note") || "已在工作台执行");
573
+ if (tool === "workbench_image_get_config" || tool === "workbench_video_get_config")
574
+ return "已读取工作台配置";
575
+ return "";
576
+ }
577
+ /** 按节点类型生成人类可读的画布内容概览。 */
578
+ function canvasContentSummary(nodes, connections) {
579
+ const counts = nodes.reduce((result, node) => {
580
+ const type = String(field(node, "type") || "other");
581
+ result[type] = (result[type] || 0) + 1;
582
+ return result;
583
+ }, {});
584
+ const known = new Set(["text", "image", "config", "video", "audio", "group"]);
585
+ const other = Object.entries(counts).reduce((total, [type, count]) => total + (known.has(type) ? 0 : count), 0);
586
+ const parts = [
587
+ counts.text ? `${counts.text} 个文本` : "",
588
+ counts.image ? `${counts.image} 张图片` : "",
589
+ counts.config ? `${counts.config} 个配置` : "",
590
+ counts.video ? `${counts.video} 个视频` : "",
591
+ counts.audio ? `${counts.audio} 个音频` : "",
592
+ counts.group ? `${counts.group} 个分组` : "",
593
+ other ? `${other} 个其他节点` : "",
594
+ connections ? `${connections} 条连线` : "",
595
+ ].filter(Boolean);
596
+ return parts.length ? parts.join("、") : "当前画布为空";
597
+ }
598
+ /** 从 MCP 历史结果中还原工具返回的数据。 */
599
+ function parseToolResult(result) {
600
+ const content = field(result, "content");
601
+ const text = arrayValue(content)
602
+ .map((item) => field(item, "text"))
603
+ .filter((item) => typeof item === "string")
604
+ .join("\n");
605
+ try {
606
+ return text ? JSON.parse(text) : result;
607
+ }
608
+ catch {
609
+ return text || result;
610
+ }
611
+ }
612
+ /** 提取工具参数中适合普通用户查看的信息。 */
613
+ function toolInputRows(tool, input) {
614
+ if (tool === "site_navigate")
615
+ return [textRow("目标页面", routeName(String(field(input, "path") || "/")))].filter(Boolean);
616
+ if (tool === "prompts_search")
617
+ return [textRow("搜索内容", field(input, "query"))].filter(Boolean);
618
+ if (tool === "canvas_create_text_node")
619
+ return [textRow("文本内容", field(input, "text"))].filter(Boolean);
620
+ if (tool === "canvas_apply_ops")
621
+ return [textRow("操作内容", summarizeCanvasOps(arrayValue(field(input, "ops"))))].filter(Boolean);
622
+ if (tool === "canvas_create_attachment_nodes")
623
+ return [textRow("图片数量", arrayValue(field(input, "attachmentIds")).length)].filter(Boolean);
624
+ return [];
625
+ }
626
+ function summarizeCanvasOps(ops) {
627
+ const counts = ops.reduce((result, op) => {
628
+ const type = String(field(op, "type") || "");
629
+ if (type)
630
+ result[type] = (result[type] || 0) + 1;
631
+ return result;
632
+ }, {});
633
+ return Object.entries(counts).map(([type, count]) => `${canvasOpLabel(type)} ${count}`).join(",");
634
+ }
635
+ function canvasOpLabel(type) {
636
+ if (type === "add_node")
637
+ return "新增节点";
638
+ if (type === "update_node")
639
+ return "更新节点";
640
+ if (type === "delete_node")
641
+ return "删除节点";
642
+ if (type === "delete_connections")
643
+ return "删除连线";
644
+ if (type === "connect_nodes")
645
+ return "连接";
646
+ if (type === "set_viewport")
647
+ return "调整视图";
648
+ if (type === "select_nodes")
649
+ return "选择节点";
650
+ if (type === "run_generation")
651
+ return "触发生成";
652
+ return type;
653
+ }
654
+ /** 生成人类可读的文件变更摘要。 */
655
+ function fileChangeSummary(changes) {
656
+ if (!changes.length)
657
+ return "已完成文件修改";
658
+ const names = changes.slice(0, 3).map((change) => String(field(change, "path") || "未知文件"));
659
+ if (changes.length === 1)
660
+ return `已${changeKind(field(changes[0], "kind"))} ${names[0]}`;
661
+ return `已修改 ${changes.length} 个文件:${names.join("、")}${changes.length > names.length ? " 等" : ""}`;
662
+ }
663
+ /** 生成网页搜索摘要。 */
664
+ function webSearchSummary(item) {
665
+ const action = field(item, "action");
666
+ const type = String(field(action, "type") || "");
667
+ if (type === "openPage")
668
+ return `打开网页:${String(field(action, "url") || "")}`;
669
+ if (type === "findInPage")
670
+ return `在网页中查找“${String(field(action, "pattern") || "内容")}”`;
671
+ return `搜索:${String(field(item, "query") || field(action, "query") || "相关资料")}`;
672
+ }
673
+ /** 生成网页搜索详情行。 */
674
+ function webSearchRows(item) {
675
+ const action = field(item, "action");
676
+ return [textRow("关键词", field(item, "query") || field(action, "query")), textRow("网页", field(action, "url"))].filter(Boolean);
677
+ }
678
+ /** 从 reasoning 结构中提取可读文本。 */
679
+ function readableText(value) {
680
+ if (typeof value === "string")
681
+ return value.trim();
682
+ if (Array.isArray(value))
683
+ return value.map(readableText).filter(Boolean).join("\n");
684
+ if (!value || typeof value !== "object")
685
+ return "";
686
+ return readableText(field(value, "text"));
687
+ }
688
+ /** 将历史工具参数解析为对象。 */
689
+ function toolArguments(value) {
690
+ if (typeof value !== "string")
691
+ return value;
692
+ try {
693
+ return JSON.parse(value);
694
+ }
695
+ catch {
696
+ return {};
697
+ }
698
+ }
699
+ /** 创建非空详情行。 */
700
+ function textRow(label, value) {
701
+ return value === undefined || value === null || value === "" ? null : { label, value: String(value) };
702
+ }
703
+ /** 创建命令耗时详情行。 */
704
+ function durationRow(value) {
705
+ const duration = Number(value || 0);
706
+ return duration > 0 ? { label: "耗时", value: `${(duration / 1000).toFixed(1)} 秒` } : null;
707
+ }
708
+ /** 将未知数值转换为有限数字。 */
709
+ function numberValue(value) {
710
+ const number = Number(value || 0);
711
+ return Number.isFinite(number) ? number : 0;
712
+ }
713
+ /** 将文件变更类型转换为中文。 */
714
+ function changeKind(value) {
715
+ if (value === "add")
716
+ return "新增";
717
+ if (value === "delete")
718
+ return "删除";
719
+ return "修改";
720
+ }
721
+ /** 将站点路由转换为中文页面名称。 */
722
+ function routeName(path) {
723
+ if (path === "/")
724
+ return "首页";
725
+ if (path === "/canvas")
726
+ return "画布页面";
727
+ if (path.startsWith("/canvas/"))
728
+ return "指定画布";
729
+ if (path.startsWith("/image"))
730
+ return "生图工作台";
731
+ if (path.startsWith("/video"))
732
+ return "视频工作台";
733
+ if (path.startsWith("/prompts"))
734
+ return "提示词中心";
735
+ if (path.startsWith("/assets"))
736
+ return "我的素材";
737
+ if (path.startsWith("/config"))
738
+ return "配置页面";
739
+ return path;
740
+ }
741
+ /** 将 MCP 工具名称转换为聊天记录中的中文标题。 */
742
+ function toolName(name) {
743
+ if (name === "imagegen" || name.endsWith("__imagegen"))
744
+ return "生成图片";
745
+ if (name === "view_image" || name.endsWith("__view_image"))
746
+ return "查看图片";
747
+ if (name === "exec" || name === "exec_command" || name.endsWith("__exec_command"))
748
+ return "执行命令";
749
+ if (name === "apply_patch" || name.endsWith("__apply_patch"))
750
+ return "修改文件";
751
+ if (name === "web__run" || name.endsWith("__web__run"))
752
+ return "搜索资料";
753
+ if (name === "site_navigate")
754
+ return "打开页面";
755
+ if (name === "canvas_list_projects")
756
+ return "画布列表";
757
+ if (name === "canvas_apply_ops")
758
+ return "画布操作";
759
+ if (name === "canvas_get_state")
760
+ return "读取画布";
761
+ if (name === "canvas_get_selection")
762
+ return "读取选区";
763
+ if (name === "canvas_export_snapshot")
764
+ return "导出快照";
765
+ if (name === "canvas_create_node")
766
+ return "创建节点";
767
+ if (name === "canvas_create_attachment_nodes")
768
+ return "添加附件图片";
769
+ if (name === "canvas_create_text_node")
770
+ return "创建文本";
771
+ if (name === "canvas_create_text_nodes")
772
+ return "批量创建文本";
773
+ if (name === "canvas_create_config_node")
774
+ return "创建生成配置";
775
+ if (name === "canvas_create_image_prompt_flow")
776
+ return "创建生图流程";
777
+ if (name === "canvas_create_generation_flow")
778
+ return "创建生成流程";
779
+ if (name === "canvas_generate_text")
780
+ return "生成文本";
781
+ if (name === "canvas_generate_image")
782
+ return "生成图片";
783
+ if (name === "canvas_generate_video")
784
+ return "生成视频";
785
+ if (name === "canvas_generate_audio")
786
+ return "生成音频";
787
+ if (name === "canvas_update_node")
788
+ return "更新节点";
789
+ if (name === "canvas_update_node_text")
790
+ return "更新文本";
791
+ if (name === "canvas_move_nodes")
792
+ return "移动节点";
793
+ if (name === "canvas_resize_node")
794
+ return "调整节点尺寸";
795
+ if (name === "canvas_delete_nodes")
796
+ return "删除节点";
797
+ if (name === "canvas_connect_nodes")
798
+ return "连接节点";
799
+ if (name === "canvas_select_nodes")
800
+ return "选择节点";
801
+ if (name === "canvas_set_viewport")
802
+ return "调整视口";
803
+ if (name === "canvas_run_generation")
804
+ return "触发生成";
805
+ if (name === "workbench_image_get_config")
806
+ return "生图配置";
807
+ if (name === "workbench_image_generate")
808
+ return "生图工作台生成";
809
+ if (name === "workbench_video_get_config")
810
+ return "视频配置";
811
+ if (name === "workbench_video_generate")
812
+ return "视频创作台生成";
813
+ if (name === "prompts_search")
814
+ return "搜索提示词";
815
+ if (name === "assets_list")
816
+ return "资产列表";
817
+ if (name === "assets_add")
818
+ return "添加资产";
819
+ if (name === "generation_get_status")
820
+ return "生成任务状态";
821
+ return name ? `调用工具:${name}` : "工具操作";
822
+ }