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.
- package/.agents/skills/create-chatccc-feishu-app/SKILL.md +85 -85
- package/.claude/skills/create-chatccc-feishu-app/SKILL.md +85 -85
- package/.cursor/skills/create-chatccc-feishu-app/SKILL.md +85 -85
- package/README.md +90 -90
- package/agent-prompts/claude_specific.md +45 -45
- package/agent-prompts/codex_specific.md +2 -2
- package/agent-prompts/cursor_specific.md +13 -13
- package/bin/cccagent.mjs +17 -17
- package/im-skills/feishu-skill/receive-send-file.md +63 -63
- package/im-skills/feishu-skill/receive-send-image.md +24 -24
- package/im-skills/feishu-skill/skill.md +3 -3
- package/im-skills/wechat-file-skill/receive-send-file.md +38 -38
- package/im-skills/wechat-file-skill/send-file.mjs +83 -83
- package/im-skills/wechat-file-skill/skill.md +10 -10
- package/im-skills/wechat-image-skill/skill.md +10 -10
- package/im-skills/wechat-video-skill/receive-send-video.md +38 -38
- package/im-skills/wechat-video-skill/send-video.mjs +79 -79
- package/im-skills/wechat-video-skill/skill.md +10 -10
- package/package.json +1 -1
- package/scripts/postinstall-sharp-check.mjs +58 -58
- package/src/__tests__/agent-activity.test.ts +86 -76
- package/src/__tests__/agent-delegate-task-rpc.test.ts +165 -165
- package/src/__tests__/agent-reload-config-rpc.test.ts +99 -99
- package/src/__tests__/builtin-chat-session.test.ts +65 -1
- package/src/__tests__/builtin-cli-json.test.ts +39 -39
- package/src/__tests__/builtin-config.test.ts +26 -26
- package/src/__tests__/builtin-context.test.ts +202 -163
- package/src/__tests__/builtin-permissions.test.ts +211 -211
- package/src/__tests__/builtin-session-select.test.ts +116 -116
- package/src/__tests__/builtin-sigint.test.ts +56 -56
- package/src/__tests__/builtin-web-tools.test.ts +220 -220
- package/src/__tests__/card-action-routing.test.ts +18 -18
- package/src/__tests__/cardkit.test.ts +60 -60
- package/src/__tests__/ccc-adapter.test.ts +170 -136
- package/src/__tests__/chatgpt-subscription-rpc.test.ts +89 -89
- package/src/__tests__/chatgpt-subscription.test.ts +135 -135
- package/src/__tests__/chrome-devtools-guard.test.ts +165 -165
- package/src/__tests__/claude-adapter.test.ts +614 -614
- package/src/__tests__/claude-raw-stream-log.test.ts +87 -87
- package/src/__tests__/codex-adapter.test.ts +58 -58
- package/src/__tests__/codex-raw-stream-log.test.ts +170 -170
- package/src/__tests__/codex-reset-actions.test.ts +146 -146
- package/src/__tests__/cursor-adapter.test.ts +268 -268
- package/src/__tests__/feishu-api.test.ts +60 -60
- package/src/__tests__/feishu-avatar.test.ts +164 -164
- package/src/__tests__/feishu-message-ingress.test.ts +138 -138
- package/src/__tests__/feishu-platform.test.ts +22 -22
- package/src/__tests__/format-message.test.ts +47 -47
- package/src/__tests__/jsonl-stream.test.ts +79 -79
- package/src/__tests__/package-files.test.ts +24 -24
- package/src/__tests__/privacy.test.ts +198 -198
- package/src/__tests__/progress-reducer.test.ts +121 -110
- package/src/__tests__/raw-stream-log.test.ts +106 -106
- package/src/__tests__/response-stall.test.ts +49 -49
- package/src/__tests__/session.test.ts +174 -16
- package/src/__tests__/shared-prefix.test.ts +36 -36
- package/src/__tests__/sim-platform.test.ts +16 -16
- package/src/__tests__/startup-lifecycle.test.ts +231 -231
- package/src/__tests__/stop-session.test.ts +34 -34
- package/src/__tests__/stream-state.test.ts +42 -42
- package/src/__tests__/terminal-error.test.ts +54 -0
- package/src/__tests__/terminal-renderer.test.ts +247 -247
- package/src/__tests__/update-command-guard.test.ts +144 -144
- package/src/__tests__/web-ui.test.ts +326 -326
- package/src/adapters/adapter-interface.ts +24 -18
- package/src/adapters/ccc-adapter.ts +141 -131
- package/src/adapters/claude-adapter.ts +620 -620
- package/src/adapters/claude-session-meta-store.ts +120 -120
- package/src/adapters/codex-adapter.ts +426 -426
- package/src/adapters/cursor-adapter.ts +681 -681
- package/src/adapters/jsonl-stream.ts +157 -157
- package/src/adapters/raw-stream-log.ts +124 -124
- package/src/adapters/resource-monitor.ts +140 -140
- package/src/agent-activity.ts +175 -170
- package/src/agent-delegate-task-rpc.ts +153 -153
- package/src/agent-delegate-task.ts +91 -91
- package/src/agent-reload-config-rpc.ts +34 -34
- package/src/agent-stop-stuck.ts +129 -129
- package/src/builtin/cli.ts +9 -2
- package/src/builtin/config.ts +84 -84
- package/src/builtin/context.ts +62 -4
- package/src/builtin/file-log.ts +38 -38
- package/src/builtin/index.ts +83 -19
- package/src/builtin/proc-tree-kill.ts +61 -61
- package/src/builtin/progress/cards-helpers.ts +76 -76
- package/src/builtin/progress/reducer.ts +113 -108
- package/src/builtin/progress/terminal-renderer.ts +294 -294
- package/src/builtin/progress/view.ts +77 -77
- package/src/builtin/raw-stream-log.ts +124 -124
- package/src/builtin/session-select.ts +48 -48
- package/src/builtin/sigint.ts +50 -50
- package/src/builtin/web-tools.ts +313 -313
- package/src/card-action-routing.ts +14 -14
- package/src/chatgpt-subscription-rpc.ts +27 -27
- package/src/chatgpt-subscription.ts +299 -299
- package/src/chrome-devtools-guard.ts +318 -318
- package/src/codex-reset-actions.ts +184 -184
- package/src/feishu-api.ts +193 -193
- package/src/feishu-message-ingress.ts +195 -195
- package/src/feishu-platform.ts +20 -20
- package/src/format-message.ts +293 -293
- package/src/litellm-proxy.ts +374 -374
- package/src/orchestrator.ts +8 -3
- package/src/platform-adapter.ts +6 -6
- package/src/privacy.ts +118 -118
- package/src/progress/reducer.ts +113 -108
- package/src/progress/terminal-renderer.ts +294 -294
- package/src/progress/view.ts +77 -77
- package/src/response-stall.ts +28 -28
- package/src/runtime-reload.ts +34 -34
- package/src/session-chat-binding.ts +82 -82
- package/src/session-name.ts +8 -8
- package/src/session.ts +149 -26
- package/src/shared-prefix.ts +29 -29
- package/src/sim-platform.ts +20 -20
- package/src/startup-lifecycle.ts +250 -250
- package/src/stream-state.ts +21 -18
- package/src/terminal-error.ts +129 -0
- package/src/turn-cards.ts +117 -117
- package/src/update-command-guard.ts +165 -165
|
@@ -1,106 +1,106 @@
|
|
|
1
|
-
import { gunzipSync } from "node:zlib";
|
|
2
|
-
import { mkdtemp, readFile, rm, stat, writeFile } from "node:fs/promises";
|
|
3
|
-
import { join } from "node:path";
|
|
4
|
-
import { tmpdir } from "node:os";
|
|
5
|
-
|
|
6
|
-
import { describe, expect, it } from "vitest";
|
|
7
|
-
|
|
8
|
-
import {
|
|
9
|
-
createRawStreamLog,
|
|
10
|
-
sanitizeLogPathSegment,
|
|
11
|
-
} from "../adapters/raw-stream-log.ts";
|
|
12
|
-
|
|
13
|
-
describe("raw stream log", () => {
|
|
14
|
-
it("does nothing when disabled", async () => {
|
|
15
|
-
const root = await mkdtemp(join(tmpdir(), "chatccc-raw-log-"));
|
|
16
|
-
try {
|
|
17
|
-
const log = await createRawStreamLog({
|
|
18
|
-
enabled: false,
|
|
19
|
-
rootDir: root,
|
|
20
|
-
tool: "cursor",
|
|
21
|
-
sessionId: "sid",
|
|
22
|
-
label: "turn",
|
|
23
|
-
maxBytesPerTurn: 1024,
|
|
24
|
-
retentionDays: 7,
|
|
25
|
-
});
|
|
26
|
-
expect(log).toBeNull();
|
|
27
|
-
} finally {
|
|
28
|
-
await rm(root, { recursive: true, force: true });
|
|
29
|
-
}
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
it("writes gzipped JSONL and can keep the file", async () => {
|
|
33
|
-
const root = await mkdtemp(join(tmpdir(), "chatccc-raw-log-"));
|
|
34
|
-
try {
|
|
35
|
-
const log = await createRawStreamLog({
|
|
36
|
-
enabled: true,
|
|
37
|
-
rootDir: root,
|
|
38
|
-
tool: "cursor",
|
|
39
|
-
sessionId: "sid/unsafe",
|
|
40
|
-
label: "turn:1",
|
|
41
|
-
maxBytesPerTurn: 1024,
|
|
42
|
-
retentionDays: 7,
|
|
43
|
-
});
|
|
44
|
-
expect(log).not.toBeNull();
|
|
45
|
-
log!.writeLine('{"type":"assistant","text":"hello"}');
|
|
46
|
-
await log!.close({ keep: true });
|
|
47
|
-
|
|
48
|
-
const raw = gunzipSync(await readFile(log!.filePath)).toString("utf-8");
|
|
49
|
-
expect(raw).toBe('{"type":"assistant","text":"hello"}\n');
|
|
50
|
-
expect(await stat(log!.filePath)).toBeTruthy();
|
|
51
|
-
} finally {
|
|
52
|
-
await rm(root, { recursive: true, force: true });
|
|
53
|
-
}
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
it("writes a truncation marker once maxBytesPerTurn is exceeded", async () => {
|
|
57
|
-
const root = await mkdtemp(join(tmpdir(), "chatccc-raw-log-"));
|
|
58
|
-
try {
|
|
59
|
-
const log = await createRawStreamLog({
|
|
60
|
-
enabled: true,
|
|
61
|
-
rootDir: root,
|
|
62
|
-
tool: "cursor",
|
|
63
|
-
sessionId: "sid",
|
|
64
|
-
label: "turn",
|
|
65
|
-
maxBytesPerTurn: 12,
|
|
66
|
-
retentionDays: 7,
|
|
67
|
-
});
|
|
68
|
-
log!.writeLine('{"a":1}');
|
|
69
|
-
log!.writeLine('{"too":"large"}');
|
|
70
|
-
log!.writeLine('{"ignored":true}');
|
|
71
|
-
await log!.close({ keep: true });
|
|
72
|
-
|
|
73
|
-
const raw = gunzipSync(await readFile(log!.filePath)).toString("utf-8");
|
|
74
|
-
expect(raw).toContain('{"a":1}');
|
|
75
|
-
expect(raw).toContain("chatccc_raw_stream_log_truncated");
|
|
76
|
-
expect(raw).not.toContain("ignored");
|
|
77
|
-
} finally {
|
|
78
|
-
await rm(root, { recursive: true, force: true });
|
|
79
|
-
}
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
it("removes the file when keep is false", async () => {
|
|
83
|
-
const root = await mkdtemp(join(tmpdir(), "chatccc-raw-log-"));
|
|
84
|
-
try {
|
|
85
|
-
const log = await createRawStreamLog({
|
|
86
|
-
enabled: true,
|
|
87
|
-
rootDir: root,
|
|
88
|
-
tool: "cursor",
|
|
89
|
-
sessionId: "sid",
|
|
90
|
-
label: "turn",
|
|
91
|
-
maxBytesPerTurn: 1024,
|
|
92
|
-
retentionDays: 7,
|
|
93
|
-
});
|
|
94
|
-
log!.writeLine('{"type":"result"}');
|
|
95
|
-
await log!.close({ keep: false });
|
|
96
|
-
await expect(stat(log!.filePath)).rejects.toThrow();
|
|
97
|
-
} finally {
|
|
98
|
-
await rm(root, { recursive: true, force: true });
|
|
99
|
-
}
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
it("sanitizes path segments", () => {
|
|
103
|
-
expect(sanitizeLogPathSegment("../sid:1/2")).toBe("sid_1_2");
|
|
104
|
-
expect(sanitizeLogPathSegment("")).toBe("unknown");
|
|
105
|
-
});
|
|
106
|
-
});
|
|
1
|
+
import { gunzipSync } from "node:zlib";
|
|
2
|
+
import { mkdtemp, readFile, rm, stat, writeFile } from "node:fs/promises";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
|
|
6
|
+
import { describe, expect, it } from "vitest";
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
createRawStreamLog,
|
|
10
|
+
sanitizeLogPathSegment,
|
|
11
|
+
} from "../adapters/raw-stream-log.ts";
|
|
12
|
+
|
|
13
|
+
describe("raw stream log", () => {
|
|
14
|
+
it("does nothing when disabled", async () => {
|
|
15
|
+
const root = await mkdtemp(join(tmpdir(), "chatccc-raw-log-"));
|
|
16
|
+
try {
|
|
17
|
+
const log = await createRawStreamLog({
|
|
18
|
+
enabled: false,
|
|
19
|
+
rootDir: root,
|
|
20
|
+
tool: "cursor",
|
|
21
|
+
sessionId: "sid",
|
|
22
|
+
label: "turn",
|
|
23
|
+
maxBytesPerTurn: 1024,
|
|
24
|
+
retentionDays: 7,
|
|
25
|
+
});
|
|
26
|
+
expect(log).toBeNull();
|
|
27
|
+
} finally {
|
|
28
|
+
await rm(root, { recursive: true, force: true });
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("writes gzipped JSONL and can keep the file", async () => {
|
|
33
|
+
const root = await mkdtemp(join(tmpdir(), "chatccc-raw-log-"));
|
|
34
|
+
try {
|
|
35
|
+
const log = await createRawStreamLog({
|
|
36
|
+
enabled: true,
|
|
37
|
+
rootDir: root,
|
|
38
|
+
tool: "cursor",
|
|
39
|
+
sessionId: "sid/unsafe",
|
|
40
|
+
label: "turn:1",
|
|
41
|
+
maxBytesPerTurn: 1024,
|
|
42
|
+
retentionDays: 7,
|
|
43
|
+
});
|
|
44
|
+
expect(log).not.toBeNull();
|
|
45
|
+
log!.writeLine('{"type":"assistant","text":"hello"}');
|
|
46
|
+
await log!.close({ keep: true });
|
|
47
|
+
|
|
48
|
+
const raw = gunzipSync(await readFile(log!.filePath)).toString("utf-8");
|
|
49
|
+
expect(raw).toBe('{"type":"assistant","text":"hello"}\n');
|
|
50
|
+
expect(await stat(log!.filePath)).toBeTruthy();
|
|
51
|
+
} finally {
|
|
52
|
+
await rm(root, { recursive: true, force: true });
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("writes a truncation marker once maxBytesPerTurn is exceeded", async () => {
|
|
57
|
+
const root = await mkdtemp(join(tmpdir(), "chatccc-raw-log-"));
|
|
58
|
+
try {
|
|
59
|
+
const log = await createRawStreamLog({
|
|
60
|
+
enabled: true,
|
|
61
|
+
rootDir: root,
|
|
62
|
+
tool: "cursor",
|
|
63
|
+
sessionId: "sid",
|
|
64
|
+
label: "turn",
|
|
65
|
+
maxBytesPerTurn: 12,
|
|
66
|
+
retentionDays: 7,
|
|
67
|
+
});
|
|
68
|
+
log!.writeLine('{"a":1}');
|
|
69
|
+
log!.writeLine('{"too":"large"}');
|
|
70
|
+
log!.writeLine('{"ignored":true}');
|
|
71
|
+
await log!.close({ keep: true });
|
|
72
|
+
|
|
73
|
+
const raw = gunzipSync(await readFile(log!.filePath)).toString("utf-8");
|
|
74
|
+
expect(raw).toContain('{"a":1}');
|
|
75
|
+
expect(raw).toContain("chatccc_raw_stream_log_truncated");
|
|
76
|
+
expect(raw).not.toContain("ignored");
|
|
77
|
+
} finally {
|
|
78
|
+
await rm(root, { recursive: true, force: true });
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("removes the file when keep is false", async () => {
|
|
83
|
+
const root = await mkdtemp(join(tmpdir(), "chatccc-raw-log-"));
|
|
84
|
+
try {
|
|
85
|
+
const log = await createRawStreamLog({
|
|
86
|
+
enabled: true,
|
|
87
|
+
rootDir: root,
|
|
88
|
+
tool: "cursor",
|
|
89
|
+
sessionId: "sid",
|
|
90
|
+
label: "turn",
|
|
91
|
+
maxBytesPerTurn: 1024,
|
|
92
|
+
retentionDays: 7,
|
|
93
|
+
});
|
|
94
|
+
log!.writeLine('{"type":"result"}');
|
|
95
|
+
await log!.close({ keep: false });
|
|
96
|
+
await expect(stat(log!.filePath)).rejects.toThrow();
|
|
97
|
+
} finally {
|
|
98
|
+
await rm(root, { recursive: true, force: true });
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("sanitizes path segments", () => {
|
|
103
|
+
expect(sanitizeLogPathSegment("../sid:1/2")).toBe("sid_1_2");
|
|
104
|
+
expect(sanitizeLogPathSegment("")).toBe("unknown");
|
|
105
|
+
});
|
|
106
|
+
});
|
|
@@ -1,49 +1,49 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest";
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
hasResponseStalled,
|
|
5
|
-
observeResponseProgress,
|
|
6
|
-
} from "../response-stall.ts";
|
|
7
|
-
|
|
8
|
-
describe("response stall detection", () => {
|
|
9
|
-
it("starts tracking while responding even when the total character count is zero", () => {
|
|
10
|
-
const observation = observeResponseProgress(undefined, true, 0, 1_000);
|
|
11
|
-
|
|
12
|
-
expect(observation).toEqual({
|
|
13
|
-
totalChars: 0,
|
|
14
|
-
unchangedSince: 1_000,
|
|
15
|
-
});
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
it("auto-ends only after the same character count has lasted three minutes", () => {
|
|
19
|
-
const first = observeResponseProgress(undefined, true, 12, 1_000);
|
|
20
|
-
const unchanged = observeResponseProgress(first, true, 12, 90_000);
|
|
21
|
-
|
|
22
|
-
expect(unchanged).toBe(first);
|
|
23
|
-
expect(hasResponseStalled(unchanged, 180_999, 180_000)).toBe(false);
|
|
24
|
-
expect(hasResponseStalled(unchanged, 181_000, 180_000)).toBe(true);
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
it("restarts the timer whenever the total character count changes", () => {
|
|
28
|
-
const first = observeResponseProgress(undefined, true, 12, 1_000);
|
|
29
|
-
const changed = observeResponseProgress(first, true, 13, 150_000);
|
|
30
|
-
|
|
31
|
-
expect(changed).toEqual({
|
|
32
|
-
totalChars: 13,
|
|
33
|
-
unchangedSince: 150_000,
|
|
34
|
-
});
|
|
35
|
-
expect(hasResponseStalled(changed, 181_000, 180_000)).toBe(false);
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
it("clears tracking outside responding and starts a fresh window after returning", () => {
|
|
39
|
-
const first = observeResponseProgress(undefined, true, 0, 1_000);
|
|
40
|
-
const cleared = observeResponseProgress(first, false, 0, 100_000);
|
|
41
|
-
const resumed = observeResponseProgress(cleared, true, 0, 200_000);
|
|
42
|
-
|
|
43
|
-
expect(cleared).toBeUndefined();
|
|
44
|
-
expect(resumed).toEqual({
|
|
45
|
-
totalChars: 0,
|
|
46
|
-
unchangedSince: 200_000,
|
|
47
|
-
});
|
|
48
|
-
});
|
|
49
|
-
});
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
hasResponseStalled,
|
|
5
|
+
observeResponseProgress,
|
|
6
|
+
} from "../response-stall.ts";
|
|
7
|
+
|
|
8
|
+
describe("response stall detection", () => {
|
|
9
|
+
it("starts tracking while responding even when the total character count is zero", () => {
|
|
10
|
+
const observation = observeResponseProgress(undefined, true, 0, 1_000);
|
|
11
|
+
|
|
12
|
+
expect(observation).toEqual({
|
|
13
|
+
totalChars: 0,
|
|
14
|
+
unchangedSince: 1_000,
|
|
15
|
+
});
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("auto-ends only after the same character count has lasted three minutes", () => {
|
|
19
|
+
const first = observeResponseProgress(undefined, true, 12, 1_000);
|
|
20
|
+
const unchanged = observeResponseProgress(first, true, 12, 90_000);
|
|
21
|
+
|
|
22
|
+
expect(unchanged).toBe(first);
|
|
23
|
+
expect(hasResponseStalled(unchanged, 180_999, 180_000)).toBe(false);
|
|
24
|
+
expect(hasResponseStalled(unchanged, 181_000, 180_000)).toBe(true);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("restarts the timer whenever the total character count changes", () => {
|
|
28
|
+
const first = observeResponseProgress(undefined, true, 12, 1_000);
|
|
29
|
+
const changed = observeResponseProgress(first, true, 13, 150_000);
|
|
30
|
+
|
|
31
|
+
expect(changed).toEqual({
|
|
32
|
+
totalChars: 13,
|
|
33
|
+
unchangedSince: 150_000,
|
|
34
|
+
});
|
|
35
|
+
expect(hasResponseStalled(changed, 181_000, 180_000)).toBe(false);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("clears tracking outside responding and starts a fresh window after returning", () => {
|
|
39
|
+
const first = observeResponseProgress(undefined, true, 0, 1_000);
|
|
40
|
+
const cleared = observeResponseProgress(first, false, 0, 100_000);
|
|
41
|
+
const resumed = observeResponseProgress(cleared, true, 0, 200_000);
|
|
42
|
+
|
|
43
|
+
expect(cleared).toBeUndefined();
|
|
44
|
+
expect(resumed).toEqual({
|
|
45
|
+
totalChars: 0,
|
|
46
|
+
unchangedSince: 200_000,
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
});
|
|
@@ -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");
|
|
@@ -848,7 +907,7 @@ describe("runAgentSession response stall watchdog", () => {
|
|
|
848
907
|
});
|
|
849
908
|
});
|
|
850
909
|
|
|
851
|
-
it("
|
|
910
|
+
it("does not apply the reply-stall timeout while an Agent is compacting context", async () => {
|
|
852
911
|
vi.setSystemTime(0);
|
|
853
912
|
_setResponseStallTimeoutForTest(180_000);
|
|
854
913
|
_setResponseStallCheckIntervalForTest(1_000);
|
|
@@ -875,6 +934,7 @@ describe("runAgentSession response stall watchdog", () => {
|
|
|
875
934
|
) {
|
|
876
935
|
options?.onSessionCreated?.(closeSession);
|
|
877
936
|
options?.onProcessStart?.({ pid: 4343 });
|
|
937
|
+
yield { type: "assistant", blocks: [{ type: "agent_status", status: "compacting" }] };
|
|
878
938
|
await new Promise<void>((resolve) => {
|
|
879
939
|
if (signal?.aborted) {
|
|
880
940
|
resolve();
|
|
@@ -899,11 +959,8 @@ describe("runAgentSession response stall watchdog", () => {
|
|
|
899
959
|
await vi.waitFor(() => {
|
|
900
960
|
expect(activePrompts.get("sid-starting-stall")).toMatchObject({
|
|
901
961
|
processPid: 4343,
|
|
902
|
-
responseProgress: {
|
|
903
|
-
totalChars: 0,
|
|
904
|
-
unchangedSince: expect.any(Number),
|
|
905
|
-
},
|
|
906
962
|
});
|
|
963
|
+
expect(activePrompts.get("sid-starting-stall")?.responseProgress).toBeUndefined();
|
|
907
964
|
expect(closeSession).toHaveBeenCalledTimes(0);
|
|
908
965
|
});
|
|
909
966
|
|
|
@@ -915,19 +972,16 @@ describe("runAgentSession response stall watchdog", () => {
|
|
|
915
972
|
.length;
|
|
916
973
|
|
|
917
974
|
// 旧实现不会结束真正的零事件启动;先清理测试会话,避免失败用例悬挂。
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
await vi.advanceTimersByTimeAsync(1_000);
|
|
921
|
-
}
|
|
975
|
+
stopSession("sid-starting-stall");
|
|
976
|
+
await vi.advanceTimersByTimeAsync(1_000);
|
|
922
977
|
await runPromise;
|
|
923
978
|
|
|
924
979
|
expect(stateAfterDeadline).toMatchObject({
|
|
925
|
-
status: "
|
|
926
|
-
|
|
927
|
-
autoEndedAt: expect.any(Number),
|
|
980
|
+
status: "running",
|
|
981
|
+
activity: { kind: "compacting" },
|
|
928
982
|
});
|
|
929
|
-
expect(closeCountAfterDeadline).toBe(
|
|
930
|
-
expect(treeKillCountAfterDeadline).
|
|
983
|
+
expect(closeCountAfterDeadline).toBe(0);
|
|
984
|
+
expect(treeKillCountAfterDeadline).toBe(0);
|
|
931
985
|
});
|
|
932
986
|
|
|
933
987
|
it("self-heals a missing trigger-chat binding and gives automatic recovery the normal card lifecycle", async () => {
|
|
@@ -1371,7 +1425,7 @@ describe("runAgentSession response stall watchdog", () => {
|
|
|
1371
1425
|
expect(receivedPrompts[1]).toContain(recoveryPrompt);
|
|
1372
1426
|
expect(platform.sendText).toHaveBeenCalledWith(
|
|
1373
1427
|
"chat-recovery-limit",
|
|
1374
|
-
"⚠️ 自动续跑仍连续 3
|
|
1428
|
+
"⚠️ 自动续跑仍连续 3 分钟没有生成新回复,本次不再自动继续。",
|
|
1375
1429
|
);
|
|
1376
1430
|
});
|
|
1377
1431
|
|
|
@@ -1630,11 +1684,115 @@ describe("unified display loop terminal card update", () => {
|
|
|
1630
1684
|
expect(card.header.template).toBe("orange");
|
|
1631
1685
|
expect(platform.sendText).toHaveBeenCalledWith(
|
|
1632
1686
|
"chat-auto-ended",
|
|
1633
|
-
"⚠️
|
|
1687
|
+
"⚠️ 已自动结束:生成回复阶段连续 3 分钟没有字符变化。本轮没有可发送的回复内容。",
|
|
1634
1688
|
);
|
|
1635
1689
|
expect(displayCards.has("chat-auto-ended")).toBe(false);
|
|
1636
1690
|
});
|
|
1637
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
|
+
|
|
1638
1796
|
it("does not repeat the same terminal CardKit sequence while the prompt is still active", async () => {
|
|
1639
1797
|
const platform = mockPlatform("feishu");
|
|
1640
1798
|
platform.cardUpdate = vi.fn(async () => {
|
|
@@ -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
|
+
});
|