chatccc 0.2.133 → 0.2.135

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 CHANGED
@@ -205,7 +205,7 @@ ChatCCC 只负责把聊天消息转给本地 AI 工具,不捆绑这些 CLI。
205
205
 
206
206
  #### Claude Code
207
207
 
208
- 本机完成 Claude CLI 登录后即可使用。同时支持官方和第三方 Anthropic 兼容 API:使用官方服务无需额外配置,使用第三方 API 则填写 `claude.apiKey` 和 `claude.baseUrl`。`claude.model`、`claude.subagentModel`、`claude.effort`、`claude.apiKey`、`claude.baseUrl` 均为选填;`claude.maxTurn` 控制每次对话的最大轮数(默认 25)。填写后会把对应配置传给 Claude CLI 子进程;留空则以 `~/.claude/settings.json` 为准。
208
+ 本机完成 Claude Code 登录后即可使用。ChatCCC 通过 Anthropic Claude Agent SDK 调用 Claude Code 能力,同时支持官方和第三方 Anthropic 兼容 API:使用官方服务无需额外配置,使用第三方 API 则填写 `claude.apiKey` 和 `claude.baseUrl`。`claude.model`、`claude.subagentModel`、`claude.effort`、`claude.apiKey`、`claude.baseUrl` 均为选填;`claude.maxTurn` 控制每次对话的最大轮数(默认 25)。填写后会把对应配置传给 Claude Agent SDK;留空则以 `~/.claude/settings.json` 为准。
209
209
 
210
210
  #### Cursor Agent CLI
211
211
 
@@ -283,9 +283,9 @@ Codex 的默认模型和推理强度可继续由 `~/.codex/config.toml` 管理
283
283
  | `*.enabled` | 是否启用对应 AI Agent |
284
284
  | `*.defaultAgent` | `/new` 未指定 Agent 时使用哪个工具 |
285
285
  | `cursor.path` / `codex.path` | CLI 可执行文件路径;留空时自动探测或使用 PATH |
286
- | `claude.model` / `claude.subagentModel` / `claude.effort` | 选填;设置后传给 Claude CLI 子进程,留空以 `~/.claude/settings.json` 为准 |
287
- | `claude.apiKey` / `claude.baseUrl` | 选填;设置后传给 Claude CLI 子进程,留空以 `~/.claude/settings.json` 为准 |
288
- | `claude.maxTurn` | 选填;Claude CLI 最大对话轮数,默认 25,可在 Web UI 编辑 |
286
+ | `claude.model` / `claude.subagentModel` / `claude.effort` | 选填;设置后传给 Claude Agent SDK,留空以 `~/.claude/settings.json` 为准 |
287
+ | `claude.apiKey` / `claude.baseUrl` | 选填;设置后传给 Claude Agent SDK,留空以 `~/.claude/settings.json` 为准 |
288
+ | `claude.maxTurn` | 选填;Claude 最大对话轮数,默认 25,可在 Web UI 编辑 |
289
289
 
290
290
  > 当前 ChatCCC 以 `bypassPermissions` 模式运行,会跳过 Agent 操作确认。请只在可信环境中使用。
291
291
 
@@ -1,6 +1,6 @@
1
1
  # Claude-Specific Injection Prompt
2
2
 
3
- Use this prompt as the Claude CLI injection prompt for this project.
3
+ Use this prompt as the Claude Agent SDK injection prompt for this project.
4
4
 
5
5
  Project workspace:
6
6
 
@@ -10,7 +10,7 @@ f:\users\weizhangjian\feishuclauderprivate
10
10
 
11
11
  ## Repeated Successful Command Guard
12
12
 
13
- When working in this project through Claude CLI, repeated successful shell commands are a completion signal, not a reason to keep using tools.
13
+ When working in this project through Claude Agent SDK, repeated successful shell commands are a completion signal, not a reason to keep using tools.
14
14
 
15
15
  A shell command is considered the same command when all of these are true:
16
16
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chatccc",
3
- "version": "0.2.133",
3
+ "version": "0.2.135",
4
4
  "description": "Feishu bot bridge for Claude Code",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -41,7 +41,7 @@
41
41
  },
42
42
  "dependencies": {
43
43
  "@ai-sdk/openai-compatible": "^2.0.47",
44
- "@anthropic-ai/claude-agent-sdk": "^0.3.160",
44
+ "@anthropic-ai/claude-agent-sdk": "0.3.153",
45
45
  "@larksuiteoapi/node-sdk": "^1.59.0",
46
46
  "@openilink/openilink-sdk-node": "^0.6.0",
47
47
  "ai": "^6.0.184",
@@ -3,6 +3,7 @@ import {
3
3
  normalizeSdkMessage,
4
4
  createClaudeAdapter,
5
5
  buildClaudePromptText,
6
+ buildSdkEnv,
6
7
  } from "../adapters/claude-adapter.ts";
7
8
  import type { UnifiedStreamMessage } from "../adapters/adapter-interface.ts";
8
9
  import type {
@@ -463,3 +464,50 @@ describe("buildClaudePromptText", () => {
463
464
  expect(buildClaudePromptText("hello", null)).toBe("hello");
464
465
  });
465
466
  });
467
+
468
+ describe("buildSdkEnv", () => {
469
+ it("returns undefined when no SDK env override is configured", () => {
470
+ expect(buildSdkEnv("", " ", undefined)).toBeUndefined();
471
+ });
472
+
473
+ it("sets requested SDK env overrides and removes conflicting Claude auth/model vars", () => {
474
+ const original = {
475
+ ANTHROPIC_AUTH_TOKEN: process.env.ANTHROPIC_AUTH_TOKEN,
476
+ CLAUDE_CODE_OAUTH_TOKEN: process.env.CLAUDE_CODE_OAUTH_TOKEN,
477
+ ANTHROPIC_MODEL: process.env.ANTHROPIC_MODEL,
478
+ CLAUDE_CODE_EFFORT_LEVEL: process.env.CLAUDE_CODE_EFFORT_LEVEL,
479
+ CLAUDE_CODE_SUBAGENT_MODEL: process.env.CLAUDE_CODE_SUBAGENT_MODEL,
480
+ };
481
+
482
+ process.env.ANTHROPIC_AUTH_TOKEN = "token";
483
+ process.env.CLAUDE_CODE_OAUTH_TOKEN = "oauth";
484
+ process.env.ANTHROPIC_MODEL = "old-model";
485
+ process.env.CLAUDE_CODE_EFFORT_LEVEL = "old-effort";
486
+ process.env.CLAUDE_CODE_SUBAGENT_MODEL = "old-subagent";
487
+
488
+ try {
489
+ const env = buildSdkEnv(
490
+ " claude-haiku-4-5-20251001 ",
491
+ " sk-test ",
492
+ " https://api.example.com ",
493
+ );
494
+
495
+ expect(env).toBeDefined();
496
+ expect(env!.ANTHROPIC_AUTH_TOKEN).toBeUndefined();
497
+ expect(env!.CLAUDE_CODE_OAUTH_TOKEN).toBeUndefined();
498
+ expect(env!.ANTHROPIC_MODEL).toBeUndefined();
499
+ expect(env!.CLAUDE_CODE_EFFORT_LEVEL).toBeUndefined();
500
+ expect(env!.CLAUDE_CODE_SUBAGENT_MODEL).toBe("claude-haiku-4-5-20251001");
501
+ expect(env!.ANTHROPIC_API_KEY).toBe("sk-test");
502
+ expect(env!.ANTHROPIC_BASE_URL).toBe("https://api.example.com");
503
+ } finally {
504
+ for (const [key, value] of Object.entries(original)) {
505
+ if (value === undefined) {
506
+ delete process.env[key];
507
+ } else {
508
+ process.env[key] = value;
509
+ }
510
+ }
511
+ }
512
+ });
513
+ });
@@ -47,9 +47,10 @@ const baseAppConfig: AppConfig = {
47
47
  defaultAgent: true,
48
48
  model: "initial-model",
49
49
  subagentModel: "initial-subagent-model",
50
- effort: "initial-effort",
51
- apiKey: "",
52
- baseUrl: "",
50
+ effort: "initial-effort",
51
+ apiKey: "",
52
+ baseUrl: "",
53
+ maxTurn: 25,
53
54
  },
54
55
  cursor: { enabled: true, defaultAgent: false, path: "/initial/cursor", model: "initial-cursor-model" },
55
56
  codex: { enabled: true, defaultAgent: false, path: "/initial/codex", model: "initial-codex-model", effort: "initial-codex-effort" },
@@ -88,10 +89,11 @@ describe("applyLoadedConfig — 刷新 export let 常量", () => {
88
89
  defaultAgent: true,
89
90
  model: "claude-sonnet-4-6",
90
91
  subagentModel: "claude-haiku-4-5-20251001",
91
- effort: "high",
92
- apiKey: "sk-test",
93
- baseUrl: "https://api.example.com",
94
- },
92
+ effort: "high",
93
+ apiKey: "sk-test",
94
+ baseUrl: "https://api.example.com",
95
+ maxTurn: 25,
96
+ },
95
97
  });
96
98
 
97
99
  expect(CLAUDE_MODEL).toBe("claude-sonnet-4-6");
@@ -1,538 +1,467 @@
1
- // =============================================================================
2
- // claude-adapter.ts Claude CLI 适配器
3
- // =============================================================================
4
- // 通过直接 spawn claude.exe(不再使用 @anthropic-ai/claude-agent-sdk JS API)
5
- // 以确保 --mcp-config 参数能正确传递给 CLI 子进程,加载用户 MCP 服务器。
6
- // =============================================================================
7
-
8
- import { spawn, type ChildProcess } from "node:child_process";
9
- import { createInterface } from "node:readline";
10
- import { existsSync, readFileSync } from "node:fs";
11
- import { join, dirname } from "node:path";
12
- import { homedir } from "node:os";
13
- import { createRequire } from "node:module";
14
- import { fileURLToPath } from "node:url";
15
-
16
- import type {
17
- ToolAdapter,
18
- UnifiedBlock,
19
- UnifiedStreamMessage,
20
- CreateSessionResult,
21
- SessionInfo,
22
- ToolPromptOptions,
23
- } from "./adapter-interface.ts";
24
- import { killProcessTree } from "./proc-tree-kill.ts";
25
- import {
26
- defaultClaudeSessionMetaStore,
27
- type ClaudeSessionMetaStore,
28
- } from "./claude-session-meta-store.ts";
29
-
30
- // ---------------------------------------------------------------------------
31
- // 常量
32
- // ---------------------------------------------------------------------------
33
-
34
- /** 根据当前平台动态解析 claude-agent-sdk 二进制路径 */
35
- function resolveClaudeBinary(): string {
36
- const platform = process.platform; // 'win32', 'linux', 'darwin'
37
- const arch = process.arch; // 'x64', 'arm64'
38
-
39
- // 通过 Node 模块解析找到 SDK 主包目录,确保测试和生产环境一致
40
- const _require = createRequire(import.meta.url);
41
- const sdkMainDir = dirname(_require.resolve("@anthropic-ai/claude-agent-sdk"));
42
-
43
- // 读取 manifest.json 获取各平台二进制文件名
44
- let platforms: Record<string, { binary: string }> | null = null;
45
- try {
46
- const manifest = JSON.parse(
47
- readFileSync(join(sdkMainDir, "manifest.json"), "utf-8"),
48
- ) as { platforms?: Record<string, { binary: string }> };
49
- platforms = manifest.platforms ?? null;
50
- } catch { /* manifest 缺失时用默认规则推断 */ }
51
-
52
- // Linux 上需区分 musl / glibc,先检测 musl 变体
53
- const candidates: string[] = [];
54
- if (platform === "linux") {
55
- candidates.push(`${platform}-${arch}-musl`, `${platform}-${arch}`);
56
- } else {
57
- candidates.push(`${platform}-${arch}`);
58
- }
59
-
60
- for (const triple of candidates) {
61
- const binaryName = platforms?.[triple]?.binary
62
- ?? (platform === "win32" ? "claude.exe" : "claude");
63
- const pkgDir = join(sdkMainDir, "..", `claude-agent-sdk-${triple}`);
64
- if (!existsSync(pkgDir)) continue;
65
- const exePath = join(pkgDir, binaryName);
66
- if (existsSync(exePath)) return exePath;
67
- }
68
-
69
- throw new Error(
70
- `No Claude CLI binary found for platform ${platform}-${arch}. ` +
71
- `Tried: ${candidates.map((c) => `@anthropic-ai/claude-agent-sdk-${c}`).join(", ")}`,
72
- );
73
- }
74
-
75
- const CLAUDE_EXE = resolveClaudeBinary();
76
- const PROJECT_ROOT = dirname(dirname(dirname(fileURLToPath(import.meta.url))));
77
- const CLAUDE_SPECIFIC_PROMPT_PATH = join(
78
- PROJECT_ROOT,
79
- "agent-prompts",
80
- "claude_specific.md",
81
- );
82
-
83
- // ---------------------------------------------------------------------------
84
- // 类型别名(CLI JSONL 消息格式,与旧 SDK 消息格式兼容)
85
- // ---------------------------------------------------------------------------
86
-
87
- interface SdkContentBlock {
88
- type: string;
89
- text?: string;
90
- thinking?: string;
91
- name?: string;
92
- input?: unknown;
93
- tool_use_id?: string;
94
- content?: unknown;
95
- is_error?: boolean;
96
- query?: string;
97
- [key: string]: unknown;
98
- }
99
-
100
- interface SdkMessageLike {
101
- type?: string;
102
- subtype?: string;
103
- message?: { content?: SdkContentBlock[] };
104
- compact_metadata?: {
105
- trigger?: "manual" | "auto";
106
- pre_tokens?: number;
107
- post_tokens?: number;
108
- };
109
- session_id?: string;
110
- model?: string;
111
- cwd?: string;
112
- }
113
-
114
- // ---------------------------------------------------------------------------
115
- // ClaudeAdapterOptions
116
- // ---------------------------------------------------------------------------
117
-
118
- export interface ClaudeAdapterOptions {
119
- model: string;
120
- subagentModel?: string;
121
- effort: string;
122
- /** Anthropic API Key(选填,留空则不注入环境变量) */
123
- apiKey?: string;
124
- /** Anthropic 兼容 API Base URL(选填,留空则不注入环境变量) */
125
- baseUrl?: string;
126
- /** 判断字段是否为"不传给 CLI"的占位(空字符串/全空白) */
127
- isEmpty: (value: string) => boolean;
128
- /** 注入自定义 meta 持久化 store(测试用);未提供时使用全局默认实例。 */
129
- metaStore?: ClaudeSessionMetaStore;
130
- /** Claude CLI maxTurns 设置,默认 25 */
131
- maxTurn?: number;
132
- }
133
-
134
- // ---------------------------------------------------------------------------
135
- // buildCliEnv 为 CLI 子进程构造 env(仅子进程级别,不污染主进程)
136
- // ---------------------------------------------------------------------------
137
-
138
- function buildCliEnv(
139
- subagentModel: string | undefined,
140
- apiKey: string | undefined,
141
- baseUrl: string | undefined,
142
- ): Record<string, string | undefined> | undefined {
143
- const subagentModelTrim = (subagentModel ?? "").trim();
144
- const apiKeyTrim = (apiKey ?? "").trim();
145
- const baseUrlTrim = (baseUrl ?? "").trim();
146
-
147
- if (!subagentModelTrim && !apiKeyTrim && !baseUrlTrim) return undefined;
148
-
149
- const env: Record<string, string | undefined> = { ...process.env };
150
- delete env.ANTHROPIC_AUTH_TOKEN;
151
- delete env.CLAUDE_CODE_OAUTH_TOKEN;
152
- delete env.ANTHROPIC_MODEL;
153
- delete env.ANTHROPIC_DEFAULT_OPUS_MODEL;
154
- delete env.ANTHROPIC_DEFAULT_SONNET_MODEL;
155
- delete env.ANTHROPIC_DEFAULT_HAIKU_MODEL;
156
- delete env.CLAUDE_CODE_EFFORT_LEVEL;
157
-
158
- if (subagentModelTrim) env.CLAUDE_CODE_SUBAGENT_MODEL = subagentModelTrim;
159
- if (apiKeyTrim) env.ANTHROPIC_API_KEY = apiKeyTrim;
160
- if (baseUrlTrim) env.ANTHROPIC_BASE_URL = baseUrlTrim;
161
-
162
- return env;
163
- }
164
-
165
- // ---------------------------------------------------------------------------
166
- // MCP 配置读取
167
- // ---------------------------------------------------------------------------
168
-
169
- function readMcpConfigJson(): string | null {
170
- const settingsPath = join(homedir(), ".claude", "settings.json");
171
- try {
172
- if (!existsSync(settingsPath)) return null;
173
- const raw = readFileSync(settingsPath, "utf-8");
174
- const settings = JSON.parse(raw);
175
- const mcpServers = settings?.mcpServers;
176
- if (!mcpServers || Object.keys(mcpServers).length === 0) return null;
177
- // CLI 期望 {"mcpServers": {...}} 格式,而非裸 mcpServers 值
178
- return JSON.stringify({ mcpServers });
179
- } catch {
180
- return null;
181
- }
182
- }
183
-
184
- // ---------------------------------------------------------------------------
185
- // 诊断日志
186
- // ---------------------------------------------------------------------------
187
-
188
- function logMcpConfig(): void {
189
- const settingsPath = join(homedir(), ".claude", "settings.json");
190
- const ts = new Date().toISOString();
191
- try {
192
- if (!existsSync(settingsPath)) {
193
- console.log(`[${ts}] [MCP-DIAG] settings.json not found at ${settingsPath}`);
194
- return;
195
- }
196
- const raw = readFileSync(settingsPath, "utf-8");
197
- const settings = JSON.parse(raw);
198
- const mcpServers = settings?.mcpServers;
199
- if (!mcpServers || Object.keys(mcpServers).length === 0) {
200
- console.log(`[${ts}] [MCP-DIAG] No mcpServers configured in settings.json`);
201
- return;
202
- }
203
- console.log(`[${ts}] [MCP-DIAG] mcpServers found: ${JSON.stringify(Object.keys(mcpServers))}`);
204
- for (const [name, cfg] of Object.entries(mcpServers) as [string, { type?: string; command?: string; args?: string[] }][]) {
205
- console.log(`[${ts}] [MCP-DIAG] ${name}: type=${cfg.type}, command=${cfg.command}, args=${JSON.stringify(cfg.args)}`);
206
- }
207
- } catch (err) {
208
- console.log(`[${ts}] [MCP-DIAG] Failed to read settings.json: ${(err as Error).message}`);
209
- }
210
- }
211
-
212
- // ---------------------------------------------------------------------------
213
- // normalizeSdkMessage — CLI JSONL 消息 → UnifiedStreamMessage | null
214
- // (CLI 与 SDK 使用相同 JSONL 格式,导出名保留以兼容现有测试)
215
- // ---------------------------------------------------------------------------
216
-
217
- export function normalizeSdkMessage(msg: SdkMessageLike): UnifiedStreamMessage | null {
218
- if (
219
- (msg.type === "assistant" || msg.type === "user") &&
220
- msg.message?.content
221
- ) {
222
- const blocks: UnifiedBlock[] = [];
223
- for (const block of msg.message.content) {
224
- if (block.type === "thinking" && block.thinking) {
225
- blocks.push({ type: "thinking", thinking: block.thinking });
226
- } else if (block.type === "tool_use") {
227
- blocks.push({
228
- type: "tool_use",
229
- id: (block as { id?: string }).id,
230
- name: block.name ?? "unknown",
231
- input: block.input,
232
- });
233
- } else if (block.type === "tool_result") {
234
- blocks.push({
235
- type: "tool_result",
236
- tool_use_id: block.tool_use_id ?? "",
237
- content: block.content,
238
- is_error: block.is_error,
239
- });
240
- } else if (block.type === "redacted_thinking") {
241
- blocks.push({ type: "redacted_thinking" });
242
- } else if (block.type === "search_result") {
243
- blocks.push({
244
- type: "search_result",
245
- query: block.query ?? "",
246
- });
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { dirname, join } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+
6
+ import {
7
+ getSessionInfo as sdkGetSessionInfo,
8
+ query,
9
+ type Options as ClaudeSdkOptions,
10
+ type SDKMessage,
11
+ } from "@anthropic-ai/claude-agent-sdk";
12
+
13
+ import type {
14
+ CreateSessionResult,
15
+ SessionInfo,
16
+ ToolAdapter,
17
+ ToolPromptOptions,
18
+ UnifiedBlock,
19
+ UnifiedStreamMessage,
20
+ } from "./adapter-interface.ts";
21
+ import {
22
+ defaultClaudeSessionMetaStore,
23
+ type ClaudeSessionMetaStore,
24
+ } from "./claude-session-meta-store.ts";
25
+
26
+ const PROJECT_ROOT = dirname(dirname(dirname(fileURLToPath(import.meta.url))));
27
+ const CLAUDE_SPECIFIC_PROMPT_PATH = join(
28
+ PROJECT_ROOT,
29
+ "agent-prompts",
30
+ "claude_specific.md",
31
+ );
32
+
33
+ type SettingSource = "user" | "project" | "local";
34
+
35
+ interface SdkContentBlock {
36
+ type: string;
37
+ text?: string;
38
+ thinking?: string;
39
+ name?: string;
40
+ input?: unknown;
41
+ tool_use_id?: string;
42
+ content?: unknown;
43
+ is_error?: boolean;
44
+ query?: string;
45
+ [key: string]: unknown;
46
+ }
47
+
48
+ interface SdkMessageLike {
49
+ type?: string;
50
+ subtype?: string;
51
+ message?: { content?: SdkContentBlock[] };
52
+ compact_metadata?: {
53
+ trigger?: "manual" | "auto";
54
+ pre_tokens?: number;
55
+ post_tokens?: number;
56
+ };
57
+ session_id?: string;
58
+ model?: string;
59
+ cwd?: string;
60
+ }
61
+
62
+ export interface ClaudeAdapterOptions {
63
+ model: string;
64
+ subagentModel?: string;
65
+ effort: string;
66
+ apiKey?: string;
67
+ baseUrl?: string;
68
+ isEmpty: (value: string) => boolean;
69
+ metaStore?: ClaudeSessionMetaStore;
70
+ maxTurn?: number;
71
+ }
72
+
73
+ export function buildSdkEnv(
74
+ subagentModel: string | undefined,
75
+ apiKey: string | undefined,
76
+ baseUrl: string | undefined,
77
+ ): Record<string, string | undefined> | undefined {
78
+ const subagentModelTrim = (subagentModel ?? "").trim();
79
+ const apiKeyTrim = (apiKey ?? "").trim();
80
+ const baseUrlTrim = (baseUrl ?? "").trim();
81
+
82
+ if (!subagentModelTrim && !apiKeyTrim && !baseUrlTrim) return undefined;
83
+
84
+ const env: Record<string, string | undefined> = { ...process.env };
85
+ delete env.ANTHROPIC_AUTH_TOKEN;
86
+ delete env.CLAUDE_CODE_OAUTH_TOKEN;
87
+ delete env.ANTHROPIC_MODEL;
88
+ delete env.ANTHROPIC_DEFAULT_OPUS_MODEL;
89
+ delete env.ANTHROPIC_DEFAULT_SONNET_MODEL;
90
+ delete env.ANTHROPIC_DEFAULT_HAIKU_MODEL;
91
+ delete env.CLAUDE_CODE_EFFORT_LEVEL;
92
+ delete env.CLAUDE_CODE_SUBAGENT_MODEL;
93
+
94
+ if (subagentModelTrim) env.CLAUDE_CODE_SUBAGENT_MODEL = subagentModelTrim;
95
+ if (apiKeyTrim) env.ANTHROPIC_API_KEY = apiKeyTrim;
96
+ if (baseUrlTrim) env.ANTHROPIC_BASE_URL = baseUrlTrim;
97
+
98
+ return env;
99
+ }
100
+
101
+ function readMcpServersConfig(): Record<string, unknown> | undefined {
102
+ const settingsPath = join(homedir(), ".claude", "settings.json");
103
+ try {
104
+ if (!existsSync(settingsPath)) return undefined;
105
+ const raw = readFileSync(settingsPath, "utf-8");
106
+ const settings = JSON.parse(raw) as { mcpServers?: Record<string, unknown> };
107
+ const mcpServers = settings.mcpServers;
108
+ if (!mcpServers || Object.keys(mcpServers).length === 0) return undefined;
109
+ return mcpServers;
110
+ } catch {
111
+ return undefined;
112
+ }
113
+ }
114
+
115
+ function logMcpConfig(): void {
116
+ const settingsPath = join(homedir(), ".claude", "settings.json");
117
+ const ts = new Date().toISOString();
118
+ try {
119
+ if (!existsSync(settingsPath)) {
120
+ console.log(`[${ts}] [MCP-DIAG] settings.json not found at ${settingsPath}`);
121
+ return;
122
+ }
123
+ const raw = readFileSync(settingsPath, "utf-8");
124
+ const settings = JSON.parse(raw) as { mcpServers?: Record<string, unknown> };
125
+ const mcpServers = settings.mcpServers;
126
+ if (!mcpServers || Object.keys(mcpServers).length === 0) {
127
+ console.log(`[${ts}] [MCP-DIAG] No mcpServers configured in settings.json`);
128
+ return;
129
+ }
130
+ console.log(`[${ts}] [MCP-DIAG] mcpServers found: ${JSON.stringify(Object.keys(mcpServers))}`);
131
+ for (const [name, cfg] of Object.entries(mcpServers)) {
132
+ const item = cfg as { type?: string; command?: string; args?: string[] };
133
+ console.log(`[${ts}] [MCP-DIAG] ${name}: type=${item.type}, command=${item.command}, args=${JSON.stringify(item.args)}`);
134
+ }
135
+ } catch (err) {
136
+ console.log(`[${ts}] [MCP-DIAG] Failed to read settings.json: ${(err as Error).message}`);
137
+ }
138
+ }
139
+
140
+ export function normalizeSdkMessage(msg: SdkMessageLike): UnifiedStreamMessage | null {
141
+ if (
142
+ (msg.type === "assistant" || msg.type === "user") &&
143
+ msg.message?.content
144
+ ) {
145
+ const blocks: UnifiedBlock[] = [];
146
+ for (const block of msg.message.content) {
147
+ if (block.type === "thinking" && block.thinking) {
148
+ blocks.push({ type: "thinking", thinking: block.thinking });
149
+ } else if (block.type === "tool_use") {
150
+ blocks.push({
151
+ type: "tool_use",
152
+ id: (block as { id?: string }).id,
153
+ name: block.name ?? "unknown",
154
+ input: block.input,
155
+ });
156
+ } else if (block.type === "tool_result") {
157
+ blocks.push({
158
+ type: "tool_result",
159
+ tool_use_id: block.tool_use_id ?? "",
160
+ content: block.content,
161
+ is_error: block.is_error,
162
+ });
163
+ } else if (block.type === "redacted_thinking") {
164
+ blocks.push({ type: "redacted_thinking" });
165
+ } else if (block.type === "search_result") {
166
+ blocks.push({
167
+ type: "search_result",
168
+ query: block.query ?? "",
169
+ });
247
170
  } else if (block.type === "text" && block.text) {
248
- // user text is host input, not assistant output. Keep it out of the
249
- // final reply if the CLI ever emits it.
250
171
  if (msg.type === "user") continue;
251
172
  blocks.push({ type: "text", text: block.text });
252
173
  }
253
- }
254
- return { type: msg.type, blocks };
255
- }
256
-
257
- if (msg.type === "system" && msg.subtype === "compact_boundary") {
258
- const meta = msg.compact_metadata;
259
- if (!meta) return null;
260
- return {
261
- type: "system",
262
- blocks: [
263
- {
264
- type: "compact_boundary",
265
- trigger: meta.trigger ?? "auto",
266
- pre_tokens: meta.pre_tokens ?? 0,
267
- post_tokens: meta.post_tokens,
268
- },
269
- ],
270
- };
271
- }
272
-
273
- return null;
274
- }
275
-
276
- // ---------------------------------------------------------------------------
277
- // CLI spawn helpers
278
- // ---------------------------------------------------------------------------
279
-
280
- function buildCliArgs(
281
- model: string,
282
- effort: string,
283
- isEmpty: (value: string) => boolean,
284
- mcpConfigJson: string | null,
285
- extraArgs: string[],
286
- maxTurn: number,
287
- planMode?: boolean,
288
- ): string[] {
289
- const args = [
290
- "-p",
291
- "--output-format", "stream-json",
292
- "--verbose",
293
- "--setting-sources", "user,project,local",
294
- "--permission-mode", planMode ? "plan" : "bypassPermissions",
295
- "--settings", `{"maxTurns":${maxTurn},"maxBudget":999999999}`,
296
- ];
297
- if (!planMode) args.push("--dangerously-skip-permissions");
298
-
299
- if (!isEmpty(model)) args.push("--model", model);
300
- if (!isEmpty(effort)) args.push("--effort", effort);
301
-
302
- // extraArgs(如 prompt "ok" 或 --resume <id>)必须在 --mcp-config 之前,
303
- // 因为 --mcp-config 接受多个空格分隔的值,会把后续非 flag 参数都当作 MCP 配置
304
- args.push(...extraArgs);
305
-
306
- if (mcpConfigJson) args.push("--mcp-config", mcpConfigJson);
307
-
308
- const ts = new Date().toISOString();
309
- const safeArgs = args.filter(a => a !== mcpConfigJson);
310
- console.log(`[${ts}] [CLAUDE-CLI] spawn: ${CLAUDE_EXE} ${safeArgs.join(" ")}`);
311
- if (mcpConfigJson) {
312
- console.log(`[${ts}] [CLAUDE-CLI] --mcp-config: ${mcpConfigJson}`);
313
- }
314
-
315
- return args;
316
- }
317
-
318
- function spawnCli(
319
- args: string[],
320
- cwd: string,
321
- env: Record<string, string | undefined> | undefined,
322
- pipeStdin: boolean,
323
- ): ChildProcess {
324
- const proc = spawn(CLAUDE_EXE, args, {
325
- cwd,
326
- stdio: [pipeStdin ? "pipe" : "ignore", "pipe", "pipe"],
327
- windowsHide: true,
328
- env: env ?? process.env,
329
- });
330
-
331
- const ts = new Date().toISOString();
332
- console.log(`[${ts}] [CLAUDE-CLI] spawned, pid=${proc.pid}`);
333
-
334
- let stderr = "";
335
- proc.stderr!.on("data", (chunk: Buffer) => { stderr += chunk.toString(); });
336
- proc.on("close", (code) => {
337
- if (stderr.trim()) {
338
- const ts2 = new Date().toISOString();
339
- console.log(`[${ts2}] [CLAUDE-STDERR] exit=${code}: ${stderr.trim().slice(0, 2000)}`);
340
- }
341
- });
342
-
343
- return proc;
344
- }
345
-
346
- async function* readJsonLines(
347
- proc: ChildProcess,
348
- signal?: AbortSignal,
349
- ): AsyncGenerator<SdkMessageLike> {
350
- const rl = createInterface({ input: proc.stdout!, crlfDelay: Infinity });
351
- const onAbort = () => { rl.close(); };
352
- signal?.addEventListener("abort", onAbort, { once: true });
353
- let lineCount = 0;
354
- try {
355
- for await (const line of rl) {
356
- if (signal?.aborted) break;
357
- lineCount++;
358
- const trimmed = line.trim();
359
- if (!trimmed) continue;
360
- try {
361
- yield JSON.parse(trimmed) as SdkMessageLike;
362
- } catch { /* 非 JSON 行静默跳过 */ }
363
- }
364
- } finally {
365
- signal?.removeEventListener("abort", onAbort);
366
- rl.close();
367
- const ts = new Date().toISOString();
368
- console.log(`[${ts}] [CLAUDE-CLI] readJsonLines done: ${lineCount} raw lines`);
369
- }
370
- }
371
-
372
- /** 构造 stream-json 格式的 stdin 输入行 */
373
- function buildStreamJsonInput(text: string): string {
374
- return JSON.stringify({
375
- type: "user",
376
- message: {
377
- role: "user",
378
- content: [{ type: "text", text }],
379
- },
380
- }) + "\n";
381
- }
382
-
383
- export function readClaudeSpecificInjectionPrompt(): string | null {
384
- try {
385
- if (!existsSync(CLAUDE_SPECIFIC_PROMPT_PATH)) return null;
386
- const prompt = readFileSync(CLAUDE_SPECIFIC_PROMPT_PATH, "utf-8").trim();
387
- return prompt.length > 0 ? prompt : null;
388
- } catch {
389
- return null;
390
- }
391
- }
392
-
393
- export function buildClaudePromptText(
394
- userText: string,
395
- injectionPrompt: string | null = readClaudeSpecificInjectionPrompt(),
396
- ): string {
397
- const prompt = injectionPrompt?.trim();
398
- if (!prompt) return userText;
399
-
400
- return [
401
- "[ChatCCC Claude-specific injection prompt]",
402
- prompt,
403
- "[/ChatCCC Claude-specific injection prompt]",
404
- "",
405
- userText,
406
- ].join("\n");
407
- }
408
-
409
- // ---------------------------------------------------------------------------
410
- // ClaudeAdapter
411
- // ---------------------------------------------------------------------------
412
-
413
- class ClaudeAdapter implements ToolAdapter {
414
- readonly displayName = "Claude Code";
415
- readonly sessionDescPrefix = "Claude Code Session:";
416
- private model: string;
417
- private effort: string;
418
- private subagentModel: string | undefined;
419
- private apiKey: string | undefined;
420
- private baseUrl: string | undefined;
421
- private isEmpty: (value: string) => boolean;
422
- private metaStore: ClaudeSessionMetaStore;
423
- private maxTurn: number;
424
-
425
- constructor(options: ClaudeAdapterOptions) {
426
- this.model = options.model;
427
- this.effort = options.effort;
428
- this.subagentModel = options.subagentModel;
429
- this.apiKey = options.apiKey;
430
- this.baseUrl = options.baseUrl;
431
- this.isEmpty = options.isEmpty;
432
- this.metaStore = options.metaStore ?? defaultClaudeSessionMetaStore;
433
- this.maxTurn = options.maxTurn ?? 25;
434
- }
435
-
436
- async createSession(cwd: string): Promise<CreateSessionResult> {
437
- logMcpConfig();
438
- const mcpConfigJson = readMcpConfigJson();
439
- const env = buildCliEnv(this.subagentModel, this.apiKey, this.baseUrl);
440
- const args = buildCliArgs(
441
- this.model, this.effort, this.isEmpty, mcpConfigJson,
442
- ["ok"],
443
- this.maxTurn,
444
- );
445
-
446
- const proc = spawnCli(args, cwd, env, false);
447
-
448
- for await (const msg of readJsonLines(proc)) {
449
- if (msg.session_id) {
450
- const sessionId = msg.session_id;
451
- await this.metaStore.set(sessionId, { cwd }).catch(() => {});
452
- await killProcessTree(proc.pid);
453
- const ts = new Date().toISOString();
454
- console.log(`[${ts}] [CLAUDE-CLI] createSession: ${sessionId}`);
455
- return { sessionId };
456
- }
457
- }
458
-
459
- await killProcessTree(proc.pid);
460
- throw new Error("No session ID in Claude init event");
461
- }
462
-
463
- async *prompt(
464
- sessionId: string,
465
- userText: string,
466
- cwd: string,
467
- signal?: AbortSignal,
468
- options?: ToolPromptOptions,
469
- ): AsyncIterable<UnifiedStreamMessage> {
470
- const mcpConfigJson = readMcpConfigJson();
471
- const env = buildCliEnv(this.subagentModel, this.apiKey, this.baseUrl);
472
- const args = buildCliArgs(
473
- this.model, this.effort, this.isEmpty, mcpConfigJson,
474
- ["--resume", sessionId, "--input-format", "stream-json"],
475
- this.maxTurn,
476
- options?.planMode,
477
- );
478
-
479
- const proc = spawnCli(args, cwd, env, true);
480
- if (proc.pid !== undefined) options?.onProcessStart?.({ pid: proc.pid });
481
-
482
- const onAbort = () => { void killProcessTree(proc.pid); };
483
- signal?.addEventListener("abort", onAbort, { once: true });
484
-
485
- proc.stdin!.write(buildStreamJsonInput(buildClaudePromptText(userText)));
486
- proc.stdin!.end();
487
-
488
- try {
489
- for await (const raw of readJsonLines(proc, signal)) {
490
- if (signal?.aborted) break;
491
-
492
- if (raw.type === "system" && raw.subtype === "init" && raw.session_id) {
493
- const meta: { cwd?: string; model?: string } = {};
494
- if (raw.cwd) meta.cwd = raw.cwd;
495
- if (raw.model) meta.model = raw.model;
496
- if (Object.keys(meta).length > 0) {
497
- this.metaStore.set(raw.session_id, meta).catch(() => {});
498
- }
499
- }
500
-
501
- const normalized = normalizeSdkMessage(raw);
502
- if (normalized) yield normalized;
503
-
504
- // result 是流末事件,收到后立即结束进程,防止 CLI 僵死导致 readline 挂起。
505
- if (raw.type === "result") {
506
- void killProcessTree(proc.pid);
507
- break;
508
- }
509
- }
510
- } finally {
511
- signal?.removeEventListener("abort", onAbort);
512
- await killProcessTree(proc.pid);
513
- if (proc.pid !== undefined) options?.onProcessExit?.({ pid: proc.pid });
514
- }
515
- }
516
-
517
- async getSessionInfo(sessionId: string): Promise<SessionInfo | undefined> {
518
- const meta = await this.metaStore.get(sessionId);
519
- if (!meta) return { sessionId };
520
- return meta.model
521
- ? { sessionId, cwd: meta.cwd, model: meta.model }
522
- : { sessionId, cwd: meta.cwd };
523
- }
524
-
525
- async closeSession(_sessionId: string): Promise<void> {
526
- // 子进程由 prompt 的 finally 自动 kill
527
- }
528
- }
529
-
530
- // ---------------------------------------------------------------------------
531
- // 工厂函数
532
- // ---------------------------------------------------------------------------
533
-
534
- export function createClaudeAdapter(
535
- options: ClaudeAdapterOptions,
536
- ): ToolAdapter {
537
- return new ClaudeAdapter(options);
538
- }
174
+ }
175
+ return { type: msg.type, blocks };
176
+ }
177
+
178
+ if (msg.type === "system" && msg.subtype === "compact_boundary") {
179
+ const meta = msg.compact_metadata;
180
+ if (!meta) return null;
181
+ return {
182
+ type: "system",
183
+ blocks: [
184
+ {
185
+ type: "compact_boundary",
186
+ trigger: meta.trigger ?? "auto",
187
+ pre_tokens: meta.pre_tokens ?? 0,
188
+ post_tokens: meta.post_tokens,
189
+ },
190
+ ],
191
+ };
192
+ }
193
+
194
+ return null;
195
+ }
196
+
197
+ export function readClaudeSpecificInjectionPrompt(): string | null {
198
+ try {
199
+ if (!existsSync(CLAUDE_SPECIFIC_PROMPT_PATH)) return null;
200
+ const prompt = readFileSync(CLAUDE_SPECIFIC_PROMPT_PATH, "utf-8").trim();
201
+ return prompt.length > 0 ? prompt : null;
202
+ } catch {
203
+ return null;
204
+ }
205
+ }
206
+
207
+ export function buildClaudePromptText(
208
+ userText: string,
209
+ injectionPrompt: string | null = readClaudeSpecificInjectionPrompt(),
210
+ ): string {
211
+ const prompt = injectionPrompt?.trim();
212
+ if (!prompt) return userText;
213
+
214
+ return [
215
+ "[ChatCCC Claude-specific injection prompt]",
216
+ prompt,
217
+ "[/ChatCCC Claude-specific injection prompt]",
218
+ "",
219
+ userText,
220
+ ].join("\n");
221
+ }
222
+
223
+ function buildSdkOptions(args: {
224
+ cwd: string;
225
+ model: string;
226
+ effort: string;
227
+ isEmpty: (value: string) => boolean;
228
+ subagentModel?: string;
229
+ apiKey?: string;
230
+ baseUrl?: string;
231
+ maxTurn: number;
232
+ planMode?: boolean;
233
+ resume?: string;
234
+ abortController?: AbortController;
235
+ }): ClaudeSdkOptions {
236
+ const {
237
+ cwd,
238
+ model,
239
+ effort,
240
+ isEmpty,
241
+ subagentModel,
242
+ apiKey,
243
+ baseUrl,
244
+ maxTurn,
245
+ planMode,
246
+ resume,
247
+ abortController,
248
+ } = args;
249
+
250
+ const options: ClaudeSdkOptions = {
251
+ cwd,
252
+ abortController,
253
+ settingSources: ["user", "project", "local"] as SettingSource[],
254
+ permissionMode: planMode ? "plan" : "bypassPermissions",
255
+ maxTurns: maxTurn,
256
+ maxBudgetUsd: 999999999,
257
+ skills: "all",
258
+ stderr: (data) => {
259
+ const trimmed = data.trim();
260
+ if (!trimmed) return;
261
+ const ts = new Date().toISOString();
262
+ console.log(`[${ts}] [CLAUDE-STDERR] ${trimmed.slice(0, 2000)}`);
263
+ },
264
+ };
265
+
266
+ if (!planMode) {
267
+ options.allowDangerouslySkipPermissions = true;
268
+ }
269
+ if (!isEmpty(model)) {
270
+ options.model = model;
271
+ }
272
+ if (!isEmpty(effort)) {
273
+ options.effort = effort as ClaudeSdkOptions["effort"];
274
+ }
275
+ if (resume) {
276
+ options.resume = resume;
277
+ }
278
+
279
+ const env = buildSdkEnv(subagentModel, apiKey, baseUrl);
280
+ if (env) {
281
+ options.env = env;
282
+ }
283
+
284
+ const mcpServers = readMcpServersConfig();
285
+ if (mcpServers) {
286
+ options.mcpServers = mcpServers as ClaudeSdkOptions["mcpServers"];
287
+ }
288
+
289
+ return options;
290
+ }
291
+
292
+ function toMessageLike(message: SDKMessage): SdkMessageLike {
293
+ return message as unknown as SdkMessageLike;
294
+ }
295
+
296
+ function bridgeAbortSignal(
297
+ signal: AbortSignal | undefined,
298
+ controller: AbortController,
299
+ ): (() => void) | undefined {
300
+ if (!signal) return undefined;
301
+ const onAbort = () => controller.abort();
302
+ if (signal.aborted) {
303
+ controller.abort();
304
+ return undefined;
305
+ }
306
+ signal.addEventListener("abort", onAbort, { once: true });
307
+ return () => signal.removeEventListener("abort", onAbort);
308
+ }
309
+
310
+ class ClaudeAdapter implements ToolAdapter {
311
+ readonly displayName = "Claude Code";
312
+ readonly sessionDescPrefix = "Claude Code Session:";
313
+ private model: string;
314
+ private effort: string;
315
+ private subagentModel: string | undefined;
316
+ private apiKey: string | undefined;
317
+ private baseUrl: string | undefined;
318
+ private isEmpty: (value: string) => boolean;
319
+ private metaStore: ClaudeSessionMetaStore;
320
+ private maxTurn: number;
321
+
322
+ constructor(options: ClaudeAdapterOptions) {
323
+ this.model = options.model;
324
+ this.effort = options.effort;
325
+ this.subagentModel = options.subagentModel;
326
+ this.apiKey = options.apiKey;
327
+ this.baseUrl = options.baseUrl;
328
+ this.isEmpty = options.isEmpty;
329
+ this.metaStore = options.metaStore ?? defaultClaudeSessionMetaStore;
330
+ this.maxTurn = options.maxTurn ?? 25;
331
+ }
332
+
333
+ async createSession(cwd: string): Promise<CreateSessionResult> {
334
+ logMcpConfig();
335
+ const abortController = new AbortController();
336
+ let sessionId: string | undefined;
337
+ const stream = query({
338
+ prompt: "ok",
339
+ options: buildSdkOptions({
340
+ cwd,
341
+ model: this.model,
342
+ effort: this.effort,
343
+ isEmpty: this.isEmpty,
344
+ subagentModel: this.subagentModel,
345
+ apiKey: this.apiKey,
346
+ baseUrl: this.baseUrl,
347
+ maxTurn: this.maxTurn,
348
+ abortController,
349
+ }),
350
+ });
351
+
352
+ try {
353
+ for await (const raw of stream) {
354
+ const msg = toMessageLike(raw);
355
+ if (msg.session_id && !sessionId) {
356
+ sessionId = msg.session_id;
357
+ await this.metaStore.set(sessionId, {
358
+ cwd: msg.cwd ?? cwd,
359
+ model: msg.model,
360
+ }).catch(() => {});
361
+ const ts = new Date().toISOString();
362
+ console.log(`[${ts}] [CLAUDE-SDK] createSession: ${sessionId}`);
363
+ }
364
+ }
365
+ } finally {
366
+ stream.close();
367
+ }
368
+
369
+ if (sessionId) return { sessionId };
370
+ throw new Error("No session ID in Claude init event");
371
+ }
372
+
373
+ async *prompt(
374
+ sessionId: string,
375
+ userText: string,
376
+ cwd: string,
377
+ signal?: AbortSignal,
378
+ options?: ToolPromptOptions,
379
+ ): AsyncIterable<UnifiedStreamMessage> {
380
+ const abortController = new AbortController();
381
+ const removeAbortListener = bridgeAbortSignal(signal, abortController);
382
+ if (abortController.signal.aborted) return;
383
+ let aborted = false;
384
+
385
+ const stream = query({
386
+ prompt: buildClaudePromptText(userText),
387
+ options: buildSdkOptions({
388
+ cwd,
389
+ model: this.model,
390
+ effort: this.effort,
391
+ isEmpty: this.isEmpty,
392
+ subagentModel: this.subagentModel,
393
+ apiKey: this.apiKey,
394
+ baseUrl: this.baseUrl,
395
+ maxTurn: this.maxTurn,
396
+ planMode: options?.planMode,
397
+ resume: sessionId,
398
+ abortController,
399
+ }),
400
+ });
401
+
402
+ try {
403
+ for await (const raw of stream) {
404
+ if (abortController.signal.aborted) {
405
+ aborted = true;
406
+ break;
407
+ }
408
+
409
+ const msg = toMessageLike(raw);
410
+ if (msg.type === "system" && msg.subtype === "init" && msg.session_id) {
411
+ const meta: { cwd?: string; model?: string } = {};
412
+ if (msg.cwd) meta.cwd = msg.cwd;
413
+ if (msg.model) meta.model = msg.model;
414
+ if (Object.keys(meta).length > 0) {
415
+ this.metaStore.set(msg.session_id, meta).catch(() => {});
416
+ }
417
+ }
418
+
419
+ const normalized = normalizeSdkMessage(msg);
420
+ if (normalized) yield normalized;
421
+ }
422
+ } finally {
423
+ removeAbortListener?.();
424
+ if (aborted || abortController.signal.aborted) {
425
+ abortController.abort();
426
+ stream.close();
427
+ }
428
+ }
429
+ }
430
+
431
+ async getSessionInfo(sessionId: string): Promise<SessionInfo | undefined> {
432
+ const meta = await this.metaStore.get(sessionId);
433
+
434
+ try {
435
+ const sdkInfo = await sdkGetSessionInfo(
436
+ sessionId,
437
+ meta?.cwd ? { dir: meta.cwd } : undefined,
438
+ );
439
+ if (sdkInfo) {
440
+ return {
441
+ sessionId: sdkInfo.sessionId,
442
+ cwd: sdkInfo.cwd ?? meta?.cwd,
443
+ summary: sdkInfo.summary,
444
+ lastModified: sdkInfo.lastModified,
445
+ model: meta?.model,
446
+ };
447
+ }
448
+ } catch {
449
+ // Fall back to the local meta store below.
450
+ }
451
+
452
+ if (!meta) return { sessionId };
453
+ return meta.model
454
+ ? { sessionId, cwd: meta.cwd, model: meta.model }
455
+ : { sessionId, cwd: meta.cwd };
456
+ }
457
+
458
+ async closeSession(_sessionId: string): Promise<void> {
459
+ // SDK query streams are request scoped and are closed in prompt/createSession.
460
+ }
461
+ }
462
+
463
+ export function createClaudeAdapter(
464
+ options: ClaudeAdapterOptions,
465
+ ): ToolAdapter {
466
+ return new ClaudeAdapter(options);
467
+ }
@@ -1,8 +1,8 @@
1
1
  // =============================================================================
2
2
  // claude-session-meta-store.ts — Claude 会话 sessionId → meta 持久化映射
3
3
  // =============================================================================
4
- // 背景:切换到直接调用 Claude CLI 后,不再有 SDK 的 getSessionInfo 可用。
5
- // ChatCCC 必须自己维护 sessionId → { cwd, model } 映射。
4
+ // 背景:Claude Agent SDK 的 getSessionInfo 可能因 cwd / 本地记录缺失而查不到。
5
+ // ChatCCC 额外维护 sessionId → { cwd, model } 映射作为展示与恢复兜底。
6
6
  //
7
7
  // 存储:
8
8
  // 文件 state/claude-session-meta.json,结构:
@@ -117,4 +117,4 @@ export function createClaudeSessionMetaStore(
117
117
  };
118
118
  }
119
119
 
120
- export const defaultClaudeSessionMetaStore = createClaudeSessionMetaStore();
120
+ export const defaultClaudeSessionMetaStore = createClaudeSessionMetaStore();
package/src/config.ts CHANGED
@@ -65,11 +65,11 @@ export interface ClaudeConfig {
65
65
  model: string;
66
66
  subagentModel: string;
67
67
  effort: string;
68
- /** Anthropic API Key(选填,留空则使用 Claude CLI 默认认证) */
68
+ /** Anthropic API Key(选填,留空则使用 Claude Code 默认认证) */
69
69
  apiKey: string;
70
70
  /** Anthropic 兼容 API Base URL(选填,留空则使用默认端点) */
71
71
  baseUrl: string;
72
- /** Claude CLI maxTurns 设置,默认 25 */
72
+ /** Claude Agent SDK maxTurns 设置,默认 25 */
73
73
  maxTurn: number;
74
74
  }
75
75
 
package/src/web-ui.ts CHANGED
@@ -646,7 +646,7 @@ header .badge{font-size:13px;padding:4px 12px;border-radius:12px;font-weight:500
646
646
  </div>
647
647
  <div class="form-group">
648
648
  <label>API Key(选填)</label>
649
- <input type="password" id="field-CHATCCC_ANTHROPIC_API_KEY" placeholder="留空使用 Claude CLI 默认认证">
649
+ <input type="password" id="field-CHATCCC_ANTHROPIC_API_KEY" placeholder="留空使用 Claude Code 默认认证">
650
650
  </div>
651
651
  <div class="form-group">
652
652
  <label>Base URL(选填)</label>