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
|
@@ -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
|
-
|
|
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.
|
|
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)
|
|
105
|
-
|
|
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
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
const
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
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
|
-
|
|
422
|
-
|
|
423
|
-
|
|
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
|
|
1327
|
+
export function clearAdapterCache(): void {
|
|
1303
1328
|
adapterCache.clear();
|
|
1304
1329
|
}
|
|
1330
|
+
|
|
1331
|
+
export function _clearAdapterCacheForTest(): void {
|
|
1332
|
+
clearAdapterCache();
|
|
1333
|
+
}
|
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
|
+
}
|