chatccc 0.2.129 → 0.2.131

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
 
@@ -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.129",
3
+ "version": "0.2.131",
4
4
  "description": "Feishu bot bridge for Claude Code",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -130,9 +130,9 @@ describe("normalizeSdkMessage", () => {
130
130
  expect((result!.blocks[0] as any).content).toEqual(content);
131
131
  });
132
132
 
133
- it("skips text blocks in user messages (replayed input from --replay-user-messages)", () => {
134
- // user 消息中的 text block 来自 --replay-user-messages 重放的用户输入
135
- // (含内嵌的 IM skill prompt),不应出现在最终回复中
133
+ it("skips text blocks in user messages", () => {
134
+ // user text is host input, not assistant output. If the CLI emits it for
135
+ // any reason, it should not appear in the final reply.
136
136
  const result = normalizeSdkMessage({
137
137
  type: "user",
138
138
  message: {
@@ -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
  // ---------------------------------------------------------------------------
@@ -242,12 +244,12 @@ export function normalizeSdkMessage(msg: SdkMessageLike): UnifiedStreamMessage |
242
244
  type: "search_result",
243
245
  query: block.query ?? "",
244
246
  });
245
- } else if (block.type === "text" && block.text) {
246
- // 跳过 user 消息中的 text block:--replay-user-messages 会重放
247
- // 之前的用户消息(含内嵌的 IM skill prompt),这些不应出现在最终回复中
248
- if (msg.type === "user") continue;
249
- blocks.push({ type: "text", text: block.text });
250
- }
247
+ } 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
+ if (msg.type === "user") continue;
251
+ blocks.push({ type: "text", text: block.text });
252
+ }
251
253
  }
252
254
  return { type: msg.type, blocks };
253
255
  }
@@ -281,6 +283,7 @@ function buildCliArgs(
281
283
  isEmpty: (value: string) => boolean,
282
284
  mcpConfigJson: string | null,
283
285
  extraArgs: string[],
286
+ maxTurn: number,
284
287
  planMode?: boolean,
285
288
  ): string[] {
286
289
  const args = [
@@ -289,7 +292,7 @@ function buildCliArgs(
289
292
  "--verbose",
290
293
  "--setting-sources", "user,project,local",
291
294
  "--permission-mode", planMode ? "plan" : "bypassPermissions",
292
- "--settings", "{\"maxTurns\":999999999,\"maxBudget\":999999999}",
295
+ "--settings", `{"maxTurns":${maxTurn},"maxBudget":999999999}`,
293
296
  ];
294
297
  if (!planMode) args.push("--dangerously-skip-permissions");
295
298
 
@@ -417,6 +420,7 @@ class ClaudeAdapter implements ToolAdapter {
417
420
  private baseUrl: string | undefined;
418
421
  private isEmpty: (value: string) => boolean;
419
422
  private metaStore: ClaudeSessionMetaStore;
423
+ private maxTurn: number;
420
424
 
421
425
  constructor(options: ClaudeAdapterOptions) {
422
426
  this.model = options.model;
@@ -426,6 +430,7 @@ class ClaudeAdapter implements ToolAdapter {
426
430
  this.baseUrl = options.baseUrl;
427
431
  this.isEmpty = options.isEmpty;
428
432
  this.metaStore = options.metaStore ?? defaultClaudeSessionMetaStore;
433
+ this.maxTurn = options.maxTurn ?? 25;
429
434
  }
430
435
 
431
436
  async createSession(cwd: string): Promise<CreateSessionResult> {
@@ -435,6 +440,7 @@ class ClaudeAdapter implements ToolAdapter {
435
440
  const args = buildCliArgs(
436
441
  this.model, this.effort, this.isEmpty, mcpConfigJson,
437
442
  ["ok"],
443
+ this.maxTurn,
438
444
  );
439
445
 
440
446
  const proc = spawnCli(args, cwd, env, false);
@@ -465,7 +471,8 @@ class ClaudeAdapter implements ToolAdapter {
465
471
  const env = buildCliEnv(this.subagentModel, this.apiKey, this.baseUrl);
466
472
  const args = buildCliArgs(
467
473
  this.model, this.effort, this.isEmpty, mcpConfigJson,
468
- ["--resume", sessionId, "--input-format", "stream-json", "--replay-user-messages"],
474
+ ["--resume", sessionId, "--input-format", "stream-json"],
475
+ this.maxTurn,
469
476
  options?.planMode,
470
477
  );
471
478
 
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
 
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);
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 || '';