chatccc 0.2.55 → 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 -4
- package/config.sample.json +8 -7
- package/package.json +59 -59
- package/src/__tests__/claude-adapter.test.ts +48 -9
- package/src/__tests__/config-reload.test.ts +14 -10
- package/src/__tests__/config-sample.test.ts +22 -11
- 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 -6
- package/src/__tests__/wechat-platform.test.ts +25 -0
- package/src/adapters/claude-adapter.ts +49 -28
- package/src/adapters/codex-adapter.ts +4 -0
- package/src/adapters/cursor-adapter.ts +4 -0
- package/src/config.ts +14 -1
- package/src/index.ts +15 -12
- package/src/privacy.ts +67 -67
- package/src/session.ts +10 -3
- package/src/simplify.ts +119 -119
- 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,28 +84,35 @@ 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
|
-
const apiKeyTrim = (apiKey ?? "").trim();
|
|
88
|
-
const baseUrlTrim = (baseUrl ?? "").trim();
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
delete env.
|
|
97
|
-
delete env.
|
|
98
|
-
delete env.
|
|
99
|
-
delete env.
|
|
100
|
-
delete env.
|
|
101
|
-
delete env.
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
+
}
|
|
108
116
|
|
|
109
117
|
function resolveSettingSources(
|
|
110
118
|
_apiKey: string | undefined,
|
|
@@ -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
|
}
|
|
@@ -213,6 +222,7 @@ class ClaudeAdapter implements ToolAdapter {
|
|
|
213
222
|
readonly sessionDescPrefix = "Claude Code Session:";
|
|
214
223
|
private model: string;
|
|
215
224
|
private effort: string;
|
|
225
|
+
private subagentModel: string | undefined;
|
|
216
226
|
private isEmpty: (value: string) => boolean;
|
|
217
227
|
private apiKey: string | undefined;
|
|
218
228
|
private baseUrl: string | undefined;
|
|
@@ -220,6 +230,7 @@ class ClaudeAdapter implements ToolAdapter {
|
|
|
220
230
|
constructor(options: ClaudeAdapterOptions) {
|
|
221
231
|
this.model = options.model;
|
|
222
232
|
this.effort = options.effort;
|
|
233
|
+
this.subagentModel = options.subagentModel;
|
|
223
234
|
this.isEmpty = options.isEmpty;
|
|
224
235
|
this.apiKey = options.apiKey;
|
|
225
236
|
this.baseUrl = options.baseUrl;
|
|
@@ -233,6 +244,7 @@ class ClaudeAdapter implements ToolAdapter {
|
|
|
233
244
|
this.isEmpty,
|
|
234
245
|
this.apiKey,
|
|
235
246
|
this.baseUrl,
|
|
247
|
+
this.subagentModel,
|
|
236
248
|
);
|
|
237
249
|
const session = unstable_v2_createSession(sessionOpts as any);
|
|
238
250
|
|
|
@@ -277,13 +289,21 @@ class ClaudeAdapter implements ToolAdapter {
|
|
|
277
289
|
this.isEmpty,
|
|
278
290
|
this.apiKey,
|
|
279
291
|
this.baseUrl,
|
|
292
|
+
this.subagentModel,
|
|
280
293
|
);
|
|
281
|
-
const session = unstable_v2_resumeSession(
|
|
282
|
-
sessionId,
|
|
283
|
-
sessionOpts as any,
|
|
284
|
-
);
|
|
285
|
-
|
|
286
|
-
|
|
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);
|
|
287
307
|
|
|
288
308
|
const stream = session.stream();
|
|
289
309
|
|
|
@@ -296,6 +316,7 @@ class ClaudeAdapter implements ToolAdapter {
|
|
|
296
316
|
if (normalized) yield normalized;
|
|
297
317
|
}
|
|
298
318
|
} finally {
|
|
319
|
+
signal?.removeEventListener("abort", onAbort);
|
|
299
320
|
session.close();
|
|
300
321
|
}
|
|
301
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
|
}
|
|
@@ -346,6 +346,9 @@ class CursorAdapter implements ToolAdapter {
|
|
|
346
346
|
const proc = spawnAgent(["--resume", sessionId], cwd, userText);
|
|
347
347
|
this.activeProcs.add(proc);
|
|
348
348
|
|
|
349
|
+
const onAbort = () => { proc.kill(); };
|
|
350
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
351
|
+
|
|
349
352
|
try {
|
|
350
353
|
for await (const raw of readJsonLines(proc, signal)) {
|
|
351
354
|
if (signal?.aborted) break;
|
|
@@ -367,6 +370,7 @@ class CursorAdapter implements ToolAdapter {
|
|
|
367
370
|
if (normalized) yield normalized;
|
|
368
371
|
}
|
|
369
372
|
} finally {
|
|
373
|
+
signal?.removeEventListener("abort", onAbort);
|
|
370
374
|
proc.kill();
|
|
371
375
|
this.activeProcs.delete(proc);
|
|
372
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
|
@@ -84,8 +84,9 @@ import {
|
|
|
84
84
|
updateCardKitCard,
|
|
85
85
|
} from "./cardkit.ts";
|
|
86
86
|
import {
|
|
87
|
-
MAX_PROCESSED,
|
|
88
|
-
|
|
87
|
+
MAX_PROCESSED,
|
|
88
|
+
clearAdapterCache,
|
|
89
|
+
loadSessionRegistryForBinding,
|
|
89
90
|
processedMessages,
|
|
90
91
|
rebuildBindingsFromRegistry,
|
|
91
92
|
resetState,
|
|
@@ -743,11 +744,12 @@ async function main(): Promise<void> {
|
|
|
743
744
|
// "保存并启动" 时,web-ui 会调用本回调,把磁盘上刚保存的 config.json
|
|
744
745
|
// 刷进进程内的 export let 常量(live binding 让 CLAUDE_MODEL 等下次创建
|
|
745
746
|
// 会话时自动看到新值)。setup 首次激活走 onActivate 路径,不依赖此 hook。
|
|
746
|
-
setReloadConfigHook(() => {
|
|
747
|
-
reloadConfigFromDisk();
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
747
|
+
setReloadConfigHook(() => {
|
|
748
|
+
reloadConfigFromDisk();
|
|
749
|
+
clearAdapterCache();
|
|
750
|
+
appendStartupTrace("reload-from-ui: config reloaded", {
|
|
751
|
+
appIdMask: maskAppId(APP_ID),
|
|
752
|
+
});
|
|
751
753
|
});
|
|
752
754
|
setExtraApiHandler(async (req, res) => {
|
|
753
755
|
return (await handleAgentImageRequest(req, res)) || (await handleAgentFileRequest(req, res));
|
|
@@ -767,11 +769,12 @@ async function main(): Promise<void> {
|
|
|
767
769
|
// 凭证不全:进 setup 向导。注入 onActivate 回调让用户点"保存并启动"
|
|
768
770
|
// 时,原地(同进程)调用 startBotService,复用 setup HTTP server。
|
|
769
771
|
startSetupMode(CHATCCC_PORT, {
|
|
770
|
-
onActivate: async (httpServer: Server) => {
|
|
771
|
-
reloadConfigFromDisk();
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
772
|
+
onActivate: async (httpServer: Server) => {
|
|
773
|
+
reloadConfigFromDisk();
|
|
774
|
+
clearAdapterCache();
|
|
775
|
+
appendStartupTrace("setup-activate: reloaded config from disk", {
|
|
776
|
+
appIdMaskAfterReload: maskAppId(APP_ID),
|
|
777
|
+
});
|
|
775
778
|
try {
|
|
776
779
|
await startBotService({ httpServer, port: CHATCCC_PORT });
|
|
777
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
|
@@ -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,
|
|
@@ -172,6 +173,7 @@ export function getAdapterForTool(tool: string): ToolAdapter {
|
|
|
172
173
|
} else {
|
|
173
174
|
adapter = createClaudeAdapter({
|
|
174
175
|
model: CLAUDE_MODEL,
|
|
176
|
+
subagentModel: CLAUDE_SUBAGENT_MODEL,
|
|
175
177
|
effort: CLAUDE_EFFORT,
|
|
176
178
|
isEmpty: isAnthropicConfigEmpty,
|
|
177
179
|
apiKey: CLAUDE_API_KEY,
|
|
@@ -391,7 +393,8 @@ export function accumulateBlockContent(
|
|
|
391
393
|
switch (block.type) {
|
|
392
394
|
case "thinking":
|
|
393
395
|
state.chunkCount++;
|
|
394
|
-
|
|
396
|
+
// 用引用块标记思考内容(中文无法斜体,引用块有视觉区分)
|
|
397
|
+
state.accumulatedContent += `\n> ${block.thinking.replace(/\n/g, "\n> ")}\n`;
|
|
395
398
|
break;
|
|
396
399
|
case "tool_use": {
|
|
397
400
|
// 记录 tool_use 信息供后续 tool_result 使用
|
|
@@ -606,7 +609,7 @@ function formatToolConfigForLog(tool: string, sessionModel?: string): string {
|
|
|
606
609
|
: "effort=(由 codex config.toml 决定)";
|
|
607
610
|
return `model=${modelStr}, ${effortStr}`;
|
|
608
611
|
}
|
|
609
|
-
return `model=${anthropicConfigDisplay(CLAUDE_MODEL)}, effort=${anthropicConfigDisplay(CLAUDE_EFFORT)}`;
|
|
612
|
+
return `model=${anthropicConfigDisplay(CLAUDE_MODEL)}, subagentModel=${anthropicConfigDisplay(CLAUDE_SUBAGENT_MODEL)}, effort=${anthropicConfigDisplay(CLAUDE_EFFORT)}`;
|
|
610
613
|
}
|
|
611
614
|
|
|
612
615
|
export async function initClaudeSession(tool: string, overrideCwd?: string, chatId?: string): Promise<{ sessionId: string; cwd: string }> {
|
|
@@ -1322,6 +1325,10 @@ export function _setAdapterForToolForTest(tool: string, adapter: ToolAdapter): v
|
|
|
1322
1325
|
adapterCache.set(tool, adapter);
|
|
1323
1326
|
}
|
|
1324
1327
|
|
|
1325
|
-
export function
|
|
1328
|
+
export function clearAdapterCache(): void {
|
|
1326
1329
|
adapterCache.clear();
|
|
1327
1330
|
}
|
|
1331
|
+
|
|
1332
|
+
export function _clearAdapterCacheForTest(): void {
|
|
1333
|
+
clearAdapterCache();
|
|
1334
|
+
}
|