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 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 操作确认。请只在可信环境中使用。
@@ -13,7 +13,8 @@
13
13
  "claude": {
14
14
  "enabled": false,
15
15
  "defaultAgent": true,
16
- "model": "",
16
+ "model": "claude-sonnet-4-6",
17
+ "subagentModel": "",
17
18
  "effort": "",
18
19
  "apiKey": "",
19
20
  "baseUrl": ""
package/package.json CHANGED
@@ -1,59 +1,59 @@
1
- {
2
- "name": "chatccc",
3
- "version": "0.2.54",
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: "deepseek-v4-pro",
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("deepseek-v4-pro");
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("file_path");
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
- expect(s.accumulatedContent).toContain("...");
696
- // Should be truncated to 300 + "..."
697
- const inputPart = s.accumulatedContent.split("`")[1];
698
- expect(inputPart.length).toBeLessThanOrEqual(304); // 300 + "..."
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
  // 关键护栏:
@@ -39,6 +39,8 @@ export interface UnifiedTextFinalBlock {
39
39
 
40
40
  export interface UnifiedToolUseBlock {
41
41
  type: "tool_use";
42
+ /** 工具调用 ID,用于与 tool_result 对应(Claude SDK 会提供,Cursor 旧格式可能无) */
43
+ id?: string;
42
44
  name: string;
43
45
  input: unknown;
44
46
  }