chatccc 0.2.231 → 0.2.233

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 (130) 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__/crash-logging.test.ts +95 -1
  47. package/src/__tests__/cursor-adapter.test.ts +268 -268
  48. package/src/__tests__/feishu-api.test.ts +60 -60
  49. package/src/__tests__/feishu-avatar.test.ts +164 -164
  50. package/src/__tests__/feishu-message-ingress.test.ts +138 -138
  51. package/src/__tests__/feishu-platform.test.ts +22 -22
  52. package/src/__tests__/format-message.test.ts +47 -47
  53. package/src/__tests__/jsonl-stream.test.ts +79 -79
  54. package/src/__tests__/package-files.test.ts +24 -24
  55. package/src/__tests__/privacy.test.ts +198 -198
  56. package/src/__tests__/progress-reducer.test.ts +121 -121
  57. package/src/__tests__/raw-stream-log.test.ts +106 -106
  58. package/src/__tests__/response-stall.test.ts +49 -49
  59. package/src/__tests__/restart.test.ts +55 -10
  60. package/src/__tests__/session.test.ts +199 -199
  61. package/src/__tests__/shared-prefix.test.ts +36 -36
  62. package/src/__tests__/sim-platform.test.ts +16 -16
  63. package/src/__tests__/startup-lifecycle.test.ts +231 -231
  64. package/src/__tests__/stop-session.test.ts +34 -34
  65. package/src/__tests__/stream-state.test.ts +42 -42
  66. package/src/__tests__/terminal-error.test.ts +54 -54
  67. package/src/__tests__/terminal-renderer.test.ts +247 -247
  68. package/src/__tests__/update-command-guard.test.ts +144 -144
  69. package/src/__tests__/web-ui.test.ts +326 -326
  70. package/src/adapters/adapter-interface.ts +33 -33
  71. package/src/adapters/ccc-adapter.ts +141 -141
  72. package/src/adapters/claude-adapter.ts +620 -620
  73. package/src/adapters/claude-session-meta-store.ts +120 -120
  74. package/src/adapters/codex-adapter.ts +426 -426
  75. package/src/adapters/cursor-adapter.ts +681 -681
  76. package/src/adapters/jsonl-stream.ts +157 -157
  77. package/src/adapters/raw-stream-log.ts +124 -124
  78. package/src/adapters/resource-monitor.ts +140 -140
  79. package/src/agent-activity.ts +175 -175
  80. package/src/agent-delegate-task-rpc.ts +153 -153
  81. package/src/agent-delegate-task.ts +91 -91
  82. package/src/agent-reload-config-rpc.ts +34 -34
  83. package/src/agent-stop-stuck.ts +129 -129
  84. package/src/builtin/cli.ts +6 -0
  85. package/src/builtin/config.ts +84 -84
  86. package/src/builtin/context.ts +465 -391
  87. package/src/builtin/file-log.ts +38 -38
  88. package/src/builtin/file-tools.ts +34 -0
  89. package/src/builtin/index.ts +24 -23
  90. package/src/builtin/proc-tree-kill.ts +61 -61
  91. package/src/builtin/progress/cards-helpers.ts +76 -76
  92. package/src/builtin/progress/reducer.ts +113 -113
  93. package/src/builtin/progress/terminal-renderer.ts +294 -294
  94. package/src/builtin/progress/view.ts +77 -77
  95. package/src/builtin/raw-stream-log.ts +124 -124
  96. package/src/builtin/session-search.ts +363 -0
  97. package/src/builtin/session-select.ts +48 -48
  98. package/src/builtin/sigint.ts +50 -50
  99. package/src/builtin/skills.ts +205 -205
  100. package/src/builtin/web-tools.ts +313 -313
  101. package/src/card-action-routing.ts +14 -14
  102. package/src/chatgpt-subscription-rpc.ts +27 -27
  103. package/src/chatgpt-subscription.ts +299 -299
  104. package/src/chrome-devtools-guard.ts +318 -318
  105. package/src/codex-reset-actions.ts +184 -184
  106. package/src/feishu-api.ts +193 -193
  107. package/src/feishu-message-ingress.ts +195 -195
  108. package/src/feishu-platform.ts +20 -20
  109. package/src/format-message.ts +293 -293
  110. package/src/index.ts +6 -1
  111. package/src/litellm-proxy.ts +374 -374
  112. package/src/orchestrator.ts +36 -13
  113. package/src/platform-adapter.ts +6 -6
  114. package/src/privacy.ts +118 -118
  115. package/src/progress/reducer.ts +113 -113
  116. package/src/progress/terminal-renderer.ts +294 -294
  117. package/src/progress/view.ts +77 -77
  118. package/src/response-stall.ts +28 -28
  119. package/src/runtime-reload.ts +34 -34
  120. package/src/session-chat-binding.ts +82 -82
  121. package/src/session-name.ts +8 -8
  122. package/src/session.ts +201 -201
  123. package/src/shared-prefix.ts +29 -29
  124. package/src/shared.ts +44 -0
  125. package/src/sim-platform.ts +20 -20
  126. package/src/startup-lifecycle.ts +250 -250
  127. package/src/stream-state.ts +21 -21
  128. package/src/terminal-error.ts +129 -129
  129. package/src/turn-cards.ts +117 -117
  130. 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
+ });