chatccc 0.2.56 → 0.2.57
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 +6 -6
- package/config.sample.json +8 -8
- package/package.json +1 -1
- package/src/__tests__/claude-adapter.test.ts +48 -48
- package/src/__tests__/config-reload.test.ts +14 -14
- package/src/__tests__/config-sample.test.ts +22 -22
- package/src/__tests__/privacy.test.ts +142 -142
- package/src/__tests__/session.test.ts +2 -2
- package/src/__tests__/simplify.test.ts +282 -282
- package/src/__tests__/web-ui.test.ts +23 -23
- package/src/adapters/claude-adapter.ts +40 -40
- package/src/index.ts +15 -15
- package/src/privacy.ts +67 -67
- package/src/session.ts +2 -1
- package/src/simplify.ts +119 -119
|
@@ -86,33 +86,33 @@ function buildSdkEnv(
|
|
|
86
86
|
baseUrl: string | undefined,
|
|
87
87
|
subagentModel: string | undefined,
|
|
88
88
|
): Record<string, string | undefined> | undefined {
|
|
89
|
-
const apiKeyTrim = (apiKey ?? "").trim();
|
|
90
|
-
const baseUrlTrim = (baseUrl ?? "").trim();
|
|
91
|
-
const subagentModelTrim = (subagentModel ?? "").trim();
|
|
92
|
-
const hasApiOverride = Boolean(apiKeyTrim || baseUrlTrim);
|
|
93
|
-
if (!hasApiOverride) return undefined;
|
|
94
|
-
|
|
95
|
-
const env: Record<string, string | undefined> = { ...process.env };
|
|
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.
|
|
98
|
-
delete env.ANTHROPIC_AUTH_TOKEN;
|
|
99
|
-
delete env.CLAUDE_CODE_OAUTH_TOKEN;
|
|
100
|
-
delete env.ANTHROPIC_MODEL;
|
|
101
|
-
delete env.ANTHROPIC_DEFAULT_OPUS_MODEL;
|
|
102
|
-
delete env.ANTHROPIC_DEFAULT_SONNET_MODEL;
|
|
103
|
-
delete env.ANTHROPIC_DEFAULT_HAIKU_MODEL;
|
|
104
|
-
delete env.CLAUDE_CODE_SUBAGENT_MODEL;
|
|
105
|
-
delete env.CLAUDE_CODE_EFFORT_LEVEL;
|
|
106
|
-
|
|
107
|
-
if (apiKeyTrim) env.ANTHROPIC_API_KEY = apiKeyTrim;
|
|
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;
|
|
114
|
-
return env;
|
|
115
|
-
}
|
|
89
|
+
const apiKeyTrim = (apiKey ?? "").trim();
|
|
90
|
+
const baseUrlTrim = (baseUrl ?? "").trim();
|
|
91
|
+
const subagentModelTrim = (subagentModel ?? "").trim();
|
|
92
|
+
const hasApiOverride = Boolean(apiKeyTrim || baseUrlTrim);
|
|
93
|
+
if (!hasApiOverride) return undefined;
|
|
94
|
+
|
|
95
|
+
const env: Record<string, string | undefined> = { ...process.env };
|
|
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.
|
|
98
|
+
delete env.ANTHROPIC_AUTH_TOKEN;
|
|
99
|
+
delete env.CLAUDE_CODE_OAUTH_TOKEN;
|
|
100
|
+
delete env.ANTHROPIC_MODEL;
|
|
101
|
+
delete env.ANTHROPIC_DEFAULT_OPUS_MODEL;
|
|
102
|
+
delete env.ANTHROPIC_DEFAULT_SONNET_MODEL;
|
|
103
|
+
delete env.ANTHROPIC_DEFAULT_HAIKU_MODEL;
|
|
104
|
+
delete env.CLAUDE_CODE_SUBAGENT_MODEL;
|
|
105
|
+
delete env.CLAUDE_CODE_EFFORT_LEVEL;
|
|
106
|
+
|
|
107
|
+
if (apiKeyTrim) env.ANTHROPIC_API_KEY = apiKeyTrim;
|
|
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;
|
|
114
|
+
return env;
|
|
115
|
+
}
|
|
116
116
|
|
|
117
117
|
function resolveSettingSources(
|
|
118
118
|
_apiKey: string | undefined,
|
|
@@ -291,19 +291,19 @@ class ClaudeAdapter implements ToolAdapter {
|
|
|
291
291
|
this.baseUrl,
|
|
292
292
|
this.subagentModel,
|
|
293
293
|
);
|
|
294
|
-
const session = unstable_v2_resumeSession(
|
|
295
|
-
sessionId,
|
|
296
|
-
sessionOpts as any,
|
|
297
|
-
);
|
|
298
|
-
|
|
299
|
-
if (signal?.aborted) {
|
|
300
|
-
session.close();
|
|
301
|
-
return;
|
|
302
|
-
}
|
|
303
|
-
const onAbort = () => { session.close(); };
|
|
304
|
-
signal?.addEventListener("abort", onAbort, { once: true });
|
|
305
|
-
|
|
306
|
-
await session.send(userText);
|
|
294
|
+
const session = unstable_v2_resumeSession(
|
|
295
|
+
sessionId,
|
|
296
|
+
sessionOpts as any,
|
|
297
|
+
);
|
|
298
|
+
|
|
299
|
+
if (signal?.aborted) {
|
|
300
|
+
session.close();
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
const onAbort = () => { session.close(); };
|
|
304
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
305
|
+
|
|
306
|
+
await session.send(userText);
|
|
307
307
|
|
|
308
308
|
const stream = session.stream();
|
|
309
309
|
|
package/src/index.ts
CHANGED
|
@@ -84,9 +84,9 @@ import {
|
|
|
84
84
|
updateCardKitCard,
|
|
85
85
|
} from "./cardkit.ts";
|
|
86
86
|
import {
|
|
87
|
-
MAX_PROCESSED,
|
|
88
|
-
clearAdapterCache,
|
|
89
|
-
loadSessionRegistryForBinding,
|
|
87
|
+
MAX_PROCESSED,
|
|
88
|
+
clearAdapterCache,
|
|
89
|
+
loadSessionRegistryForBinding,
|
|
90
90
|
processedMessages,
|
|
91
91
|
rebuildBindingsFromRegistry,
|
|
92
92
|
resetState,
|
|
@@ -744,12 +744,12 @@ async function main(): Promise<void> {
|
|
|
744
744
|
// "保存并启动" 时,web-ui 会调用本回调,把磁盘上刚保存的 config.json
|
|
745
745
|
// 刷进进程内的 export let 常量(live binding 让 CLAUDE_MODEL 等下次创建
|
|
746
746
|
// 会话时自动看到新值)。setup 首次激活走 onActivate 路径,不依赖此 hook。
|
|
747
|
-
setReloadConfigHook(() => {
|
|
748
|
-
reloadConfigFromDisk();
|
|
749
|
-
clearAdapterCache();
|
|
750
|
-
appendStartupTrace("reload-from-ui: config reloaded", {
|
|
751
|
-
appIdMask: maskAppId(APP_ID),
|
|
752
|
-
});
|
|
747
|
+
setReloadConfigHook(() => {
|
|
748
|
+
reloadConfigFromDisk();
|
|
749
|
+
clearAdapterCache();
|
|
750
|
+
appendStartupTrace("reload-from-ui: config reloaded", {
|
|
751
|
+
appIdMask: maskAppId(APP_ID),
|
|
752
|
+
});
|
|
753
753
|
});
|
|
754
754
|
setExtraApiHandler(async (req, res) => {
|
|
755
755
|
return (await handleAgentImageRequest(req, res)) || (await handleAgentFileRequest(req, res));
|
|
@@ -769,12 +769,12 @@ async function main(): Promise<void> {
|
|
|
769
769
|
// 凭证不全:进 setup 向导。注入 onActivate 回调让用户点"保存并启动"
|
|
770
770
|
// 时,原地(同进程)调用 startBotService,复用 setup HTTP server。
|
|
771
771
|
startSetupMode(CHATCCC_PORT, {
|
|
772
|
-
onActivate: async (httpServer: Server) => {
|
|
773
|
-
reloadConfigFromDisk();
|
|
774
|
-
clearAdapterCache();
|
|
775
|
-
appendStartupTrace("setup-activate: reloaded config from disk", {
|
|
776
|
-
appIdMaskAfterReload: maskAppId(APP_ID),
|
|
777
|
-
});
|
|
772
|
+
onActivate: async (httpServer: Server) => {
|
|
773
|
+
reloadConfigFromDisk();
|
|
774
|
+
clearAdapterCache();
|
|
775
|
+
appendStartupTrace("setup-activate: reloaded config from disk", {
|
|
776
|
+
appIdMaskAfterReload: maskAppId(APP_ID),
|
|
777
|
+
});
|
|
778
778
|
try {
|
|
779
779
|
await startBotService({ httpServer, port: CHATCCC_PORT });
|
|
780
780
|
installShutdownHandlers(httpServer);
|
package/src/privacy.ts
CHANGED
|
@@ -1,68 +1,68 @@
|
|
|
1
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
-
import { join } from "node:path";
|
|
3
|
-
import { USER_DATA_DIR } from "./config.ts";
|
|
4
|
-
|
|
5
|
-
// ---------------------------------------------------------------------------
|
|
6
|
-
// 隐私替换规则
|
|
7
|
-
// ---------------------------------------------------------------------------
|
|
8
|
-
|
|
9
|
-
interface PrivacyRules {
|
|
10
|
-
[key: string]: string;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
let rules: PrivacyRules | null = null;
|
|
14
|
-
let loaded = false;
|
|
15
|
-
|
|
16
|
-
function loadRules(): PrivacyRules {
|
|
17
|
-
const filePath = join(USER_DATA_DIR, "privacy.json");
|
|
18
|
-
if (!existsSync(filePath)) {
|
|
19
|
-
return {};
|
|
20
|
-
}
|
|
21
|
-
try {
|
|
22
|
-
const raw = readFileSync(filePath, "utf-8");
|
|
23
|
-
const parsed = JSON.parse(raw);
|
|
24
|
-
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
25
|
-
console.error(`[PRIVACY] privacy.json 格式错误:应为对象`);
|
|
26
|
-
return {};
|
|
27
|
-
}
|
|
28
|
-
for (const [k, v] of Object.entries(parsed)) {
|
|
29
|
-
if (typeof v !== "string") {
|
|
30
|
-
console.error(`[PRIVACY] privacy.json 值必须为字符串,跳过 "${k}"`);
|
|
31
|
-
delete (parsed as Record<string, unknown>)[k];
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
return parsed as PrivacyRules;
|
|
35
|
-
} catch (err) {
|
|
36
|
-
console.error(`[PRIVACY] 读取 privacy.json 失败: ${(err as Error).message}`);
|
|
37
|
-
return {};
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
export function getPrivacyRules(): PrivacyRules {
|
|
42
|
-
if (!loaded) {
|
|
43
|
-
rules = loadRules();
|
|
44
|
-
loaded = true;
|
|
45
|
-
}
|
|
46
|
-
return rules!;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/** 重新加载规则(热更新用) */
|
|
50
|
-
export function reloadPrivacyRules(): void {
|
|
51
|
-
loaded = false;
|
|
52
|
-
rules = null;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* 对文本应用隐私替换规则。
|
|
57
|
-
* 若无规则或文本为空,直接返回原文。
|
|
58
|
-
*/
|
|
59
|
-
export function applyPrivacy(text: string): string {
|
|
60
|
-
const r = getPrivacyRules();
|
|
61
|
-
if (Object.keys(r).length === 0 || !text) return text;
|
|
62
|
-
let result = text;
|
|
63
|
-
for (const [from, to] of Object.entries(r)) {
|
|
64
|
-
// 用 split+join 替代 replaceAll,避免正则特殊字符问题
|
|
65
|
-
result = result.split(from).join(to);
|
|
66
|
-
}
|
|
67
|
-
return result;
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { USER_DATA_DIR } from "./config.ts";
|
|
4
|
+
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
// 隐私替换规则
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
|
|
9
|
+
interface PrivacyRules {
|
|
10
|
+
[key: string]: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
let rules: PrivacyRules | null = null;
|
|
14
|
+
let loaded = false;
|
|
15
|
+
|
|
16
|
+
function loadRules(): PrivacyRules {
|
|
17
|
+
const filePath = join(USER_DATA_DIR, "privacy.json");
|
|
18
|
+
if (!existsSync(filePath)) {
|
|
19
|
+
return {};
|
|
20
|
+
}
|
|
21
|
+
try {
|
|
22
|
+
const raw = readFileSync(filePath, "utf-8");
|
|
23
|
+
const parsed = JSON.parse(raw);
|
|
24
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
25
|
+
console.error(`[PRIVACY] privacy.json 格式错误:应为对象`);
|
|
26
|
+
return {};
|
|
27
|
+
}
|
|
28
|
+
for (const [k, v] of Object.entries(parsed)) {
|
|
29
|
+
if (typeof v !== "string") {
|
|
30
|
+
console.error(`[PRIVACY] privacy.json 值必须为字符串,跳过 "${k}"`);
|
|
31
|
+
delete (parsed as Record<string, unknown>)[k];
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return parsed as PrivacyRules;
|
|
35
|
+
} catch (err) {
|
|
36
|
+
console.error(`[PRIVACY] 读取 privacy.json 失败: ${(err as Error).message}`);
|
|
37
|
+
return {};
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function getPrivacyRules(): PrivacyRules {
|
|
42
|
+
if (!loaded) {
|
|
43
|
+
rules = loadRules();
|
|
44
|
+
loaded = true;
|
|
45
|
+
}
|
|
46
|
+
return rules!;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** 重新加载规则(热更新用) */
|
|
50
|
+
export function reloadPrivacyRules(): void {
|
|
51
|
+
loaded = false;
|
|
52
|
+
rules = null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* 对文本应用隐私替换规则。
|
|
57
|
+
* 若无规则或文本为空,直接返回原文。
|
|
58
|
+
*/
|
|
59
|
+
export function applyPrivacy(text: string): string {
|
|
60
|
+
const r = getPrivacyRules();
|
|
61
|
+
if (Object.keys(r).length === 0 || !text) return text;
|
|
62
|
+
let result = text;
|
|
63
|
+
for (const [from, to] of Object.entries(r)) {
|
|
64
|
+
// 用 split+join 替代 replaceAll,避免正则特殊字符问题
|
|
65
|
+
result = result.split(from).join(to);
|
|
66
|
+
}
|
|
67
|
+
return result;
|
|
68
68
|
}
|
package/src/session.ts
CHANGED
|
@@ -393,7 +393,8 @@ export function accumulateBlockContent(
|
|
|
393
393
|
switch (block.type) {
|
|
394
394
|
case "thinking":
|
|
395
395
|
state.chunkCount++;
|
|
396
|
-
|
|
396
|
+
// 用引用块标记思考内容(中文无法斜体,引用块有视觉区分)
|
|
397
|
+
state.accumulatedContent += `\n> ${block.thinking.replace(/\n/g, "\n> ")}\n`;
|
|
397
398
|
break;
|
|
398
399
|
case "tool_use": {
|
|
399
400
|
// 记录 tool_use 信息供后续 tool_result 使用
|
package/src/simplify.ts
CHANGED
|
@@ -1,120 +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;
|
|
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
120
|
}
|