chatccc 0.2.222 → 0.2.224

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/src/config.ts CHANGED
@@ -88,40 +88,45 @@ export interface CursorConfig {
88
88
  onDemandMonthlyBudget: number;
89
89
  }
90
90
 
91
- export interface CodexConfig {
92
- /** 是否启用 Codex Agent;缺省时按"有任意字段非空"自动判定(向后兼容) */
93
- enabled: boolean;
91
+ export interface CodexConfig {
92
+ /** 是否启用 Codex Agent;缺省时按"有任意字段非空"自动判定(向后兼容) */
93
+ enabled: boolean;
94
94
  /** 是否作为 /new 未指定工具时使用的默认 Agent */
95
95
  defaultAgent: boolean;
96
96
  /** Codex CLI 可执行文件绝对路径;留空时退回到 PATH 中的 `codex` */
97
97
  path: string;
98
98
  model: string;
99
99
  /** /model 可切换的单个备选模型;留空则不加入候选列表 */
100
- alternativeModel: string;
101
- effort: string;
102
- /** Codex Priority service tier. False explicitly forces the standard tier. */
103
- fastMode: boolean;
104
- }
105
-
106
- export interface CccConfig {
107
- /** Whether the built-in CCC Agent is available for new sessions. */
108
- enabled: boolean;
109
- /** Whether /new without an explicit tool should use CCC Agent. */
110
- defaultAgent: boolean;
111
- /** DeepSeek API Key for the ChatCCC self-developed agent. */
112
- DEEPSEEK_API_KEY: string;
113
- /** DeepSeek-compatible API Base URL for the ChatCCC self-developed agent. */
114
- DEEPSEEK_BASE_URL: string;
115
- /** Model used by the ChatCCC self-developed agent. */
116
- model: string;
117
- /** Optional model exposed through /model for manual per-session switching. */
118
- alternativeModel: string;
119
- }
120
-
121
- export interface FeishuConfig {
122
- appId: string;
123
- appSecret: string;
124
- }
100
+ alternativeModel: string;
101
+ effort: string;
102
+ /** Codex Priority service tier. False explicitly forces the standard tier. */
103
+ fastMode: boolean;
104
+ }
105
+
106
+ export interface CccConfig {
107
+ /** Whether the built-in CCC Agent is available for new sessions. */
108
+ enabled: boolean;
109
+ /** Whether /new without an explicit tool should use CCC Agent. */
110
+ defaultAgent: boolean;
111
+ /** DeepSeek API Key for the ChatCCC self-developed agent. */
112
+ DEEPSEEK_API_KEY: string;
113
+ /** DeepSeek-compatible API Base URL for the ChatCCC self-developed agent. */
114
+ DEEPSEEK_BASE_URL: string;
115
+ /** Model used by the ChatCCC self-developed agent. */
116
+ model: string;
117
+ /** Optional model exposed through /model for manual per-session switching. */
118
+ alternativeModel: string;
119
+ /**
120
+ * Reasoning effort (none/minimal/low/medium/high/xhigh/max),透传到
121
+ * DeepSeek reasoning_effort 请求字段;留空表示不传(服务端默认 medium)。
122
+ */
123
+ effort: string;
124
+ }
125
+
126
+ export interface FeishuConfig {
127
+ appId: string;
128
+ appSecret: string;
129
+ }
125
130
 
126
131
  export interface PlatformConfig {
127
132
  enabled: boolean;
@@ -151,39 +156,39 @@ export interface RawStreamAgentLogConfig {
151
156
  keepCompleted: boolean;
152
157
  }
153
158
 
154
- export interface RawStreamLogsConfig {
155
- claude: RawStreamAgentLogConfig;
156
- cursor: RawStreamAgentLogConfig;
157
- codex: RawStreamAgentLogConfig;
158
- ccc: RawStreamAgentLogConfig;
159
- }
160
-
161
- export interface AppConfig {
162
- feishu: FeishuConfig;
163
- platforms: PlatformsConfig;
164
- webUi: { openOnStart: boolean };
165
- chromeDevtools: ChromeDevtoolsConfig;
159
+ export interface RawStreamLogsConfig {
160
+ claude: RawStreamAgentLogConfig;
161
+ cursor: RawStreamAgentLogConfig;
162
+ codex: RawStreamAgentLogConfig;
163
+ ccc: RawStreamAgentLogConfig;
164
+ }
165
+
166
+ export interface AppConfig {
167
+ feishu: FeishuConfig;
168
+ platforms: PlatformsConfig;
169
+ webUi: { openOnStart: boolean };
170
+ chromeDevtools: ChromeDevtoolsConfig;
166
171
  port: number;
167
172
  gitTimeoutSeconds: number;
168
173
  /** 若为 false,AI 生成过程中用户发送消息不会打断,须先点「停止」再发送新消息 */
169
174
  allowInterrupt: boolean;
170
175
  rawStreamLogs: RawStreamLogsConfig;
171
- claude: ClaudeConfig;
172
- cursor: CursorConfig;
173
- codex: CodexConfig;
174
- ccc: CccConfig;
175
- }
176
-
177
- export type AgentTool = "claude" | "cursor" | "codex" | "ccc";
178
- export const AGENT_TOOLS: AgentTool[] = ["claude", "cursor", "codex", "ccc"];
176
+ claude: ClaudeConfig;
177
+ cursor: CursorConfig;
178
+ codex: CodexConfig;
179
+ ccc: CccConfig;
180
+ }
181
+
182
+ export type AgentTool = "claude" | "cursor" | "codex" | "ccc";
183
+ export const AGENT_TOOLS: AgentTool[] = ["claude", "cursor", "codex", "ccc"];
179
184
  export type CursorAvatarBatteryMode = "apiPercent" | "onDemandUse";
180
185
 
181
186
  /** 获取指定 agent 配置中所有模型相关的值(最多 100 个,去重) */
182
- export function getAllModelsForTool(tool: string, cfg: AppConfig = config): string[] {
183
- const seen = new Set<string>();
184
- const collect = (v: unknown) => {
185
- if (typeof v === "string" && v.trim()) seen.add(v.trim());
186
- };
187
+ export function getAllModelsForTool(tool: string, cfg: AppConfig = config): string[] {
188
+ const seen = new Set<string>();
189
+ const collect = (v: unknown) => {
190
+ if (typeof v === "string" && v.trim()) seen.add(v.trim());
191
+ };
187
192
 
188
193
  if (tool === "claude") {
189
194
  collect(cfg.claude.model);
@@ -191,36 +196,39 @@ export function getAllModelsForTool(tool: string, cfg: AppConfig = config): stri
191
196
  } else if (tool === "cursor") {
192
197
  collect(cfg.cursor.model);
193
198
  collect(cfg.cursor.alternativeModel);
194
- } else if (tool === "codex") {
195
- collect(cfg.codex.model);
196
- collect(cfg.codex.alternativeModel);
197
- } else if (tool === "ccc") {
198
- collect(cfg.ccc.model);
199
- collect(cfg.ccc.alternativeModel);
200
- }
201
-
202
- return Array.from(seen).slice(0, 100);
203
- }
204
-
205
- export const CLAUDE_EFFORT_LEVELS = ["low", "medium", "high", "xhigh", "max"] as const;
206
- export const CODEX_EFFORT_LEVELS = ["minimal", "low", "medium", "high", "xhigh"] as const;
207
-
208
- export function getAllEffortsForTool(tool: string): string[] {
209
- if (tool === "claude") return [...CLAUDE_EFFORT_LEVELS];
210
- if (tool === "codex") return [...CODEX_EFFORT_LEVELS];
211
- return [];
212
- }
213
-
214
- export function getDefaultEffortForTool(tool: AgentTool, cfg: AppConfig = config): string {
215
- if (tool === "claude") return cfg.claude.effort;
216
- if (tool === "codex") return cfg.codex.effort;
217
- return "";
218
- }
219
-
220
- const CONFIG_FILE = join(USER_DATA_DIR, "config.json");
221
- const CONFIG_SAMPLE_FILE = join(PROJECT_ROOT, "config.sample.json");
222
- export const DEFAULT_CCC_DEEPSEEK_BASE_URL = "https://api.deepseek.com/v1";
223
- export const DEFAULT_CCC_MODEL = "deepseek-v4-pro";
199
+ } else if (tool === "codex") {
200
+ collect(cfg.codex.model);
201
+ collect(cfg.codex.alternativeModel);
202
+ } else if (tool === "ccc") {
203
+ collect(cfg.ccc.model);
204
+ collect(cfg.ccc.alternativeModel);
205
+ }
206
+
207
+ return Array.from(seen).slice(0, 100);
208
+ }
209
+
210
+ export const CLAUDE_EFFORT_LEVELS = ["low", "medium", "high", "xhigh", "max"] as const;
211
+ export const CODEX_EFFORT_LEVELS = ["minimal", "low", "medium", "high", "xhigh"] as const;
212
+ export const CCC_EFFORT_LEVELS = ["none", "minimal", "low", "medium", "high", "xhigh", "max"] as const;
213
+
214
+ export function getAllEffortsForTool(tool: string): string[] {
215
+ if (tool === "claude") return [...CLAUDE_EFFORT_LEVELS];
216
+ if (tool === "codex") return [...CODEX_EFFORT_LEVELS];
217
+ if (tool === "ccc") return [...CCC_EFFORT_LEVELS];
218
+ return [];
219
+ }
220
+
221
+ export function getDefaultEffortForTool(tool: AgentTool, cfg: AppConfig = config): string {
222
+ if (tool === "claude") return cfg.claude.effort;
223
+ if (tool === "codex") return cfg.codex.effort;
224
+ if (tool === "ccc") return cfg.ccc.effort;
225
+ return "";
226
+ }
227
+
228
+ const CONFIG_FILE = join(USER_DATA_DIR, "config.json");
229
+ const CONFIG_SAMPLE_FILE = join(PROJECT_ROOT, "config.sample.json");
230
+ export const DEFAULT_CCC_DEEPSEEK_BASE_URL = "https://api.deepseek.com/v1";
231
+ export const DEFAULT_CCC_MODEL = "deepseek-v4-pro";
224
232
 
225
233
  /**
226
234
  * 将旧位置(PROJECT_ROOT)的持久化数据一次性迁移到 USER_DATA_DIR。
@@ -428,20 +436,20 @@ function normalizeRawStreamAgentLogConfig(raw: unknown): RawStreamAgentLogConfig
428
436
 
429
437
  function loadConfig(): AppConfig {
430
438
  const defaults: AppConfig = {
431
- feishu: { appId: "", appSecret: "" },
432
- platforms: { feishu: { enabled: true }, ilink: { enabled: true } },
433
- webUi: { openOnStart: true },
434
- chromeDevtools: { enabled: false, port: 15166, chromePath: "" },
439
+ feishu: { appId: "", appSecret: "" },
440
+ platforms: { feishu: { enabled: true }, ilink: { enabled: true } },
441
+ webUi: { openOnStart: true },
442
+ chromeDevtools: { enabled: false, port: 15166, chromePath: "" },
435
443
  port: 18080,
436
444
  gitTimeoutSeconds: 180,
437
445
  allowInterrupt: false,
438
- rawStreamLogs: {
439
- claude: { enabled: false, maxBytesPerTurn: 50 * 1024 * 1024, retentionDays: 7, keepCompleted: false },
440
- cursor: { enabled: false, maxBytesPerTurn: 50 * 1024 * 1024, retentionDays: 7, keepCompleted: false },
441
- codex: { enabled: false, maxBytesPerTurn: 50 * 1024 * 1024, retentionDays: 7, keepCompleted: false },
442
- ccc: { enabled: false, maxBytesPerTurn: 50 * 1024 * 1024, retentionDays: 7, keepCompleted: false },
443
- },
444
- claude: { enabled: false, defaultAgent: true, model: "", subagentModel: "", effort: "", apiKey: "", baseUrl: "", maxTurn: 0 },
446
+ rawStreamLogs: {
447
+ claude: { enabled: false, maxBytesPerTurn: 50 * 1024 * 1024, retentionDays: 7, keepCompleted: false },
448
+ cursor: { enabled: false, maxBytesPerTurn: 50 * 1024 * 1024, retentionDays: 7, keepCompleted: false },
449
+ codex: { enabled: false, maxBytesPerTurn: 50 * 1024 * 1024, retentionDays: 7, keepCompleted: false },
450
+ ccc: { enabled: false, maxBytesPerTurn: 50 * 1024 * 1024, retentionDays: 7, keepCompleted: false },
451
+ },
452
+ claude: { enabled: false, defaultAgent: true, model: "", subagentModel: "", effort: "", apiKey: "", baseUrl: "", maxTurn: 0 },
445
453
  cursor: {
446
454
  enabled: false,
447
455
  defaultAgent: false,
@@ -450,17 +458,18 @@ function loadConfig(): AppConfig {
450
458
  alternativeModel: "",
451
459
  avatarBatteryMode: "apiPercent",
452
460
  onDemandMonthlyBudget: 1000,
453
- },
454
- codex: { enabled: false, defaultAgent: false, path: "", model: "", alternativeModel: "", effort: "", fastMode: false },
455
- ccc: {
456
- enabled: false,
457
- defaultAgent: false,
458
- DEEPSEEK_API_KEY: "",
459
- DEEPSEEK_BASE_URL: DEFAULT_CCC_DEEPSEEK_BASE_URL,
460
- model: DEFAULT_CCC_MODEL,
461
- alternativeModel: "",
462
- },
463
- };
461
+ },
462
+ codex: { enabled: false, defaultAgent: false, path: "", model: "", alternativeModel: "", effort: "", fastMode: false },
463
+ ccc: {
464
+ enabled: false,
465
+ defaultAgent: false,
466
+ DEEPSEEK_API_KEY: "",
467
+ DEEPSEEK_BASE_URL: DEFAULT_CCC_DEEPSEEK_BASE_URL,
468
+ model: DEFAULT_CCC_MODEL,
469
+ alternativeModel: "",
470
+ effort: "",
471
+ },
472
+ };
464
473
 
465
474
  if (!IS_TEST_ENV) {
466
475
  migrateLegacyData();
@@ -510,20 +519,21 @@ function loadConfig(): AppConfig {
510
519
  alternativeModel?: unknown;
511
520
  avatarBatteryMode?: unknown;
512
521
  onDemandMonthlyBudget?: unknown;
513
- };
514
- codex?: { enabled?: unknown; defaultAgent?: unknown; path?: unknown; command?: unknown; model?: unknown; alternativeModel?: unknown; effort?: unknown; fastMode?: unknown };
515
- ccc?: {
516
- enabled?: unknown;
517
- defaultAgent?: unknown;
518
- DEEPSEEK_API_KEY?: unknown;
519
- DEEPSEEK_BASE_URL?: unknown;
520
- model?: unknown;
521
- alternativeModel?: unknown;
522
- };
523
- webUi?: { openOnStart?: unknown };
524
- chromeDevtools?: { enabled?: unknown; port?: unknown; chromePath?: unknown };
525
- rawStreamLogs?: unknown;
526
- };
522
+ };
523
+ codex?: { enabled?: unknown; defaultAgent?: unknown; path?: unknown; command?: unknown; model?: unknown; alternativeModel?: unknown; effort?: unknown; fastMode?: unknown };
524
+ ccc?: {
525
+ enabled?: unknown;
526
+ defaultAgent?: unknown;
527
+ DEEPSEEK_API_KEY?: unknown;
528
+ DEEPSEEK_BASE_URL?: unknown;
529
+ model?: unknown;
530
+ alternativeModel?: unknown;
531
+ effort?: unknown;
532
+ };
533
+ webUi?: { openOnStart?: unknown };
534
+ chromeDevtools?: { enabled?: unknown; port?: unknown; chromePath?: unknown };
535
+ rawStreamLogs?: unknown;
536
+ };
527
537
  try {
528
538
  parsed = JSON.parse(raw);
529
539
  } catch (err) {
@@ -533,11 +543,11 @@ function loadConfig(): AppConfig {
533
543
 
534
544
  const feishu = parsed.feishu ?? { appId: "", appSecret: "" };
535
545
  const claude = parsed.claude ?? {} as Partial<ClaudeConfig>;
536
- const cursorRaw = (parsed.cursor ?? {}) as NonNullable<typeof parsed.cursor>;
537
- const codexRaw = (parsed.codex ?? {}) as NonNullable<typeof parsed.codex>;
538
- const cccRaw = (parsed.ccc ?? {}) as NonNullable<typeof parsed.ccc>;
539
- const webUiRaw = (parsed.webUi ?? {}) as NonNullable<typeof parsed.webUi>;
540
- const chromeDevtoolsRaw = (parsed.chromeDevtools ?? {}) as NonNullable<typeof parsed.chromeDevtools>;
546
+ const cursorRaw = (parsed.cursor ?? {}) as NonNullable<typeof parsed.cursor>;
547
+ const codexRaw = (parsed.codex ?? {}) as NonNullable<typeof parsed.codex>;
548
+ const cccRaw = (parsed.ccc ?? {}) as NonNullable<typeof parsed.ccc>;
549
+ const webUiRaw = (parsed.webUi ?? {}) as NonNullable<typeof parsed.webUi>;
550
+ const chromeDevtoolsRaw = (parsed.chromeDevtools ?? {}) as NonNullable<typeof parsed.chromeDevtools>;
541
551
  const rawStreamLogsRaw = typeof parsed.rawStreamLogs === "object" && parsed.rawStreamLogs !== null
542
552
  ? parsed.rawStreamLogs as unknown as Record<string, unknown>
543
553
  : {};
@@ -574,36 +584,36 @@ function loadConfig(): AppConfig {
574
584
  (typeof cursorRaw.model === "string" && (cursorRaw.model as string).trim()) ||
575
585
  (typeof cursorRaw.alternativeModel === "string" && (cursorRaw.alternativeModel as string).trim()),
576
586
  );
577
- const codexNonEmpty = (): boolean =>
587
+ const codexNonEmpty = (): boolean =>
578
588
  Boolean(
579
589
  (typeof codexRaw.path === "string" && codexRaw.path.trim()) ||
580
590
  (typeof codexRaw.command === "string" && (codexRaw.command as string).trim()) ||
581
- (typeof codexRaw.model === "string" && (codexRaw.model as string).trim()) ||
582
- (typeof codexRaw.alternativeModel === "string" && (codexRaw.alternativeModel as string).trim()) ||
583
- (typeof codexRaw.effort === "string" && (codexRaw.effort as string).trim()) ||
584
- codexRaw.fastMode === true,
585
- );
586
- // 旧版 ccc 配置没有 enabled。只用 API Key 推断启用,避免 sample 中自带的
587
- // 默认 Base URL / model 让升级用户在未配置凭证时意外启用 CCC Agent。
588
- const cccNonEmpty = (): boolean =>
589
- Boolean(typeof cccRaw.DEEPSEEK_API_KEY === "string" && cccRaw.DEEPSEEK_API_KEY.trim());
591
+ (typeof codexRaw.model === "string" && (codexRaw.model as string).trim()) ||
592
+ (typeof codexRaw.alternativeModel === "string" && (codexRaw.alternativeModel as string).trim()) ||
593
+ (typeof codexRaw.effort === "string" && (codexRaw.effort as string).trim()) ||
594
+ codexRaw.fastMode === true,
595
+ );
596
+ // 旧版 ccc 配置没有 enabled。只用 API Key 推断启用,避免 sample 中自带的
597
+ // 默认 Base URL / model 让升级用户在未配置凭证时意外启用 CCC Agent。
598
+ const cccNonEmpty = (): boolean =>
599
+ Boolean(typeof cccRaw.DEEPSEEK_API_KEY === "string" && cccRaw.DEEPSEEK_API_KEY.trim());
590
600
 
591
601
  const claudeEnabled = resolveEnabled(claude.enabled, claudeNonEmpty);
592
602
  const cursorEnabled = resolveEnabled(cursorRaw.enabled, cursorNonEmpty);
593
- const codexEnabled = resolveEnabled(codexRaw.enabled, codexNonEmpty);
594
- const cccEnabled = resolveEnabled(cccRaw.enabled, cccNonEmpty);
603
+ const codexEnabled = resolveEnabled(codexRaw.enabled, codexNonEmpty);
604
+ const cccEnabled = resolveEnabled(cccRaw.enabled, cccNonEmpty);
595
605
  const chromeDevtoolsPort = Number(chromeDevtoolsRaw.port);
596
606
  const explicitDefaultTool: AgentTool | null =
597
607
  typeof claude.defaultAgent === "boolean" && claude.defaultAgent && claudeEnabled ? "claude" :
598
608
  typeof cursorRaw.defaultAgent === "boolean" && cursorRaw.defaultAgent && cursorEnabled ? "cursor" :
599
- typeof codexRaw.defaultAgent === "boolean" && codexRaw.defaultAgent && codexEnabled ? "codex" :
600
- typeof cccRaw.defaultAgent === "boolean" && cccRaw.defaultAgent && cccEnabled ? "ccc" :
609
+ typeof codexRaw.defaultAgent === "boolean" && codexRaw.defaultAgent && codexEnabled ? "codex" :
610
+ typeof cccRaw.defaultAgent === "boolean" && cccRaw.defaultAgent && cccEnabled ? "ccc" :
601
611
  null;
602
612
  const fallbackDefaultTool: AgentTool =
603
613
  claudeEnabled ? "claude" :
604
614
  cursorEnabled ? "cursor" :
605
- codexEnabled ? "codex" :
606
- cccEnabled ? "ccc" :
615
+ codexEnabled ? "codex" :
616
+ cccEnabled ? "ccc" :
607
617
  "claude";
608
618
  const defaultTool = explicitDefaultTool ?? fallbackDefaultTool;
609
619
 
@@ -612,7 +622,7 @@ function loadConfig(): AppConfig {
612
622
  appId: feishu.appId ?? "",
613
623
  appSecret: feishu.appSecret ?? "",
614
624
  },
615
- platforms: {
625
+ platforms: {
616
626
  feishu: {
617
627
  enabled: typeof (parsed.platforms as unknown as Record<string, unknown> | undefined)?.feishu === "object"
618
628
  ? Boolean(((parsed.platforms as unknown as Record<string, unknown>).feishu as Record<string, unknown>).enabled ?? true)
@@ -630,13 +640,13 @@ function loadConfig(): AppConfig {
630
640
  reuseTokenOnStart: typeof (parsed.platforms as unknown as Record<string, unknown> | undefined)?.ilink === "object"
631
641
  ? Boolean(((parsed.platforms as unknown as Record<string, unknown>).ilink as Record<string, unknown>).reuseTokenOnStart ?? true)
632
642
  : true,
633
- },
634
- },
635
- webUi: {
636
- // 兼容升级前没有 webUi 字段的 config.json:缺省仍按“自动打开”处理。
637
- openOnStart: typeof webUiRaw.openOnStart === "boolean" ? webUiRaw.openOnStart : true,
638
- },
639
- chromeDevtools: {
643
+ },
644
+ },
645
+ webUi: {
646
+ // 兼容升级前没有 webUi 字段的 config.json:缺省仍按“自动打开”处理。
647
+ openOnStart: typeof webUiRaw.openOnStart === "boolean" ? webUiRaw.openOnStart : true,
648
+ },
649
+ chromeDevtools: {
640
650
  enabled: typeof chromeDevtoolsRaw.enabled === "boolean" ? chromeDevtoolsRaw.enabled : false,
641
651
  port: Number.isInteger(chromeDevtoolsPort) && chromeDevtoolsPort >= 1 && chromeDevtoolsPort <= 65535
642
652
  ? chromeDevtoolsPort
@@ -646,12 +656,12 @@ function loadConfig(): AppConfig {
646
656
  port: typeof parsed.port === "number" ? parsed.port : 18080,
647
657
  gitTimeoutSeconds: typeof parsed.gitTimeoutSeconds === "number" ? parsed.gitTimeoutSeconds : 180,
648
658
  allowInterrupt: typeof parsed.allowInterrupt === "boolean" ? parsed.allowInterrupt : false,
649
- rawStreamLogs: {
650
- claude: normalizeRawStreamAgentLogConfig(rawStreamLogsRaw.claude),
651
- cursor: normalizeRawStreamAgentLogConfig(rawStreamLogsRaw.cursor),
652
- codex: normalizeRawStreamAgentLogConfig(rawStreamLogsRaw.codex),
653
- ccc: normalizeRawStreamAgentLogConfig(rawStreamLogsRaw.ccc),
654
- },
659
+ rawStreamLogs: {
660
+ claude: normalizeRawStreamAgentLogConfig(rawStreamLogsRaw.claude),
661
+ cursor: normalizeRawStreamAgentLogConfig(rawStreamLogsRaw.cursor),
662
+ codex: normalizeRawStreamAgentLogConfig(rawStreamLogsRaw.codex),
663
+ ccc: normalizeRawStreamAgentLogConfig(rawStreamLogsRaw.ccc),
664
+ },
655
665
  claude: {
656
666
  enabled: claudeEnabled,
657
667
  defaultAgent: defaultTool === "claude",
@@ -673,28 +683,29 @@ function loadConfig(): AppConfig {
673
683
  avatarBatteryMode: normalizeCursorAvatarBatteryMode(cursorRaw.avatarBatteryMode),
674
684
  onDemandMonthlyBudget: normalizeCursorOnDemandMonthlyBudget(cursorRaw.onDemandMonthlyBudget),
675
685
  },
676
- codex: {
677
- enabled: codexEnabled,
678
- defaultAgent: defaultTool === "codex",
679
- path: readToolCliPath(codexRaw, { label: "codex", onLegacyField }),
680
- model: normalizeOptionalConfigField(codexRaw.model, { label: "codex.model" }),
681
- alternativeModel: normalizeOptionalConfigField(codexRaw.alternativeModel, { label: "codex.alternativeModel" }),
682
- effort: normalizeOptionalConfigField(codexRaw.effort, { label: "codex.effort" }),
683
- fastMode: codexRaw.fastMode === true,
684
- },
685
- ccc: {
686
- enabled: cccEnabled,
687
- defaultAgent: defaultTool === "ccc",
688
- DEEPSEEK_API_KEY: normalizeOptionalConfigField(cccRaw.DEEPSEEK_API_KEY, { label: "ccc.DEEPSEEK_API_KEY" }),
689
- DEEPSEEK_BASE_URL: normalizeOptionalConfigField(cccRaw.DEEPSEEK_BASE_URL, {
690
- label: "ccc.DEEPSEEK_BASE_URL",
691
- fallback: DEFAULT_CCC_DEEPSEEK_BASE_URL,
692
- }),
693
- model: normalizeOptionalConfigField(cccRaw.model, { label: "ccc.model", fallback: DEFAULT_CCC_MODEL }),
694
- alternativeModel: normalizeOptionalConfigField(cccRaw.alternativeModel, { label: "ccc.alternativeModel" }),
695
- },
696
- };
697
- }
686
+ codex: {
687
+ enabled: codexEnabled,
688
+ defaultAgent: defaultTool === "codex",
689
+ path: readToolCliPath(codexRaw, { label: "codex", onLegacyField }),
690
+ model: normalizeOptionalConfigField(codexRaw.model, { label: "codex.model" }),
691
+ alternativeModel: normalizeOptionalConfigField(codexRaw.alternativeModel, { label: "codex.alternativeModel" }),
692
+ effort: normalizeOptionalConfigField(codexRaw.effort, { label: "codex.effort" }),
693
+ fastMode: codexRaw.fastMode === true,
694
+ },
695
+ ccc: {
696
+ enabled: cccEnabled,
697
+ defaultAgent: defaultTool === "ccc",
698
+ DEEPSEEK_API_KEY: normalizeOptionalConfigField(cccRaw.DEEPSEEK_API_KEY, { label: "ccc.DEEPSEEK_API_KEY" }),
699
+ DEEPSEEK_BASE_URL: normalizeOptionalConfigField(cccRaw.DEEPSEEK_BASE_URL, {
700
+ label: "ccc.DEEPSEEK_BASE_URL",
701
+ fallback: DEFAULT_CCC_DEEPSEEK_BASE_URL,
702
+ }),
703
+ model: normalizeOptionalConfigField(cccRaw.model, { label: "ccc.model", fallback: DEFAULT_CCC_MODEL }),
704
+ alternativeModel: normalizeOptionalConfigField(cccRaw.alternativeModel, { label: "ccc.alternativeModel" }),
705
+ effort: normalizeOptionalConfigField(cccRaw.effort, { label: "ccc.effort" }),
706
+ },
707
+ };
708
+ }
698
709
 
699
710
  /**
700
711
  * 全局可变 config 对象。
@@ -1010,26 +1021,26 @@ export function explainMissingFeishuCredentialsAndExit(): never {
1010
1021
  export const CLAUDE_SESSION_PREFIX = "Claude Code Session:";
1011
1022
  /** 群描述中用于识别 Cursor 会话的前缀 */
1012
1023
  export const CURSOR_SESSION_PREFIX = "Cursor Session:";
1013
- /** 群描述中用于识别 Codex 会话的前缀 */
1014
- export const CODEX_SESSION_PREFIX = "Codex Session:";
1015
- /** 群描述中用于识别 CCC Agent 会话的前缀 */
1016
- export const CCC_SESSION_PREFIX = "CCC Session:";
1024
+ /** 群描述中用于识别 Codex 会话的前缀 */
1025
+ export const CODEX_SESSION_PREFIX = "Codex Session:";
1026
+ /** 群描述中用于识别 CCC Agent 会话的前缀 */
1027
+ export const CCC_SESSION_PREFIX = "CCC Session:";
1017
1028
 
1018
1029
  /** 根据 tool 名称返回对应的群描述前缀 */
1019
- export function sessionPrefixForTool(tool: string): string {
1020
- if (tool === "cursor") return CURSOR_SESSION_PREFIX;
1021
- if (tool === "codex") return CODEX_SESSION_PREFIX;
1022
- if (tool === "ccc") return CCC_SESSION_PREFIX;
1023
- return CLAUDE_SESSION_PREFIX;
1024
- }
1030
+ export function sessionPrefixForTool(tool: string): string {
1031
+ if (tool === "cursor") return CURSOR_SESSION_PREFIX;
1032
+ if (tool === "codex") return CODEX_SESSION_PREFIX;
1033
+ if (tool === "ccc") return CCC_SESSION_PREFIX;
1034
+ return CLAUDE_SESSION_PREFIX;
1035
+ }
1025
1036
 
1026
1037
  /** 根据 tool 名称返回用于状态展示的标签 */
1027
- export function toolDisplayName(tool: string): string {
1028
- if (tool === "cursor") return "Cursor";
1029
- if (tool === "codex") return "Codex";
1030
- if (tool === "ccc") return "CCC Agent";
1031
- return "Claude Code";
1032
- }
1038
+ export function toolDisplayName(tool: string): string {
1039
+ if (tool === "cursor") return "Cursor";
1040
+ if (tool === "codex") return "Codex";
1041
+ if (tool === "ccc") return "CCC Agent";
1042
+ return "Claude Code";
1043
+ }
1033
1044
 
1034
1045
  /** 解析 /new 未指定工具时使用的默认 Agent。旧配置缺省 defaultAgent 时保持 Claude 优先。 */
1035
1046
  export function resolveDefaultAgentTool(cfg: AppConfig = config): AgentTool {
@@ -0,0 +1,108 @@
1
+ /**
2
+ * progress/reducer.ts — ChatEvent 事件流 → ProgressView 的增量合并
3
+ *
4
+ * 这是飞书与终端共享的唯一一份"事件 → 过程视图"公共逻辑:
5
+ * 终端 REPL 直接消费 ChatSession.chat() 的事件流,逐条 reduce;
6
+ * 飞书侧 display loop 若未来接入事件流,同样使用本 reducer,保证两端
7
+ * 看到的过程状态(正文累积、工具调用、终态判定)完全一致。
8
+ */
9
+
10
+ import type { ChatEvent } from "../builtin/index.ts";
11
+ import {
12
+ withProgressView,
13
+ type ProgressToolCall,
14
+ type ProgressView,
15
+ } from "./view.ts";
16
+
17
+ /** 工具调用 input 单行摘要(终端折叠行展示用) */
18
+ export function summarizeToolInput(input: unknown, maxChars = 120): string {
19
+ let raw: string;
20
+ if (typeof input === "string") {
21
+ raw = input;
22
+ } else {
23
+ try {
24
+ raw = JSON.stringify(input);
25
+ } catch {
26
+ raw = String(input);
27
+ }
28
+ }
29
+ const oneLine = raw.replace(/\s+/g, " ").trim();
30
+ return oneLine.length > maxChars ? oneLine.slice(0, maxChars) + "…" : oneLine;
31
+ }
32
+
33
+ /** 工具结果单行摘要(成功/失败各取首行) */
34
+ export function summarizeToolResult(content: unknown, maxChars = 120): string {
35
+ let raw: string;
36
+ if (typeof content === "string") {
37
+ raw = content;
38
+ } else if (content instanceof Error) {
39
+ raw = content.message;
40
+ } else {
41
+ try {
42
+ raw = JSON.stringify(content);
43
+ } catch {
44
+ raw = String(content);
45
+ }
46
+ }
47
+ const firstLine = raw.split("\n")[0] ?? "";
48
+ return firstLine.length > maxChars ? firstLine.slice(0, maxChars) + "…" : firstLine;
49
+ }
50
+
51
+ /**
52
+ * 把一条 ChatEvent 合并进 ProgressView,返回新视图(不可变更新)。
53
+ * 未识别的/不影响展示的事件(如 compact)返回原视图。
54
+ */
55
+ export function reduceProgress(prev: ProgressView, event: ChatEvent): ProgressView {
56
+ switch (event.type) {
57
+ case "text":
58
+ // accumulated 是全文累积,直接全量替换,天然幂等
59
+ return withProgressView(prev, { text: event.accumulated });
60
+
61
+ case "tool_use": {
62
+ const tool: ProgressToolCall = {
63
+ id: event.id ?? `tool-${prev.tools.length + 1}`,
64
+ name: event.name,
65
+ status: "running",
66
+ detail: summarizeToolInput(event.input),
67
+ };
68
+ return withProgressView(prev, { tools: [...prev.tools, tool] });
69
+ }
70
+
71
+ case "tool_result": {
72
+ const nextStatus = event.is_error ? ("error" as const) : ("ok" as const);
73
+ const summary = summarizeToolResult(event.content);
74
+ let matched = false;
75
+ const tools = prev.tools.map((t) => {
76
+ if (matched || t.id !== event.tool_use_id) return t;
77
+ matched = true;
78
+ return { ...t, status: nextStatus, summary };
79
+ });
80
+ if (!matched) {
81
+ // id 缺失或失配:兜底更新最后一个 running 工具,避免状态悬挂
82
+ const lastRunning = [...prev.tools].reverse().findIndex((t) => t.status === "running");
83
+ if (lastRunning >= 0) {
84
+ const idx = prev.tools.length - 1 - lastRunning;
85
+ const updated = prev.tools.map((t, i) =>
86
+ i === idx ? { ...t, status: nextStatus, summary } : t,
87
+ );
88
+ return withProgressView(prev, { tools: updated });
89
+ }
90
+ }
91
+ return withProgressView(prev, { tools });
92
+ }
93
+
94
+ case "done":
95
+ return withProgressView(prev, {
96
+ status: "done",
97
+ showStop: false,
98
+ text: event.text,
99
+ });
100
+
101
+ case "error":
102
+ return withProgressView(prev, { status: "error", showStop: false });
103
+
104
+ case "compact":
105
+ // 旧上下文压缩不影响当前过程展示
106
+ return prev;
107
+ }
108
+ }