chatccc 0.2.235 → 0.2.237
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/README.md +1 -1
- package/package.json +1 -2
- package/src/__tests__/builtin-session-search.test.ts +34 -0
- package/src/__tests__/claude-raw-stream-log.test.ts +96 -96
- package/src/__tests__/claude-sdk-installer.test.ts +285 -285
- package/src/__tests__/restart.test.ts +55 -0
- package/src/__tests__/web-ui.test.ts +436 -411
- package/src/adapters/claude-adapter.ts +673 -673
- package/src/builtin/session-search.ts +7 -0
- package/src/claude-sdk-installer.ts +324 -324
- package/src/orchestrator.ts +41 -15
- package/src/web-ui.ts +51 -5
package/README.md
CHANGED
|
@@ -230,7 +230,7 @@ CCC Agent 是 ChatCCC 内置的编程 Agent,不需要额外安装 CLI,开箱
|
|
|
230
230
|
|
|
231
231
|
#### Claude Code
|
|
232
232
|
|
|
233
|
-
ChatCCC 通过 Anthropic Claude Agent SDK 调用 Claude Code 能力。SDK 引擎(含 Claude Code CLI 原生二进制,约 220MB)**按需下载**:在首次配置向导或 Web 管理页的「Claude Code
|
|
233
|
+
ChatCCC 通过 Anthropic Claude Agent SDK 调用 Claude Code 能力。SDK 引擎(含 Claude Code CLI 原生二进制,约 220MB)**按需下载**:在首次配置向导或 Web 管理页的「Claude Code」卡片点「安装 Claude Code SDK」(开关从关闭切到打开时也会弹出安装确认),页面会显示下载进度条,完成后即可使用。**只有启用 Claude Code 的用户才需要下载**,不使用 Claude Code 的安装不包含该引擎,chatccc 主包体积保持精简。
|
|
234
234
|
|
|
235
235
|
SDK 始终使用其内置的 Claude Code CLI(档位 b),不依赖用户自行安装的 `claude` 命令。本机完成 Claude Code 登录后即可使用(SDK 复用 `~/.claude` 登录态);同时支持官方和第三方 Anthropic 兼容 API:使用官方服务无需额外配置,使用第三方 API 则填写 `claude.apiKey` 和 `claude.baseUrl`。`claude.model`、`claude.subagentModel`、`claude.effort`、`claude.apiKey`、`claude.baseUrl` 均为选填;`claude.maxTurn` 控制每次对话的最大轮数(默认 0,即无限制)。填写后会把对应配置传给 Claude Agent SDK;留空则以 `~/.claude/settings.json` 为准。
|
|
236
236
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "chatccc",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.237",
|
|
4
4
|
"description": "Feishu bot bridge for Claude Code",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -59,7 +59,6 @@
|
|
|
59
59
|
"ws": "^8.18.0"
|
|
60
60
|
},
|
|
61
61
|
"devDependencies": {
|
|
62
|
-
"@anthropic-ai/claude-agent-sdk": "0.2.133",
|
|
63
62
|
"@types/node": "^20.0.0",
|
|
64
63
|
"@types/qrcode-terminal": "^0.12.2",
|
|
65
64
|
"@types/ws": "^8.18.1",
|
|
@@ -210,6 +210,40 @@ describe("searchBuiltinSessions (raw stream logs)", () => {
|
|
|
210
210
|
expect(enabled.scannedRawLogFiles).toBe(1);
|
|
211
211
|
});
|
|
212
212
|
|
|
213
|
+
it("skips truncated/corrupt gzip files without crashing (unexpected end of file)", async () => {
|
|
214
|
+
// 护栏:线上事故——截断的 gzip 解压报 "unexpected end of file",若 error 事件
|
|
215
|
+
// 无人监听会升级为 uncaughtException 杀死整个服务。必须跳过并继续。
|
|
216
|
+
const contextDir = await mkdtemp(join(tmpdir(), "deepccc-search-raw-trunc-"));
|
|
217
|
+
const rawLogsDir = await mkdtemp(join(tmpdir(), "deepccc-search-raw-trunc-logs-"));
|
|
218
|
+
await writeContextSession(contextDir, "trunc", {
|
|
219
|
+
messages: [{ role: "user", content: "kw-trunc" }],
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
const sessionDir = join(rawLogsDir, "deepccc", "trunc");
|
|
223
|
+
await mkdir(sessionDir, { recursive: true });
|
|
224
|
+
const good = gzipSync(JSON.stringify({ type: "text-delta", text: "关键词 kw-good" }));
|
|
225
|
+
await writeFile(join(sessionDir, "2026-08-04T00-00-00-000Z-a.jsonl.gz"), good, "utf8");
|
|
226
|
+
// 截断一半:必然触发 zlib unexpected end of file
|
|
227
|
+
const truncated = gzipSync(JSON.stringify({ type: "text-delta", text: "关键词 kw-trunc-in-gzip" }));
|
|
228
|
+
await writeFile(join(sessionDir, "2026-08-04T00-00-00-000Z-b.jsonl.gz"), truncated.subarray(0, Math.floor(truncated.length / 2)), "utf8");
|
|
229
|
+
|
|
230
|
+
let output;
|
|
231
|
+
try {
|
|
232
|
+
output = await searchBuiltinSessions("kw-good", {
|
|
233
|
+
contextDir,
|
|
234
|
+
rawLogsDir,
|
|
235
|
+
includeRawLogs: true,
|
|
236
|
+
});
|
|
237
|
+
} catch (err) {
|
|
238
|
+
// 若损坏 gzip 导致 reject,这里直接断言失败并暴露错误
|
|
239
|
+
expect.fail(`searchBuiltinSessions 不应 reject: ${String(err)}`);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// 好文件正常命中;截断文件被跳过但不崩溃
|
|
243
|
+
expect(output!.matches.map((m) => m.snippet).join("\n")).toContain("kw-good");
|
|
244
|
+
expect(output!.scannedRawLogFiles).toBe(2);
|
|
245
|
+
});
|
|
246
|
+
|
|
213
247
|
it("ignores missing raw log roots", async () => {
|
|
214
248
|
const contextDir = await mkdtemp(join(tmpdir(), "deepccc-search-raw-missing-"));
|
|
215
249
|
await writeContextSession(contextDir, "s", {
|
|
@@ -1,96 +1,96 @@
|
|
|
1
|
-
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
-
|
|
3
|
-
const resumeSessionMock = vi.hoisted(() => vi.fn());
|
|
4
|
-
const createRawStreamLogMock = vi.hoisted(() => vi.fn());
|
|
5
|
-
const rawLogWriteLineMock = vi.hoisted(() => vi.fn());
|
|
6
|
-
const rawLogCloseMock = vi.hoisted(() => vi.fn());
|
|
7
|
-
|
|
8
|
-
// 注意:SDK 已从 chatccc 依赖中移除,改为按需安装 + 动态 import(变量路径),
|
|
9
|
-
// vi.mock 无法拦截;这里通过 __setClaudeSdkModuleForTest 注入假 SDK 模块。
|
|
10
|
-
|
|
11
|
-
vi.mock("../adapters/raw-stream-log.ts", () => ({
|
|
12
|
-
createRawStreamLog: createRawStreamLogMock,
|
|
13
|
-
}));
|
|
14
|
-
|
|
15
|
-
import { config } from "../config.ts";
|
|
16
|
-
import {
|
|
17
|
-
__setClaudeSdkModuleForTest,
|
|
18
|
-
createClaudeAdapter,
|
|
19
|
-
} from "../adapters/claude-adapter.ts";
|
|
20
|
-
|
|
21
|
-
const originalRawStreamLogs = structuredClone(config.rawStreamLogs);
|
|
22
|
-
|
|
23
|
-
async function collect(iterable: AsyncIterable<unknown>): Promise<unknown[]> {
|
|
24
|
-
const events: unknown[] = [];
|
|
25
|
-
for await (const event of iterable) events.push(event);
|
|
26
|
-
return events;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
beforeEach(() => {
|
|
30
|
-
__setClaudeSdkModuleForTest({
|
|
31
|
-
getSessionInfo: vi.fn(),
|
|
32
|
-
unstable_v2_createSession: vi.fn(),
|
|
33
|
-
unstable_v2_resumeSession: resumeSessionMock,
|
|
34
|
-
});
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
afterEach(() => {
|
|
38
|
-
__setClaudeSdkModuleForTest(null);
|
|
39
|
-
resumeSessionMock.mockReset();
|
|
40
|
-
createRawStreamLogMock.mockReset();
|
|
41
|
-
rawLogWriteLineMock.mockReset();
|
|
42
|
-
rawLogCloseMock.mockReset();
|
|
43
|
-
config.rawStreamLogs = structuredClone(originalRawStreamLogs);
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
describe("Claude raw stream logs", () => {
|
|
47
|
-
it("writes raw Claude SDK stream messages when enabled", async () => {
|
|
48
|
-
const rawMessages = [
|
|
49
|
-
{ type: "system", subtype: "init", session_id: "sid-raw", cwd: "F:/project", model: "claude-test" },
|
|
50
|
-
{ type: "assistant", message: { content: [{ type: "text", text: "hello" }] } },
|
|
51
|
-
];
|
|
52
|
-
const session = {
|
|
53
|
-
send: vi.fn(async () => {}),
|
|
54
|
-
stream: async function* () {
|
|
55
|
-
for (const msg of rawMessages) yield msg;
|
|
56
|
-
},
|
|
57
|
-
close: vi.fn(),
|
|
58
|
-
};
|
|
59
|
-
resumeSessionMock.mockReturnValueOnce(session);
|
|
60
|
-
createRawStreamLogMock.mockResolvedValueOnce({
|
|
61
|
-
filePath: "claude.jsonl.gz",
|
|
62
|
-
writeLine: rawLogWriteLineMock,
|
|
63
|
-
close: rawLogCloseMock,
|
|
64
|
-
});
|
|
65
|
-
config.rawStreamLogs.claude = {
|
|
66
|
-
enabled: true,
|
|
67
|
-
maxBytesPerTurn: 4096,
|
|
68
|
-
retentionDays: 5,
|
|
69
|
-
keepCompleted: false,
|
|
70
|
-
};
|
|
71
|
-
|
|
72
|
-
const adapter = createClaudeAdapter({
|
|
73
|
-
model: "claude-test",
|
|
74
|
-
effort: "high",
|
|
75
|
-
isEmpty: (value) => value.trim() === "",
|
|
76
|
-
});
|
|
77
|
-
const events = await collect(adapter.prompt("sid-raw", "hi", "F:/project"));
|
|
78
|
-
|
|
79
|
-
expect(createRawStreamLogMock).toHaveBeenCalledWith(expect.objectContaining({
|
|
80
|
-
enabled: true,
|
|
81
|
-
tool: "claude",
|
|
82
|
-
sessionId: "sid-raw",
|
|
83
|
-
label: "prompt",
|
|
84
|
-
maxBytesPerTurn: 4096,
|
|
85
|
-
retentionDays: 5,
|
|
86
|
-
}));
|
|
87
|
-
expect(rawLogWriteLineMock).toHaveBeenNthCalledWith(1, JSON.stringify(rawMessages[0]));
|
|
88
|
-
expect(rawLogWriteLineMock).toHaveBeenNthCalledWith(2, JSON.stringify(rawMessages[1]));
|
|
89
|
-
expect(rawLogCloseMock).toHaveBeenCalledWith({ keep: false });
|
|
90
|
-
expect(session.close).toHaveBeenCalledOnce();
|
|
91
|
-
expect(events).toContainEqual({
|
|
92
|
-
type: "assistant",
|
|
93
|
-
blocks: [{ type: "text", text: "hello" }],
|
|
94
|
-
});
|
|
95
|
-
});
|
|
96
|
-
});
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
const resumeSessionMock = vi.hoisted(() => vi.fn());
|
|
4
|
+
const createRawStreamLogMock = vi.hoisted(() => vi.fn());
|
|
5
|
+
const rawLogWriteLineMock = vi.hoisted(() => vi.fn());
|
|
6
|
+
const rawLogCloseMock = vi.hoisted(() => vi.fn());
|
|
7
|
+
|
|
8
|
+
// 注意:SDK 已从 chatccc 依赖中移除,改为按需安装 + 动态 import(变量路径),
|
|
9
|
+
// vi.mock 无法拦截;这里通过 __setClaudeSdkModuleForTest 注入假 SDK 模块。
|
|
10
|
+
|
|
11
|
+
vi.mock("../adapters/raw-stream-log.ts", () => ({
|
|
12
|
+
createRawStreamLog: createRawStreamLogMock,
|
|
13
|
+
}));
|
|
14
|
+
|
|
15
|
+
import { config } from "../config.ts";
|
|
16
|
+
import {
|
|
17
|
+
__setClaudeSdkModuleForTest,
|
|
18
|
+
createClaudeAdapter,
|
|
19
|
+
} from "../adapters/claude-adapter.ts";
|
|
20
|
+
|
|
21
|
+
const originalRawStreamLogs = structuredClone(config.rawStreamLogs);
|
|
22
|
+
|
|
23
|
+
async function collect(iterable: AsyncIterable<unknown>): Promise<unknown[]> {
|
|
24
|
+
const events: unknown[] = [];
|
|
25
|
+
for await (const event of iterable) events.push(event);
|
|
26
|
+
return events;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
beforeEach(() => {
|
|
30
|
+
__setClaudeSdkModuleForTest({
|
|
31
|
+
getSessionInfo: vi.fn(),
|
|
32
|
+
unstable_v2_createSession: vi.fn(),
|
|
33
|
+
unstable_v2_resumeSession: resumeSessionMock,
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
afterEach(() => {
|
|
38
|
+
__setClaudeSdkModuleForTest(null);
|
|
39
|
+
resumeSessionMock.mockReset();
|
|
40
|
+
createRawStreamLogMock.mockReset();
|
|
41
|
+
rawLogWriteLineMock.mockReset();
|
|
42
|
+
rawLogCloseMock.mockReset();
|
|
43
|
+
config.rawStreamLogs = structuredClone(originalRawStreamLogs);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
describe("Claude raw stream logs", () => {
|
|
47
|
+
it("writes raw Claude SDK stream messages when enabled", async () => {
|
|
48
|
+
const rawMessages = [
|
|
49
|
+
{ type: "system", subtype: "init", session_id: "sid-raw", cwd: "F:/project", model: "claude-test" },
|
|
50
|
+
{ type: "assistant", message: { content: [{ type: "text", text: "hello" }] } },
|
|
51
|
+
];
|
|
52
|
+
const session = {
|
|
53
|
+
send: vi.fn(async () => {}),
|
|
54
|
+
stream: async function* () {
|
|
55
|
+
for (const msg of rawMessages) yield msg;
|
|
56
|
+
},
|
|
57
|
+
close: vi.fn(),
|
|
58
|
+
};
|
|
59
|
+
resumeSessionMock.mockReturnValueOnce(session);
|
|
60
|
+
createRawStreamLogMock.mockResolvedValueOnce({
|
|
61
|
+
filePath: "claude.jsonl.gz",
|
|
62
|
+
writeLine: rawLogWriteLineMock,
|
|
63
|
+
close: rawLogCloseMock,
|
|
64
|
+
});
|
|
65
|
+
config.rawStreamLogs.claude = {
|
|
66
|
+
enabled: true,
|
|
67
|
+
maxBytesPerTurn: 4096,
|
|
68
|
+
retentionDays: 5,
|
|
69
|
+
keepCompleted: false,
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const adapter = createClaudeAdapter({
|
|
73
|
+
model: "claude-test",
|
|
74
|
+
effort: "high",
|
|
75
|
+
isEmpty: (value) => value.trim() === "",
|
|
76
|
+
});
|
|
77
|
+
const events = await collect(adapter.prompt("sid-raw", "hi", "F:/project"));
|
|
78
|
+
|
|
79
|
+
expect(createRawStreamLogMock).toHaveBeenCalledWith(expect.objectContaining({
|
|
80
|
+
enabled: true,
|
|
81
|
+
tool: "claude",
|
|
82
|
+
sessionId: "sid-raw",
|
|
83
|
+
label: "prompt",
|
|
84
|
+
maxBytesPerTurn: 4096,
|
|
85
|
+
retentionDays: 5,
|
|
86
|
+
}));
|
|
87
|
+
expect(rawLogWriteLineMock).toHaveBeenNthCalledWith(1, JSON.stringify(rawMessages[0]));
|
|
88
|
+
expect(rawLogWriteLineMock).toHaveBeenNthCalledWith(2, JSON.stringify(rawMessages[1]));
|
|
89
|
+
expect(rawLogCloseMock).toHaveBeenCalledWith({ keep: false });
|
|
90
|
+
expect(session.close).toHaveBeenCalledOnce();
|
|
91
|
+
expect(events).toContainEqual({
|
|
92
|
+
type: "assistant",
|
|
93
|
+
blocks: [{ type: "text", text: "hello" }],
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
});
|