chatccc 0.2.229 → 0.2.231

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 (120) hide show
  1. package/.agents/skills/create-chatccc-feishu-app/SKILL.md +85 -85
  2. package/.claude/skills/create-chatccc-feishu-app/SKILL.md +85 -85
  3. package/.cursor/skills/create-chatccc-feishu-app/SKILL.md +85 -85
  4. package/README.md +90 -90
  5. package/agent-prompts/claude_specific.md +45 -45
  6. package/agent-prompts/codex_specific.md +2 -2
  7. package/agent-prompts/cursor_specific.md +13 -13
  8. package/bin/cccagent.mjs +17 -17
  9. package/im-skills/feishu-skill/receive-send-file.md +63 -63
  10. package/im-skills/feishu-skill/receive-send-image.md +24 -24
  11. package/im-skills/feishu-skill/skill.md +3 -3
  12. package/im-skills/wechat-file-skill/receive-send-file.md +38 -38
  13. package/im-skills/wechat-file-skill/send-file.mjs +83 -83
  14. package/im-skills/wechat-file-skill/skill.md +10 -10
  15. package/im-skills/wechat-image-skill/skill.md +10 -10
  16. package/im-skills/wechat-video-skill/receive-send-video.md +38 -38
  17. package/im-skills/wechat-video-skill/send-video.mjs +79 -79
  18. package/im-skills/wechat-video-skill/skill.md +10 -10
  19. package/package.json +1 -1
  20. package/scripts/postinstall-sharp-check.mjs +58 -58
  21. package/src/__tests__/agent-activity.test.ts +86 -76
  22. package/src/__tests__/agent-delegate-task-rpc.test.ts +165 -165
  23. package/src/__tests__/agent-reload-config-rpc.test.ts +99 -99
  24. package/src/__tests__/builtin-chat-session.test.ts +65 -1
  25. package/src/__tests__/builtin-cli-json.test.ts +39 -39
  26. package/src/__tests__/builtin-config.test.ts +26 -26
  27. package/src/__tests__/builtin-context.test.ts +202 -163
  28. package/src/__tests__/builtin-permissions.test.ts +211 -211
  29. package/src/__tests__/builtin-session-select.test.ts +116 -116
  30. package/src/__tests__/builtin-sigint.test.ts +56 -56
  31. package/src/__tests__/builtin-web-tools.test.ts +220 -220
  32. package/src/__tests__/card-action-routing.test.ts +18 -18
  33. package/src/__tests__/cardkit.test.ts +60 -60
  34. package/src/__tests__/ccc-adapter.test.ts +170 -136
  35. package/src/__tests__/chatgpt-subscription-rpc.test.ts +89 -89
  36. package/src/__tests__/chatgpt-subscription.test.ts +135 -135
  37. package/src/__tests__/chrome-devtools-guard.test.ts +165 -165
  38. package/src/__tests__/claude-adapter.test.ts +614 -614
  39. package/src/__tests__/claude-raw-stream-log.test.ts +87 -87
  40. package/src/__tests__/codex-adapter.test.ts +58 -58
  41. package/src/__tests__/codex-raw-stream-log.test.ts +170 -170
  42. package/src/__tests__/codex-reset-actions.test.ts +146 -146
  43. package/src/__tests__/cursor-adapter.test.ts +268 -268
  44. package/src/__tests__/feishu-api.test.ts +60 -60
  45. package/src/__tests__/feishu-avatar.test.ts +164 -164
  46. package/src/__tests__/feishu-message-ingress.test.ts +138 -138
  47. package/src/__tests__/feishu-platform.test.ts +22 -22
  48. package/src/__tests__/format-message.test.ts +47 -47
  49. package/src/__tests__/jsonl-stream.test.ts +79 -79
  50. package/src/__tests__/package-files.test.ts +24 -24
  51. package/src/__tests__/privacy.test.ts +198 -198
  52. package/src/__tests__/progress-reducer.test.ts +121 -110
  53. package/src/__tests__/raw-stream-log.test.ts +106 -106
  54. package/src/__tests__/response-stall.test.ts +49 -49
  55. package/src/__tests__/session.test.ts +174 -16
  56. package/src/__tests__/shared-prefix.test.ts +36 -36
  57. package/src/__tests__/sim-platform.test.ts +16 -16
  58. package/src/__tests__/startup-lifecycle.test.ts +231 -231
  59. package/src/__tests__/stop-session.test.ts +34 -34
  60. package/src/__tests__/stream-state.test.ts +42 -42
  61. package/src/__tests__/terminal-error.test.ts +54 -0
  62. package/src/__tests__/terminal-renderer.test.ts +247 -247
  63. package/src/__tests__/update-command-guard.test.ts +144 -144
  64. package/src/__tests__/web-ui.test.ts +326 -326
  65. package/src/adapters/adapter-interface.ts +24 -18
  66. package/src/adapters/ccc-adapter.ts +141 -131
  67. package/src/adapters/claude-adapter.ts +620 -620
  68. package/src/adapters/claude-session-meta-store.ts +120 -120
  69. package/src/adapters/codex-adapter.ts +426 -426
  70. package/src/adapters/cursor-adapter.ts +681 -681
  71. package/src/adapters/jsonl-stream.ts +157 -157
  72. package/src/adapters/raw-stream-log.ts +124 -124
  73. package/src/adapters/resource-monitor.ts +140 -140
  74. package/src/agent-activity.ts +175 -170
  75. package/src/agent-delegate-task-rpc.ts +153 -153
  76. package/src/agent-delegate-task.ts +91 -91
  77. package/src/agent-reload-config-rpc.ts +34 -34
  78. package/src/agent-stop-stuck.ts +129 -129
  79. package/src/builtin/cli.ts +9 -2
  80. package/src/builtin/config.ts +84 -84
  81. package/src/builtin/context.ts +62 -4
  82. package/src/builtin/file-log.ts +38 -38
  83. package/src/builtin/index.ts +83 -19
  84. package/src/builtin/proc-tree-kill.ts +61 -61
  85. package/src/builtin/progress/cards-helpers.ts +76 -76
  86. package/src/builtin/progress/reducer.ts +113 -108
  87. package/src/builtin/progress/terminal-renderer.ts +294 -294
  88. package/src/builtin/progress/view.ts +77 -77
  89. package/src/builtin/raw-stream-log.ts +124 -124
  90. package/src/builtin/session-select.ts +48 -48
  91. package/src/builtin/sigint.ts +50 -50
  92. package/src/builtin/web-tools.ts +313 -313
  93. package/src/card-action-routing.ts +14 -14
  94. package/src/chatgpt-subscription-rpc.ts +27 -27
  95. package/src/chatgpt-subscription.ts +299 -299
  96. package/src/chrome-devtools-guard.ts +318 -318
  97. package/src/codex-reset-actions.ts +184 -184
  98. package/src/feishu-api.ts +193 -193
  99. package/src/feishu-message-ingress.ts +195 -195
  100. package/src/feishu-platform.ts +20 -20
  101. package/src/format-message.ts +293 -293
  102. package/src/litellm-proxy.ts +374 -374
  103. package/src/orchestrator.ts +8 -3
  104. package/src/platform-adapter.ts +6 -6
  105. package/src/privacy.ts +118 -118
  106. package/src/progress/reducer.ts +113 -108
  107. package/src/progress/terminal-renderer.ts +294 -294
  108. package/src/progress/view.ts +77 -77
  109. package/src/response-stall.ts +28 -28
  110. package/src/runtime-reload.ts +34 -34
  111. package/src/session-chat-binding.ts +82 -82
  112. package/src/session-name.ts +8 -8
  113. package/src/session.ts +149 -26
  114. package/src/shared-prefix.ts +29 -29
  115. package/src/sim-platform.ts +20 -20
  116. package/src/startup-lifecycle.ts +250 -250
  117. package/src/stream-state.ts +21 -18
  118. package/src/terminal-error.ts +129 -0
  119. package/src/turn-cards.ts +117 -117
  120. package/src/update-command-guard.ts +165 -165
package/src/session.ts CHANGED
@@ -43,6 +43,12 @@ import { resourceMonitor, registerProcess, unregisterProcess } from "./adapters/
43
43
  import { buildImSkillsPromptCached, exportSkillSubDocs, clearImSkillsPromptCache } from "./im-skills.ts";
44
44
  import type { PlatformAdapter } from "./platform-adapter.ts";
45
45
  import { hasResponseStalled, observeResponseProgress } from "./response-stall.ts";
46
+ import {
47
+ classifyTerminalError,
48
+ formatTerminalErrorNotice,
49
+ formatTerminalErrorReason,
50
+ type TerminalErrorInfo,
51
+ } from "./terminal-error.ts";
46
52
  import {
47
53
  MAX_PROCESSED,
48
54
  clearFeishuMessageLedgerMemory,
@@ -176,7 +182,7 @@ export const RESPONSE_STALL_RECOVERY_PROMPT = "完成了吗?如果没完成继
176
182
  export const RESPONSE_STALL_RECOVERY_NOTICE =
177
183
  `检测到会话停滞,正在自动确认并继续。\n\n${RESPONSE_STALL_RECOVERY_PROMPT}`;
178
184
  export const RESPONSE_STALL_RECOVERY_EXHAUSTED_NOTICE =
179
- "⚠️ 自动续跑仍连续 3 分钟没有启动进展或新回复,本次不再自动继续。";
185
+ "⚠️ 自动续跑仍连续 3 分钟没有生成新回复,本次不再自动继续。";
180
186
  const RESPONSE_STALL_RECOVERY_DELAY_MS = 200;
181
187
  let processMonitorIntervalMs = DEFAULT_PROCESS_MONITOR_INTERVAL_MS;
182
188
  let responseStallTimeoutMs = DEFAULT_RESPONSE_STALL_TIMEOUT_MS;
@@ -305,13 +311,18 @@ function scheduleFinalResponseCloseGuard(
305
311
  runningPrompt.finalResponseCloseTimer = handle;
306
312
  }
307
313
 
308
- function formatTerminalHeader(status: "running" | "done" | "stopped" | "error" | "auto_ended"): {
314
+ function formatTerminalHeader(
315
+ status: "running" | "done" | "stopped" | "error" | "auto_ended",
316
+ terminalError?: TerminalErrorInfo,
317
+ ): {
309
318
  title: string;
310
319
  template?: string;
311
320
  } {
312
321
  if (status === "auto_ended") return { title: "已自动结束 · 3分钟无新内容", template: "orange" };
313
322
  if (status === "stopped") return { title: "已停止", template: "red" };
314
- if (status === "error") return { title: "异常结束", template: "red" };
323
+ if (status === "error") {
324
+ return { title: terminalError ? `异常结束 · ${terminalError.title}` : "异常结束", template: "red" };
325
+ }
315
326
  return { title: "完成" };
316
327
  }
317
328
 
@@ -320,28 +331,59 @@ function turnFinalStatus(status: "running" | "done" | "stopped" | "error" | "aut
320
331
  }
321
332
 
322
333
  function formatAutoEndedReply(finalReply: string): string {
323
- const reason = "⚠️ 已自动结束:连续 3 分钟没有启动进展或回复字符变化。";
334
+ const reason = "⚠️ 已自动结束:生成回复阶段连续 3 分钟没有字符变化。";
324
335
  return finalReply
325
336
  ? `${reason}以下回复可能不完整。\n\n${finalReply}`
326
337
  : `${reason}本轮没有可发送的回复内容。`;
327
338
  }
328
339
 
329
340
  /**
330
- * 只监控用户无法判断是否仍有进展的两个阶段。思考、工具调用和搜索可能合法地
331
- * 长时间不产生回复字符,由资源监控负责识别真正僵死,不能在这里误杀。
341
+ * 只监控明确的回复生成阶段。启动、压缩、思考、工具调用和搜索都有各自的
342
+ * 状态或资源保护,不能把它们消耗的时间算入回复停滞窗口。
332
343
  */
333
344
  function monitorsOutputProgress(kind: AgentActivityKind): boolean {
334
- return kind === "starting" || kind === "responding";
345
+ return kind === "responding";
335
346
  }
336
347
 
337
348
  function formatTerminalReply(
338
349
  status: "running" | "done" | "stopped" | "error" | "auto_ended",
339
350
  finalReply: string,
351
+ terminalError?: TerminalErrorInfo,
340
352
  ): string | null {
341
353
  if (status === "auto_ended") return formatAutoEndedReply(finalReply);
354
+ if (status === "error") {
355
+ const error = terminalError ?? {
356
+ kind: "unknown" as const,
357
+ title: "原因未记录",
358
+ message: "当前状态中没有可用的错误详情,请查看运行日志。",
359
+ occurredAt: Date.now(),
360
+ };
361
+ return formatTerminalErrorNotice(error, finalReply);
362
+ }
342
363
  return finalReply || null;
343
364
  }
344
365
 
366
+ function formatTerminalCardContent(state: {
367
+ status: "running" | "done" | "stopped" | "error" | "auto_ended";
368
+ accumulatedContent: string;
369
+ finalReply: string;
370
+ terminalError?: TerminalErrorInfo;
371
+ }): string {
372
+ const content = state.accumulatedContent + state.finalReply;
373
+ if (state.status !== "error") return content;
374
+
375
+ const error = state.terminalError ?? {
376
+ kind: "unknown" as const,
377
+ title: "原因未记录",
378
+ message: "当前状态中没有可用的错误详情,请查看运行日志。",
379
+ occurredAt: Date.now(),
380
+ };
381
+ const reason = formatTerminalErrorReason(error);
382
+ return content.trim()
383
+ ? `${content}\n\n${reason}\n以上内容可能不完整。`
384
+ : reason;
385
+ }
386
+
345
387
  function isCardKitSequenceConflict(err: unknown): boolean {
346
388
  return err instanceof Error && err.message.includes("300317");
347
389
  }
@@ -912,6 +954,8 @@ export function accumulateBlockContent(
912
954
  `\n\n🔄 上下文压缩(${triggerLabel}): **${block.pre_tokens}** → **${block.post_tokens}** tokens\n`; // 🔄 / →
913
955
  break;
914
956
  }
957
+ case "agent_status":
958
+ break;
915
959
  }
916
960
  }
917
961
 
@@ -1110,7 +1154,7 @@ export async function resumeAndPrompt(
1110
1154
  msgTimestamp: number,
1111
1155
  tool: string,
1112
1156
  traceId?: string,
1113
- ): Promise<void> {
1157
+ ): Promise<SessionRunOutcome> {
1114
1158
  return runAgentSession(sessionId, userText, platform, chatId, msgTimestamp, tool, traceId);
1115
1159
  }
1116
1160
 
@@ -1126,6 +1170,8 @@ interface RunAgentSessionOptions {
1126
1170
  autoRecovery?: boolean;
1127
1171
  }
1128
1172
 
1173
+ export type SessionRunOutcome = "busy" | "done" | "stopped" | "error" | "auto_ended";
1174
+
1129
1175
  export async function runAgentSession(
1130
1176
  sessionId: string,
1131
1177
  userText: string,
@@ -1135,7 +1181,7 @@ export async function runAgentSession(
1135
1181
  tool: string,
1136
1182
  traceId?: string,
1137
1183
  options: RunAgentSessionOptions = {},
1138
- ): Promise<void> {
1184
+ ): Promise<SessionRunOutcome> {
1139
1185
  const tid = traceId ?? "";
1140
1186
 
1141
1187
  // runAgentSession 是飞书用户消息、队列消息与内部自动恢复共同使用的唯一
@@ -1162,7 +1208,7 @@ export async function runAgentSession(
1162
1208
  ? "当前正在生成回复中,请等待完成后再发送消息。也可以发送 /stop 结束,已完成的步骤不会丢失。"
1163
1209
  : "该会话正在生成回复中,请等待完成后再发送消息。也可以发送 /stop 结束,已完成的步骤不会丢失。";
1164
1210
  await platform.sendText(_chatId, busyMsg).catch(() => {});
1165
- return;
1211
+ return "busy";
1166
1212
  }
1167
1213
 
1168
1214
  // 立即标记活跃,确保 /sessions、isSessionRunning 等查询在异步准备阶段就能看到运行状态。
@@ -1290,7 +1336,7 @@ export async function runAgentSession(
1290
1336
  // 再开始缓存问题对应的任务"。
1291
1337
  const prevState = await readStreamState(sessionId);
1292
1338
  if (prevState && prevState.status !== "running") {
1293
- const prevTerminalReply = formatTerminalReply(prevState.status, prevState.finalReply);
1339
+ const prevTerminalReply = formatTerminalReply(prevState.status, prevState.finalReply, prevState.terminalError);
1294
1340
  const displayChatId = pickDisplayChat(sessionId);
1295
1341
  if (displayChatId) {
1296
1342
  const pp = platformForChat(displayChatId);
@@ -1309,8 +1355,8 @@ export async function runAgentSession(
1309
1355
  setSessionChatAvatar(pp, displayChatId, prevState.tool, "idle", sessionId).catch(() => {});
1310
1356
  } else {
1311
1357
  const nextSeq = display.sequence + 1;
1312
- const { title: headerTitle, template: headerTemplate } = formatTerminalHeader(prevState.status);
1313
- const cardContent = truncateContent(prevState.accumulatedContent + prevState.finalReply) || " ";
1358
+ const { title: headerTitle, template: headerTemplate } = formatTerminalHeader(prevState.status, prevState.terminalError);
1359
+ const cardContent = truncateContent(formatTerminalCardContent(prevState)) || " ";
1314
1360
  const doneCard = buildProgressCard(progressView({ text: cardContent, status: "done", showStop: false, headerTitle, headerTemplate }));
1315
1361
  await pp.cardUpdate(display.cardId, doneCard, nextSeq).catch(err => {
1316
1362
  console.error(`[${ts()}] [DISPLAY] prevState final cardUpdate failed: ${(err as Error).message}`);
@@ -1405,15 +1451,16 @@ export async function runAgentSession(
1405
1451
  const FILE_WRITE_INTERVAL_MS = 2000;
1406
1452
  const toolCallMap = new Map<string, { name: string; input: unknown }>();
1407
1453
  let streamErrored = false;
1454
+ let streamTerminalError: TerminalErrorInfo | undefined;
1455
+ let runOutcome: SessionRunOutcome = "error";
1408
1456
 
1409
1457
  const runningPrompt = activePrompts.get(sessionId);
1410
1458
  if (runningPrompt) {
1411
- // 必须在消费第一个事件前建立零字符基线。部分 CLI 卡死时只启动了进程,
1412
- // 甚至一个事件都不会 yield;若等循环体更新进度,这种会话会永久停在
1413
- // “正在启动 Agent”,也永远触发不了三分钟保护。
1459
+ // 在消费第一个事件前建立阶段感知的零字符基线;启动阶段不计时,只有后续
1460
+ // 收到明确的 responding 状态后才会启动三分钟回复停滞保护。
1414
1461
  runningPrompt.responseProgress = observeResponseProgress(
1415
1462
  undefined,
1416
- true,
1463
+ monitorsOutputProgress(activityTracker.activity.kind),
1417
1464
  0,
1418
1465
  activityTracker.activity.startedAt,
1419
1466
  );
@@ -1486,7 +1533,7 @@ export async function runAgentSession(
1486
1533
  current.controller.abort();
1487
1534
  await killProcessTree(current.processPid);
1488
1535
  console.warn(
1489
- `[${ts()}] [RESPONSE-STALL] Session ${sessionId} auto-ended after 3 minutes without startup or reply progress`,
1536
+ `[${ts()}] [RESPONSE-STALL] Session ${sessionId} auto-ended after 3 minutes without reply progress`,
1490
1537
  );
1491
1538
  };
1492
1539
 
@@ -1580,6 +1627,7 @@ export async function runAgentSession(
1580
1627
  }
1581
1628
  } catch (streamErr) {
1582
1629
  streamErrored = true;
1630
+ streamTerminalError = classifyTerminalError(streamErr);
1583
1631
  console.error(`[${ts()}] [STREAM] Error in stream loop for ${sessionId}: ${(streamErr as Error).message}`);
1584
1632
  } finally {
1585
1633
  // 标记 prompt 结束
@@ -1629,6 +1677,23 @@ export async function runAgentSession(
1629
1677
  ? "stopped"
1630
1678
  : "done";
1631
1679
  const finalReply = pickFinalReply(state).trim();
1680
+ const terminalError = streamTerminalError
1681
+ ?? (wasAbnormalExit
1682
+ ? {
1683
+ kind: "process" as const,
1684
+ title: "Agent 进程意外退出",
1685
+ message: "Agent CLI 进程已退出。若回复不完整,请重新发送上一条指令。",
1686
+ occurredAt: Date.now(),
1687
+ }
1688
+ : wasResourceStuck
1689
+ ? {
1690
+ kind: "resource" as const,
1691
+ title: "Agent 进程失去响应",
1692
+ message: "Agent 进程长时间没有 CPU 或内存变化,已被强制停止。若回复不完整,请重新发送上一条指令。",
1693
+ occurredAt: Date.now(),
1694
+ }
1695
+ : undefined);
1696
+ runOutcome = finalStatus;
1632
1697
 
1633
1698
  // stop-stuck-loop 接口可能在 fire-and-forget 中已写入带 final_reply 的
1634
1699
  // stream state,finally 不应覆盖它。同时保留 stuckAt 标记,防止
@@ -1659,6 +1724,7 @@ export async function runAgentSession(
1659
1724
  tool,
1660
1725
  ...(preserveStuckAt ? { stuckAt: preserveStuckAt } : {}),
1661
1726
  ...(autoEndedAt !== undefined ? { autoEndedAt } : {}),
1727
+ ...(terminalError ? { terminalError } : {}),
1662
1728
  });
1663
1729
 
1664
1730
  // display loop 下一轮会读到最终状态并发送消息
@@ -1730,7 +1796,7 @@ export async function runAgentSession(
1730
1796
  autoRecoveryTarget = { chatId: activeAutoEnded, platform: pp };
1731
1797
  }
1732
1798
  }
1733
- console.warn(`[${ts()}] Session ${sessionId} auto-ended after stalled startup or response output (content chunks: ${state.chunkCount})`);
1799
+ console.warn(`[${ts()}] Session ${sessionId} auto-ended after stalled response output (content chunks: ${state.chunkCount})`);
1734
1800
  if (tid) logTrace(tid, "SESSION_END", { sessionId, outcome: "response_stall", chunks: state.chunkCount });
1735
1801
  } else if (wasAbnormalExit) {
1736
1802
  for (const cid of finalizationChatIds) {
@@ -1749,6 +1815,52 @@ export async function runAgentSession(
1749
1815
  if (activeErr) setSessionChatAvatar(platform, activeErr, tool, "idle", sessionId).catch(() => {});
1750
1816
  console.log(`[${ts()}] Session ${sessionId} process exited unexpectedly (content chunks: ${state.chunkCount})`);
1751
1817
  if (tid) logTrace(tid, "SESSION_END", { sessionId, outcome: "process_missing", chunks: state.chunkCount });
1818
+ } else if (streamErrored || wasResourceStuck) {
1819
+ for (const cid of finalizationChatIds) {
1820
+ const finfo = sessionInfoMap.get(cid);
1821
+ await recordSessionRegistry({
1822
+ chatId: cid,
1823
+ sessionId,
1824
+ tool,
1825
+ turnCount: finfo?.turnCount ?? nextTurnCount,
1826
+ lastContextTokens: finfo?.lastContextTokens ?? nextContextTokens,
1827
+ startTime: finfo?.startTime ?? now,
1828
+ running: false,
1829
+ });
1830
+ }
1831
+ const activeErr = getLastActiveChat(sessionId) ?? finalizationChatIds[0];
1832
+ if (activeErr) {
1833
+ const pp = platformForChat(activeErr) ?? platform;
1834
+ const terminalState = await readStreamState(sessionId);
1835
+ if (
1836
+ terminalError
1837
+ && !displayCards.has(activeErr)
1838
+ && (!terminalState || !isFinalReplySentForTurn(terminalState))
1839
+ ) {
1840
+ await sendFinalReplyTextOnce(
1841
+ pp,
1842
+ activeErr,
1843
+ sessionId,
1844
+ nextTurnCount,
1845
+ formatTerminalErrorNotice(terminalError, finalReplyToWrite),
1846
+ );
1847
+ }
1848
+ setSessionChatAvatar(pp, activeErr, tool, "idle", sessionId).catch(() => {});
1849
+ }
1850
+ const errorOutcome = wasResourceStuck ? "resource_stuck" : "stream_error";
1851
+ console.error(
1852
+ `[${ts()}] Session ${sessionId} ended with ${errorOutcome}` +
1853
+ `${terminalError ? ` (${terminalError.kind}: ${terminalError.title})` : ""} (content chunks: ${state.chunkCount})`,
1854
+ );
1855
+ if (tid) {
1856
+ logTrace(tid, "SESSION_END", {
1857
+ sessionId,
1858
+ outcome: errorOutcome,
1859
+ errorKind: terminalError?.kind,
1860
+ errorTitle: terminalError?.title,
1861
+ chunks: state.chunkCount,
1862
+ });
1863
+ }
1752
1864
  } else {
1753
1865
  for (const cid of finalizationChatIds) {
1754
1866
  const finfo = sessionInfoMap.get(cid);
@@ -1862,6 +1974,7 @@ export async function runAgentSession(
1862
1974
  clearSessionFinalizing(sessionId);
1863
1975
  }
1864
1976
  }
1977
+ return runOutcome;
1865
1978
  }
1866
1979
 
1867
1980
  // ---------------------------------------------------------------------------
@@ -1941,9 +2054,11 @@ export function startUnifiedDisplayLoop(): void {
1941
2054
  const tail = "━━━ 回答结束 ━━━";
1942
2055
  const finalMsg = state.status === "auto_ended"
1943
2056
  ? formatAutoEndedReply(remaining)
1944
- : remaining
1945
- ? remaining + "\n" + tail
1946
- : tail;
2057
+ : state.status === "error"
2058
+ ? formatTerminalReply(state.status, remaining, state.terminalError) ?? tail
2059
+ : remaining
2060
+ ? remaining + "\n" + tail
2061
+ : tail;
1947
2062
  if (!isFinalReplySentForTurn(state)) {
1948
2063
  await sendFinalReplyTextOnce(p, chatId, sessionId, state.turnCount, finalMsg);
1949
2064
  }
@@ -1965,8 +2080,8 @@ export function startUnifiedDisplayLoop(): void {
1965
2080
  let terminalCardUpdateAccepted = terminalCardAlreadyUpdated;
1966
2081
  if (!terminalCardAlreadyUpdated) {
1967
2082
  const nextSeq = display.sequence + 1;
1968
- const { title: headerTitle, template: headerTemplate } = formatTerminalHeader(state.status);
1969
- const cardContent = truncateContent(state.accumulatedContent + state.finalReply) || " ";
2083
+ const { title: headerTitle, template: headerTemplate } = formatTerminalHeader(state.status, state.terminalError);
2084
+ const cardContent = truncateContent(formatTerminalCardContent(state)) || " ";
1970
2085
  const doneCard = buildProgressCard(progressView({ text: cardContent, status: "done", showStop: false, headerTitle, headerTemplate }));
1971
2086
  await p.cardUpdate(display.cardId, doneCard, nextSeq).then(() => {
1972
2087
  display.sequence = nextSeq;
@@ -1993,8 +2108,16 @@ export function startUnifiedDisplayLoop(): void {
1993
2108
  }
1994
2109
 
1995
2110
  let terminalTextDelivered = true;
1996
- const terminalReply = formatTerminalReply(state.status, state.finalReply);
1997
- if (terminalReply) {
2111
+ const terminalReply = formatTerminalReply(state.status, state.finalReply, state.terminalError);
2112
+ const errorWasDeliveredByCard =
2113
+ state.status === "error"
2114
+ && !state.finalReply.trim()
2115
+ && terminalCardUpdateAccepted;
2116
+ if (errorWasDeliveredByCard) {
2117
+ if (!isFinalReplySentForTurn(state)) {
2118
+ await markFinalReplySent(sessionId, state.turnCount);
2119
+ }
2120
+ } else if (terminalReply) {
1998
2121
  if (!isFinalReplySentForTurn(state)) {
1999
2122
  terminalTextDelivered = await sendFinalReplyTextOnce(p, chatId, sessionId, state.turnCount, terminalReply);
2000
2123
  }
@@ -1,29 +1,29 @@
1
- export const ABD_PREFIX = "/abd";
2
-
3
- export const ABD_APPEND_PROMPT =
4
- "请从第一性原理出发挖掘我的真实需求。你觉得我的需求合理吗?开始实现之前有什么问题要问我的?";
5
-
6
- export const ABD_HELP_LINE =
7
- `发送 **${ABD_PREFIX}** 前缀消息,将以第一性原理追问真实需求、合理性和实现前问题`;
8
-
9
- export interface SharedPrefixResult {
10
- matched: boolean;
11
- text: string;
12
- body: string;
13
- prefix?: typeof ABD_PREFIX;
14
- }
15
-
16
- export function applySharedPrefix(text: string): SharedPrefixResult {
17
- if (!text.toLowerCase().startsWith(ABD_PREFIX)) {
18
- return { matched: false, text, body: text };
19
- }
20
-
21
- const body = text.slice(ABD_PREFIX.length).replace(/^\s+/, "");
22
- const appendix = ["---", ABD_APPEND_PROMPT].join("\n");
23
- return {
24
- matched: true,
25
- prefix: ABD_PREFIX,
26
- body,
27
- text: body ? [body, appendix].join("\n\n") : appendix,
28
- };
29
- }
1
+ export const ABD_PREFIX = "/abd";
2
+
3
+ export const ABD_APPEND_PROMPT =
4
+ "请从第一性原理出发挖掘我的真实需求。你觉得我的需求合理吗?开始实现之前有什么问题要问我的?";
5
+
6
+ export const ABD_HELP_LINE =
7
+ `发送 **${ABD_PREFIX}** 前缀消息,将以第一性原理追问真实需求、合理性和实现前问题`;
8
+
9
+ export interface SharedPrefixResult {
10
+ matched: boolean;
11
+ text: string;
12
+ body: string;
13
+ prefix?: typeof ABD_PREFIX;
14
+ }
15
+
16
+ export function applySharedPrefix(text: string): SharedPrefixResult {
17
+ if (!text.toLowerCase().startsWith(ABD_PREFIX)) {
18
+ return { matched: false, text, body: text };
19
+ }
20
+
21
+ const body = text.slice(ABD_PREFIX.length).replace(/^\s+/, "");
22
+ const appendix = ["---", ABD_APPEND_PROMPT].join("\n");
23
+ return {
24
+ matched: true,
25
+ prefix: ABD_PREFIX,
26
+ body,
27
+ text: body ? [body, appendix].join("\n\n") : appendix,
28
+ };
29
+ }
@@ -123,25 +123,25 @@ export const SimulatedPlatform: FeishuPlatform = {
123
123
  },
124
124
 
125
125
  // ---- 头像 ----
126
- async setChatAvatar(_token, _chatId, _tool, _status) {
127
- // 模拟模式不需要头像
128
- },
129
-
130
- async getCodexUsageSummary() {
131
- return {
132
- fiveHour: { usedPercent: 0, remainingPercent: 100, resetAtEpochSeconds: null, resetAfterSeconds: null },
133
- weekly: { usedPercent: 0, remainingPercent: 100, resetAtEpochSeconds: null, resetAfterSeconds: null },
134
- rateLimitResetCreditsAvailable: null,
135
- rateLimitResetCredits: null,
136
- };
137
- },
138
-
139
- async consumeCodexRateLimitResetCredit(_redeemRequestId) {
140
- return { code: "no_credit" as const, windowsReset: 0 };
141
- },
142
-
143
- // ---- 图片下载 ----
144
- async getOrDownloadImage(_token, _messageId, fileKey) {
126
+ async setChatAvatar(_token, _chatId, _tool, _status) {
127
+ // 模拟模式不需要头像
128
+ },
129
+
130
+ async getCodexUsageSummary() {
131
+ return {
132
+ fiveHour: { usedPercent: 0, remainingPercent: 100, resetAtEpochSeconds: null, resetAfterSeconds: null },
133
+ weekly: { usedPercent: 0, remainingPercent: 100, resetAtEpochSeconds: null, resetAfterSeconds: null },
134
+ rateLimitResetCreditsAvailable: null,
135
+ rateLimitResetCredits: null,
136
+ };
137
+ },
138
+
139
+ async consumeCodexRateLimitResetCredit(_redeemRequestId) {
140
+ return { code: "no_credit" as const, windowsReset: 0 };
141
+ },
142
+
143
+ // ---- 图片下载 ----
144
+ async getOrDownloadImage(_token, _messageId, fileKey) {
145
145
  return join(SIM_DIR, "images", fileKey);
146
146
  },
147
147
 
@@ -174,4 +174,4 @@ export const SimulatedPlatform: FeishuPlatform = {
174
174
  };
175
175
 
176
176
  /** 模拟模式下的默认 chat_id(重新导出以保持向后兼容) */
177
- export { SIM_DEFAULT_CHAT_ID } from "./sim-store.ts";
177
+ export { SIM_DEFAULT_CHAT_ID } from "./sim-store.ts";