chatccc 0.2.231 → 0.2.232

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 (127) 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 -86
  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 +449 -393
  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 +319 -202
  28. package/src/__tests__/builtin-file-tools.test.ts +240 -240
  29. package/src/__tests__/builtin-permissions.test.ts +211 -211
  30. package/src/__tests__/builtin-session-search.test.ts +228 -0
  31. package/src/__tests__/builtin-session-select.test.ts +116 -116
  32. package/src/__tests__/builtin-sigint.test.ts +56 -56
  33. package/src/__tests__/builtin-skills.test.ts +284 -284
  34. package/src/__tests__/builtin-web-tools.test.ts +220 -220
  35. package/src/__tests__/card-action-routing.test.ts +18 -18
  36. package/src/__tests__/cardkit.test.ts +60 -60
  37. package/src/__tests__/ccc-adapter.test.ts +170 -170
  38. package/src/__tests__/chatgpt-subscription-rpc.test.ts +89 -89
  39. package/src/__tests__/chatgpt-subscription.test.ts +135 -135
  40. package/src/__tests__/chrome-devtools-guard.test.ts +165 -165
  41. package/src/__tests__/claude-adapter.test.ts +614 -614
  42. package/src/__tests__/claude-raw-stream-log.test.ts +87 -87
  43. package/src/__tests__/codex-adapter.test.ts +58 -58
  44. package/src/__tests__/codex-raw-stream-log.test.ts +170 -170
  45. package/src/__tests__/codex-reset-actions.test.ts +146 -146
  46. package/src/__tests__/cursor-adapter.test.ts +268 -268
  47. package/src/__tests__/feishu-api.test.ts +60 -60
  48. package/src/__tests__/feishu-avatar.test.ts +164 -164
  49. package/src/__tests__/feishu-message-ingress.test.ts +138 -138
  50. package/src/__tests__/feishu-platform.test.ts +22 -22
  51. package/src/__tests__/format-message.test.ts +47 -47
  52. package/src/__tests__/jsonl-stream.test.ts +79 -79
  53. package/src/__tests__/package-files.test.ts +24 -24
  54. package/src/__tests__/privacy.test.ts +198 -198
  55. package/src/__tests__/progress-reducer.test.ts +121 -121
  56. package/src/__tests__/raw-stream-log.test.ts +106 -106
  57. package/src/__tests__/response-stall.test.ts +49 -49
  58. package/src/__tests__/restart.test.ts +132 -132
  59. package/src/__tests__/session.test.ts +199 -199
  60. package/src/__tests__/shared-prefix.test.ts +36 -36
  61. package/src/__tests__/sim-platform.test.ts +16 -16
  62. package/src/__tests__/startup-lifecycle.test.ts +231 -231
  63. package/src/__tests__/stop-session.test.ts +34 -34
  64. package/src/__tests__/stream-state.test.ts +42 -42
  65. package/src/__tests__/terminal-error.test.ts +54 -54
  66. package/src/__tests__/terminal-renderer.test.ts +247 -247
  67. package/src/__tests__/update-command-guard.test.ts +144 -144
  68. package/src/__tests__/web-ui.test.ts +326 -326
  69. package/src/adapters/adapter-interface.ts +33 -33
  70. package/src/adapters/ccc-adapter.ts +141 -141
  71. package/src/adapters/claude-adapter.ts +620 -620
  72. package/src/adapters/claude-session-meta-store.ts +120 -120
  73. package/src/adapters/codex-adapter.ts +426 -426
  74. package/src/adapters/cursor-adapter.ts +681 -681
  75. package/src/adapters/jsonl-stream.ts +157 -157
  76. package/src/adapters/raw-stream-log.ts +124 -124
  77. package/src/adapters/resource-monitor.ts +140 -140
  78. package/src/agent-activity.ts +175 -175
  79. package/src/agent-delegate-task-rpc.ts +153 -153
  80. package/src/agent-delegate-task.ts +91 -91
  81. package/src/agent-reload-config-rpc.ts +34 -34
  82. package/src/agent-stop-stuck.ts +129 -129
  83. package/src/builtin/cli.ts +6 -0
  84. package/src/builtin/config.ts +84 -84
  85. package/src/builtin/context.ts +465 -391
  86. package/src/builtin/file-log.ts +38 -38
  87. package/src/builtin/file-tools.ts +34 -0
  88. package/src/builtin/index.ts +24 -23
  89. package/src/builtin/proc-tree-kill.ts +61 -61
  90. package/src/builtin/progress/cards-helpers.ts +76 -76
  91. package/src/builtin/progress/reducer.ts +113 -113
  92. package/src/builtin/progress/terminal-renderer.ts +294 -294
  93. package/src/builtin/progress/view.ts +77 -77
  94. package/src/builtin/raw-stream-log.ts +124 -124
  95. package/src/builtin/session-search.ts +363 -0
  96. package/src/builtin/session-select.ts +48 -48
  97. package/src/builtin/sigint.ts +50 -50
  98. package/src/builtin/skills.ts +205 -205
  99. package/src/builtin/web-tools.ts +313 -313
  100. package/src/card-action-routing.ts +14 -14
  101. package/src/chatgpt-subscription-rpc.ts +27 -27
  102. package/src/chatgpt-subscription.ts +299 -299
  103. package/src/chrome-devtools-guard.ts +318 -318
  104. package/src/codex-reset-actions.ts +184 -184
  105. package/src/feishu-api.ts +193 -193
  106. package/src/feishu-message-ingress.ts +195 -195
  107. package/src/feishu-platform.ts +20 -20
  108. package/src/format-message.ts +293 -293
  109. package/src/litellm-proxy.ts +374 -374
  110. package/src/orchestrator.ts +2494 -2494
  111. package/src/platform-adapter.ts +6 -6
  112. package/src/privacy.ts +118 -118
  113. package/src/progress/reducer.ts +113 -113
  114. package/src/progress/terminal-renderer.ts +294 -294
  115. package/src/progress/view.ts +77 -77
  116. package/src/response-stall.ts +28 -28
  117. package/src/runtime-reload.ts +34 -34
  118. package/src/session-chat-binding.ts +82 -82
  119. package/src/session-name.ts +8 -8
  120. package/src/session.ts +201 -201
  121. package/src/shared-prefix.ts +29 -29
  122. package/src/sim-platform.ts +20 -20
  123. package/src/startup-lifecycle.ts +250 -250
  124. package/src/stream-state.ts +21 -21
  125. package/src/terminal-error.ts +129 -129
  126. package/src/turn-cards.ts +117 -117
  127. package/src/update-command-guard.ts +165 -165
@@ -9,7 +9,7 @@ vi.mock("../adapters/proc-tree-kill.ts", () => ({
9
9
  }));
10
10
 
11
11
  // mock stream-state 以支持在测试中控制累积长度
12
- const mockStreamStates = new Map<string, {
12
+ const mockStreamStates = new Map<string, {
13
13
  accumulatedContent: string;
14
14
  finalReply: string;
15
15
  activity?: {
@@ -22,14 +22,14 @@ const mockStreamStates = new Map<string, {
22
22
  autoEndedAt?: number;
23
23
  turnCount?: number;
24
24
  finalReplySentTurn?: number;
25
- finalReplySentAt?: number;
26
- terminalError?: {
27
- kind: "network_timeout" | "network" | "authentication" | "rate_limit" | "provider" | "process" | "resource" | "unknown";
28
- title: string;
29
- message: string;
30
- occurredAt: number;
31
- };
32
- }>();
25
+ finalReplySentAt?: number;
26
+ terminalError?: {
27
+ kind: "network_timeout" | "network" | "authentication" | "rate_limit" | "provider" | "process" | "resource" | "unknown";
28
+ title: string;
29
+ message: string;
30
+ occurredAt: number;
31
+ };
32
+ }>();
33
33
  vi.mock("../stream-state.ts", () => ({
34
34
  readStreamState: async (sid: string) => {
35
35
  const state = mockStreamStates.get(sid);
@@ -40,8 +40,8 @@ vi.mock("../stream-state.ts", () => ({
40
40
  finalReply: state.finalReply,
41
41
  activity: state.activity,
42
42
  finalReplySentTurn: state.finalReplySentTurn,
43
- finalReplySentAt: state.finalReplySentAt,
44
- terminalError: state.terminalError,
43
+ finalReplySentAt: state.finalReplySentAt,
44
+ terminalError: state.terminalError,
45
45
  autoEndedAt: state.autoEndedAt,
46
46
  status: state.status ?? "running",
47
47
  chunkCount: 0,
@@ -66,14 +66,14 @@ vi.mock("../stream-state.ts", () => ({
66
66
  autoEndedAt?: number;
67
67
  turnCount?: number;
68
68
  finalReplySentTurn?: number;
69
- finalReplySentAt?: number;
70
- terminalError?: {
71
- kind: "network_timeout" | "network" | "authentication" | "rate_limit" | "provider" | "process" | "resource" | "unknown";
72
- title: string;
73
- message: string;
74
- occurredAt: number;
75
- };
76
- }) => {
69
+ finalReplySentAt?: number;
70
+ terminalError?: {
71
+ kind: "network_timeout" | "network" | "authentication" | "rate_limit" | "provider" | "process" | "resource" | "unknown";
72
+ title: string;
73
+ message: string;
74
+ occurredAt: number;
75
+ };
76
+ }) => {
77
77
  mockStreamStates.set(state.sessionId, {
78
78
  accumulatedContent: state.accumulatedContent,
79
79
  finalReply: state.finalReply,
@@ -82,8 +82,8 @@ vi.mock("../stream-state.ts", () => ({
82
82
  autoEndedAt: state.autoEndedAt,
83
83
  turnCount: state.turnCount,
84
84
  finalReplySentTurn: state.finalReplySentTurn,
85
- finalReplySentAt: state.finalReplySentAt,
86
- terminalError: state.terminalError,
85
+ finalReplySentAt: state.finalReplySentAt,
86
+ terminalError: state.terminalError,
87
87
  });
88
88
  },
89
89
  createEmptyStreamState: (sid: string, cwd: string, tool: string, turnCount: number) => ({
@@ -466,7 +466,7 @@ describe("runAgentSession process monitor", () => {
466
466
  vi.useRealTimers();
467
467
  });
468
468
 
469
- it("marks the turn as error and sends a separate notice when the CLI process disappears", async () => {
469
+ it("marks the turn as error and sends a separate notice when the CLI process disappears", async () => {
470
470
  const platform = mockPlatform("feishu");
471
471
  setSessionPlatform(platform);
472
472
  bindChatToSession("sid-process", "chat-process");
@@ -522,52 +522,52 @@ describe("runAgentSession process monitor", () => {
522
522
  "chat-process",
523
523
  expect.stringContaining("进程异常结束"),
524
524
  );
525
- });
526
-
527
- it("persists and sends the root cause when the agent stream fails before producing output", async () => {
528
- vi.spyOn(console, "error").mockImplementation(() => {});
529
- const platform = mockPlatform("feishu");
530
- platform.cardCreate = vi.fn(async () => {
531
- throw new Error("CardKit unavailable");
532
- });
533
- setSessionPlatform(platform);
534
- bindChatToSession("sid-stream-error", "chat-stream-error");
535
- recordLastActiveChat("sid-stream-error", "chat-stream-error");
536
-
537
- const adapter: ToolAdapter = {
538
- displayName: "CCC Agent",
539
- sessionDescPrefix: "CCC Session:",
540
- createSession: async () => ({ sessionId: "sid-stream-error" }),
541
- getSessionInfo: async (sid) => ({ sessionId: sid, cwd: "/tmp" }),
542
- closeSession: async () => {},
543
- prompt: async function* () {
544
- throw new Error(
545
- "Failed after 3 attempts. Last error: Cannot connect to API: " +
546
- "Connect Timeout Error (timeout: 10000ms)",
547
- );
548
- },
549
- };
550
- _setAdapterForToolForTest("ccc", adapter);
551
-
552
- const outcome = await runAgentSession(
553
- "sid-stream-error",
554
- "prompt",
555
- platform,
556
- "chat-stream-error",
557
- Date.now(),
558
- "ccc",
559
- );
560
-
561
- const state = mockStreamStates.get("sid-stream-error");
562
- expect(outcome).toBe("error");
563
- expect(state?.status).toBe("error");
564
- expect(state?.terminalError?.kind).toBe("network_timeout");
565
- expect(state?.terminalError?.message).toContain("已重试 3 次");
566
- expect(platform.sendText).toHaveBeenCalledWith(
567
- "chat-stream-error",
568
- expect.stringContaining("网络连接超时"),
569
- );
570
- });
525
+ });
526
+
527
+ it("persists and sends the root cause when the agent stream fails before producing output", async () => {
528
+ vi.spyOn(console, "error").mockImplementation(() => {});
529
+ const platform = mockPlatform("feishu");
530
+ platform.cardCreate = vi.fn(async () => {
531
+ throw new Error("CardKit unavailable");
532
+ });
533
+ setSessionPlatform(platform);
534
+ bindChatToSession("sid-stream-error", "chat-stream-error");
535
+ recordLastActiveChat("sid-stream-error", "chat-stream-error");
536
+
537
+ const adapter: ToolAdapter = {
538
+ displayName: "CCC Agent",
539
+ sessionDescPrefix: "CCC Session:",
540
+ createSession: async () => ({ sessionId: "sid-stream-error" }),
541
+ getSessionInfo: async (sid) => ({ sessionId: sid, cwd: "/tmp" }),
542
+ closeSession: async () => {},
543
+ prompt: async function* () {
544
+ throw new Error(
545
+ "Failed after 3 attempts. Last error: Cannot connect to API: " +
546
+ "Connect Timeout Error (timeout: 10000ms)",
547
+ );
548
+ },
549
+ };
550
+ _setAdapterForToolForTest("ccc", adapter);
551
+
552
+ const outcome = await runAgentSession(
553
+ "sid-stream-error",
554
+ "prompt",
555
+ platform,
556
+ "chat-stream-error",
557
+ Date.now(),
558
+ "ccc",
559
+ );
560
+
561
+ const state = mockStreamStates.get("sid-stream-error");
562
+ expect(outcome).toBe("error");
563
+ expect(state?.status).toBe("error");
564
+ expect(state?.terminalError?.kind).toBe("network_timeout");
565
+ expect(state?.terminalError?.message).toContain("已重试 3 次");
566
+ expect(platform.sendText).toHaveBeenCalledWith(
567
+ "chat-stream-error",
568
+ expect.stringContaining("网络连接超时"),
569
+ );
570
+ });
571
571
 
572
572
  it("recreates the progress card when the initial CardKit send fails", async () => {
573
573
  vi.spyOn(console, "error").mockImplementation(() => {});
@@ -907,7 +907,7 @@ describe("runAgentSession response stall watchdog", () => {
907
907
  });
908
908
  });
909
909
 
910
- it("does not apply the reply-stall timeout while an Agent is compacting context", async () => {
910
+ it("does not apply the reply-stall timeout while an Agent is compacting context", async () => {
911
911
  vi.setSystemTime(0);
912
912
  _setResponseStallTimeoutForTest(180_000);
913
913
  _setResponseStallCheckIntervalForTest(1_000);
@@ -932,10 +932,10 @@ describe("runAgentSession response stall watchdog", () => {
932
932
  signal?: AbortSignal,
933
933
  options?: ToolPromptOptions,
934
934
  ) {
935
- options?.onSessionCreated?.(closeSession);
936
- options?.onProcessStart?.({ pid: 4343 });
937
- yield { type: "assistant", blocks: [{ type: "agent_status", status: "compacting" }] };
938
- await new Promise<void>((resolve) => {
935
+ options?.onSessionCreated?.(closeSession);
936
+ options?.onProcessStart?.({ pid: 4343 });
937
+ yield { type: "assistant", blocks: [{ type: "agent_status", status: "compacting" }] };
938
+ await new Promise<void>((resolve) => {
939
939
  if (signal?.aborted) {
940
940
  resolve();
941
941
  return;
@@ -957,11 +957,11 @@ describe("runAgentSession response stall watchdog", () => {
957
957
  );
958
958
 
959
959
  await vi.waitFor(() => {
960
- expect(activePrompts.get("sid-starting-stall")).toMatchObject({
961
- processPid: 4343,
962
- });
963
- expect(activePrompts.get("sid-starting-stall")?.responseProgress).toBeUndefined();
964
- expect(closeSession).toHaveBeenCalledTimes(0);
960
+ expect(activePrompts.get("sid-starting-stall")).toMatchObject({
961
+ processPid: 4343,
962
+ });
963
+ expect(activePrompts.get("sid-starting-stall")?.responseProgress).toBeUndefined();
964
+ expect(closeSession).toHaveBeenCalledTimes(0);
965
965
  });
966
966
 
967
967
  await vi.advanceTimersByTimeAsync(181_001);
@@ -972,17 +972,17 @@ describe("runAgentSession response stall watchdog", () => {
972
972
  .length;
973
973
 
974
974
  // 旧实现不会结束真正的零事件启动;先清理测试会话,避免失败用例悬挂。
975
- stopSession("sid-starting-stall");
976
- await vi.advanceTimersByTimeAsync(1_000);
977
- await runPromise;
978
-
979
- expect(stateAfterDeadline).toMatchObject({
980
- status: "running",
981
- activity: { kind: "compacting" },
982
- });
983
- expect(closeCountAfterDeadline).toBe(0);
984
- expect(treeKillCountAfterDeadline).toBe(0);
985
- });
975
+ stopSession("sid-starting-stall");
976
+ await vi.advanceTimersByTimeAsync(1_000);
977
+ await runPromise;
978
+
979
+ expect(stateAfterDeadline).toMatchObject({
980
+ status: "running",
981
+ activity: { kind: "compacting" },
982
+ });
983
+ expect(closeCountAfterDeadline).toBe(0);
984
+ expect(treeKillCountAfterDeadline).toBe(0);
985
+ });
986
986
 
987
987
  it("self-heals a missing trigger-chat binding and gives automatic recovery the normal card lifecycle", async () => {
988
988
  vi.setSystemTime(0);
@@ -1425,7 +1425,7 @@ describe("runAgentSession response stall watchdog", () => {
1425
1425
  expect(receivedPrompts[1]).toContain(recoveryPrompt);
1426
1426
  expect(platform.sendText).toHaveBeenCalledWith(
1427
1427
  "chat-recovery-limit",
1428
- "⚠️ 自动续跑仍连续 3 分钟没有生成新回复,本次不再自动继续。",
1428
+ "⚠️ 自动续跑仍连续 3 分钟没有生成新回复,本次不再自动继续。",
1429
1429
  );
1430
1430
  });
1431
1431
 
@@ -1627,7 +1627,7 @@ describe("unified display loop activity status", () => {
1627
1627
  });
1628
1628
  });
1629
1629
 
1630
- describe("unified display loop terminal card update", () => {
1630
+ describe("unified display loop terminal card update", () => {
1631
1631
  beforeEach(() => {
1632
1632
  vi.useFakeTimers();
1633
1633
  resetState();
@@ -1641,7 +1641,7 @@ describe("unified display loop terminal card update", () => {
1641
1641
  vi.useRealTimers();
1642
1642
  });
1643
1643
 
1644
- it("shows a distinct auto-ended state and sends a warning even with an empty reply", async () => {
1644
+ it("shows a distinct auto-ended state and sends a warning even with an empty reply", async () => {
1645
1645
  const platform = mockPlatform("feishu");
1646
1646
  setSessionPlatform(platform);
1647
1647
 
@@ -1683,115 +1683,115 @@ describe("unified display loop terminal card update", () => {
1683
1683
  expect(card.header.title.content).toBe("已自动结束 · 3分钟无新内容");
1684
1684
  expect(card.header.template).toBe("orange");
1685
1685
  expect(platform.sendText).toHaveBeenCalledWith(
1686
- "chat-auto-ended",
1687
- "⚠️ 已自动结束:生成回复阶段连续 3 分钟没有字符变化。本轮没有可发送的回复内容。",
1686
+ "chat-auto-ended",
1687
+ "⚠️ 已自动结束:生成回复阶段连续 3 分钟没有字符变化。本轮没有可发送的回复内容。",
1688
1688
  );
1689
1689
  expect(displayCards.has("chat-auto-ended")).toBe(false);
1690
- });
1691
-
1692
- it("shows the stream root cause in the error card without a duplicate text notice", async () => {
1693
- const platform = mockPlatform("feishu");
1694
- setSessionPlatform(platform);
1695
-
1696
- bindChatToSession("sid-network-error", "chat-network-error");
1697
- recordLastActiveChat("sid-network-error", "chat-network-error");
1698
- sessionInfoMap.set("chat-network-error", {
1699
- sessionId: "sid-network-error",
1700
- turnCount: 1,
1701
- lastContextTokens: 0,
1702
- startTime: 0,
1703
- tool: "ccc",
1704
- });
1705
- displayCards.set("chat-network-error", {
1706
- cardId: "card-network-error",
1707
- sequence: 1,
1708
- cardBusy: false,
1709
- cardCreatedAt: Date.now(),
1710
- lastSentContent: "",
1711
- streamErrorNotified: false,
1712
- sessionId: "sid-network-error",
1713
- turnCount: 1,
1714
- dotCount: 0,
1715
- });
1716
- mockStreamStates.set("sid-network-error", {
1717
- accumulatedContent: "",
1718
- finalReply: "",
1719
- status: "error",
1720
- turnCount: 1,
1721
- terminalError: {
1722
- kind: "network_timeout",
1723
- title: "网络连接超时",
1724
- message: "连接模型服务失败,已重试 3 次,单次连接等待 10 秒。请检查网络、VPN或模型服务状态后重试。",
1725
- occurredAt: Date.now(),
1726
- },
1727
- });
1728
-
1729
- startUnifiedDisplayLoop();
1730
- await vi.advanceTimersByTimeAsync(3_000);
1731
-
1732
- const payload = vi.mocked(platform.cardUpdate).mock.calls[0]?.[1];
1733
- const card = JSON.parse(payload as string) as {
1734
- header: { title: { content: string }; template?: string };
1735
- body: { elements: Array<{ content?: string }> };
1736
- };
1737
- expect(card.header.title.content).toBe("异常结束 · 网络连接超时");
1738
- expect(card.header.template).toBe("red");
1739
- expect(card.body.elements[0]?.content).toContain("已重试 3 次");
1740
- expect(platform.sendText).not.toHaveBeenCalled();
1741
- expect(mockStreamStates.get("sid-network-error")?.finalReplySentTurn).toBe(1);
1742
- expect(displayCards.has("chat-network-error")).toBe(false);
1743
- });
1744
-
1745
- it("falls back to a root-cause text when the error card cannot be updated", async () => {
1746
- vi.spyOn(console, "error").mockImplementation(() => {});
1747
- const platform = mockPlatform("feishu");
1748
- platform.cardUpdate = vi.fn(async () => {
1749
- throw new Error("CardKit unavailable");
1750
- });
1751
- setSessionPlatform(platform);
1752
-
1753
- bindChatToSession("sid-error-fallback", "chat-error-fallback");
1754
- recordLastActiveChat("sid-error-fallback", "chat-error-fallback");
1755
- sessionInfoMap.set("chat-error-fallback", {
1756
- sessionId: "sid-error-fallback",
1757
- turnCount: 1,
1758
- lastContextTokens: 0,
1759
- startTime: 0,
1760
- tool: "ccc",
1761
- });
1762
- displayCards.set("chat-error-fallback", {
1763
- cardId: "card-error-fallback",
1764
- sequence: 1,
1765
- cardBusy: false,
1766
- cardCreatedAt: Date.now(),
1767
- lastSentContent: "",
1768
- streamErrorNotified: false,
1769
- sessionId: "sid-error-fallback",
1770
- turnCount: 1,
1771
- dotCount: 0,
1772
- });
1773
- mockStreamStates.set("sid-error-fallback", {
1774
- accumulatedContent: "",
1775
- finalReply: "",
1776
- status: "error",
1777
- turnCount: 1,
1778
- terminalError: {
1779
- kind: "network_timeout",
1780
- title: "网络连接超时",
1781
- message: "连接模型服务失败,已重试 3 次。",
1782
- occurredAt: Date.now(),
1783
- },
1784
- });
1785
-
1786
- startUnifiedDisplayLoop();
1787
- await vi.advanceTimersByTimeAsync(3_000);
1788
-
1789
- expect(platform.sendText).toHaveBeenCalledWith(
1790
- "chat-error-fallback",
1791
- expect.stringContaining("异常结束:网络连接超时"),
1792
- );
1793
- expect(displayCards.has("chat-error-fallback")).toBe(false);
1794
- });
1690
+ });
1691
+
1692
+ it("shows the stream root cause in the error card without a duplicate text notice", async () => {
1693
+ const platform = mockPlatform("feishu");
1694
+ setSessionPlatform(platform);
1695
+
1696
+ bindChatToSession("sid-network-error", "chat-network-error");
1697
+ recordLastActiveChat("sid-network-error", "chat-network-error");
1698
+ sessionInfoMap.set("chat-network-error", {
1699
+ sessionId: "sid-network-error",
1700
+ turnCount: 1,
1701
+ lastContextTokens: 0,
1702
+ startTime: 0,
1703
+ tool: "ccc",
1704
+ });
1705
+ displayCards.set("chat-network-error", {
1706
+ cardId: "card-network-error",
1707
+ sequence: 1,
1708
+ cardBusy: false,
1709
+ cardCreatedAt: Date.now(),
1710
+ lastSentContent: "",
1711
+ streamErrorNotified: false,
1712
+ sessionId: "sid-network-error",
1713
+ turnCount: 1,
1714
+ dotCount: 0,
1715
+ });
1716
+ mockStreamStates.set("sid-network-error", {
1717
+ accumulatedContent: "",
1718
+ finalReply: "",
1719
+ status: "error",
1720
+ turnCount: 1,
1721
+ terminalError: {
1722
+ kind: "network_timeout",
1723
+ title: "网络连接超时",
1724
+ message: "连接模型服务失败,已重试 3 次,单次连接等待 10 秒。请检查网络、VPN或模型服务状态后重试。",
1725
+ occurredAt: Date.now(),
1726
+ },
1727
+ });
1728
+
1729
+ startUnifiedDisplayLoop();
1730
+ await vi.advanceTimersByTimeAsync(3_000);
1731
+
1732
+ const payload = vi.mocked(platform.cardUpdate).mock.calls[0]?.[1];
1733
+ const card = JSON.parse(payload as string) as {
1734
+ header: { title: { content: string }; template?: string };
1735
+ body: { elements: Array<{ content?: string }> };
1736
+ };
1737
+ expect(card.header.title.content).toBe("异常结束 · 网络连接超时");
1738
+ expect(card.header.template).toBe("red");
1739
+ expect(card.body.elements[0]?.content).toContain("已重试 3 次");
1740
+ expect(platform.sendText).not.toHaveBeenCalled();
1741
+ expect(mockStreamStates.get("sid-network-error")?.finalReplySentTurn).toBe(1);
1742
+ expect(displayCards.has("chat-network-error")).toBe(false);
1743
+ });
1744
+
1745
+ it("falls back to a root-cause text when the error card cannot be updated", async () => {
1746
+ vi.spyOn(console, "error").mockImplementation(() => {});
1747
+ const platform = mockPlatform("feishu");
1748
+ platform.cardUpdate = vi.fn(async () => {
1749
+ throw new Error("CardKit unavailable");
1750
+ });
1751
+ setSessionPlatform(platform);
1752
+
1753
+ bindChatToSession("sid-error-fallback", "chat-error-fallback");
1754
+ recordLastActiveChat("sid-error-fallback", "chat-error-fallback");
1755
+ sessionInfoMap.set("chat-error-fallback", {
1756
+ sessionId: "sid-error-fallback",
1757
+ turnCount: 1,
1758
+ lastContextTokens: 0,
1759
+ startTime: 0,
1760
+ tool: "ccc",
1761
+ });
1762
+ displayCards.set("chat-error-fallback", {
1763
+ cardId: "card-error-fallback",
1764
+ sequence: 1,
1765
+ cardBusy: false,
1766
+ cardCreatedAt: Date.now(),
1767
+ lastSentContent: "",
1768
+ streamErrorNotified: false,
1769
+ sessionId: "sid-error-fallback",
1770
+ turnCount: 1,
1771
+ dotCount: 0,
1772
+ });
1773
+ mockStreamStates.set("sid-error-fallback", {
1774
+ accumulatedContent: "",
1775
+ finalReply: "",
1776
+ status: "error",
1777
+ turnCount: 1,
1778
+ terminalError: {
1779
+ kind: "network_timeout",
1780
+ title: "网络连接超时",
1781
+ message: "连接模型服务失败,已重试 3 次。",
1782
+ occurredAt: Date.now(),
1783
+ },
1784
+ });
1785
+
1786
+ startUnifiedDisplayLoop();
1787
+ await vi.advanceTimersByTimeAsync(3_000);
1788
+
1789
+ expect(platform.sendText).toHaveBeenCalledWith(
1790
+ "chat-error-fallback",
1791
+ expect.stringContaining("异常结束:网络连接超时"),
1792
+ );
1793
+ expect(displayCards.has("chat-error-fallback")).toBe(false);
1794
+ });
1795
1795
 
1796
1796
  it("does not repeat the same terminal CardKit sequence while the prompt is still active", async () => {
1797
1797
  const platform = mockPlatform("feishu");
@@ -1,36 +1,36 @@
1
- import { describe, expect, it } from "vitest";
2
-
3
- import {
4
- ABD_APPEND_PROMPT,
5
- applySharedPrefix,
6
- } from "../shared-prefix.ts";
7
-
8
- describe("applySharedPrefix", () => {
9
- it("leaves normal messages unchanged", () => {
10
- expect(applySharedPrefix("帮我分析")).toEqual({
11
- matched: false,
12
- text: "帮我分析",
13
- body: "帮我分析",
14
- });
15
- });
16
-
17
- it("removes /abd without requiring a space", () => {
18
- const result = applySharedPrefix("/abd帮我分析");
19
-
20
- expect(result.matched).toBe(true);
21
- expect(result.body).toBe("帮我分析");
22
- expect(result.text).toBe(`帮我分析\n\n---\n${ABD_APPEND_PROMPT}`);
23
- });
24
-
25
- it("removes whitespace after /abd", () => {
26
- const result = applySharedPrefix("/abd 帮我分析");
27
-
28
- expect(result.text).toBe(`帮我分析\n\n---\n${ABD_APPEND_PROMPT}`);
29
- });
30
-
31
- it("keeps the appendix when the user body is empty", () => {
32
- const result = applySharedPrefix("/abd ");
33
-
34
- expect(result.text).toBe(`---\n${ABD_APPEND_PROMPT}`);
35
- });
36
- });
1
+ import { describe, expect, it } from "vitest";
2
+
3
+ import {
4
+ ABD_APPEND_PROMPT,
5
+ applySharedPrefix,
6
+ } from "../shared-prefix.ts";
7
+
8
+ describe("applySharedPrefix", () => {
9
+ it("leaves normal messages unchanged", () => {
10
+ expect(applySharedPrefix("帮我分析")).toEqual({
11
+ matched: false,
12
+ text: "帮我分析",
13
+ body: "帮我分析",
14
+ });
15
+ });
16
+
17
+ it("removes /abd without requiring a space", () => {
18
+ const result = applySharedPrefix("/abd帮我分析");
19
+
20
+ expect(result.matched).toBe(true);
21
+ expect(result.body).toBe("帮我分析");
22
+ expect(result.text).toBe(`帮我分析\n\n---\n${ABD_APPEND_PROMPT}`);
23
+ });
24
+
25
+ it("removes whitespace after /abd", () => {
26
+ const result = applySharedPrefix("/abd 帮我分析");
27
+
28
+ expect(result.text).toBe(`帮我分析\n\n---\n${ABD_APPEND_PROMPT}`);
29
+ });
30
+
31
+ it("keeps the appendix when the user body is empty", () => {
32
+ const result = applySharedPrefix("/abd ");
33
+
34
+ expect(result.text).toBe(`---\n${ABD_APPEND_PROMPT}`);
35
+ });
36
+ });
@@ -62,21 +62,21 @@ describe("SimulatedPlatform", () => {
62
62
  await expect(SimulatedPlatform.setChatAvatar("t", "ch1", "claude", "busy")).resolves.toBeUndefined();
63
63
  });
64
64
 
65
- it("纯函数 extractSessionInfo 正常工作", () => {
66
- const result = SimulatedPlatform.extractSessionInfo("Claude Code Session: a1b2c3d4-e5f6-7890-abcd-ef1234567890");
67
- expect(result).toEqual({ sessionId: "a1b2c3d4-e5f6-7890-abcd-ef1234567890", tool: "claude" });
68
- });
69
-
70
- it("pure extractSessionInfo recognizes ccc session ids", () => {
71
- const result = SimulatedPlatform.extractSessionInfo("CCC Session: session-20260702-121530-a1b2c3");
72
- expect(result).toEqual({ sessionId: "session-20260702-121530-a1b2c3", tool: "ccc" });
73
- });
74
-
75
- it("纯函数 formatDelayNotice 正常工作", () => {
76
- const notice = SimulatedPlatform.formatDelayNotice(Date.now() - 20 * 60 * 1000, "测试消息");
77
- expect(notice).toBeDefined();
78
- expect(notice).toContain("延迟送达");
79
- expect(notice).not.toContain("因服务离线");
65
+ it("纯函数 extractSessionInfo 正常工作", () => {
66
+ const result = SimulatedPlatform.extractSessionInfo("Claude Code Session: a1b2c3d4-e5f6-7890-abcd-ef1234567890");
67
+ expect(result).toEqual({ sessionId: "a1b2c3d4-e5f6-7890-abcd-ef1234567890", tool: "claude" });
68
+ });
69
+
70
+ it("pure extractSessionInfo recognizes ccc session ids", () => {
71
+ const result = SimulatedPlatform.extractSessionInfo("CCC Session: session-20260702-121530-a1b2c3");
72
+ expect(result).toEqual({ sessionId: "session-20260702-121530-a1b2c3", tool: "ccc" });
73
+ });
74
+
75
+ it("纯函数 formatDelayNotice 正常工作", () => {
76
+ const notice = SimulatedPlatform.formatDelayNotice(Date.now() - 20 * 60 * 1000, "测试消息");
77
+ expect(notice).toBeDefined();
78
+ expect(notice).toContain("延迟送达");
79
+ expect(notice).not.toContain("因服务离线");
80
80
  // 近期消息不触发
81
81
  expect(SimulatedPlatform.formatDelayNotice(Date.now(), "test")).toBeNull();
82
82
  });
@@ -90,4 +90,4 @@ describe("SimulatedPlatform", () => {
90
90
  // 未来时间戳(时钟偏移)不触发
91
91
  expect(SimulatedPlatform.formatDelayNotice(now + 60 * 1000, "边界", now)).toBeNull();
92
92
  });
93
- });
93
+ });