chatccc 0.2.228 → 0.2.230
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.
- package/package.json +1 -1
- package/src/__tests__/builtin-chat-session.test.ts +329 -350
- package/src/__tests__/builtin-file-tools.test.ts +240 -275
- package/src/__tests__/builtin-skills.test.ts +284 -252
- package/src/__tests__/builtin-web-tools.test.ts +220 -220
- package/src/__tests__/restart.test.ts +132 -0
- package/src/__tests__/session.test.ts +163 -0
- package/src/__tests__/terminal-error.test.ts +54 -0
- package/src/builtin/context.ts +333 -323
- package/src/builtin/file-tools.ts +17 -2
- package/src/builtin/index.ts +12 -5
- package/src/builtin/skills.ts +205 -190
- package/src/builtin/web-tools.ts +313 -313
- package/src/index.ts +315 -310
- package/src/orchestrator.ts +2494 -2388
- package/src/session.ts +137 -15
- package/src/stream-state.ts +21 -18
- package/src/terminal-error.ts +129 -0
|
@@ -23,6 +23,12 @@ const mockStreamStates = new Map<string, {
|
|
|
23
23
|
turnCount?: number;
|
|
24
24
|
finalReplySentTurn?: number;
|
|
25
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
|
+
};
|
|
26
32
|
}>();
|
|
27
33
|
vi.mock("../stream-state.ts", () => ({
|
|
28
34
|
readStreamState: async (sid: string) => {
|
|
@@ -35,6 +41,7 @@ vi.mock("../stream-state.ts", () => ({
|
|
|
35
41
|
activity: state.activity,
|
|
36
42
|
finalReplySentTurn: state.finalReplySentTurn,
|
|
37
43
|
finalReplySentAt: state.finalReplySentAt,
|
|
44
|
+
terminalError: state.terminalError,
|
|
38
45
|
autoEndedAt: state.autoEndedAt,
|
|
39
46
|
status: state.status ?? "running",
|
|
40
47
|
chunkCount: 0,
|
|
@@ -60,6 +67,12 @@ vi.mock("../stream-state.ts", () => ({
|
|
|
60
67
|
turnCount?: number;
|
|
61
68
|
finalReplySentTurn?: number;
|
|
62
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
|
+
};
|
|
63
76
|
}) => {
|
|
64
77
|
mockStreamStates.set(state.sessionId, {
|
|
65
78
|
accumulatedContent: state.accumulatedContent,
|
|
@@ -70,6 +83,7 @@ vi.mock("../stream-state.ts", () => ({
|
|
|
70
83
|
turnCount: state.turnCount,
|
|
71
84
|
finalReplySentTurn: state.finalReplySentTurn,
|
|
72
85
|
finalReplySentAt: state.finalReplySentAt,
|
|
86
|
+
terminalError: state.terminalError,
|
|
73
87
|
});
|
|
74
88
|
},
|
|
75
89
|
createEmptyStreamState: (sid: string, cwd: string, tool: string, turnCount: number) => ({
|
|
@@ -510,6 +524,51 @@ describe("runAgentSession process monitor", () => {
|
|
|
510
524
|
);
|
|
511
525
|
});
|
|
512
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
|
+
|
|
513
572
|
it("recreates the progress card when the initial CardKit send fails", async () => {
|
|
514
573
|
vi.spyOn(console, "error").mockImplementation(() => {});
|
|
515
574
|
const platform = mockPlatform("feishu");
|
|
@@ -1635,6 +1694,110 @@ describe("unified display loop terminal card update", () => {
|
|
|
1635
1694
|
expect(displayCards.has("chat-auto-ended")).toBe(false);
|
|
1636
1695
|
});
|
|
1637
1696
|
|
|
1697
|
+
it("shows the stream root cause in the error card without a duplicate text notice", async () => {
|
|
1698
|
+
const platform = mockPlatform("feishu");
|
|
1699
|
+
setSessionPlatform(platform);
|
|
1700
|
+
|
|
1701
|
+
bindChatToSession("sid-network-error", "chat-network-error");
|
|
1702
|
+
recordLastActiveChat("sid-network-error", "chat-network-error");
|
|
1703
|
+
sessionInfoMap.set("chat-network-error", {
|
|
1704
|
+
sessionId: "sid-network-error",
|
|
1705
|
+
turnCount: 1,
|
|
1706
|
+
lastContextTokens: 0,
|
|
1707
|
+
startTime: 0,
|
|
1708
|
+
tool: "ccc",
|
|
1709
|
+
});
|
|
1710
|
+
displayCards.set("chat-network-error", {
|
|
1711
|
+
cardId: "card-network-error",
|
|
1712
|
+
sequence: 1,
|
|
1713
|
+
cardBusy: false,
|
|
1714
|
+
cardCreatedAt: Date.now(),
|
|
1715
|
+
lastSentContent: "",
|
|
1716
|
+
streamErrorNotified: false,
|
|
1717
|
+
sessionId: "sid-network-error",
|
|
1718
|
+
turnCount: 1,
|
|
1719
|
+
dotCount: 0,
|
|
1720
|
+
});
|
|
1721
|
+
mockStreamStates.set("sid-network-error", {
|
|
1722
|
+
accumulatedContent: "",
|
|
1723
|
+
finalReply: "",
|
|
1724
|
+
status: "error",
|
|
1725
|
+
turnCount: 1,
|
|
1726
|
+
terminalError: {
|
|
1727
|
+
kind: "network_timeout",
|
|
1728
|
+
title: "网络连接超时",
|
|
1729
|
+
message: "连接模型服务失败,已重试 3 次,单次连接等待 10 秒。请检查网络、VPN或模型服务状态后重试。",
|
|
1730
|
+
occurredAt: Date.now(),
|
|
1731
|
+
},
|
|
1732
|
+
});
|
|
1733
|
+
|
|
1734
|
+
startUnifiedDisplayLoop();
|
|
1735
|
+
await vi.advanceTimersByTimeAsync(3_000);
|
|
1736
|
+
|
|
1737
|
+
const payload = vi.mocked(platform.cardUpdate).mock.calls[0]?.[1];
|
|
1738
|
+
const card = JSON.parse(payload as string) as {
|
|
1739
|
+
header: { title: { content: string }; template?: string };
|
|
1740
|
+
body: { elements: Array<{ content?: string }> };
|
|
1741
|
+
};
|
|
1742
|
+
expect(card.header.title.content).toBe("异常结束 · 网络连接超时");
|
|
1743
|
+
expect(card.header.template).toBe("red");
|
|
1744
|
+
expect(card.body.elements[0]?.content).toContain("已重试 3 次");
|
|
1745
|
+
expect(platform.sendText).not.toHaveBeenCalled();
|
|
1746
|
+
expect(mockStreamStates.get("sid-network-error")?.finalReplySentTurn).toBe(1);
|
|
1747
|
+
expect(displayCards.has("chat-network-error")).toBe(false);
|
|
1748
|
+
});
|
|
1749
|
+
|
|
1750
|
+
it("falls back to a root-cause text when the error card cannot be updated", async () => {
|
|
1751
|
+
vi.spyOn(console, "error").mockImplementation(() => {});
|
|
1752
|
+
const platform = mockPlatform("feishu");
|
|
1753
|
+
platform.cardUpdate = vi.fn(async () => {
|
|
1754
|
+
throw new Error("CardKit unavailable");
|
|
1755
|
+
});
|
|
1756
|
+
setSessionPlatform(platform);
|
|
1757
|
+
|
|
1758
|
+
bindChatToSession("sid-error-fallback", "chat-error-fallback");
|
|
1759
|
+
recordLastActiveChat("sid-error-fallback", "chat-error-fallback");
|
|
1760
|
+
sessionInfoMap.set("chat-error-fallback", {
|
|
1761
|
+
sessionId: "sid-error-fallback",
|
|
1762
|
+
turnCount: 1,
|
|
1763
|
+
lastContextTokens: 0,
|
|
1764
|
+
startTime: 0,
|
|
1765
|
+
tool: "ccc",
|
|
1766
|
+
});
|
|
1767
|
+
displayCards.set("chat-error-fallback", {
|
|
1768
|
+
cardId: "card-error-fallback",
|
|
1769
|
+
sequence: 1,
|
|
1770
|
+
cardBusy: false,
|
|
1771
|
+
cardCreatedAt: Date.now(),
|
|
1772
|
+
lastSentContent: "",
|
|
1773
|
+
streamErrorNotified: false,
|
|
1774
|
+
sessionId: "sid-error-fallback",
|
|
1775
|
+
turnCount: 1,
|
|
1776
|
+
dotCount: 0,
|
|
1777
|
+
});
|
|
1778
|
+
mockStreamStates.set("sid-error-fallback", {
|
|
1779
|
+
accumulatedContent: "",
|
|
1780
|
+
finalReply: "",
|
|
1781
|
+
status: "error",
|
|
1782
|
+
turnCount: 1,
|
|
1783
|
+
terminalError: {
|
|
1784
|
+
kind: "network_timeout",
|
|
1785
|
+
title: "网络连接超时",
|
|
1786
|
+
message: "连接模型服务失败,已重试 3 次。",
|
|
1787
|
+
occurredAt: Date.now(),
|
|
1788
|
+
},
|
|
1789
|
+
});
|
|
1790
|
+
|
|
1791
|
+
startUnifiedDisplayLoop();
|
|
1792
|
+
await vi.advanceTimersByTimeAsync(3_000);
|
|
1793
|
+
|
|
1794
|
+
expect(platform.sendText).toHaveBeenCalledWith(
|
|
1795
|
+
"chat-error-fallback",
|
|
1796
|
+
expect.stringContaining("异常结束:网络连接超时"),
|
|
1797
|
+
);
|
|
1798
|
+
expect(displayCards.has("chat-error-fallback")).toBe(false);
|
|
1799
|
+
});
|
|
1800
|
+
|
|
1638
1801
|
it("does not repeat the same terminal CardKit sequence while the prompt is still active", async () => {
|
|
1639
1802
|
const platform = mockPlatform("feishu");
|
|
1640
1803
|
platform.cardUpdate = vi.fn(async () => {
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
|
|
3
|
+
import { classifyTerminalError, formatTerminalErrorNotice } from "../terminal-error.ts";
|
|
4
|
+
|
|
5
|
+
describe("terminal error classification", () => {
|
|
6
|
+
it("turns a retried connection timeout into an actionable root-cause summary", () => {
|
|
7
|
+
const error = classifyTerminalError(new Error(
|
|
8
|
+
"Failed after 3 attempts. Last error: Cannot connect to API: Connect Timeout Error " +
|
|
9
|
+
"(attempted addresses: 172.19.67.251:443, 172.19.68.1:443, timeout: 10000ms)",
|
|
10
|
+
), 123);
|
|
11
|
+
|
|
12
|
+
expect(error).toEqual({
|
|
13
|
+
kind: "network_timeout",
|
|
14
|
+
title: "网络连接超时",
|
|
15
|
+
message: "连接模型服务失败,已重试 3 次,单次连接等待 10 秒。请检查网络、VPN或模型服务状态后重试。",
|
|
16
|
+
occurredAt: 123,
|
|
17
|
+
});
|
|
18
|
+
expect(JSON.stringify(error)).not.toContain("172.19.67.251");
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("redacts credentials from an otherwise unknown error", () => {
|
|
22
|
+
const error = classifyTerminalError(
|
|
23
|
+
new Error("custom provider rejected api_key=secret-value Bearer eyJ.private sk-live-secret"),
|
|
24
|
+
456,
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
expect(error.kind).toBe("unknown");
|
|
28
|
+
expect(error.message).toContain("custom provider rejected");
|
|
29
|
+
expect(error.message).not.toContain("secret-value");
|
|
30
|
+
expect(error.message).not.toContain("eyJ.private");
|
|
31
|
+
expect(error.message).not.toContain("sk-live-secret");
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("marks an existing reply as potentially incomplete", () => {
|
|
35
|
+
const error = classifyTerminalError(new Error("HTTP 429 rate limit"), 789);
|
|
36
|
+
const notice = formatTerminalErrorNotice(error, "partial answer");
|
|
37
|
+
|
|
38
|
+
expect(notice).toContain("请求受到限流");
|
|
39
|
+
expect(notice).toContain("以下回复可能不完整");
|
|
40
|
+
expect(notice).toContain("partial answer");
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it.each([
|
|
44
|
+
["HTTP 401 unauthorized", "authentication", "模型服务鉴权失败"],
|
|
45
|
+
["HTTP 429 too many requests", "rate_limit", "请求受到限流"],
|
|
46
|
+
["HTTP 503 service unavailable", "provider", "模型服务暂时不可用"],
|
|
47
|
+
["getaddrinfo ENOTFOUND api.example.test", "network", "无法连接模型服务"],
|
|
48
|
+
] as const)("classifies %s as %s", (message, kind, title) => {
|
|
49
|
+
const error = classifyTerminalError(new Error(message), 999);
|
|
50
|
+
|
|
51
|
+
expect(error.kind).toBe(kind);
|
|
52
|
+
expect(error.title).toBe(title);
|
|
53
|
+
});
|
|
54
|
+
});
|