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.
@@ -53,6 +53,7 @@ interface SdkMessageLike {
53
53
 
54
54
  export interface ClaudeAdapterOptions {
55
55
  model: string;
56
+ subagentModel?: string;
56
57
  effort: string;
57
58
  /** 判断字段是否为"不传给 SDK"的占位(项目约定:空字符串/全空白) */
58
59
  isEmpty: (value: string) => boolean;
@@ -83,14 +84,17 @@ export interface ClaudeAdapterOptions {
83
84
  function buildSdkEnv(
84
85
  apiKey: string | undefined,
85
86
  baseUrl: string | undefined,
87
+ subagentModel: string | undefined,
86
88
  ): Record<string, string | undefined> | undefined {
87
89
  const apiKeyTrim = (apiKey ?? "").trim();
88
90
  const baseUrlTrim = (baseUrl ?? "").trim();
89
- if (!apiKeyTrim && !baseUrlTrim) return undefined;
91
+ const subagentModelTrim = (subagentModel ?? "").trim();
92
+ const hasApiOverride = Boolean(apiKeyTrim || baseUrlTrim);
93
+ if (!hasApiOverride) return undefined;
90
94
 
91
95
  const env: Record<string, string | undefined> = { ...process.env };
92
- // ChatCCC's Claude API config is authoritative when present. Remove Claude
93
- // Code/user settings env that can silently override gateway/auth/model choice.
96
+ // ChatCCC's third-party Claude API config is authoritative when present.
97
+ // Remove Claude Code/user settings env that can silently override gateway/auth/model choice.
94
98
  delete env.ANTHROPIC_AUTH_TOKEN;
95
99
  delete env.CLAUDE_CODE_OAUTH_TOKEN;
96
100
  delete env.ANTHROPIC_MODEL;
@@ -101,8 +105,12 @@ function buildSdkEnv(
101
105
  delete env.CLAUDE_CODE_EFFORT_LEVEL;
102
106
 
103
107
  if (apiKeyTrim) env.ANTHROPIC_API_KEY = apiKeyTrim;
104
- if (baseUrlTrim) env.ANTHROPIC_BASE_URL = baseUrlTrim;
105
- else delete env.ANTHROPIC_BASE_URL;
108
+ if (baseUrlTrim) {
109
+ env.ANTHROPIC_BASE_URL = baseUrlTrim;
110
+ } else {
111
+ delete env.ANTHROPIC_BASE_URL;
112
+ }
113
+ if (subagentModelTrim) env.CLAUDE_CODE_SUBAGENT_MODEL = subagentModelTrim;
106
114
  return env;
107
115
  }
108
116
 
@@ -126,6 +134,7 @@ function buildSessionOptions(
126
134
  isEmpty: (value: string) => boolean,
127
135
  apiKey: string | undefined,
128
136
  baseUrl: string | undefined,
137
+ subagentModel: string | undefined,
129
138
  ): Record<string, unknown> {
130
139
  const o: Record<string, unknown> = {
131
140
  cwd,
@@ -136,7 +145,7 @@ function buildSessionOptions(
136
145
  };
137
146
  if (!isEmpty(model)) o.model = model;
138
147
  if (!isEmpty(effort)) o.effort = effort;
139
- const env = buildSdkEnv(apiKey, baseUrl);
148
+ const env = buildSdkEnv(apiKey, baseUrl, subagentModel);
140
149
  if (env) o.env = env;
141
150
  return o;
142
151
  }
@@ -158,6 +167,7 @@ export function normalizeSdkMessage(msg: SdkMessageLike): UnifiedStreamMessage |
158
167
  } else if (block.type === "tool_use") {
159
168
  blocks.push({
160
169
  type: "tool_use",
170
+ id: (block as { id?: string }).id,
161
171
  name: block.name ?? "unknown",
162
172
  input: block.input,
163
173
  });
@@ -212,6 +222,7 @@ class ClaudeAdapter implements ToolAdapter {
212
222
  readonly sessionDescPrefix = "Claude Code Session:";
213
223
  private model: string;
214
224
  private effort: string;
225
+ private subagentModel: string | undefined;
215
226
  private isEmpty: (value: string) => boolean;
216
227
  private apiKey: string | undefined;
217
228
  private baseUrl: string | undefined;
@@ -219,6 +230,7 @@ class ClaudeAdapter implements ToolAdapter {
219
230
  constructor(options: ClaudeAdapterOptions) {
220
231
  this.model = options.model;
221
232
  this.effort = options.effort;
233
+ this.subagentModel = options.subagentModel;
222
234
  this.isEmpty = options.isEmpty;
223
235
  this.apiKey = options.apiKey;
224
236
  this.baseUrl = options.baseUrl;
@@ -232,6 +244,7 @@ class ClaudeAdapter implements ToolAdapter {
232
244
  this.isEmpty,
233
245
  this.apiKey,
234
246
  this.baseUrl,
247
+ this.subagentModel,
235
248
  );
236
249
  const session = unstable_v2_createSession(sessionOpts as any);
237
250
 
@@ -276,12 +289,20 @@ class ClaudeAdapter implements ToolAdapter {
276
289
  this.isEmpty,
277
290
  this.apiKey,
278
291
  this.baseUrl,
292
+ this.subagentModel,
279
293
  );
280
294
  const session = unstable_v2_resumeSession(
281
295
  sessionId,
282
296
  sessionOpts as any,
283
297
  );
284
298
 
299
+ if (signal?.aborted) {
300
+ session.close();
301
+ return;
302
+ }
303
+ const onAbort = () => { session.close(); };
304
+ signal?.addEventListener("abort", onAbort, { once: true });
305
+
285
306
  await session.send(userText);
286
307
 
287
308
  const stream = session.stream();
@@ -295,6 +316,7 @@ class ClaudeAdapter implements ToolAdapter {
295
316
  if (normalized) yield normalized;
296
317
  }
297
318
  } finally {
319
+ signal?.removeEventListener("abort", onAbort);
298
320
  session.close();
299
321
  }
300
322
  }
@@ -242,6 +242,9 @@ class CodexAdapter implements ToolAdapter {
242
242
 
243
243
  const proc = spawnCodex(args, cwd, userText);
244
244
 
245
+ const onAbort = () => { proc.kill(); };
246
+ signal?.addEventListener("abort", onAbort, { once: true });
247
+
245
248
  try {
246
249
  for await (const raw of readJsonLines(proc, signal)) {
247
250
  if (signal?.aborted) break;
@@ -261,6 +264,7 @@ class CodexAdapter implements ToolAdapter {
261
264
  if (normalized) yield normalized;
262
265
  }
263
266
  } finally {
267
+ signal?.removeEventListener("abort", onAbort);
264
268
  proc.kill();
265
269
  }
266
270
  }
@@ -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
  });
@@ -345,6 +346,9 @@ class CursorAdapter implements ToolAdapter {
345
346
  const proc = spawnAgent(["--resume", sessionId], cwd, userText);
346
347
  this.activeProcs.add(proc);
347
348
 
349
+ const onAbort = () => { proc.kill(); };
350
+ signal?.addEventListener("abort", onAbort, { once: true });
351
+
348
352
  try {
349
353
  for await (const raw of readJsonLines(proc, signal)) {
350
354
  if (signal?.aborted) break;
@@ -366,6 +370,7 @@ class CursorAdapter implements ToolAdapter {
366
370
  if (normalized) yield normalized;
367
371
  }
368
372
  } finally {
373
+ signal?.removeEventListener("abort", onAbort);
369
374
  proc.kill();
370
375
  this.activeProcs.delete(proc);
371
376
  }
package/src/config.ts CHANGED
@@ -63,6 +63,7 @@ export interface ClaudeConfig {
63
63
  /** 是否作为 /new 未指定工具时使用的默认 Agent */
64
64
  defaultAgent: boolean;
65
65
  model: string;
66
+ subagentModel: string;
66
67
  effort: string;
67
68
  apiKey: string;
68
69
  baseUrl: string;
@@ -302,7 +303,7 @@ function loadConfig(): AppConfig {
302
303
  port: 18080,
303
304
  gitTimeoutSeconds: 180,
304
305
  allowInterrupt: false,
305
- claude: { enabled: false, defaultAgent: true, model: "", effort: "", apiKey: "", baseUrl: "" },
306
+ claude: { enabled: false, defaultAgent: true, model: "", subagentModel: "", effort: "", apiKey: "", baseUrl: "" },
306
307
  cursor: { enabled: false, defaultAgent: false, path: "", model: "claude-opus-4-7-max" },
307
308
  codex: { enabled: false, defaultAgent: false, path: "", model: "", effort: "" },
308
309
  };
@@ -336,6 +337,9 @@ function loadConfig(): AppConfig {
336
337
  let raw: string;
337
338
  try {
338
339
  raw = readFileSync(CONFIG_FILE, "utf-8");
340
+ // 移除可能意外写入的 UTF-8 BOM(如通过记事本编辑等场景),
341
+ // 避免 JSON.parse 因 BOM 失败导致返回空默认值、丢失所有配置。
342
+ if (raw.charCodeAt(0) === 0xfeff) raw = raw.slice(1);
339
343
  } catch (err) {
340
344
  console.error(`[CONFIG] 无法读取 config.json: ${(err as Error).message}`);
341
345
  return defaults;
@@ -378,6 +382,7 @@ function loadConfig(): AppConfig {
378
382
  const claudeNonEmpty = (): boolean =>
379
383
  Boolean(
380
384
  (typeof claude.model === "string" && claude.model.trim()) ||
385
+ (typeof claude.subagentModel === "string" && claude.subagentModel.trim()) ||
381
386
  (typeof claude.effort === "string" && claude.effort.trim()) ||
382
387
  (typeof claude.apiKey === "string" && claude.apiKey.trim()) ||
383
388
  (typeof claude.baseUrl === "string" && claude.baseUrl.trim()),
@@ -438,6 +443,7 @@ function loadConfig(): AppConfig {
438
443
  enabled: claudeEnabled,
439
444
  defaultAgent: defaultTool === "claude",
440
445
  model: normalizeOptionalConfigField(claude.model, { label: "claude.model" }),
446
+ subagentModel: normalizeOptionalConfigField(claude.subagentModel, { label: "claude.subagentModel" }),
441
447
  effort: normalizeOptionalConfigField(claude.effort, { label: "claude.effort" }),
442
448
  apiKey: claude.apiKey ?? "",
443
449
  baseUrl: claude.baseUrl ?? "",
@@ -498,6 +504,7 @@ export const CHATCCC_PORT = config.port;
498
504
  export const LOCAL_RELAY_URL = `ws://127.0.0.1:${CHATCCC_PORT}`;
499
505
 
500
506
  export let CLAUDE_MODEL = config.claude.model;
507
+ export let CLAUDE_SUBAGENT_MODEL = config.claude.subagentModel;
501
508
  export let CLAUDE_EFFORT = config.claude.effort;
502
509
  /** Anthropic 兼容网关的 API key(仅经 SDK 子进程 env 传递,从不写入主进程 process.env) */
503
510
  export let CLAUDE_API_KEY = config.claude.apiKey;
@@ -573,6 +580,7 @@ export function applyLoadedConfig(next: AppConfig): void {
573
580
  ILINK_ENABLED = next.platforms.ilink.enabled;
574
581
  ILINK_REUSE_TOKEN_ON_START = next.platforms.ilink.reuseTokenOnStart ?? true;
575
582
  CLAUDE_MODEL = next.claude.model;
583
+ CLAUDE_SUBAGENT_MODEL = next.claude.subagentModel;
576
584
  CLAUDE_EFFORT = next.claude.effort;
577
585
  CLAUDE_API_KEY = next.claude.apiKey;
578
586
  CLAUDE_BASE_URL = next.claude.baseUrl;
@@ -705,6 +713,11 @@ export function reportEnvironmentVariableReadout(): void {
705
713
  ` Claude 模型: ${anthropicConfigDisplay(CLAUDE_MODEL)}(留空时不向 SDK 传 model)`
706
714
  );
707
715
 
716
+ console.log(` [默认] [可选] claude.subagentModel`);
717
+ console.log(
718
+ ` Claude Subagent 模型: ${anthropicConfigDisplay(CLAUDE_SUBAGENT_MODEL)}(留空时不向 SDK 传 CLAUDE_CODE_SUBAGENT_MODEL)`
719
+ );
720
+
708
721
  console.log(` [默认] [可选] claude.effort`);
709
722
  console.log(
710
723
  ` 思考深度: ${anthropicConfigDisplay(CLAUDE_EFFORT)}(留空时不向 SDK 传 effort)`
package/src/index.ts CHANGED
@@ -85,6 +85,7 @@ import {
85
85
  } from "./cardkit.ts";
86
86
  import {
87
87
  MAX_PROCESSED,
88
+ clearAdapterCache,
88
89
  loadSessionRegistryForBinding,
89
90
  processedMessages,
90
91
  rebuildBindingsFromRegistry,
@@ -745,6 +746,7 @@ async function main(): Promise<void> {
745
746
  // 会话时自动看到新值)。setup 首次激活走 onActivate 路径,不依赖此 hook。
746
747
  setReloadConfigHook(() => {
747
748
  reloadConfigFromDisk();
749
+ clearAdapterCache();
748
750
  appendStartupTrace("reload-from-ui: config reloaded", {
749
751
  appIdMask: maskAppId(APP_ID),
750
752
  });
@@ -769,6 +771,7 @@ async function main(): Promise<void> {
769
771
  startSetupMode(CHATCCC_PORT, {
770
772
  onActivate: async (httpServer: Server) => {
771
773
  reloadConfigFromDisk();
774
+ clearAdapterCache();
772
775
  appendStartupTrace("setup-activate: reloaded config from disk", {
773
776
  appIdMaskAfterReload: maskAppId(APP_ID),
774
777
  });
package/src/session.ts CHANGED
@@ -6,6 +6,7 @@ import {
6
6
  CLAUDE_BASE_URL,
7
7
  CLAUDE_EFFORT,
8
8
  CLAUDE_MODEL,
9
+ CLAUDE_SUBAGENT_MODEL,
9
10
  CHATCCC_PORT,
10
11
  PROJECT_ROOT,
11
12
  SESSIONS_FILE,
@@ -20,6 +21,7 @@ import {
20
21
  ts,
21
22
  } from "./config.ts";
22
23
  import { buildProgressCard, getToolEmoji, truncateContent } from "./cards.ts";
24
+ import { simplifyToolUse, simplifyToolResult } from "./simplify.ts";
23
25
  import { logTrace } from "./trace.ts";
24
26
  import type { UnifiedBlock } from "./adapters/adapter-interface.ts";
25
27
  import type { ToolAdapter } from "./adapters/adapter-interface.ts";
@@ -171,6 +173,7 @@ export function getAdapterForTool(tool: string): ToolAdapter {
171
173
  } else {
172
174
  adapter = createClaudeAdapter({
173
175
  model: CLAUDE_MODEL,
176
+ subagentModel: CLAUDE_SUBAGENT_MODEL,
174
177
  effort: CLAUDE_EFFORT,
175
178
  isEmpty: isAnthropicConfigEmpty,
176
179
  apiKey: CLAUDE_API_KEY,
@@ -385,6 +388,7 @@ export function pickFinalReply(state: AccumulatorState): string {
385
388
  export function accumulateBlockContent(
386
389
  block: UnifiedBlock,
387
390
  state: AccumulatorState,
391
+ toolCallMap?: Map<string, { name: string; input: unknown }>,
388
392
  ): void {
389
393
  switch (block.type) {
390
394
  case "thinking":
@@ -392,35 +396,55 @@ export function accumulateBlockContent(
392
396
  state.accumulatedContent += block.thinking;
393
397
  break;
394
398
  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`;
399
+ // 记录 tool_use 信息供后续 tool_result 使用
400
+ if (toolCallMap && block.id) {
401
+ toolCallMap.set(block.id, { name: block.name, input: block.input });
402
+ }
403
+ const simplified = simplifyToolUse(block.name, block.input);
404
+ if (simplified !== null) {
405
+ state.accumulatedContent += `\n\n${simplified}\n`;
406
+ } else {
407
+ const inputStr =
408
+ typeof block.input === "object"
409
+ ? JSON.stringify(block.input)
410
+ : String(block.input ?? "");
411
+ const shortInput =
412
+ inputStr.length > 300 ? inputStr.slice(0, 300) + "..." : inputStr;
413
+ state.accumulatedContent +=
414
+ `\n\n${getToolEmoji(block.name)} **${block.name}**\n\`${shortInput}\`\n`;
415
+ }
403
416
  break;
404
417
  }
405
418
  case "tool_result": {
406
419
  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
420
  const isError = block.is_error;
421
- const icon = isError ? "❌" : "✅"; // :
422
- state.accumulatedContent +=
423
- `${icon} *${toolUseId.slice(-6)}*: ${shortResult}\n`;
421
+ // 查找对应的 tool_use 以获取工具名和输入
422
+ const toolCall = toolCallMap?.get(toolUseId);
423
+ const toolName = toolCall?.name;
424
+ const toolInput = toolCall?.input;
425
+ const simplified = toolName
426
+ ? simplifyToolResult(toolName, toolUseId, !!isError, toolInput)
427
+ : null;
428
+ if (simplified !== null) {
429
+ state.accumulatedContent += `${simplified}\n`;
430
+ } else {
431
+ const resultContent = block.content;
432
+ let resultStr = "";
433
+ if (typeof resultContent === "string") {
434
+ resultStr = resultContent;
435
+ } else if (Array.isArray(resultContent)) {
436
+ resultStr = resultContent
437
+ .map((c: { type?: string; text?: string }) => c.text ?? "")
438
+ .join("");
439
+ } else if (resultContent) {
440
+ resultStr = JSON.stringify(resultContent);
441
+ }
442
+ const shortResult =
443
+ resultStr.length > 200 ? resultStr.slice(0, 200) + "..." : resultStr;
444
+ const icon = isError ? "❌" : "✅"; // ❌ : ✅
445
+ state.accumulatedContent +=
446
+ `${icon} *${toolUseId.slice(-6)}*: ${shortResult}\n`;
447
+ }
424
448
  break;
425
449
  }
426
450
  case "redacted_thinking":
@@ -584,7 +608,7 @@ function formatToolConfigForLog(tool: string, sessionModel?: string): string {
584
608
  : "effort=(由 codex config.toml 决定)";
585
609
  return `model=${modelStr}, ${effortStr}`;
586
610
  }
587
- return `model=${anthropicConfigDisplay(CLAUDE_MODEL)}, effort=${anthropicConfigDisplay(CLAUDE_EFFORT)}`;
611
+ return `model=${anthropicConfigDisplay(CLAUDE_MODEL)}, subagentModel=${anthropicConfigDisplay(CLAUDE_SUBAGENT_MODEL)}, effort=${anthropicConfigDisplay(CLAUDE_EFFORT)}`;
588
612
  }
589
613
 
590
614
  export async function initClaudeSession(tool: string, overrideCwd?: string, chatId?: string): Promise<{ sessionId: string; cwd: string }> {
@@ -756,11 +780,12 @@ export async function runAgentSession(
756
780
 
757
781
  let lastFileWrite = Date.now();
758
782
  const FILE_WRITE_INTERVAL_MS = 2000;
783
+ const toolCallMap = new Map<string, { name: string; input: unknown }>();
759
784
 
760
785
  try {
761
786
  for await (const unifiedMsg of adapter.prompt(sessionId, userTextWithCapabilities, cwd, controller.signal)) {
762
787
  for (const block of unifiedMsg.blocks) {
763
- accumulateBlockContent(block, state);
788
+ accumulateBlockContent(block, state, toolCallMap);
764
789
 
765
790
  if (block.type === "compact_boundary" && block.post_tokens) {
766
791
  for (const cid of getChatsForSession(sessionId)) {
@@ -1299,6 +1324,10 @@ export function _setAdapterForToolForTest(tool: string, adapter: ToolAdapter): v
1299
1324
  adapterCache.set(tool, adapter);
1300
1325
  }
1301
1326
 
1302
- export function _clearAdapterCacheForTest(): void {
1327
+ export function clearAdapterCache(): void {
1303
1328
  adapterCache.clear();
1304
1329
  }
1330
+
1331
+ export function _clearAdapterCacheForTest(): void {
1332
+ clearAdapterCache();
1333
+ }
@@ -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
+ }