chatccc 0.2.54 → 0.2.55

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chatccc",
3
- "version": "0.2.54",
3
+ "version": "0.2.55",
4
4
  "description": "Feishu bot bridge for Claude Code",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -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
+ });
@@ -29,29 +29,4 @@ describe("createWechatAdapter", () => {
29
29
  expect(text).toContain("/new");
30
30
  expect(log).toHaveBeenCalledWith("[WECHAT] sendRawCard degraded to text");
31
31
  });
32
-
33
- it("reports unsent non-final messages after the claw limit", async () => {
34
- const wire = {
35
- push: vi.fn(async (_chatId: string, _text: string) => "msg-id"),
36
- sendText: vi.fn(
37
- async (_chatId: string, _text: string, _contextToken?: string) =>
38
- "msg-id",
39
- ),
40
- };
41
- const log = vi.fn();
42
- const platform = createWechatAdapter({
43
- getWire: () => wire,
44
- log,
45
- });
46
-
47
- for (let i = 0; i < 10; i++) {
48
- await expect(platform.sendText("wx-chat-limit", `chunk ${i}`)).resolves.toBe(true);
49
- }
50
-
51
- await expect(platform.sendText("wx-chat-limit", "chunk 10")).resolves.toBe(false);
52
- expect(wire.push).toHaveBeenCalledTimes(10);
53
- expect(log).toHaveBeenCalledWith(
54
- "[WECHAT] sendText skipped (claw limit): chatId=wx-chat-limit count=11",
55
- );
56
- });
57
32
  });
@@ -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
  }
@@ -158,6 +158,7 @@ export function normalizeSdkMessage(msg: SdkMessageLike): UnifiedStreamMessage |
158
158
  } else if (block.type === "tool_use") {
159
159
  blocks.push({
160
160
  type: "tool_use",
161
+ id: (block as { id?: string }).id,
161
162
  name: block.name ?? "unknown",
162
163
  input: block.input,
163
164
  });
@@ -117,6 +117,7 @@ export function normalizeCursorMessage(
117
117
  } else if (block.type === "tool_use") {
118
118
  blocks.push({
119
119
  type: "tool_use",
120
+ id: block.tool_use_id,
120
121
  name: block.name ?? "unknown",
121
122
  input: block.input,
122
123
  });
package/src/session.ts CHANGED
@@ -20,6 +20,7 @@ import {
20
20
  ts,
21
21
  } from "./config.ts";
22
22
  import { buildProgressCard, getToolEmoji, truncateContent } from "./cards.ts";
23
+ import { simplifyToolUse, simplifyToolResult } from "./simplify.ts";
23
24
  import { logTrace } from "./trace.ts";
24
25
  import type { UnifiedBlock } from "./adapters/adapter-interface.ts";
25
26
  import type { ToolAdapter } from "./adapters/adapter-interface.ts";
@@ -385,6 +386,7 @@ export function pickFinalReply(state: AccumulatorState): string {
385
386
  export function accumulateBlockContent(
386
387
  block: UnifiedBlock,
387
388
  state: AccumulatorState,
389
+ toolCallMap?: Map<string, { name: string; input: unknown }>,
388
390
  ): void {
389
391
  switch (block.type) {
390
392
  case "thinking":
@@ -392,35 +394,55 @@ export function accumulateBlockContent(
392
394
  state.accumulatedContent += block.thinking;
393
395
  break;
394
396
  case "tool_use": {
395
- const inputStr =
396
- typeof block.input === "object"
397
- ? JSON.stringify(block.input)
398
- : String(block.input ?? "");
399
- const shortInput =
400
- inputStr.length > 300 ? inputStr.slice(0, 300) + "..." : inputStr;
401
- state.accumulatedContent +=
402
- `\n\n${getToolEmoji(block.name)} **${block.name}**\n\`${shortInput}\`\n`;
397
+ // 记录 tool_use 信息供后续 tool_result 使用
398
+ if (toolCallMap && block.id) {
399
+ toolCallMap.set(block.id, { name: block.name, input: block.input });
400
+ }
401
+ const simplified = simplifyToolUse(block.name, block.input);
402
+ if (simplified !== null) {
403
+ state.accumulatedContent += `\n\n${simplified}\n`;
404
+ } else {
405
+ const inputStr =
406
+ typeof block.input === "object"
407
+ ? JSON.stringify(block.input)
408
+ : String(block.input ?? "");
409
+ const shortInput =
410
+ inputStr.length > 300 ? inputStr.slice(0, 300) + "..." : inputStr;
411
+ state.accumulatedContent +=
412
+ `\n\n${getToolEmoji(block.name)} **${block.name}**\n\`${shortInput}\`\n`;
413
+ }
403
414
  break;
404
415
  }
405
416
  case "tool_result": {
406
417
  const toolUseId = block.tool_use_id;
407
- const resultContent = block.content;
408
- let resultStr = "";
409
- if (typeof resultContent === "string") {
410
- resultStr = resultContent;
411
- } else if (Array.isArray(resultContent)) {
412
- resultStr = resultContent
413
- .map((c: { type?: string; text?: string }) => c.text ?? "")
414
- .join("");
415
- } else if (resultContent) {
416
- resultStr = JSON.stringify(resultContent);
417
- }
418
- const shortResult =
419
- resultStr.length > 200 ? resultStr.slice(0, 200) + "..." : resultStr;
420
418
  const isError = block.is_error;
421
- const icon = isError ? "❌" : "✅"; // :
422
- state.accumulatedContent +=
423
- `${icon} *${toolUseId.slice(-6)}*: ${shortResult}\n`;
419
+ // 查找对应的 tool_use 以获取工具名和输入
420
+ const toolCall = toolCallMap?.get(toolUseId);
421
+ const toolName = toolCall?.name;
422
+ const toolInput = toolCall?.input;
423
+ const simplified = toolName
424
+ ? simplifyToolResult(toolName, toolUseId, !!isError, toolInput)
425
+ : null;
426
+ if (simplified !== null) {
427
+ state.accumulatedContent += `${simplified}\n`;
428
+ } else {
429
+ const resultContent = block.content;
430
+ let resultStr = "";
431
+ if (typeof resultContent === "string") {
432
+ resultStr = resultContent;
433
+ } else if (Array.isArray(resultContent)) {
434
+ resultStr = resultContent
435
+ .map((c: { type?: string; text?: string }) => c.text ?? "")
436
+ .join("");
437
+ } else if (resultContent) {
438
+ resultStr = JSON.stringify(resultContent);
439
+ }
440
+ const shortResult =
441
+ resultStr.length > 200 ? resultStr.slice(0, 200) + "..." : resultStr;
442
+ const icon = isError ? "❌" : "✅"; // ❌ : ✅
443
+ state.accumulatedContent +=
444
+ `${icon} *${toolUseId.slice(-6)}*: ${shortResult}\n`;
445
+ }
424
446
  break;
425
447
  }
426
448
  case "redacted_thinking":
@@ -756,11 +778,12 @@ export async function runAgentSession(
756
778
 
757
779
  let lastFileWrite = Date.now();
758
780
  const FILE_WRITE_INTERVAL_MS = 2000;
781
+ const toolCallMap = new Map<string, { name: string; input: unknown }>();
759
782
 
760
783
  try {
761
784
  for await (const unifiedMsg of adapter.prompt(sessionId, userTextWithCapabilities, cwd, controller.signal)) {
762
785
  for (const block of unifiedMsg.blocks) {
763
- accumulateBlockContent(block, state);
786
+ accumulateBlockContent(block, state, toolCallMap);
764
787
 
765
788
  if (block.type === "compact_boundary" && block.post_tokens) {
766
789
  for (const cid of getChatsForSession(sessionId)) {
@@ -0,0 +1,120 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { resolve as resolvePath } from "node:path";
3
+ import { PROJECT_ROOT } from "./config.ts";
4
+
5
+ // ---------------------------------------------------------------------------
6
+ // 消息简化规则 —— 数据驱动,在 simplify.json 中配置
7
+ // ---------------------------------------------------------------------------
8
+
9
+ interface ToolRule {
10
+ template: string;
11
+ maxLength: number;
12
+ }
13
+
14
+ interface SimplifyConfig {
15
+ tool_use?: Record<string, ToolRule>;
16
+ tool_result?: Record<string, ToolRule>;
17
+ }
18
+
19
+ let config: SimplifyConfig | null = null;
20
+ let loaded = false;
21
+
22
+ function loadConfig(): SimplifyConfig {
23
+ const filePath = resolvePath(PROJECT_ROOT, "simplify.json");
24
+ if (!existsSync(filePath)) return {};
25
+ try {
26
+ const raw = readFileSync(filePath, "utf-8");
27
+ const parsed = JSON.parse(raw);
28
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
29
+ console.error("[SIMPLIFY] simplify.json 格式错误:应为对象");
30
+ return {};
31
+ }
32
+ return parsed as SimplifyConfig;
33
+ } catch (err) {
34
+ console.error(`[SIMPLIFY] 读取 simplify.json 失败: ${(err as Error).message}`);
35
+ return {};
36
+ }
37
+ }
38
+
39
+ function getConfig(): SimplifyConfig {
40
+ if (!loaded) {
41
+ config = loadConfig();
42
+ loaded = true;
43
+ }
44
+ return config!;
45
+ }
46
+
47
+ /** 热重载 */
48
+ export function reloadSimplifyConfig(): void {
49
+ loaded = false;
50
+ config = null;
51
+ }
52
+
53
+ /**
54
+ * 对模板字符串中的 {field} 占位符做替换。
55
+ * fields 为可用字段映射,extra 是额外的上下文(如 tool_use_id 的 id)。
56
+ */
57
+ function resolveTemplate(template: string, fields: Record<string, unknown>, extra?: Record<string, string>): string {
58
+ let result = template;
59
+ if (extra) {
60
+ for (const [k, v] of Object.entries(extra)) {
61
+ result = result.split(`{${k}}`).join(v);
62
+ }
63
+ }
64
+ for (const [k, v] of Object.entries(fields)) {
65
+ const strVal = typeof v === "string" ? v : JSON.stringify(v);
66
+ result = result.split(`{${k}}`).join(strVal);
67
+ }
68
+ return result;
69
+ }
70
+
71
+ /**
72
+ * 简化 tool_use 展示。
73
+ * 返回 null 表示无规则,调用方应回退到默认格式化。
74
+ */
75
+ export function simplifyToolUse(name: string, input: unknown): string | null {
76
+ const cfg = getConfig();
77
+ const rules = cfg.tool_use;
78
+ if (!rules) return null;
79
+ const rule = rules[name];
80
+ if (!rule) return null;
81
+
82
+ const fields = typeof input === "object" && input !== null
83
+ ? input as Record<string, unknown>
84
+ : {};
85
+ let result = resolveTemplate(rule.template, fields);
86
+ if (result.length > rule.maxLength) {
87
+ result = result.slice(0, rule.maxLength) + "...";
88
+ }
89
+ return result;
90
+ }
91
+
92
+ /**
93
+ * 简化 tool_result 展示。
94
+ * 返回 null 表示无规则,调用方应回退到默认格式化。
95
+ * toolCallInput 为对应的 tool_use 输入(可选),用于在 result 模板中引用输入字段。
96
+ */
97
+ export function simplifyToolResult(
98
+ name: string,
99
+ toolUseId: string,
100
+ isError: boolean,
101
+ toolCallInput?: unknown,
102
+ ): string | null {
103
+ const cfg = getConfig();
104
+ const rules = cfg.tool_result;
105
+ if (!rules) return null;
106
+ const rule = rules[name];
107
+ if (!rule) return null;
108
+
109
+ const id = toolUseId.slice(-6);
110
+ const extra = { id };
111
+ const fields = toolCallInput && typeof toolCallInput === "object"
112
+ ? toolCallInput as Record<string, unknown>
113
+ : {};
114
+ let result = resolveTemplate(rule.template, fields, extra);
115
+ if (isError) result = "❌ " + result;
116
+ if (result.length > rule.maxLength) {
117
+ result = result.slice(0, rule.maxLength) + "...";
118
+ }
119
+ return result;
120
+ }