chatccc 0.2.128 → 0.2.130

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.apiKey`、`claude.baseUrl` 均为选填;填写后会把对应配置传给 Claude CLI 子进程,覆盖其默认值;留空则一切以 `~/.claude/settings.json` 为准。
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` 为准。
209
209
 
210
210
  #### Cursor Agent CLI
211
211
 
@@ -285,6 +285,7 @@ Codex 的默认模型和推理强度可继续由 `~/.codex/config.toml` 管理
285
285
  | `cursor.path` / `codex.path` | CLI 可执行文件路径;留空时自动探测或使用 PATH |
286
286
  | `claude.model` / `claude.subagentModel` / `claude.effort` | 选填;设置后传给 Claude CLI 子进程,留空以 `~/.claude/settings.json` 为准 |
287
287
  | `claude.apiKey` / `claude.baseUrl` | 选填;设置后传给 Claude CLI 子进程,留空以 `~/.claude/settings.json` 为准 |
288
+ | `claude.maxTurn` | 选填;Claude CLI 最大对话轮数,默认 25,可在 Web UI 编辑 |
288
289
 
289
290
  > 当前 ChatCCC 以 `bypassPermissions` 模式运行,会跳过 Agent 操作确认。请只在可信环境中使用。
290
291
 
@@ -310,6 +311,12 @@ Codex 的默认模型和推理强度可继续由 `~/.codex/config.toml` 管理
310
311
  | `/sessions` | 查看所有会话状态 |
311
312
  | `/git <子命令>` | 在当前会话工作目录执行 `git ...` 并回传输出 |
312
313
  | `/restart` | 重启机器人进程 |
314
+ | `/plan [内容]` | 以 plan 模式传递给 Agent,**不跳过权限检查**(无 `--dangerously-skip-permissions`) |
315
+ | `/ask [内容]` | 以 plan 模式传递给 Agent,**不跳过权限检查**(同 `/plan`) |
316
+
317
+ > **`/plan` 与 `/ask`**:这两个指令不会被 ChatCCC 当作内置命令处理,而是把用户原话(含指令前缀)完整传给 Agent。与普通消息的区别是:调用时不添加 `--dangerously-skip-permissions`,改为 `--permission-mode plan`,Agent 会先在权限沙盒内运行,遇到命令执行等操作时需要确认。适合需要安全审查的任务场景。
318
+ >
319
+ > **群名**:`/plan` / `/ask` 消息如果是群内第一条消息,也会触发群名更新。为美观起见,群名会自动去除 `/plan` 或 `/ask` 前缀。
313
320
 
314
321
  > **模型切换**:`/model` 查看可选模型清单,`/model <名称>` 模糊匹配切换,`/model clear` 恢复默认。当前仅 Claude Code 支持模型切换(需同时填写 `claude.model` 和 `claude.subagentModel`),Cursor / Codex 切换正在开发中。
315
322
 
@@ -17,7 +17,8 @@
17
17
  "subagentModel": "",
18
18
  "effort": "",
19
19
  "apiKey": "",
20
- "baseUrl": ""
20
+ "baseUrl": "",
21
+ "maxTurn": 25
21
22
  },
22
23
  "cursor": {
23
24
  "enabled": false,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chatccc",
3
- "version": "0.2.128",
3
+ "version": "0.2.130",
4
4
  "description": "Feishu bot bridge for Claude Code",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -128,6 +128,8 @@ export interface ToolPromptOptions {
128
128
  onProcessStart?: (info: ToolProcessInfo) => void;
129
129
  /** Called when the adapter leaves the prompt process scope normally or by abort. */
130
130
  onProcessExit?: (info: ToolProcessInfo) => void;
131
+ /** 是否为 /plan 或 /ask 模式:不开启 dangerous skip permissions */
132
+ planMode?: boolean;
131
133
  }
132
134
 
133
135
  // ---------------------------------------------------------------------------
@@ -127,6 +127,8 @@ export interface ClaudeAdapterOptions {
127
127
  isEmpty: (value: string) => boolean;
128
128
  /** 注入自定义 meta 持久化 store(测试用);未提供时使用全局默认实例。 */
129
129
  metaStore?: ClaudeSessionMetaStore;
130
+ /** Claude CLI maxTurns 设置,默认 25 */
131
+ maxTurn?: number;
130
132
  }
131
133
 
132
134
  // ---------------------------------------------------------------------------
@@ -281,16 +283,18 @@ function buildCliArgs(
281
283
  isEmpty: (value: string) => boolean,
282
284
  mcpConfigJson: string | null,
283
285
  extraArgs: string[],
286
+ maxTurn: number,
287
+ planMode?: boolean,
284
288
  ): string[] {
285
289
  const args = [
286
290
  "-p",
287
291
  "--output-format", "stream-json",
288
292
  "--verbose",
289
293
  "--setting-sources", "user,project,local",
290
- "--permission-mode", "bypassPermissions",
291
- "--dangerously-skip-permissions",
292
- "--settings", "{\"maxTurns\":999999999,\"maxBudget\":999999999}",
294
+ "--permission-mode", planMode ? "plan" : "bypassPermissions",
295
+ "--settings", `{"maxTurns":${maxTurn},"maxBudget":999999999}`,
293
296
  ];
297
+ if (!planMode) args.push("--dangerously-skip-permissions");
294
298
 
295
299
  if (!isEmpty(model)) args.push("--model", model);
296
300
  if (!isEmpty(effort)) args.push("--effort", effort);
@@ -416,6 +420,7 @@ class ClaudeAdapter implements ToolAdapter {
416
420
  private baseUrl: string | undefined;
417
421
  private isEmpty: (value: string) => boolean;
418
422
  private metaStore: ClaudeSessionMetaStore;
423
+ private maxTurn: number;
419
424
 
420
425
  constructor(options: ClaudeAdapterOptions) {
421
426
  this.model = options.model;
@@ -425,6 +430,7 @@ class ClaudeAdapter implements ToolAdapter {
425
430
  this.baseUrl = options.baseUrl;
426
431
  this.isEmpty = options.isEmpty;
427
432
  this.metaStore = options.metaStore ?? defaultClaudeSessionMetaStore;
433
+ this.maxTurn = options.maxTurn ?? 25;
428
434
  }
429
435
 
430
436
  async createSession(cwd: string): Promise<CreateSessionResult> {
@@ -434,6 +440,7 @@ class ClaudeAdapter implements ToolAdapter {
434
440
  const args = buildCliArgs(
435
441
  this.model, this.effort, this.isEmpty, mcpConfigJson,
436
442
  ["ok"],
443
+ this.maxTurn,
437
444
  );
438
445
 
439
446
  const proc = spawnCli(args, cwd, env, false);
@@ -465,6 +472,8 @@ class ClaudeAdapter implements ToolAdapter {
465
472
  const args = buildCliArgs(
466
473
  this.model, this.effort, this.isEmpty, mcpConfigJson,
467
474
  ["--resume", sessionId, "--input-format", "stream-json", "--replay-user-messages"],
475
+ this.maxTurn,
476
+ options?.planMode,
468
477
  );
469
478
 
470
479
  const proc = spawnCli(args, cwd, env, true);
package/src/config.ts CHANGED
@@ -69,6 +69,8 @@ export interface ClaudeConfig {
69
69
  apiKey: string;
70
70
  /** Anthropic 兼容 API Base URL(选填,留空则使用默认端点) */
71
71
  baseUrl: string;
72
+ /** Claude CLI maxTurns 设置,默认 25 */
73
+ maxTurn: number;
72
74
  }
73
75
 
74
76
  export interface CursorConfig {
@@ -324,7 +326,7 @@ function loadConfig(): AppConfig {
324
326
  port: 18080,
325
327
  gitTimeoutSeconds: 180,
326
328
  allowInterrupt: false,
327
- claude: { enabled: false, defaultAgent: true, model: "", subagentModel: "", effort: "", apiKey: "", baseUrl: "" },
329
+ claude: { enabled: false, defaultAgent: true, model: "", subagentModel: "", effort: "", apiKey: "", baseUrl: "", maxTurn: 25 },
328
330
  cursor: { enabled: false, defaultAgent: false, path: "", model: "claude-opus-4-7-max" },
329
331
  codex: { enabled: false, defaultAgent: false, path: "", model: "", effort: "" },
330
332
  };
@@ -468,6 +470,9 @@ function loadConfig(): AppConfig {
468
470
  effort: normalizeOptionalConfigField(claude.effort, { label: "claude.effort" }),
469
471
  apiKey: normalizeOptionalConfigField(claude.apiKey, { label: "claude.apiKey" }),
470
472
  baseUrl: normalizeOptionalConfigField(claude.baseUrl, { label: "claude.baseUrl" }),
473
+ maxTurn: typeof (claude as Record<string, unknown>).maxTurn === "number"
474
+ ? (claude as Record<string, unknown>).maxTurn as number
475
+ : 25,
471
476
  },
472
477
  cursor: {
473
478
  enabled: cursorEnabled,
@@ -521,6 +526,7 @@ export const LOCAL_RELAY_URL = `ws://127.0.0.1:${CHATCCC_PORT}`;
521
526
  export let CLAUDE_MODEL = config.claude.model;
522
527
  export let CLAUDE_SUBAGENT_MODEL = config.claude.subagentModel;
523
528
  export let CLAUDE_EFFORT = config.claude.effort;
529
+ export let CLAUDE_MAX_TURN = config.claude.maxTurn;
524
530
  export let CLAUDE_API_KEY = config.claude.apiKey;
525
531
  export let CLAUDE_BASE_URL = config.claude.baseUrl;
526
532
 
@@ -94,6 +94,21 @@ export function sessionChatName(left: string, cwd: string): string {
94
94
  return `${left}-${cwdDisplayName(cwd)}`;
95
95
  }
96
96
 
97
+ /** 判断用户消息是否以 /plan 或 /ask 开头(忽略大小写和尾部空格) */
98
+ function isPlanOrAsk(text: string): boolean {
99
+ const t = text.toLowerCase().trim();
100
+ return t === "/plan" || t.startsWith("/plan ") || t === "/ask" || t.startsWith("/ask ");
101
+ }
102
+
103
+ /** 剥离 /plan 或 /ask 前缀,用于群名等展示场景 */
104
+ function stripPlanOrAskPrefix(text: string): string {
105
+ if (isPlanOrAsk(text)) {
106
+ const match = text.match(/^\/(?:plan|ask)\s*(.*)/i);
107
+ return (match?.[1] ?? "").trim() || text.trim();
108
+ }
109
+ return text;
110
+ }
111
+
97
112
  /** 模型模糊匹配:精确匹配优先,否则找子串匹配(模型名越短越优先) */
98
113
  function findModelMatch(input: string, models: string[]): string | null {
99
114
  if (models.length === 0) return null;
@@ -117,8 +132,9 @@ function shouldSendWechatProcessingAck(
117
132
  platform: PlatformAdapter,
118
133
  textLower: string,
119
134
  chatType: string,
135
+ text: string,
120
136
  ): boolean {
121
- return platform.kind === "wechat" && chatType === "p2p" && !textLower.startsWith("/");
137
+ return platform.kind === "wechat" && chatType === "p2p" && (!textLower.startsWith("/") || isPlanOrAsk(text));
122
138
  }
123
139
 
124
140
  function isNonWechatP2p(platform: PlatformAdapter, chatType: string): boolean {
@@ -600,10 +616,10 @@ export async function handleCommand(
600
616
  if (
601
617
  chatType !== "p2p" &&
602
618
  isUntitledSessionChatName(chatInfo!.name) &&
603
- !textLower.startsWith("/")
619
+ (!textLower.startsWith("/") || isPlanOrAsk(text))
604
620
  ) {
605
621
  const MAX_PREFIX = 10;
606
- const prefix = text.slice(0, MAX_PREFIX);
622
+ const prefix = stripPlanOrAskPrefix(text).slice(0, MAX_PREFIX);
607
623
  const adapter = getAdapterForTool(descriptionTool, sessionId);
608
624
  const info = await adapter
609
625
  .getSessionInfo(sessionId)
@@ -635,7 +651,7 @@ export async function handleCommand(
635
651
  if (
636
652
  chatType === "p2p" &&
637
653
  platform.kind === "wechat" &&
638
- !textLower.startsWith("/")
654
+ (!textLower.startsWith("/") || isPlanOrAsk(text))
639
655
  ) {
640
656
  try {
641
657
  const reg = await loadSessionRegistryForBinding();
@@ -1223,7 +1239,7 @@ export async function handleCommand(
1223
1239
  return;
1224
1240
  }
1225
1241
 
1226
- if (shouldSendWechatProcessingAck(platform, textLower, chatType)) {
1242
+ if (shouldSendWechatProcessingAck(platform, textLower, chatType, text)) {
1227
1243
  await platform.sendText(chatId, "生成中...").catch(() => {});
1228
1244
  }
1229
1245
 
@@ -1237,6 +1253,7 @@ export async function handleCommand(
1237
1253
  msgTimestamp,
1238
1254
  descriptionTool,
1239
1255
  tid,
1256
+ isPlanOrAsk(text),
1240
1257
  );
1241
1258
  logTrace(tid, "DONE", { outcome: "resume_done", sessionId });
1242
1259
  console.log(`[${ts()}] [RESUME] Session ${sessionId} done`);
@@ -1311,7 +1328,7 @@ export async function handleCommand(
1311
1328
  }
1312
1329
 
1313
1330
  // 飞书私聊普通消息:不再绑定私聊本身,而是自动创建会话群并把私聊内容作为首轮 prompt。
1314
- if (isNonWechatP2p(platform, chatType) && !textLower.startsWith("/")) {
1331
+ if (isNonWechatP2p(platform, chatType) && (!textLower.startsWith("/") || isPlanOrAsk(text))) {
1315
1332
  const tool = resolveDefaultAgentTool();
1316
1333
  const toolLabel = toolDisplayName(tool);
1317
1334
  logTrace(tid, "BRANCH", { cmd: "auto_new_from_p2p", tool });
@@ -1352,7 +1369,7 @@ export async function handleCommand(
1352
1369
  }
1353
1370
 
1354
1371
  const cwd = sessionCwd;
1355
- const initialName = sessionChatName(text.slice(0, 10) || "新会话", cwd);
1372
+ const initialName = sessionChatName(stripPlanOrAskPrefix(text).slice(0, 10) || "新会话", cwd);
1356
1373
  let newChatId: string;
1357
1374
  try {
1358
1375
  newChatId = await platform.createGroup(initialName, [openId]);
@@ -1436,6 +1453,7 @@ export async function handleCommand(
1436
1453
  msgTimestamp,
1437
1454
  tool,
1438
1455
  tid,
1456
+ isPlanOrAsk(text),
1439
1457
  );
1440
1458
  console.log(`[${ts()}] [AUTO-P2P 5/5] First prompt sent → OK`);
1441
1459
  logTrace(tid, "DONE", { outcome: "auto_new_p2p_prompt_done", newChatId, sessionId, tool });
package/src/session.ts CHANGED
@@ -5,6 +5,7 @@ import {
5
5
  CLAUDE_API_KEY,
6
6
  CLAUDE_BASE_URL,
7
7
  CLAUDE_EFFORT,
8
+ CLAUDE_MAX_TURN,
8
9
  CLAUDE_MODEL,
9
10
  CLAUDE_SUBAGENT_MODEL,
10
11
  CHATCCC_PORT,
@@ -367,6 +368,7 @@ export function getAdapterForTool(tool: string, sessionId?: string): ToolAdapter
367
368
  apiKey: CLAUDE_API_KEY,
368
369
  baseUrl: CLAUDE_BASE_URL,
369
370
  isEmpty: isAnthropicConfigEmpty,
371
+ maxTurn: CLAUDE_MAX_TURN,
370
372
  });
371
373
  }
372
374
  adapterCache.set(cacheKey, adapter);
@@ -832,8 +834,9 @@ export async function resumeAndPrompt(
832
834
  msgTimestamp: number,
833
835
  tool: string,
834
836
  traceId?: string,
837
+ planMode?: boolean,
835
838
  ): Promise<void> {
836
- return runAgentSession(sessionId, userText, platform, chatId, msgTimestamp, tool, traceId);
839
+ return runAgentSession(sessionId, userText, platform, chatId, msgTimestamp, tool, traceId, planMode);
837
840
  }
838
841
 
839
842
  // ---------------------------------------------------------------------------
@@ -848,6 +851,7 @@ export async function runAgentSession(
848
851
  msgTimestamp: number,
849
852
  tool: string,
850
853
  traceId?: string,
854
+ planMode?: boolean,
851
855
  ): Promise<void> {
852
856
  const tid = traceId ?? "";
853
857
 
@@ -1106,6 +1110,7 @@ export async function runAgentSession(
1106
1110
  clearPromptProcessMonitor(sessionId);
1107
1111
  if (exitInfo.pid !== undefined) unregisterProcess(exitInfo.pid);
1108
1112
  },
1113
+ planMode,
1109
1114
  })) {
1110
1115
  for (const block of unifiedMsg.blocks) {
1111
1116
  accumulateBlockContent(block, state, toolCallMap);
package/src/web-ui.ts CHANGED
@@ -31,7 +31,7 @@ interface AppConfig {
31
31
  platforms?: { feishu?: { enabled?: boolean }; ilink?: { enabled?: boolean } };
32
32
  port?: number;
33
33
  gitTimeoutSeconds?: number;
34
- claude?: { enabled?: boolean; defaultAgent?: boolean; model?: string; subagentModel?: string; effort?: string; apiKey?: string; baseUrl?: string };
34
+ claude?: { enabled?: boolean; defaultAgent?: boolean; model?: string; subagentModel?: string; effort?: string; apiKey?: string; baseUrl?: string; maxTurn?: number };
35
35
  // `command` 是已废弃的旧字段名,保留只读以兼容升级前的 config.json
36
36
  cursor?: { enabled?: boolean; defaultAgent?: boolean; path?: string; command?: string; model?: string };
37
37
  codex?: { enabled?: boolean; defaultAgent?: boolean; path?: string; command?: string; model?: string; effort?: string };
@@ -319,6 +319,9 @@ export function unflattenConfig(flat: Record<string, unknown>): Record<string, u
319
319
  } else if (key === "CHATCCC_ANTHROPIC_BASE_URL") {
320
320
  result.claude = result.claude || {};
321
321
  (result.claude as Record<string, unknown>).baseUrl = val;
322
+ } else if (key === "CHATCCC_ANTHROPIC_MAX_TURN") {
323
+ result.claude = result.claude || {};
324
+ (result.claude as Record<string, unknown>).maxTurn = parseInt(val as string, 10) || 25;
322
325
  } else if (key === "CHATCCC_CLAUDE_ENABLED") {
323
326
  result.claude = result.claude || {};
324
327
  (result.claude as Record<string, unknown>).enabled = val === true || val === "true";
@@ -785,6 +788,7 @@ header .badge{font-size:13px;padding:4px 12px;border-radius:12px;font-weight:500
785
788
  <div class="config-row"><span class="key">Effort</span><span class="val" id="cfg-ANTHROPIC_EFFORT">-</span></div>
786
789
  <div class="config-row"><span class="key">API Key</span><span class="val" id="cfg-ANTHROPIC_API_KEY">-</span></div>
787
790
  <div class="config-row"><span class="key">Base URL</span><span class="val" id="cfg-ANTHROPIC_BASE_URL">-</span></div>
791
+ <div class="config-row"><span class="key">Max Turns</span><span class="val" id="cfg-ANTHROPIC_MAX_TURN">-</span></div>
788
792
  <label class="agent-default-row" style="margin-top:10px"><input type="checkbox" id="dash-default-claude" onchange="setDashboardDefaultAgent('claude', this.checked)"> 设为默认 Agent</label>
789
793
  <button class="btn btn-outline" style="margin-top:8px" onclick="editSection('claude')">编辑</button>
790
794
  </div>
@@ -851,7 +855,7 @@ let state = {
851
855
  var step2InputBound = false;
852
856
 
853
857
  const AGENT_FIELDS = {
854
- claude: ['CHATCCC_ANTHROPIC_MODEL','CHATCCC_ANTHROPIC_SUBAGENT_MODEL','CHATCCC_ANTHROPIC_EFFORT','CHATCCC_ANTHROPIC_API_KEY','CHATCCC_ANTHROPIC_BASE_URL'],
858
+ claude: ['CHATCCC_ANTHROPIC_MODEL','CHATCCC_ANTHROPIC_SUBAGENT_MODEL','CHATCCC_ANTHROPIC_EFFORT','CHATCCC_ANTHROPIC_API_KEY','CHATCCC_ANTHROPIC_BASE_URL','CHATCCC_ANTHROPIC_MAX_TURN'],
855
859
  cursor: ['CHATCCC_CURSOR_PATH','CHATCCC_CURSOR_MODEL'],
856
860
  codex: ['CHATCCC_CODEX_PATH','CHATCCC_CODEX_MODEL','CHATCCC_CODEX_EFFORT']
857
861
  };
@@ -1087,7 +1091,7 @@ function isAgentEnabled(node, keys) {
1087
1091
  return false;
1088
1092
  }
1089
1093
 
1090
- var CLAUDE_FALLBACK_KEYS = ['model','subagentModel','effort'];
1094
+ var CLAUDE_FALLBACK_KEYS = ['model','subagentModel','effort','maxTurn'];
1091
1095
  var CURSOR_FALLBACK_KEYS = ['path','command','model'];
1092
1096
  var CODEX_FALLBACK_KEYS = ['path','command','model','effort'];
1093
1097
 
@@ -1399,6 +1403,7 @@ function updateDashboardUI() {
1399
1403
  document.getElementById('cfg-ANTHROPIC_EFFORT').textContent = (c.claude && c.claude.effort) || '(留空)';
1400
1404
  document.getElementById('cfg-ANTHROPIC_API_KEY').textContent = (c.claude && c.claude.apiKey) ? '***已设置***' : '(留空)';
1401
1405
  document.getElementById('cfg-ANTHROPIC_BASE_URL').textContent = (c.claude && c.claude.baseUrl) || '(留空)';
1406
+ document.getElementById('cfg-ANTHROPIC_MAX_TURN').textContent = (c.claude && c.claude.maxTurn != null) ? String(c.claude.maxTurn) : '25';
1402
1407
  document.getElementById('cfg-CURSOR_PATH').textContent = (c.cursor && (c.cursor.path || c.cursor.command)) || '-';
1403
1408
  document.getElementById('cfg-CURSOR_MODEL').textContent = (c.cursor && c.cursor.model) || '(留空)';
1404
1409
  document.getElementById('cfg-CODEX_PATH').textContent = (c.codex && (c.codex.path || c.codex.command)) || 'codex';
@@ -1459,7 +1464,7 @@ function editSection(section) {
1459
1464
  var labelMap = {
1460
1465
  'CHATCCC_APP_ID': 'App ID', 'CHATCCC_APP_SECRET': 'App Secret',
1461
1466
  'CHATCCC_ANTHROPIC_MODEL': '模型', 'CHATCCC_ANTHROPIC_SUBAGENT_MODEL': 'Subagent 模型', 'CHATCCC_ANTHROPIC_EFFORT': 'Effort',
1462
- 'CHATCCC_ANTHROPIC_API_KEY': 'API Key', 'CHATCCC_ANTHROPIC_BASE_URL': 'Base URL',
1467
+ 'CHATCCC_ANTHROPIC_API_KEY': 'API Key', 'CHATCCC_ANTHROPIC_BASE_URL': 'Base URL', 'CHATCCC_ANTHROPIC_MAX_TURN': 'Max Turns',
1463
1468
  'CHATCCC_CURSOR_PATH': 'CLI 路径', 'CHATCCC_CURSOR_MODEL': '模型',
1464
1469
  'CHATCCC_CODEX_PATH': 'CLI 路径', 'CHATCCC_CODEX_MODEL': '模型', 'CHATCCC_CODEX_EFFORT': 'Effort'
1465
1470
  };
@@ -1477,6 +1482,7 @@ function editSection(section) {
1477
1482
  else if (key === 'CHATCCC_ANTHROPIC_EFFORT') val = state.config.claude.effort || '';
1478
1483
  else if (key === 'CHATCCC_ANTHROPIC_API_KEY') val = state.config.claude.apiKey || '';
1479
1484
  else if (key === 'CHATCCC_ANTHROPIC_BASE_URL') val = state.config.claude.baseUrl || '';
1485
+ else if (key === 'CHATCCC_ANTHROPIC_MAX_TURN') val = (state.config.claude.maxTurn != null) ? String(state.config.claude.maxTurn) : '25';
1480
1486
  } else if (section === 'cursor' && state.config.cursor) {
1481
1487
  if (key === 'CHATCCC_CURSOR_PATH') val = state.config.cursor.path || state.config.cursor.command || '';
1482
1488
  else if (key === 'CHATCCC_CURSOR_MODEL') val = state.config.cursor.model || '';