chatccc 0.2.195 → 0.2.197

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.
Files changed (51) hide show
  1. package/agent-prompts/cursor_specific.md +13 -13
  2. package/bin/cccagent.mjs +17 -17
  3. package/config.sample.json +27 -27
  4. package/package.json +1 -1
  5. package/src/__tests__/agent-reload-config-rpc.test.ts +99 -0
  6. package/src/__tests__/builtin-chat-session.test.ts +277 -181
  7. package/src/__tests__/builtin-cli-json.test.ts +39 -39
  8. package/src/__tests__/builtin-config.test.ts +33 -33
  9. package/src/__tests__/builtin-context.test.ts +163 -163
  10. package/src/__tests__/builtin-file-tools.test.ts +224 -196
  11. package/src/__tests__/builtin-session-select.test.ts +116 -116
  12. package/src/__tests__/builtin-sigint.test.ts +56 -56
  13. package/src/__tests__/cards.test.ts +109 -109
  14. package/src/__tests__/ccc-adapter.test.ts +114 -113
  15. package/src/__tests__/chatgpt-subscription-rpc.test.ts +89 -89
  16. package/src/__tests__/chatgpt-subscription.test.ts +135 -135
  17. package/src/__tests__/chrome-devtools-guard.test.ts +165 -165
  18. package/src/__tests__/claude-raw-stream-log.test.ts +87 -0
  19. package/src/__tests__/codex-raw-stream-log.test.ts +120 -0
  20. package/src/__tests__/config-reload.test.ts +10 -10
  21. package/src/__tests__/config-sample.test.ts +18 -18
  22. package/src/__tests__/cursor-adapter.test.ts +99 -2
  23. package/src/__tests__/feishu-avatar.test.ts +40 -40
  24. package/src/__tests__/orchestrator.test.ts +181 -154
  25. package/src/__tests__/raw-stream-log.test.ts +106 -106
  26. package/src/__tests__/session.test.ts +40 -40
  27. package/src/__tests__/sim-platform.test.ts +12 -12
  28. package/src/__tests__/web-ui.test.ts +209 -209
  29. package/src/adapters/ccc-adapter.ts +121 -119
  30. package/src/adapters/claude-adapter.ts +603 -566
  31. package/src/adapters/codex-adapter.ts +57 -32
  32. package/src/adapters/cursor-adapter.ts +182 -57
  33. package/src/adapters/raw-stream-log.ts +124 -124
  34. package/src/agent-reload-config-rpc.ts +34 -0
  35. package/src/builtin/cli.ts +473 -461
  36. package/src/builtin/context.ts +323 -323
  37. package/src/builtin/file-tools.ts +1072 -915
  38. package/src/builtin/index.ts +404 -353
  39. package/src/builtin/session-select.ts +48 -48
  40. package/src/builtin/sigint.ts +50 -50
  41. package/src/cards.ts +195 -195
  42. package/src/chatgpt-subscription-rpc.ts +27 -27
  43. package/src/chatgpt-subscription.ts +299 -299
  44. package/src/chrome-devtools-guard.ts +318 -318
  45. package/src/config.ts +125 -125
  46. package/src/feishu-api.ts +49 -49
  47. package/src/index.ts +8 -13
  48. package/src/orchestrator.ts +166 -145
  49. package/src/runtime-reload.ts +34 -0
  50. package/src/session.ts +132 -130
  51. package/src/web-ui.ts +205 -205
package/src/web-ui.ts CHANGED
@@ -26,15 +26,15 @@ const ILINK_AUTH_PATH = join(USER_DATA_DIR, "state", "ilink-auth.json");
26
26
  // Helpers — config.json parsing & generation
27
27
  // ---------------------------------------------------------------------------
28
28
 
29
- interface AppConfig {
30
- feishu?: { appId?: string; appSecret?: string };
31
- platforms?: {
32
- feishu?: { enabled?: boolean; platformType?: string };
33
- ilink?: { enabled?: boolean; reuseTokenOnStart?: boolean };
34
- };
35
- chromeDevtools?: { enabled?: boolean; port?: number; chromePath?: string };
36
- port?: number;
37
- gitTimeoutSeconds?: number;
29
+ interface AppConfig {
30
+ feishu?: { appId?: string; appSecret?: string };
31
+ platforms?: {
32
+ feishu?: { enabled?: boolean; platformType?: string };
33
+ ilink?: { enabled?: boolean; reuseTokenOnStart?: boolean };
34
+ };
35
+ chromeDevtools?: { enabled?: boolean; port?: number; chromePath?: string };
36
+ port?: number;
37
+ gitTimeoutSeconds?: number;
38
38
  claude?: { enabled?: boolean; defaultAgent?: boolean; model?: string; subagentModel?: string; effort?: string; apiKey?: string; baseUrl?: string; maxTurn?: number };
39
39
  // `command` 是已废弃的旧字段名,保留只读以兼容升级前的 config.json
40
40
  cursor?: {
@@ -104,76 +104,76 @@ function loadConfig(): AppConfig {
104
104
  }
105
105
  }
106
106
 
107
- function saveConfig(cfg: AppConfig): void {
108
- writeFileSync(CONFIG_FILE, JSON.stringify(cfg, null, 2), "utf8");
109
- }
110
-
111
- type ConfigApplyMode = "saved" | "reload" | "restart-required";
112
-
113
- interface ConfigApplyResult {
114
- mode: ConfigApplyMode;
115
- restartRequired: boolean;
116
- restartReasons: string[];
117
- }
118
-
119
- function stringValue(value: unknown): string {
120
- return typeof value === "string" ? value.trim() : "";
121
- }
122
-
123
- function portValue(cfg: AppConfig): number {
124
- return Number.isInteger(cfg.port) && cfg.port! >= 1 && cfg.port! <= 65535 ? cfg.port! : 18080;
125
- }
126
-
127
- function platformEnabled(cfg: AppConfig, platform: "feishu" | "ilink"): boolean {
128
- return cfg.platforms?.[platform]?.enabled !== false;
129
- }
130
-
131
- function feishuPlatformType(cfg: AppConfig): string {
132
- return cfg.platforms?.feishu?.platformType === "lark" ? "lark" : "feishu";
133
- }
134
-
135
- function ilinkReuseTokenOnStart(cfg: AppConfig): boolean {
136
- return cfg.platforms?.ilink?.reuseTokenOnStart !== false;
137
- }
138
-
139
- export function getRestartRequiredReasons(before: AppConfig, after: AppConfig): string[] {
140
- const reasons: string[] = [];
141
-
142
- if (portValue(before) !== portValue(after)) reasons.push("port");
143
- if (stringValue(before.feishu?.appId) !== stringValue(after.feishu?.appId)) reasons.push("feishu.appId");
144
- if (stringValue(before.feishu?.appSecret) !== stringValue(after.feishu?.appSecret)) reasons.push("feishu.appSecret");
145
- if (feishuPlatformType(before) !== feishuPlatformType(after)) reasons.push("platforms.feishu.platformType");
146
- if (platformEnabled(before, "feishu") !== platformEnabled(after, "feishu")) reasons.push("platforms.feishu.enabled");
147
- if (platformEnabled(before, "ilink") !== platformEnabled(after, "ilink")) reasons.push("platforms.ilink.enabled");
148
- if (ilinkReuseTokenOnStart(before) !== ilinkReuseTokenOnStart(after)) {
149
- reasons.push("platforms.ilink.reuseTokenOnStart");
150
- }
151
-
152
- return reasons;
153
- }
154
-
155
- async function applySavedConfigIfPossible(before: AppConfig, after: AppConfig): Promise<ConfigApplyResult> {
156
- const setupMode = Boolean(setupActivateHook && setupHttpServer);
157
- if (setupMode) {
158
- return { mode: "saved", restartRequired: false, restartReasons: [] };
159
- }
160
-
161
- const running = isServiceRunning();
162
- if (!running) {
163
- return { mode: "saved", restartRequired: false, restartReasons: [] };
164
- }
165
-
166
- const restartReasons = getRestartRequiredReasons(before, after);
167
- if (restartReasons.length > 0) {
168
- return { mode: "restart-required", restartRequired: true, restartReasons };
169
- }
170
- if (!reloadConfigHook) {
171
- return { mode: "saved", restartRequired: false, restartReasons: [] };
172
- }
173
-
174
- await reloadConfigHook();
175
- return { mode: "reload", restartRequired: false, restartReasons: [] };
176
- }
107
+ function saveConfig(cfg: AppConfig): void {
108
+ writeFileSync(CONFIG_FILE, JSON.stringify(cfg, null, 2), "utf8");
109
+ }
110
+
111
+ type ConfigApplyMode = "saved" | "reload" | "restart-required";
112
+
113
+ interface ConfigApplyResult {
114
+ mode: ConfigApplyMode;
115
+ restartRequired: boolean;
116
+ restartReasons: string[];
117
+ }
118
+
119
+ function stringValue(value: unknown): string {
120
+ return typeof value === "string" ? value.trim() : "";
121
+ }
122
+
123
+ function portValue(cfg: AppConfig): number {
124
+ return Number.isInteger(cfg.port) && cfg.port! >= 1 && cfg.port! <= 65535 ? cfg.port! : 18080;
125
+ }
126
+
127
+ function platformEnabled(cfg: AppConfig, platform: "feishu" | "ilink"): boolean {
128
+ return cfg.platforms?.[platform]?.enabled !== false;
129
+ }
130
+
131
+ function feishuPlatformType(cfg: AppConfig): string {
132
+ return cfg.platforms?.feishu?.platformType === "lark" ? "lark" : "feishu";
133
+ }
134
+
135
+ function ilinkReuseTokenOnStart(cfg: AppConfig): boolean {
136
+ return cfg.platforms?.ilink?.reuseTokenOnStart !== false;
137
+ }
138
+
139
+ export function getRestartRequiredReasons(before: AppConfig, after: AppConfig): string[] {
140
+ const reasons: string[] = [];
141
+
142
+ if (portValue(before) !== portValue(after)) reasons.push("port");
143
+ if (stringValue(before.feishu?.appId) !== stringValue(after.feishu?.appId)) reasons.push("feishu.appId");
144
+ if (stringValue(before.feishu?.appSecret) !== stringValue(after.feishu?.appSecret)) reasons.push("feishu.appSecret");
145
+ if (feishuPlatformType(before) !== feishuPlatformType(after)) reasons.push("platforms.feishu.platformType");
146
+ if (platformEnabled(before, "feishu") !== platformEnabled(after, "feishu")) reasons.push("platforms.feishu.enabled");
147
+ if (platformEnabled(before, "ilink") !== platformEnabled(after, "ilink")) reasons.push("platforms.ilink.enabled");
148
+ if (ilinkReuseTokenOnStart(before) !== ilinkReuseTokenOnStart(after)) {
149
+ reasons.push("platforms.ilink.reuseTokenOnStart");
150
+ }
151
+
152
+ return reasons;
153
+ }
154
+
155
+ async function applySavedConfigIfPossible(before: AppConfig, after: AppConfig): Promise<ConfigApplyResult> {
156
+ const setupMode = Boolean(setupActivateHook && setupHttpServer);
157
+ if (setupMode) {
158
+ return { mode: "saved", restartRequired: false, restartReasons: [] };
159
+ }
160
+
161
+ const running = isServiceRunning();
162
+ if (!running) {
163
+ return { mode: "saved", restartRequired: false, restartReasons: [] };
164
+ }
165
+
166
+ const restartReasons = getRestartRequiredReasons(before, after);
167
+ if (restartReasons.length > 0) {
168
+ return { mode: "restart-required", restartRequired: true, restartReasons };
169
+ }
170
+ if (!reloadConfigHook) {
171
+ return { mode: "saved", restartRequired: false, restartReasons: [] };
172
+ }
173
+
174
+ await reloadConfigHook();
175
+ return { mode: "reload", restartRequired: false, restartReasons: [] };
176
+ }
177
177
 
178
178
  function maskSecret(value: string | undefined): string {
179
179
  if (!value) return "(未设置)";
@@ -366,14 +366,14 @@ async function handlePostConfig(req: IncomingMessage, res: ServerResponse): Prom
366
366
  existing as unknown as Record<string, unknown>,
367
367
  unflattenConfig(updates),
368
368
  ) as AppConfig;
369
- try {
370
- saveConfig(merged);
371
- const applyResult = await applySavedConfigIfPossible(existing, merged);
372
- jsonReply(res, 200, { ok: true, ...applyResult });
373
- } catch (err) {
374
- jsonReply(res, 500, { ok: false, saved: true, error: (err as Error).message });
375
- }
376
- }
369
+ try {
370
+ saveConfig(merged);
371
+ const applyResult = await applySavedConfigIfPossible(existing, merged);
372
+ jsonReply(res, 200, { ok: true, ...applyResult });
373
+ } catch (err) {
374
+ jsonReply(res, 500, { ok: false, saved: true, error: (err as Error).message });
375
+ }
376
+ }
377
377
 
378
378
  function deepMerge(target: Record<string, unknown>, source: Record<string, unknown>): Record<string, unknown> {
379
379
  const result = { ...target };
@@ -521,18 +521,18 @@ async function handleStartService(_req: IncomingMessage, res: ServerResponse): P
521
521
  return;
522
522
  }
523
523
 
524
- if (path === "reload") {
525
- // service 已经在跑:只刷新进程内配置,让后续消息/新会话使用新值。
526
- if (!reloadConfigHook) {
527
- jsonReply(res, 200, { ok: true, pid: process.pid, mode: "saved" });
528
- return;
529
- }
530
- try {
531
- await reloadConfigHook();
532
- jsonReply(res, 200, { ok: true, pid: process.pid, mode: "reload" });
533
- } catch (err) {
534
- jsonReply(res, 500, { ok: false, error: (err as Error).message });
535
- }
524
+ if (path === "reload") {
525
+ // service 已经在跑:只刷新进程内配置,让后续消息/新会话使用新值。
526
+ if (!reloadConfigHook) {
527
+ jsonReply(res, 200, { ok: true, pid: process.pid, mode: "saved" });
528
+ return;
529
+ }
530
+ try {
531
+ await reloadConfigHook();
532
+ jsonReply(res, 200, { ok: true, pid: process.pid, mode: "reload" });
533
+ } catch (err) {
534
+ jsonReply(res, 500, { ok: false, error: (err as Error).message });
535
+ }
536
536
  return;
537
537
  }
538
538
 
@@ -930,11 +930,11 @@ header .badge{font-size:13px;padding:4px 12px;border-radius:12px;font-weight:500
930
930
  <span id="dash-platform-feishu-label">已启用</span>
931
931
  </label>
932
932
  </div>
933
- <div class="config-row"><span class="key">App ID</span><span class="val" id="cfg-APP_ID">-</span></div>
934
- <div class="config-row"><span class="key">App Secret</span><span class="val" id="cfg-APP_SECRET">-</span></div>
935
- <div class="config-row"><span class="key">平台类型</span><span class="val" id="cfg-FEISHU_PLATFORM_TYPE">-</span></div>
936
- <div class="hint" style="margin-top:6px;line-height:1.6">生效范围:飞书开关、App ID、App Secret 或平台类型变更需要重启 ChatCCC。</div>
937
- <button class="btn btn-outline" style="margin-top:8px" onclick="editSection('feishu')">编辑</button>
933
+ <div class="config-row"><span class="key">App ID</span><span class="val" id="cfg-APP_ID">-</span></div>
934
+ <div class="config-row"><span class="key">App Secret</span><span class="val" id="cfg-APP_SECRET">-</span></div>
935
+ <div class="config-row"><span class="key">平台类型</span><span class="val" id="cfg-FEISHU_PLATFORM_TYPE">-</span></div>
936
+ <div class="hint" style="margin-top:6px;line-height:1.6">生效范围:飞书开关、App ID、App Secret 或平台类型变更需要重启 ChatCCC。</div>
937
+ <button class="btn btn-outline" style="margin-top:8px" onclick="editSection('feishu')">编辑</button>
938
938
  </div>
939
939
  </details>
940
940
 
@@ -948,13 +948,13 @@ header .badge{font-size:13px;padding:4px 12px;border-radius:12px;font-weight:500
948
948
  <span id="dash-platform-ilink-label">未启用</span>
949
949
  </label>
950
950
  </div>
951
- <div id="ilink-forget-row" style="margin-top:6px;display:none">
952
- <button class="btn btn-outline" style="font-size:12px" onclick="forgetIlink()">忘记扫码</button>
953
- <span style="font-size:11px;color:#94a3b8;margin-left:8px">清除登录状态,重启后重新扫码</span>
954
- </div>
955
- <div class="hint" style="margin-top:6px;line-height:1.6">生效范围:微信 iLink 开关变更需要重启 ChatCCC。</div>
956
- </div>
957
- </details>
951
+ <div id="ilink-forget-row" style="margin-top:6px;display:none">
952
+ <button class="btn btn-outline" style="font-size:12px" onclick="forgetIlink()">忘记扫码</button>
953
+ <span style="font-size:11px;color:#94a3b8;margin-left:8px">清除登录状态,重启后重新扫码</span>
954
+ </div>
955
+ <div class="hint" style="margin-top:6px;line-height:1.6">生效范围:微信 iLink 开关变更需要重启 ChatCCC。</div>
956
+ </div>
957
+ </details>
958
958
 
959
959
  <details class="card config-section">
960
960
  <summary>Chrome CDP(选填)</summary>
@@ -962,7 +962,7 @@ header .badge{font-size:13px;padding:4px 12px;border-radius:12px;font-weight:500
962
962
  <div class="config-row"><span class="key">状态</span><span class="val" id="cfg-CHROME_DEVTOOLS_ENABLED">-</span></div>
963
963
  <div class="config-row" id="cfg-CHROME_DEVTOOLS_PORT_ROW"><span class="key">CDP 端口</span><span class="val" id="cfg-CHROME_DEVTOOLS_PORT">-</span></div>
964
964
  <div class="config-row" id="cfg-CHROME_DEVTOOLS_PATH_ROW"><span class="key">Chrome 路径</span><span class="val" id="cfg-CHROME_DEVTOOLS_PATH">-</span></div>
965
- <div class="hint" style="margin-top:6px;line-height:1.6">生效范围:保存后立即应用到 Chrome CDP 守护进程。依赖:本机 Google Chrome;ChatGPT 订阅到期查询需要在该 CDP Chrome 中登录 ChatGPT。</div>
965
+ <div class="hint" style="margin-top:6px;line-height:1.6">生效范围:保存后立即应用到 Chrome CDP 守护进程。依赖:本机 Google Chrome;ChatGPT 订阅到期查询需要在该 CDP Chrome 中登录 ChatGPT。</div>
966
966
  <button class="btn btn-outline" style="margin-top:8px" onclick="editSection('chromeDevtools')">编辑</button>
967
967
  </div>
968
968
  </details>
@@ -975,10 +975,10 @@ header .badge{font-size:13px;padding:4px 12px;border-radius:12px;font-weight:500
975
975
  <div class="config-row"><span class="key">Effort</span><span class="val" id="cfg-ANTHROPIC_EFFORT">-</span></div>
976
976
  <div class="config-row"><span class="key">API Key</span><span class="val" id="cfg-ANTHROPIC_API_KEY">-</span></div>
977
977
  <div class="config-row"><span class="key">Base URL</span><span class="val" id="cfg-ANTHROPIC_BASE_URL">-</span></div>
978
- <div class="config-row"><span class="key">Max Turns</span><span class="val" id="cfg-ANTHROPIC_MAX_TURN">-</span><span class="hint">(0=无限制)</span></div>
979
- <label class="agent-default-row" style="margin-top:10px"><input type="checkbox" id="dash-default-claude" onchange="setDashboardDefaultAgent('claude', this.checked)"> 设为默认 Agent</label>
980
- <div class="hint" style="margin-top:6px;line-height:1.6">生效范围:保存后下一条消息或下个新会话生效,当前生成不中断。</div>
981
- <button class="btn btn-outline" style="margin-top:8px" onclick="editSection('claude')">编辑</button>
978
+ <div class="config-row"><span class="key">Max Turns</span><span class="val" id="cfg-ANTHROPIC_MAX_TURN">-</span><span class="hint">(0=无限制)</span></div>
979
+ <label class="agent-default-row" style="margin-top:10px"><input type="checkbox" id="dash-default-claude" onchange="setDashboardDefaultAgent('claude', this.checked)"> 设为默认 Agent</label>
980
+ <div class="hint" style="margin-top:6px;line-height:1.6">生效范围:保存后下一条消息或下个新会话生效,当前生成不中断。</div>
981
+ <button class="btn btn-outline" style="margin-top:8px" onclick="editSection('claude')">编辑</button>
982
982
  </div>
983
983
  </details>
984
984
 
@@ -989,10 +989,10 @@ header .badge{font-size:13px;padding:4px 12px;border-radius:12px;font-weight:500
989
989
  <div class="config-row"><span class="key">模型</span><span class="val" id="cfg-CURSOR_MODEL">-</span></div>
990
990
  <div class="config-row"><span class="key">备选模型</span><span class="val" id="cfg-CURSOR_ALTERNATIVE_MODEL">-</span></div>
991
991
  <div class="config-row"><span class="key">头像电池电量</span><span class="val" id="cfg-CURSOR_AVATAR_BATTERY_MODE">-</span></div>
992
- <div class="config-row" id="cfg-CURSOR_ON_DEMAND_MONTHLY_BUDGET_ROW"><span class="key">每月On demand use预算</span><span class="val" id="cfg-CURSOR_ON_DEMAND_MONTHLY_BUDGET">-</span></div>
993
- <label class="agent-default-row" style="margin-top:10px"><input type="checkbox" id="dash-default-cursor" onchange="setDashboardDefaultAgent('cursor', this.checked)"> 设为默认 Agent</label>
994
- <div class="hint" style="margin-top:6px;line-height:1.6">生效范围:保存后下一条消息或下个新会话生效,当前生成不中断。</div>
995
- <button class="btn btn-outline" style="margin-top:8px" onclick="editSection('cursor')">编辑</button>
992
+ <div class="config-row" id="cfg-CURSOR_ON_DEMAND_MONTHLY_BUDGET_ROW"><span class="key">每月On demand use预算</span><span class="val" id="cfg-CURSOR_ON_DEMAND_MONTHLY_BUDGET">-</span></div>
993
+ <label class="agent-default-row" style="margin-top:10px"><input type="checkbox" id="dash-default-cursor" onchange="setDashboardDefaultAgent('cursor', this.checked)"> 设为默认 Agent</label>
994
+ <div class="hint" style="margin-top:6px;line-height:1.6">生效范围:保存后下一条消息或下个新会话生效,当前生成不中断。</div>
995
+ <button class="btn btn-outline" style="margin-top:8px" onclick="editSection('cursor')">编辑</button>
996
996
  </div>
997
997
  </details>
998
998
 
@@ -1002,10 +1002,10 @@ header .badge{font-size:13px;padding:4px 12px;border-radius:12px;font-weight:500
1002
1002
  <div class="config-row"><span class="key">CLI 路径</span><span class="val" id="cfg-CODEX_PATH">-</span></div>
1003
1003
  <div class="config-row"><span class="key">模型</span><span class="val" id="cfg-CODEX_MODEL">-</span></div>
1004
1004
  <div class="config-row"><span class="key">备选模型</span><span class="val" id="cfg-CODEX_ALTERNATIVE_MODEL">-</span></div>
1005
- <div class="config-row"><span class="key">Effort</span><span class="val" id="cfg-CODEX_EFFORT">-</span></div>
1006
- <label class="agent-default-row" style="margin-top:10px"><input type="checkbox" id="dash-default-codex" onchange="setDashboardDefaultAgent('codex', this.checked)"> 设为默认 Agent</label>
1007
- <div class="hint" style="margin-top:6px;line-height:1.6">生效范围:保存后下一条消息或下个新会话生效,当前生成不中断。</div>
1008
- <button class="btn btn-outline" style="margin-top:8px" onclick="editSection('codex')">编辑</button>
1005
+ <div class="config-row"><span class="key">Effort</span><span class="val" id="cfg-CODEX_EFFORT">-</span></div>
1006
+ <label class="agent-default-row" style="margin-top:10px"><input type="checkbox" id="dash-default-codex" onchange="setDashboardDefaultAgent('codex', this.checked)"> 设为默认 Agent</label>
1007
+ <div class="hint" style="margin-top:6px;line-height:1.6">生效范围:保存后下一条消息或下个新会话生效,当前生成不中断。</div>
1008
+ <button class="btn btn-outline" style="margin-top:8px" onclick="editSection('codex')">编辑</button>
1009
1009
  </div>
1010
1010
  </details>
1011
1011
 
@@ -1021,8 +1021,8 @@ header .badge{font-size:13px;padding:4px 12px;border-radius:12px;font-weight:500
1021
1021
  <!-- Edit Modal -->
1022
1022
  <div id="edit-modal" class="card hidden" style="position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);width:90%;max-width:480px;z-index:200;max-height:80vh;overflow-y:auto">
1023
1023
  <h2 id="edit-modal-title">编辑配置</h2>
1024
- <div id="edit-modal-effect" class="hint" style="margin-bottom:12px;line-height:1.6"></div>
1025
- <div id="edit-modal-fields"></div>
1024
+ <div id="edit-modal-effect" class="hint" style="margin-bottom:12px;line-height:1.6"></div>
1025
+ <div id="edit-modal-fields"></div>
1026
1026
  <div class="btn-group" style="justify-content:flex-end;margin-top:16px">
1027
1027
  <button class="btn btn-outline" onclick="closeEditModal()">取消</button>
1028
1028
  <button class="btn btn-primary" onclick="saveEdit()">保存</button>
@@ -1061,27 +1061,27 @@ function cursorBatteryModeLabel(value) {
1061
1061
  return value === 'onDemandUse' ? 'On demand use 金额' : 'API 使用比例';
1062
1062
  }
1063
1063
 
1064
- function configEffectHint(section) {
1065
- if (section === 'feishu') return '生效范围:App ID、App Secret、平台类型或飞书开关变更需要重启 ChatCCC;其它未变更项仅保存。';
1066
- if (section === 'chromeDevtools') return '生效范围:保存后立即应用到 Chrome CDP 守护进程。';
1067
- if (section === 'claude') return '生效范围:保存后下一条消息或下个新会话生效,当前生成不中断。';
1068
- if (section === 'cursor') return '生效范围:保存后下一条消息或下个新会话生效,当前生成不中断。';
1069
- if (section === 'codex') return '生效范围:保存后下一条消息或下个新会话生效,当前生成不中断。';
1070
- return '生效范围:保存后按配置类型应用。';
1071
- }
1072
-
1073
- function toastConfigApplyResult(result, savedText) {
1074
- var text = savedText || '配置已保存';
1075
- if (result && result.mode === 'reload') {
1076
- toast(text + ',后续消息/新会话已生效。');
1077
- } else if (result && result.mode === 'restart-required') {
1078
- toast(text + ',需重启服务生效。');
1079
- } else {
1080
- toast(text);
1081
- }
1082
- }
1083
-
1084
- function onCursorBatteryModeChange(prefix, value) {
1064
+ function configEffectHint(section) {
1065
+ if (section === 'feishu') return '生效范围:App ID、App Secret、平台类型或飞书开关变更需要重启 ChatCCC;其它未变更项仅保存。';
1066
+ if (section === 'chromeDevtools') return '生效范围:保存后立即应用到 Chrome CDP 守护进程。';
1067
+ if (section === 'claude') return '生效范围:保存后下一条消息或下个新会话生效,当前生成不中断。';
1068
+ if (section === 'cursor') return '生效范围:保存后下一条消息或下个新会话生效,当前生成不中断。';
1069
+ if (section === 'codex') return '生效范围:保存后下一条消息或下个新会话生效,当前生成不中断。';
1070
+ return '生效范围:保存后按配置类型应用。';
1071
+ }
1072
+
1073
+ function toastConfigApplyResult(result, savedText) {
1074
+ var text = savedText || '配置已保存';
1075
+ if (result && result.mode === 'reload') {
1076
+ toast(text + ',后续消息/新会话已生效。');
1077
+ } else if (result && result.mode === 'restart-required') {
1078
+ toast(text + ',需重启服务生效。');
1079
+ } else {
1080
+ toast(text);
1081
+ }
1082
+ }
1083
+
1084
+ function onCursorBatteryModeChange(prefix, value) {
1085
1085
  var rowId = prefix === 'edit-' ? 'edit-cursor-on-demand-budget-row' : 'field-cursor-on-demand-budget-row';
1086
1086
  var row = document.getElementById(rowId);
1087
1087
  if (row) row.style.display = value === 'onDemandUse' ? '' : 'none';
@@ -1115,10 +1115,10 @@ async function onPlatformToggle(platform, enabled) {
1115
1115
  if (result.ok) {
1116
1116
  state.config.platforms = state.config.platforms || {};
1117
1117
  state.config.platforms[platform] = state.config.platforms[platform] || {};
1118
- state.config.platforms[platform].enabled = enabled;
1119
- var label = document.getElementById('dash-platform-' + platform + '-label');
1120
- if (label) label.textContent = enabled ? '已启用' : '未启用';
1121
- toastConfigApplyResult(result, (platform === 'feishu' ? '飞书' : '微信 iLink') + (enabled ? ' 已启用' : ' 已禁用'));
1118
+ state.config.platforms[platform].enabled = enabled;
1119
+ var label = document.getElementById('dash-platform-' + platform + '-label');
1120
+ if (label) label.textContent = enabled ? '已启用' : '未启用';
1121
+ toastConfigApplyResult(result, (platform === 'feishu' ? '飞书' : '微信 iLink') + (enabled ? ' 已启用' : ' 已禁用'));
1122
1122
  } else {
1123
1123
  toast('保存失败: ' + (result.error || '未知错误'), 'error');
1124
1124
  // 还原 toggle
@@ -1524,17 +1524,17 @@ function renderStep3() {
1524
1524
  document.getElementById('review-content').innerHTML = lines.join('');
1525
1525
  }
1526
1526
 
1527
- async function saveConfig(vars, options) {
1528
- options = options || {};
1527
+ async function saveConfig(vars, options) {
1528
+ options = options || {};
1529
1529
  var result = await api('/api/config', 'POST', { vars: vars });
1530
- if (result.ok) {
1531
- state.config = Object.assign({}, state.config, vars);
1532
- if (!options.quiet) toastConfigApplyResult(result, '配置已保存');
1530
+ if (result.ok) {
1531
+ state.config = Object.assign({}, state.config, vars);
1532
+ if (!options.quiet) toastConfigApplyResult(result, '配置已保存');
1533
1533
  } else {
1534
1534
  toast('保存失败: ' + (result.error || '未知错误'), 'error');
1535
1535
  }
1536
- return result;
1537
- }
1536
+ return result;
1537
+ }
1538
1538
 
1539
1539
  async function setDashboardDefaultAgent(agent, enabled) {
1540
1540
  if (!enabled) {
@@ -1553,10 +1553,10 @@ async function setDashboardDefaultAgent(agent, enabled) {
1553
1553
  state.config.cursor = state.config.cursor || {};
1554
1554
  state.config.codex = state.config.codex || {};
1555
1555
  state.config.claude.defaultAgent = agent === 'claude';
1556
- state.config.cursor.defaultAgent = agent === 'cursor';
1557
- state.config.codex.defaultAgent = agent === 'codex';
1558
- updateDefaultAgentToggles();
1559
- toastConfigApplyResult(result, '默认 Agent 已更新');
1556
+ state.config.cursor.defaultAgent = agent === 'cursor';
1557
+ state.config.codex.defaultAgent = agent === 'codex';
1558
+ updateDefaultAgentToggles();
1559
+ toastConfigApplyResult(result, '默认 Agent 已更新');
1560
1560
  } else {
1561
1561
  toast('保存失败: ' + (result.error || '未知错误'), 'error');
1562
1562
  updateDefaultAgentToggles();
@@ -1569,36 +1569,36 @@ async function saveAndStart() {
1569
1569
  toast('飞书已启用,请先填写 App ID 和 App Secret', 'error');
1570
1570
  return;
1571
1571
  }
1572
- var saved = await saveConfig(vars, { quiet: true });
1573
- if (saved.ok !== true) return;
1574
- document.getElementById('btn-save-start').disabled = true;
1575
- document.getElementById('btn-save-start').innerHTML = '<span class="spinner"></span> 应用中...';
1576
-
1577
- if (state.running && saved.restartRequired === true) {
1578
- await api('/api/restart', 'POST');
1579
- toast('配置已保存,服务正在重启…');
1580
- pollUntilRunning();
1581
- return;
1582
- }
1583
-
1584
- if (state.running && saved.mode === 'reload') {
1585
- toastConfigApplyResult(saved, '配置已保存');
1586
- setTimeout(function(){ location.reload(); }, 1000);
1587
- return;
1588
- }
1589
-
1590
- var result = await api('/api/start', 'POST');
1591
- if (result.ok) {
1592
- var msg;
1593
- if (result.mode === 'reload') {
1594
- msg = '配置已保存,后续消息/新会话已生效。';
1595
- } else if (result.mode === 'inplace') {
1596
- msg = '服务已启动! PID: ' + result.pid;
1597
- } else {
1598
- msg = '服务已启动! PID: ' + (result.pid || '?');
1599
- }
1600
- toast(msg);
1601
- setTimeout(function(){ location.reload(); }, 1500);
1572
+ var saved = await saveConfig(vars, { quiet: true });
1573
+ if (saved.ok !== true) return;
1574
+ document.getElementById('btn-save-start').disabled = true;
1575
+ document.getElementById('btn-save-start').innerHTML = '<span class="spinner"></span> 应用中...';
1576
+
1577
+ if (state.running && saved.restartRequired === true) {
1578
+ await api('/api/restart', 'POST');
1579
+ toast('配置已保存,服务正在重启…');
1580
+ pollUntilRunning();
1581
+ return;
1582
+ }
1583
+
1584
+ if (state.running && saved.mode === 'reload') {
1585
+ toastConfigApplyResult(saved, '配置已保存');
1586
+ setTimeout(function(){ location.reload(); }, 1000);
1587
+ return;
1588
+ }
1589
+
1590
+ var result = await api('/api/start', 'POST');
1591
+ if (result.ok) {
1592
+ var msg;
1593
+ if (result.mode === 'reload') {
1594
+ msg = '配置已保存,后续消息/新会话已生效。';
1595
+ } else if (result.mode === 'inplace') {
1596
+ msg = '服务已启动! PID: ' + result.pid;
1597
+ } else {
1598
+ msg = '服务已启动! PID: ' + (result.pid || '?');
1599
+ }
1600
+ toast(msg);
1601
+ setTimeout(function(){ location.reload(); }, 1500);
1602
1602
  } else {
1603
1603
  toast('保存失败: ' + (result.error || '未知错误'), 'error');
1604
1604
  document.getElementById('btn-save-start').disabled = false;
@@ -1781,9 +1781,9 @@ function editSection(section) {
1781
1781
  var titleMap = { feishu: '飞书', chromeDevtools: 'Chrome CDP', claude: 'Claude Agent', cursor: 'Cursor Agent', codex: 'Codex Agent' };
1782
1782
  document.getElementById('edit-modal-title').textContent = '编辑 ' + (titleMap[section] || section);
1783
1783
 
1784
- document.getElementById('edit-modal-effect').textContent = configEffectHint(section);
1785
-
1786
- var html = '';
1784
+ document.getElementById('edit-modal-effect').textContent = configEffectHint(section);
1785
+
1786
+ var html = '';
1787
1787
  var labelMap = {
1788
1788
  'CHATCCC_APP_ID': 'App ID', 'CHATCCC_APP_SECRET': 'App Secret',
1789
1789
  'CHATCCC_CHROME_DEVTOOLS_ENABLED': '启用常驻 Chrome CDP(选填)',
@@ -1918,16 +1918,16 @@ async function saveEdit() {
1918
1918
  var ptEl = document.getElementById('edit-CHATCCC_FEISHU_PLATFORM_TYPE');
1919
1919
  if (ptEl && ptEl.value.trim()) vars['CHATCCC_FEISHU_PLATFORM_TYPE'] = ptEl.value.trim();
1920
1920
  }
1921
- var result = await saveConfig(vars, { quiet: true });
1922
- if (result.ok !== true) return;
1923
- try {
1924
- var fresh = await api('/api/config');
1925
- state.config = fresh.vars || state.config;
1926
- } catch(e) {}
1927
- closeEditModal();
1928
- updateDashboardUI();
1929
- toastConfigApplyResult(result, '修改已保存');
1930
- }
1921
+ var result = await saveConfig(vars, { quiet: true });
1922
+ if (result.ok !== true) return;
1923
+ try {
1924
+ var fresh = await api('/api/config');
1925
+ state.config = fresh.vars || state.config;
1926
+ } catch(e) {}
1927
+ closeEditModal();
1928
+ updateDashboardUI();
1929
+ toastConfigApplyResult(result, '修改已保存');
1930
+ }
1931
1931
 
1932
1932
  // ---- Other actions ----
1933
1933
  function reconfigure() {