chatccc 0.2.54 → 0.2.56
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 +3 -1
- package/config.sample.json +2 -1
- package/package.json +59 -59
- package/src/__tests__/claude-adapter.test.ts +39 -0
- package/src/__tests__/config-reload.test.ts +7 -3
- package/src/__tests__/config-sample.test.ts +11 -0
- package/src/__tests__/session.test.ts +8 -6
- package/src/__tests__/simplify.test.ts +283 -0
- package/src/__tests__/web-ui.test.ts +17 -0
- package/src/adapters/adapter-interface.ts +2 -0
- package/src/adapters/claude-adapter.ts +28 -6
- package/src/adapters/codex-adapter.ts +4 -0
- package/src/adapters/cursor-adapter.ts +5 -0
- package/src/config.ts +14 -1
- package/src/index.ts +3 -0
- package/src/session.ts +56 -27
- package/src/simplify.ts +120 -0
- package/src/web-ui.ts +53 -9
- package/src/wechat-platform.ts +1 -1
package/README.md
CHANGED
|
@@ -158,7 +158,8 @@ Codex 的默认模型和推理强度可继续由 `~/.codex/config.toml` 管理
|
|
|
158
158
|
"claude": {
|
|
159
159
|
"enabled": false,
|
|
160
160
|
"defaultAgent": true,
|
|
161
|
-
"model": "",
|
|
161
|
+
"model": "claude-sonnet-4-6",
|
|
162
|
+
"subagentModel": "",
|
|
162
163
|
"effort": "",
|
|
163
164
|
"apiKey": "",
|
|
164
165
|
"baseUrl": ""
|
|
@@ -189,6 +190,7 @@ Codex 的默认模型和推理强度可继续由 `~/.codex/config.toml` 管理
|
|
|
189
190
|
| `*.enabled` | 是否启用对应 AI Agent |
|
|
190
191
|
| `*.defaultAgent` | `/new` 未指定 Agent 时使用哪个工具 |
|
|
191
192
|
| `cursor.path` / `codex.path` | CLI 可执行文件路径;留空时自动探测或使用 PATH |
|
|
193
|
+
| `claude.model` / `claude.subagentModel` | Claude Code 主模型 / subagent 模型;`subagentModel` 仅在第三方 API 模式下注入 `CLAUDE_CODE_SUBAGENT_MODEL` |
|
|
192
194
|
| `claude.apiKey` / `claude.baseUrl` | 第三方 Anthropic 兼容网关配置;官方 Claude 用户留空 |
|
|
193
195
|
|
|
194
196
|
> 当前 ChatCCC 以 `bypassPermissions` 模式运行,会跳过 Agent 操作确认。请只在可信环境中使用。
|
package/config.sample.json
CHANGED
package/package.json
CHANGED
|
@@ -1,59 +1,59 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "chatccc",
|
|
3
|
-
"version": "0.2.
|
|
4
|
-
"description": "Feishu bot bridge for Claude Code",
|
|
5
|
-
"license": "Apache-2.0",
|
|
6
|
-
"type": "module",
|
|
7
|
-
"main": "./src/index.ts",
|
|
8
|
-
"bin": {
|
|
9
|
-
"chatccc": "bin/chatccc.mjs"
|
|
10
|
-
},
|
|
11
|
-
"files": [
|
|
12
|
-
"src/",
|
|
13
|
-
"bin/",
|
|
14
|
-
"demo/ilink_echo_probe.ts",
|
|
15
|
-
"im-skills/",
|
|
16
|
-
"images/img_readme_*.jpg",
|
|
17
|
-
"images/img_readme_*.png",
|
|
18
|
-
"images/avatars/status_*.png",
|
|
19
|
-
"images/avatars/badges/",
|
|
20
|
-
"package.json",
|
|
21
|
-
"README.md",
|
|
22
|
-
"config.sample.json"
|
|
23
|
-
],
|
|
24
|
-
"scripts": {
|
|
25
|
-
"dev": "tsx src/index.ts",
|
|
26
|
-
"chatccc": "tsx src/index.ts",
|
|
27
|
-
"start": "tsx src/index.ts",
|
|
28
|
-
"demo:bot-test": "tsx demo/bot_test.ts",
|
|
29
|
-
"demo:bot-test:local": "tsx demo/bot_test.ts --local",
|
|
30
|
-
"demo:create-group": "tsx src/index.ts",
|
|
31
|
-
"demo:create-group:local": "tsx src/index.ts --local",
|
|
32
|
-
"demo:permission-check": "tsx demo/permission_check.ts",
|
|
33
|
-
"demo:claude-hi": "tsx demo/claude_say_hi.ts",
|
|
34
|
-
"demo:codex-hi": "tsx demo/codex_say_hi.ts",
|
|
35
|
-
"demo:ilink-echo": "tsx demo/ilink_echo_probe.ts",
|
|
36
|
-
"test": "vitest run",
|
|
37
|
-
"test:watch": "vitest"
|
|
38
|
-
},
|
|
39
|
-
"dependencies": {
|
|
40
|
-
"@anthropic-ai/claude-agent-sdk": "0.2.133",
|
|
41
|
-
"@larksuiteoapi/node-sdk": "^1.59.0",
|
|
42
|
-
"@openilink/openilink-sdk-node": "^0.6.0",
|
|
43
|
-
"nodemailer": "^8.0.7",
|
|
44
|
-
"qrcode-terminal": "^0.12.0",
|
|
45
|
-
"sharp": "^0.34.5",
|
|
46
|
-
"tsx": "^4.0.0",
|
|
47
|
-
"ws": "^8.18.0"
|
|
48
|
-
},
|
|
49
|
-
"devDependencies": {
|
|
50
|
-
"@types/node": "^20.0.0",
|
|
51
|
-
"@types/qrcode-terminal": "^0.12.2",
|
|
52
|
-
"@types/ws": "^8.18.1",
|
|
53
|
-
"typescript": "^5.0.0",
|
|
54
|
-
"vitest": "^4.1.5"
|
|
55
|
-
},
|
|
56
|
-
"engines": {
|
|
57
|
-
"node": ">=20"
|
|
58
|
-
}
|
|
59
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "chatccc",
|
|
3
|
+
"version": "0.2.56",
|
|
4
|
+
"description": "Feishu bot bridge for Claude Code",
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "./src/index.ts",
|
|
8
|
+
"bin": {
|
|
9
|
+
"chatccc": "bin/chatccc.mjs"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"src/",
|
|
13
|
+
"bin/",
|
|
14
|
+
"demo/ilink_echo_probe.ts",
|
|
15
|
+
"im-skills/",
|
|
16
|
+
"images/img_readme_*.jpg",
|
|
17
|
+
"images/img_readme_*.png",
|
|
18
|
+
"images/avatars/status_*.png",
|
|
19
|
+
"images/avatars/badges/",
|
|
20
|
+
"package.json",
|
|
21
|
+
"README.md",
|
|
22
|
+
"config.sample.json"
|
|
23
|
+
],
|
|
24
|
+
"scripts": {
|
|
25
|
+
"dev": "tsx src/index.ts",
|
|
26
|
+
"chatccc": "tsx src/index.ts",
|
|
27
|
+
"start": "tsx src/index.ts",
|
|
28
|
+
"demo:bot-test": "tsx demo/bot_test.ts",
|
|
29
|
+
"demo:bot-test:local": "tsx demo/bot_test.ts --local",
|
|
30
|
+
"demo:create-group": "tsx src/index.ts",
|
|
31
|
+
"demo:create-group:local": "tsx src/index.ts --local",
|
|
32
|
+
"demo:permission-check": "tsx demo/permission_check.ts",
|
|
33
|
+
"demo:claude-hi": "tsx demo/claude_say_hi.ts",
|
|
34
|
+
"demo:codex-hi": "tsx demo/codex_say_hi.ts",
|
|
35
|
+
"demo:ilink-echo": "tsx demo/ilink_echo_probe.ts",
|
|
36
|
+
"test": "vitest run",
|
|
37
|
+
"test:watch": "vitest"
|
|
38
|
+
},
|
|
39
|
+
"dependencies": {
|
|
40
|
+
"@anthropic-ai/claude-agent-sdk": "0.2.133",
|
|
41
|
+
"@larksuiteoapi/node-sdk": "^1.59.0",
|
|
42
|
+
"@openilink/openilink-sdk-node": "^0.6.0",
|
|
43
|
+
"nodemailer": "^8.0.7",
|
|
44
|
+
"qrcode-terminal": "^0.12.0",
|
|
45
|
+
"sharp": "^0.34.5",
|
|
46
|
+
"tsx": "^4.0.0",
|
|
47
|
+
"ws": "^8.18.0"
|
|
48
|
+
},
|
|
49
|
+
"devDependencies": {
|
|
50
|
+
"@types/node": "^20.0.0",
|
|
51
|
+
"@types/qrcode-terminal": "^0.12.2",
|
|
52
|
+
"@types/ws": "^8.18.1",
|
|
53
|
+
"typescript": "^5.0.0",
|
|
54
|
+
"vitest": "^4.1.5"
|
|
55
|
+
},
|
|
56
|
+
"engines": {
|
|
57
|
+
"node": ">=20"
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -788,6 +788,45 @@ describe("createClaudeAdapter — env 注入", () => {
|
|
|
788
788
|
expect(opts.env.ANTHROPIC_BASE_URL).toBe("https://gateway.example/anthropic");
|
|
789
789
|
});
|
|
790
790
|
|
|
791
|
+
it("官方 API 模式下 subagentModel 不注入 env,也不覆盖 Claude 登录环境", async () => {
|
|
792
|
+
setupMockCreateSession();
|
|
793
|
+
process.env.CLAUDE_CODE_OAUTH_TOKEN = "oauth-from-login";
|
|
794
|
+
const adapter = createClaudeAdapter({
|
|
795
|
+
model: "claude-sonnet-4-6",
|
|
796
|
+
subagentModel: "claude-haiku-4-5-20251001",
|
|
797
|
+
effort: "",
|
|
798
|
+
isEmpty: (v) => v.trim() === "",
|
|
799
|
+
apiKey: "",
|
|
800
|
+
baseUrl: "",
|
|
801
|
+
});
|
|
802
|
+
|
|
803
|
+
await adapter.createSession("/cwd");
|
|
804
|
+
|
|
805
|
+
const opts = sdk.unstable_v2_createSession.mock.calls[0][0];
|
|
806
|
+
expect(opts).not.toHaveProperty("env");
|
|
807
|
+
});
|
|
808
|
+
|
|
809
|
+
it("第三方 API 模式下 subagentModel 通过 CLAUDE_CODE_SUBAGENT_MODEL 覆盖 Haiku", async () => {
|
|
810
|
+
setupMockCreateSession();
|
|
811
|
+
process.env.CLAUDE_CODE_OAUTH_TOKEN = "oauth-from-login";
|
|
812
|
+
const adapter = createClaudeAdapter({
|
|
813
|
+
model: "claude-sonnet-4-6",
|
|
814
|
+
subagentModel: "claude-haiku-4-5-20251001",
|
|
815
|
+
effort: "",
|
|
816
|
+
isEmpty: (v) => v.trim() === "",
|
|
817
|
+
apiKey: "sk-thirdparty",
|
|
818
|
+
baseUrl: "https://gateway.example/anthropic",
|
|
819
|
+
});
|
|
820
|
+
|
|
821
|
+
await adapter.createSession("/cwd");
|
|
822
|
+
|
|
823
|
+
const opts = sdk.unstable_v2_createSession.mock.calls[0][0];
|
|
824
|
+
expect(opts.model).toBe("claude-sonnet-4-6");
|
|
825
|
+
expect(opts.env.ANTHROPIC_API_KEY).toBe("sk-thirdparty");
|
|
826
|
+
expect(opts.env.CLAUDE_CODE_SUBAGENT_MODEL).toBe("claude-haiku-4-5-20251001");
|
|
827
|
+
expect(opts.env.CLAUDE_CODE_OAUTH_TOKEN).toBeUndefined();
|
|
828
|
+
});
|
|
829
|
+
|
|
791
830
|
it("ChatCCC API config is isolated from Claude settings auth/model env", async () => {
|
|
792
831
|
setupMockCreateSession();
|
|
793
832
|
process.env.ANTHROPIC_AUTH_TOKEN = "token-from-claude-settings";
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
CLAUDE_BASE_URL,
|
|
13
13
|
CLAUDE_EFFORT,
|
|
14
14
|
CLAUDE_MODEL,
|
|
15
|
+
CLAUDE_SUBAGENT_MODEL,
|
|
15
16
|
CURSOR_AGENT_ARGS,
|
|
16
17
|
CURSOR_AGENT_COMMAND,
|
|
17
18
|
FEISHU_ENABLED,
|
|
@@ -45,6 +46,7 @@ const baseAppConfig: AppConfig = {
|
|
|
45
46
|
enabled: true,
|
|
46
47
|
defaultAgent: true,
|
|
47
48
|
model: "initial-model",
|
|
49
|
+
subagentModel: "initial-subagent-model",
|
|
48
50
|
effort: "initial-effort",
|
|
49
51
|
apiKey: "sk-initial",
|
|
50
52
|
baseUrl: "https://initial.gw/anthropic",
|
|
@@ -78,20 +80,22 @@ describe("applyLoadedConfig — 刷新 export let 常量", () => {
|
|
|
78
80
|
expect(APP_SECRET).toBe("NEW_APP_SECRET");
|
|
79
81
|
});
|
|
80
82
|
|
|
81
|
-
it("更新 Claude 配置(model / effort / apiKey / baseUrl)", () => {
|
|
83
|
+
it("更新 Claude 配置(model / subagentModel / effort / apiKey / baseUrl)", () => {
|
|
82
84
|
applyLoadedConfig({
|
|
83
85
|
...structuredClone(baseAppConfig),
|
|
84
86
|
claude: {
|
|
85
87
|
enabled: true,
|
|
86
88
|
defaultAgent: true,
|
|
87
|
-
model: "
|
|
89
|
+
model: "claude-sonnet-4-6",
|
|
90
|
+
subagentModel: "claude-haiku-4-5-20251001",
|
|
88
91
|
effort: "high",
|
|
89
92
|
apiKey: "sk-newkey",
|
|
90
93
|
baseUrl: "https://gw2.example/anthropic",
|
|
91
94
|
},
|
|
92
95
|
});
|
|
93
96
|
|
|
94
|
-
expect(CLAUDE_MODEL).toBe("
|
|
97
|
+
expect(CLAUDE_MODEL).toBe("claude-sonnet-4-6");
|
|
98
|
+
expect(CLAUDE_SUBAGENT_MODEL).toBe("claude-haiku-4-5-20251001");
|
|
95
99
|
expect(CLAUDE_EFFORT).toBe("high");
|
|
96
100
|
expect(CLAUDE_API_KEY).toBe("sk-newkey");
|
|
97
101
|
expect(CLAUDE_BASE_URL).toBe("https://gw2.example/anthropic");
|
|
@@ -11,9 +11,20 @@ describe("config.sample.json", () => {
|
|
|
11
11
|
feishu?: { enabled?: unknown };
|
|
12
12
|
ilink?: { enabled?: unknown };
|
|
13
13
|
};
|
|
14
|
+
claude?: { model?: unknown; subagentModel?: unknown };
|
|
14
15
|
};
|
|
15
16
|
|
|
16
17
|
expect(sample.platforms?.feishu?.enabled).toBe(true);
|
|
17
18
|
expect(sample.platforms?.ilink?.enabled).toBe(true);
|
|
18
19
|
});
|
|
20
|
+
|
|
21
|
+
it("uses an Anthropic official main model and leaves subagent override empty by default", () => {
|
|
22
|
+
const configSamplePath = join(process.cwd(), "config.sample.json");
|
|
23
|
+
const sample = JSON.parse(readFileSync(configSamplePath, "utf8")) as {
|
|
24
|
+
claude?: { model?: unknown; subagentModel?: unknown };
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
expect(sample.claude?.model).toBe("claude-sonnet-4-6");
|
|
28
|
+
expect(sample.claude?.subagentModel).toBe("");
|
|
29
|
+
});
|
|
19
30
|
});
|
|
@@ -682,20 +682,22 @@ describe("accumulateBlockContent", () => {
|
|
|
682
682
|
);
|
|
683
683
|
expect(s.accumulatedContent).toContain("📖"); // 📖
|
|
684
684
|
expect(s.accumulatedContent).toContain("**Read**");
|
|
685
|
-
expect(s.accumulatedContent).toContain("
|
|
685
|
+
expect(s.accumulatedContent).toContain("/tmp/test.txt");
|
|
686
686
|
});
|
|
687
687
|
|
|
688
688
|
it("accumulates tool_use block with long input truncated", () => {
|
|
689
689
|
const s = freshState();
|
|
690
690
|
const longInput = "x".repeat(500);
|
|
691
691
|
accumulateBlockContent(
|
|
692
|
-
{ type: "tool_use", name: "Bash", input: longInput },
|
|
692
|
+
{ type: "tool_use", name: "Bash", input: { command: longInput } },
|
|
693
693
|
s,
|
|
694
694
|
);
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
695
|
+
// 简化规则截断到 500 chars
|
|
696
|
+
expect(s.accumulatedContent).toContain("🖥️");
|
|
697
|
+
expect(s.accumulatedContent).toContain("**Bash**");
|
|
698
|
+
// command 超过 maxLength=500 时会被截断
|
|
699
|
+
const body = s.accumulatedContent.split("**Bash** ")[1]?.trim() ?? "";
|
|
700
|
+
expect(body.length).toBeLessThanOrEqual(503); // 500 + "..."
|
|
699
701
|
});
|
|
700
702
|
|
|
701
703
|
it("accumulates tool_result block with success icon (✅)", () => {
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach, afterAll, vi } from "vitest";
|
|
2
|
+
import { mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
|
|
6
|
+
const TEST_DATA_DIR = await mkdtemp(join(tmpdir(), "chatccc-simplify-test-"));
|
|
7
|
+
vi.mock("../config.ts", async () => {
|
|
8
|
+
const actual = await vi.importActual<typeof import("../config.ts")>("../config.ts");
|
|
9
|
+
return {
|
|
10
|
+
...actual,
|
|
11
|
+
PROJECT_ROOT: TEST_DATA_DIR,
|
|
12
|
+
ts: () => "test-ts",
|
|
13
|
+
};
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
let simplifyToolUse: (name: string, input: unknown) => string | null;
|
|
17
|
+
let simplifyToolResult: (name: string, toolUseId: string, isError: boolean, toolCallInput?: unknown) => string | null;
|
|
18
|
+
let reloadSimplifyConfig: () => void;
|
|
19
|
+
|
|
20
|
+
beforeEach(async () => {
|
|
21
|
+
vi.resetModules();
|
|
22
|
+
try {
|
|
23
|
+
await rm(join(TEST_DATA_DIR, "simplify.json"), { force: true });
|
|
24
|
+
} catch {}
|
|
25
|
+
const mod = await import("../simplify.ts");
|
|
26
|
+
simplifyToolUse = mod.simplifyToolUse;
|
|
27
|
+
simplifyToolResult = mod.simplifyToolResult;
|
|
28
|
+
reloadSimplifyConfig = mod.reloadSimplifyConfig;
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
afterAll(async () => {
|
|
32
|
+
try {
|
|
33
|
+
await rm(TEST_DATA_DIR, { recursive: true, force: true });
|
|
34
|
+
} catch {}
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
describe("simplifyToolUse", () => {
|
|
38
|
+
it("无 simplify.json 时返回 null(回退默认格式化)", () => {
|
|
39
|
+
reloadSimplifyConfig();
|
|
40
|
+
expect(simplifyToolUse("Read", { file_path: "/tmp/x.ts" })).toBeNull();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("有规则时按模板格式化 tool_use", async () => {
|
|
44
|
+
await writeFile(
|
|
45
|
+
join(TEST_DATA_DIR, "simplify.json"),
|
|
46
|
+
JSON.stringify({
|
|
47
|
+
tool_use: {
|
|
48
|
+
Read: { template: "📖 **Read** {file_path}", maxLength: 300 },
|
|
49
|
+
},
|
|
50
|
+
}),
|
|
51
|
+
"utf-8",
|
|
52
|
+
);
|
|
53
|
+
reloadSimplifyConfig();
|
|
54
|
+
|
|
55
|
+
expect(simplifyToolUse("Read", { file_path: "/home/user/project/src/index.ts" }))
|
|
56
|
+
.toBe("📖 **Read** /home/user/project/src/index.ts");
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("无对应工具规则时返回 null", async () => {
|
|
60
|
+
await writeFile(
|
|
61
|
+
join(TEST_DATA_DIR, "simplify.json"),
|
|
62
|
+
JSON.stringify({
|
|
63
|
+
tool_use: {
|
|
64
|
+
Read: { template: "📖 **Read** {file_path}", maxLength: 300 },
|
|
65
|
+
},
|
|
66
|
+
}),
|
|
67
|
+
"utf-8",
|
|
68
|
+
);
|
|
69
|
+
reloadSimplifyConfig();
|
|
70
|
+
|
|
71
|
+
expect(simplifyToolUse("Write", { file_path: "/tmp/x.ts" })).toBeNull();
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("模板中未匹配的占位符保留原文", async () => {
|
|
75
|
+
await writeFile(
|
|
76
|
+
join(TEST_DATA_DIR, "simplify.json"),
|
|
77
|
+
JSON.stringify({
|
|
78
|
+
tool_use: {
|
|
79
|
+
Read: { template: "📖 **Read** {file_path} {unknown_field}", maxLength: 300 },
|
|
80
|
+
},
|
|
81
|
+
}),
|
|
82
|
+
"utf-8",
|
|
83
|
+
);
|
|
84
|
+
reloadSimplifyConfig();
|
|
85
|
+
|
|
86
|
+
expect(simplifyToolUse("Read", { file_path: "/tmp/x.ts" }))
|
|
87
|
+
.toBe("📖 **Read** /tmp/x.ts {unknown_field}");
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("超过 maxLength 时截断", async () => {
|
|
91
|
+
await writeFile(
|
|
92
|
+
join(TEST_DATA_DIR, "simplify.json"),
|
|
93
|
+
JSON.stringify({
|
|
94
|
+
tool_use: {
|
|
95
|
+
Bash: { template: "🖥️ **Bash** {command}", maxLength: 20 },
|
|
96
|
+
},
|
|
97
|
+
}),
|
|
98
|
+
"utf-8",
|
|
99
|
+
);
|
|
100
|
+
reloadSimplifyConfig();
|
|
101
|
+
|
|
102
|
+
const result = simplifyToolUse("Bash", { command: "echo hello world this is a long command" });
|
|
103
|
+
expect(result).toBeTruthy();
|
|
104
|
+
// 被截断了:长度不超过 maxLength + 3("..." 后缀)
|
|
105
|
+
expect(result!.length).toBeLessThanOrEqual(23);
|
|
106
|
+
expect(result!.endsWith("...")).toBe(true);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("多字段模板", async () => {
|
|
110
|
+
await writeFile(
|
|
111
|
+
join(TEST_DATA_DIR, "simplify.json"),
|
|
112
|
+
JSON.stringify({
|
|
113
|
+
tool_use: {
|
|
114
|
+
Grep: { template: "🔎 **Grep** {pattern} in {path}", maxLength: 300 },
|
|
115
|
+
},
|
|
116
|
+
}),
|
|
117
|
+
"utf-8",
|
|
118
|
+
);
|
|
119
|
+
reloadSimplifyConfig();
|
|
120
|
+
|
|
121
|
+
expect(simplifyToolUse("Grep", { pattern: "TODO", path: "src/" }))
|
|
122
|
+
.toBe("🔎 **Grep** TODO in src/");
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it("input 为 null 时不抛异常", async () => {
|
|
126
|
+
await writeFile(
|
|
127
|
+
join(TEST_DATA_DIR, "simplify.json"),
|
|
128
|
+
JSON.stringify({
|
|
129
|
+
tool_use: {
|
|
130
|
+
TodoWrite: { template: "✅ **TodoWrite**", maxLength: 200 },
|
|
131
|
+
},
|
|
132
|
+
}),
|
|
133
|
+
"utf-8",
|
|
134
|
+
);
|
|
135
|
+
reloadSimplifyConfig();
|
|
136
|
+
|
|
137
|
+
expect(simplifyToolUse("TodoWrite", null)).toBe("✅ **TodoWrite**");
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
describe("simplifyToolResult", () => {
|
|
142
|
+
it("无 simplify.json 时返回 null", () => {
|
|
143
|
+
reloadSimplifyConfig();
|
|
144
|
+
expect(simplifyToolResult("Read", "abc123def456", false)).toBeNull();
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it("有规则时按模板格式化 tool_result", async () => {
|
|
148
|
+
await writeFile(
|
|
149
|
+
join(TEST_DATA_DIR, "simplify.json"),
|
|
150
|
+
JSON.stringify({
|
|
151
|
+
tool_result: {
|
|
152
|
+
Read: { template: "✅ *{id}*: 已读取", maxLength: 200 },
|
|
153
|
+
},
|
|
154
|
+
}),
|
|
155
|
+
"utf-8",
|
|
156
|
+
);
|
|
157
|
+
reloadSimplifyConfig();
|
|
158
|
+
|
|
159
|
+
expect(simplifyToolResult("Read", "abc123def456", false))
|
|
160
|
+
.toBe("✅ *def456*: 已读取");
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it("isError 时结果前加 ❌", async () => {
|
|
164
|
+
await writeFile(
|
|
165
|
+
join(TEST_DATA_DIR, "simplify.json"),
|
|
166
|
+
JSON.stringify({
|
|
167
|
+
tool_result: {
|
|
168
|
+
Read: { template: "✅ *{id}*: 已读取", maxLength: 200 },
|
|
169
|
+
},
|
|
170
|
+
}),
|
|
171
|
+
"utf-8",
|
|
172
|
+
);
|
|
173
|
+
reloadSimplifyConfig();
|
|
174
|
+
|
|
175
|
+
expect(simplifyToolResult("Read", "abc123def456", true))
|
|
176
|
+
.toBe("❌ ✅ *def456*: 已读取");
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it("tool_result 模板可使用 tool_use 的输入字段", async () => {
|
|
180
|
+
await writeFile(
|
|
181
|
+
join(TEST_DATA_DIR, "simplify.json"),
|
|
182
|
+
JSON.stringify({
|
|
183
|
+
tool_result: {
|
|
184
|
+
Write: { template: "✅ *{id}*: 已写入 {file_path}", maxLength: 200 },
|
|
185
|
+
},
|
|
186
|
+
}),
|
|
187
|
+
"utf-8",
|
|
188
|
+
);
|
|
189
|
+
reloadSimplifyConfig();
|
|
190
|
+
|
|
191
|
+
expect(simplifyToolResult("Write", "abc123def456", false, { file_path: "/tmp/out.txt" }))
|
|
192
|
+
.toBe("✅ *def456*: 已写入 /tmp/out.txt");
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it("无对应工具规则时返回 null", async () => {
|
|
196
|
+
await writeFile(
|
|
197
|
+
join(TEST_DATA_DIR, "simplify.json"),
|
|
198
|
+
JSON.stringify({
|
|
199
|
+
tool_result: {
|
|
200
|
+
Read: { template: "✅ *{id}*: 已读取", maxLength: 200 },
|
|
201
|
+
},
|
|
202
|
+
}),
|
|
203
|
+
"utf-8",
|
|
204
|
+
);
|
|
205
|
+
reloadSimplifyConfig();
|
|
206
|
+
|
|
207
|
+
expect(simplifyToolResult("Write", "abc123def456", false)).toBeNull();
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
describe("accumulateBlockContent with simplification", () => {
|
|
212
|
+
it("tool_use 使用简化规则,tool_result 回退默认格式", async () => {
|
|
213
|
+
// 动态 import session 以使用当前 mock 的简单化模块
|
|
214
|
+
const { accumulateBlockContent } = await import("../session.ts");
|
|
215
|
+
|
|
216
|
+
await writeFile(
|
|
217
|
+
join(TEST_DATA_DIR, "simplify.json"),
|
|
218
|
+
JSON.stringify({
|
|
219
|
+
tool_use: {
|
|
220
|
+
Read: { template: "📖 **Read** {file_path}", maxLength: 300 },
|
|
221
|
+
},
|
|
222
|
+
}),
|
|
223
|
+
"utf-8",
|
|
224
|
+
);
|
|
225
|
+
reloadSimplifyConfig();
|
|
226
|
+
|
|
227
|
+
const state = { accumulatedContent: "", finalText: "", finalCompleteText: "", chunkCount: 0 };
|
|
228
|
+
const toolCallMap = new Map<string, { name: string; input: unknown }>();
|
|
229
|
+
|
|
230
|
+
accumulateBlockContent(
|
|
231
|
+
{ type: "tool_use", id: "toolu_001", name: "Read", input: { file_path: "/src/app.ts" } },
|
|
232
|
+
state,
|
|
233
|
+
toolCallMap,
|
|
234
|
+
);
|
|
235
|
+
|
|
236
|
+
expect(state.accumulatedContent).toBe("\n\n📖 **Read** /src/app.ts\n");
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it("无简化规则时回退默认 tool_use 格式", async () => {
|
|
240
|
+
const { accumulateBlockContent: acb } = await import("../session.ts");
|
|
241
|
+
|
|
242
|
+
reloadSimplifyConfig(); // 无 simplify.json
|
|
243
|
+
|
|
244
|
+
const state = { accumulatedContent: "", finalText: "", finalCompleteText: "", chunkCount: 0 };
|
|
245
|
+
const toolCallMap = new Map<string, { name: string; input: unknown }>();
|
|
246
|
+
|
|
247
|
+
acb(
|
|
248
|
+
{ type: "tool_use", id: "toolu_001", name: "Read", input: { file_path: "/src/app.ts" } },
|
|
249
|
+
state,
|
|
250
|
+
toolCallMap,
|
|
251
|
+
);
|
|
252
|
+
|
|
253
|
+
expect(state.accumulatedContent).toContain("📖 **Read**");
|
|
254
|
+
expect(state.accumulatedContent).toContain('{"file_path":"/src/app.ts"}');
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
it("tool_result 使用简化规则", async () => {
|
|
258
|
+
const { accumulateBlockContent: acb } = await import("../session.ts");
|
|
259
|
+
|
|
260
|
+
await writeFile(
|
|
261
|
+
join(TEST_DATA_DIR, "simplify.json"),
|
|
262
|
+
JSON.stringify({
|
|
263
|
+
tool_result: {
|
|
264
|
+
Read: { template: "✅ *{id}*: 已读取 {file_path}", maxLength: 200 },
|
|
265
|
+
},
|
|
266
|
+
}),
|
|
267
|
+
"utf-8",
|
|
268
|
+
);
|
|
269
|
+
reloadSimplifyConfig();
|
|
270
|
+
|
|
271
|
+
const state = { accumulatedContent: "", finalText: "", finalCompleteText: "", chunkCount: 0 };
|
|
272
|
+
const toolCallMap = new Map<string, { name: string; input: unknown }>();
|
|
273
|
+
toolCallMap.set("toolu_001", { name: "Read", input: { file_path: "/src/app.ts" } });
|
|
274
|
+
|
|
275
|
+
acb(
|
|
276
|
+
{ type: "tool_result", tool_use_id: "toolu_001", content: "file content here..." },
|
|
277
|
+
state,
|
|
278
|
+
toolCallMap,
|
|
279
|
+
);
|
|
280
|
+
|
|
281
|
+
expect(state.accumulatedContent).toBe("✅ *lu_001*: 已读取 /src/app.ts\n");
|
|
282
|
+
});
|
|
283
|
+
});
|
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
applyClaudeApiMode,
|
|
4
4
|
chooseStartPath,
|
|
5
5
|
detectClaudeApiMode,
|
|
6
|
+
unflattenConfig,
|
|
6
7
|
} from "../web-ui.ts";
|
|
7
8
|
|
|
8
9
|
// ---------------------------------------------------------------------------
|
|
@@ -129,6 +130,22 @@ describe("applyClaudeApiMode", () => {
|
|
|
129
130
|
});
|
|
130
131
|
});
|
|
131
132
|
|
|
133
|
+
describe("unflattenConfig", () => {
|
|
134
|
+
it("maps Claude subagent model into claude.subagentModel", () => {
|
|
135
|
+
expect(
|
|
136
|
+
unflattenConfig({
|
|
137
|
+
CHATCCC_ANTHROPIC_MODEL: "claude-sonnet-4-6",
|
|
138
|
+
CHATCCC_ANTHROPIC_SUBAGENT_MODEL: "claude-haiku-4-5-20251001",
|
|
139
|
+
}),
|
|
140
|
+
).toEqual({
|
|
141
|
+
claude: {
|
|
142
|
+
model: "claude-sonnet-4-6",
|
|
143
|
+
subagentModel: "claude-haiku-4-5-20251001",
|
|
144
|
+
},
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
|
|
132
149
|
// ---------------------------------------------------------------------------
|
|
133
150
|
// chooseStartPath — /api/start 的路径选择
|
|
134
151
|
// 关键护栏:
|