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 +1 -1
- package/src/__tests__/session.test.ts +8 -6
- package/src/__tests__/simplify.test.ts +283 -0
- package/src/__tests__/wechat-platform.test.ts +0 -25
- package/src/adapters/adapter-interface.ts +2 -0
- package/src/adapters/claude-adapter.ts +1 -0
- package/src/adapters/cursor-adapter.ts +1 -0
- package/src/session.ts +48 -25
- package/src/simplify.ts +120 -0
package/package.json
CHANGED
|
@@ -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
|
+
});
|
|
@@ -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
|
});
|
|
@@ -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
|
});
|
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
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
const
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
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
|
-
|
|
422
|
-
|
|
423
|
-
|
|
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)) {
|
package/src/simplify.ts
ADDED
|
@@ -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
|
+
}
|