chatccc 0.2.184 → 0.2.186

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
@@ -39,9 +39,10 @@ export const USER_DATA_DIR = join(homedir(), ".chatccc");
39
39
  export const PID_FILE = join(USER_DATA_DIR, "state", "runtime.pid");
40
40
 
41
41
  export const LOG_DIR = join(USER_DATA_DIR, "logs");
42
- export const fileLog = setupFileLogging(LOG_DIR, "index");
43
-
44
- export const CHAT_LOGS_DIR = join(USER_DATA_DIR, "state", "chat_logs");
42
+ export const fileLog = setupFileLogging(LOG_DIR, "index");
43
+
44
+ export const CHAT_LOGS_DIR = join(USER_DATA_DIR, "state", "chat_logs");
45
+ export const RAW_STREAM_LOGS_DIR = join(LOG_DIR, "raw-streams");
45
46
 
46
47
  export async function appendChatLog(chatId: string, sender: string, text: string): Promise<void> {
47
48
  try {
@@ -113,17 +114,41 @@ export interface PlatformsConfig {
113
114
  ilink: PlatformConfig;
114
115
  }
115
116
 
116
- export interface AppConfig {
117
+ export interface ChromeDevtoolsConfig {
118
+ /** 是否由 ChatCCC 守护一个常驻 Chrome CDP 实例 */
119
+ enabled: boolean;
120
+ /** Chrome remote debugging 端口,默认 15166 */
121
+ port: number;
122
+ /** Chrome 可执行文件路径;留空时按常见安装位置自动探测 */
123
+ chromePath: string;
124
+ }
125
+
126
+ export interface RawStreamAgentLogConfig {
127
+ enabled: boolean;
128
+ maxBytesPerTurn: number;
129
+ retentionDays: number;
130
+ keepCompleted: boolean;
131
+ }
132
+
133
+ export interface RawStreamLogsConfig {
134
+ claude: RawStreamAgentLogConfig;
135
+ cursor: RawStreamAgentLogConfig;
136
+ codex: RawStreamAgentLogConfig;
137
+ }
138
+
139
+ export interface AppConfig {
117
140
  feishu: FeishuConfig;
118
141
  platforms: PlatformsConfig;
142
+ chromeDevtools: ChromeDevtoolsConfig;
119
143
  port: number;
120
- gitTimeoutSeconds: number;
121
- /** 若为 false,AI 生成过程中用户发送消息不会打断,须先点「停止」再发送新消息 */
122
- allowInterrupt: boolean;
123
- claude: ClaudeConfig;
124
- cursor: CursorConfig;
125
- codex: CodexConfig;
126
- }
144
+ gitTimeoutSeconds: number;
145
+ /** 若为 false,AI 生成过程中用户发送消息不会打断,须先点「停止」再发送新消息 */
146
+ allowInterrupt: boolean;
147
+ rawStreamLogs: RawStreamLogsConfig;
148
+ claude: ClaudeConfig;
149
+ cursor: CursorConfig;
150
+ codex: CodexConfig;
151
+ }
127
152
 
128
153
  export type AgentTool = "claude" | "cursor" | "codex";
129
154
  export const AGENT_TOOLS: AgentTool[] = ["claude", "cursor", "codex"];
@@ -333,19 +358,42 @@ function normalizeCursorAvatarBatteryMode(raw: unknown): CursorAvatarBatteryMode
333
358
  return raw === "onDemandUse" ? "onDemandUse" : "apiPercent";
334
359
  }
335
360
 
336
- function normalizeCursorOnDemandMonthlyBudget(raw: unknown): number {
337
- const value = typeof raw === "string" ? Number(raw.trim()) : Number(raw);
338
- return Number.isFinite(value) && value > 0 ? value : 1000;
339
- }
340
-
341
- function loadConfig(): AppConfig {
342
- const defaults: AppConfig = {
343
- feishu: { appId: "", appSecret: "" },
344
- platforms: { feishu: { enabled: true }, ilink: { enabled: true } },
345
- port: 18080,
346
- gitTimeoutSeconds: 180,
347
- allowInterrupt: false,
348
- claude: { enabled: false, defaultAgent: true, model: "", subagentModel: "", effort: "", apiKey: "", baseUrl: "", maxTurn: 0 },
361
+ function normalizeCursorOnDemandMonthlyBudget(raw: unknown): number {
362
+ const value = typeof raw === "string" ? Number(raw.trim()) : Number(raw);
363
+ return Number.isFinite(value) && value > 0 ? value : 1000;
364
+ }
365
+
366
+ function normalizePositiveInteger(raw: unknown, fallback: number): number {
367
+ const value = typeof raw === "string" ? Number(raw.trim()) : Number(raw);
368
+ return Number.isInteger(value) && value > 0 ? value : fallback;
369
+ }
370
+
371
+ function normalizeRawStreamAgentLogConfig(raw: unknown): RawStreamAgentLogConfig {
372
+ const obj = typeof raw === "object" && raw !== null
373
+ ? raw as Record<string, unknown>
374
+ : {};
375
+ return {
376
+ enabled: typeof obj.enabled === "boolean" ? obj.enabled : false,
377
+ maxBytesPerTurn: normalizePositiveInteger(obj.maxBytesPerTurn, 50 * 1024 * 1024),
378
+ retentionDays: normalizePositiveInteger(obj.retentionDays, 7),
379
+ keepCompleted: typeof obj.keepCompleted === "boolean" ? obj.keepCompleted : false,
380
+ };
381
+ }
382
+
383
+ function loadConfig(): AppConfig {
384
+ const defaults: AppConfig = {
385
+ feishu: { appId: "", appSecret: "" },
386
+ platforms: { feishu: { enabled: true }, ilink: { enabled: true } },
387
+ chromeDevtools: { enabled: false, port: 15166, chromePath: "" },
388
+ port: 18080,
389
+ gitTimeoutSeconds: 180,
390
+ allowInterrupt: false,
391
+ rawStreamLogs: {
392
+ claude: { enabled: false, maxBytesPerTurn: 50 * 1024 * 1024, retentionDays: 7, keepCompleted: false },
393
+ cursor: { enabled: false, maxBytesPerTurn: 50 * 1024 * 1024, retentionDays: 7, keepCompleted: false },
394
+ codex: { enabled: false, maxBytesPerTurn: 50 * 1024 * 1024, retentionDays: 7, keepCompleted: false },
395
+ },
396
+ claude: { enabled: false, defaultAgent: true, model: "", subagentModel: "", effort: "", apiKey: "", baseUrl: "", maxTurn: 0 },
349
397
  cursor: {
350
398
  enabled: false,
351
399
  defaultAgent: false,
@@ -405,8 +453,10 @@ function loadConfig(): AppConfig {
405
453
  avatarBatteryMode?: unknown;
406
454
  onDemandMonthlyBudget?: unknown;
407
455
  };
408
- codex?: { enabled?: unknown; defaultAgent?: unknown; path?: unknown; command?: unknown; model?: unknown; effort?: unknown };
409
- };
456
+ codex?: { enabled?: unknown; defaultAgent?: unknown; path?: unknown; command?: unknown; model?: unknown; effort?: unknown };
457
+ chromeDevtools?: { enabled?: unknown; port?: unknown; chromePath?: unknown };
458
+ rawStreamLogs?: unknown;
459
+ };
410
460
  try {
411
461
  parsed = JSON.parse(raw);
412
462
  } catch (err) {
@@ -416,8 +466,12 @@ function loadConfig(): AppConfig {
416
466
 
417
467
  const feishu = parsed.feishu ?? { appId: "", appSecret: "" };
418
468
  const claude = parsed.claude ?? {} as Partial<ClaudeConfig>;
419
- const cursorRaw = (parsed.cursor ?? {}) as NonNullable<typeof parsed.cursor>;
420
- const codexRaw = (parsed.codex ?? {}) as NonNullable<typeof parsed.codex>;
469
+ const cursorRaw = (parsed.cursor ?? {}) as NonNullable<typeof parsed.cursor>;
470
+ const codexRaw = (parsed.codex ?? {}) as NonNullable<typeof parsed.codex>;
471
+ const chromeDevtoolsRaw = (parsed.chromeDevtools ?? {}) as NonNullable<typeof parsed.chromeDevtools>;
472
+ const rawStreamLogsRaw = typeof parsed.rawStreamLogs === "object" && parsed.rawStreamLogs !== null
473
+ ? parsed.rawStreamLogs as unknown as Record<string, unknown>
474
+ : {};
421
475
 
422
476
  // 兼容旧字段 `command`:命中时打印一次性 warning 提示用户改名
423
477
  const onLegacyField = (label: string, value: string): void => {
@@ -461,6 +515,7 @@ function loadConfig(): AppConfig {
461
515
  const claudeEnabled = resolveEnabled(claude.enabled, claudeNonEmpty);
462
516
  const cursorEnabled = resolveEnabled(cursorRaw.enabled, cursorNonEmpty);
463
517
  const codexEnabled = resolveEnabled(codexRaw.enabled, codexNonEmpty);
518
+ const chromeDevtoolsPort = Number(chromeDevtoolsRaw.port);
464
519
  const explicitDefaultTool: AgentTool | null =
465
520
  typeof claude.defaultAgent === "boolean" && claude.defaultAgent && claudeEnabled ? "claude" :
466
521
  typeof cursorRaw.defaultAgent === "boolean" && cursorRaw.defaultAgent && cursorEnabled ? "cursor" :
@@ -498,10 +553,22 @@ function loadConfig(): AppConfig {
498
553
  : true,
499
554
  },
500
555
  },
501
- port: typeof parsed.port === "number" ? parsed.port : 18080,
502
- gitTimeoutSeconds: typeof parsed.gitTimeoutSeconds === "number" ? parsed.gitTimeoutSeconds : 180,
503
- allowInterrupt: typeof parsed.allowInterrupt === "boolean" ? parsed.allowInterrupt : false,
504
- claude: {
556
+ chromeDevtools: {
557
+ enabled: typeof chromeDevtoolsRaw.enabled === "boolean" ? chromeDevtoolsRaw.enabled : false,
558
+ port: Number.isInteger(chromeDevtoolsPort) && chromeDevtoolsPort >= 1 && chromeDevtoolsPort <= 65535
559
+ ? chromeDevtoolsPort
560
+ : 15166,
561
+ chromePath: normalizeOptionalConfigField(chromeDevtoolsRaw.chromePath, { label: "chromeDevtools.chromePath" }),
562
+ },
563
+ port: typeof parsed.port === "number" ? parsed.port : 18080,
564
+ gitTimeoutSeconds: typeof parsed.gitTimeoutSeconds === "number" ? parsed.gitTimeoutSeconds : 180,
565
+ allowInterrupt: typeof parsed.allowInterrupt === "boolean" ? parsed.allowInterrupt : false,
566
+ rawStreamLogs: {
567
+ claude: normalizeRawStreamAgentLogConfig(rawStreamLogsRaw.claude),
568
+ cursor: normalizeRawStreamAgentLogConfig(rawStreamLogsRaw.cursor),
569
+ codex: normalizeRawStreamAgentLogConfig(rawStreamLogsRaw.codex),
570
+ },
571
+ claude: {
505
572
  enabled: claudeEnabled,
506
573
  defaultAgent: defaultTool === "claude",
507
574
  model: normalizeOptionalConfigField(claude.model, { label: "claude.model" }),
package/src/index.ts CHANGED
@@ -82,6 +82,7 @@ import { handleAgentImageRequest } from "./agent-image-rpc.ts";
82
82
  import { handleAgentFileRequest } from "./agent-file-rpc.ts";
83
83
  import { handleAgentDelegateTaskRequest } from "./agent-delegate-task-rpc.ts";
84
84
  import { handleAgentStopStuckRequest } from "./agent-stop-stuck.ts";
85
+ import { handleChatGptSubscriptionRequest } from "./chatgpt-subscription-rpc.ts";
85
86
  import { applyPrivacy } from "./privacy.ts";
86
87
  import {
87
88
  createCardKitCard,
@@ -98,6 +99,7 @@ import {
98
99
  setSessionPlatform,
99
100
  startUnifiedDisplayLoop,
100
101
  } from "./session.ts";
102
+ import { startChromeDevtoolsGuard, stopChromeDevtoolsGuard } from "./chrome-devtools-guard.ts";
101
103
  import {
102
104
  rebuildSessionChatsFromRegistry,
103
105
  setQueueConsumer,
@@ -711,7 +713,8 @@ async function main(): Promise<void> {
711
713
  return (await handleAgentImageRequest(req, res))
712
714
  || (await handleAgentFileRequest(req, res))
713
715
  || (await handleAgentDelegateTaskRequest(req, res, feishuPlatform))
714
- || (await handleAgentStopStuckRequest(req, res));
716
+ || (await handleAgentStopStuckRequest(req, res))
717
+ || (await handleChatGptSubscriptionRequest(req, res));
715
718
  });
716
719
 
717
720
  const simServer = createServer(createUiRouter());
@@ -765,6 +768,7 @@ async function main(): Promise<void> {
765
768
  setReloadConfigHook(() => {
766
769
  reloadConfigFromDisk();
767
770
  clearAdapterCache();
771
+ startChromeDevtoolsGuard();
768
772
  appendStartupTrace("reload-from-ui: config reloaded", {
769
773
  appIdMask: maskAppId(APP_ID),
770
774
  });
@@ -773,7 +777,8 @@ async function main(): Promise<void> {
773
777
  return (await handleAgentImageRequest(req, res))
774
778
  || (await handleAgentFileRequest(req, res))
775
779
  || (await handleAgentDelegateTaskRequest(req, res, feishuPlatform))
776
- || (await handleAgentStopStuckRequest(req, res));
780
+ || (await handleAgentStopStuckRequest(req, res))
781
+ || (await handleChatGptSubscriptionRequest(req, res));
777
782
  });
778
783
 
779
784
  console.log(`[启动 2/7] 环境与凭证检查`);
@@ -793,6 +798,7 @@ async function main(): Promise<void> {
793
798
  onActivate: async (httpServer: Server) => {
794
799
  reloadConfigFromDisk();
795
800
  clearAdapterCache();
801
+ startChromeDevtoolsGuard();
796
802
  appendStartupTrace("setup-activate: reloaded config from disk", {
797
803
  appIdMaskAfterReload: maskAppId(APP_ID),
798
804
  });
@@ -817,6 +823,8 @@ async function main(): Promise<void> {
817
823
  appendStartupTrace("main: feishu disabled", {});
818
824
  }
819
825
 
826
+ startChromeDevtoolsGuard();
827
+
820
828
  // 启动 HTTP server(同时挂 UI router,供 dashboard / setup / agent image/file 使用)
821
829
  appendStartupTrace("main: before freeRelayListenPort", { CHATCCC_PORT });
822
830
  const killed = freeRelayListenPort(CHATCCC_PORT);
@@ -877,8 +885,8 @@ async function listenWithRetry(
877
885
  * 先写盘,再走这里。
878
886
  */
879
887
  function installShutdownHandlers(httpServer: Server): void {
880
- process.on("SIGINT", () => { console.log("\nShutting down..."); wechatSignal.stopped = true; httpServer.close(); process.exit(0); });
881
- process.on("SIGTERM", () => { wechatSignal.stopped = true; httpServer.close(); process.exit(0); });
888
+ process.on("SIGINT", () => { console.log("\nShutting down..."); wechatSignal.stopped = true; stopChromeDevtoolsGuard(); httpServer.close(); process.exit(0); });
889
+ process.on("SIGTERM", () => { wechatSignal.stopped = true; stopChromeDevtoolsGuard(); httpServer.close(); process.exit(0); });
882
890
  }
883
891
 
884
892
  main().catch((err: Error) => {
@@ -81,6 +81,7 @@ import {
81
81
  } from "./session-chat-binding.ts";
82
82
  import { getCodexUsageSummary, getTenantAccessToken, sendPostMessage } from "./feishu-platform.ts";
83
83
  import { getCursorUsageSummary, type CursorUsageSummary } from "./cursor-usage.ts";
84
+ import { getChatGptSubscriptionStatus, type ChatGptSubscriptionResult } from "./chatgpt-subscription.ts";
84
85
  import { delegateAgentTask } from "./agent-delegate-task.ts";
85
86
  import { applySharedPrefix } from "./shared-prefix.ts";
86
87
  import { cwdDisplayName, sessionChatName } from "./session-name.ts";
@@ -107,7 +108,7 @@ function findModelMatch(input: string, models: string[]): string | null {
107
108
  return candidates[0] ?? null;
108
109
  }
109
110
 
110
- function formatCodexUsageSummary(usage: CodexUsageSummary): string {
111
+ function formatCodexUsageSummary(usage: CodexUsageSummary, chatGptSubscription: ChatGptSubscriptionResult | null = null): string {
111
112
  const progressBar = (usedPercent: number) => {
112
113
  const width = 20;
113
114
  const usedBlocks = Math.max(0, Math.min(width, Math.round((usedPercent / 100) * width)));
@@ -185,14 +186,47 @@ function formatCodexUsageSummary(usage: CodexUsageSummary): string {
185
186
  return lines.join("\n");
186
187
  };
187
188
 
189
+ const formatChatGptSubscription = () => {
190
+ if (!chatGptSubscription?.ok || !chatGptSubscription.subscription) return "";
191
+ const subscription = chatGptSubscription.subscription;
192
+ const pad = (value: number) => String(value).padStart(2, "0");
193
+ const formatExpiresAt = (value: string | null) => {
194
+ if (!value) return "暂无数据";
195
+ const date = new Date(value);
196
+ if (!Number.isFinite(date.getTime())) return value;
197
+ return [
198
+ date.getFullYear(),
199
+ "-",
200
+ pad(date.getMonth() + 1),
201
+ "-",
202
+ pad(date.getDate()),
203
+ " ",
204
+ pad(date.getHours()),
205
+ ":",
206
+ pad(date.getMinutes()),
207
+ ].join("");
208
+ };
209
+ const remaining = typeof subscription.remainingDays === "number"
210
+ ? `(剩余 ${subscription.remainingDays} 天)`
211
+ : "";
212
+ return [
213
+ "**ChatGPT 订阅:**",
214
+ `- 套餐: ${subscription.plan ?? "暂无数据"}`,
215
+ `- 到期: ${formatExpiresAt(subscription.expiresAt)}${remaining}`,
216
+ `- 自动续费: ${subscription.willRenew === null ? "暂无数据" : subscription.willRenew ? "是" : "否"}`,
217
+ ].join("\n");
218
+ };
219
+
188
220
  return [
189
221
  "Codex 用量:",
190
222
  "",
223
+ formatChatGptSubscription(),
224
+ chatGptSubscription?.ok ? "" : "",
191
225
  formatResetCredits(),
192
226
  "",
193
227
  formatWindow("5h", usage.fiveHour),
194
228
  formatWindow("周", usage.weekly),
195
- ].join("\n");
229
+ ].filter((line, index, arr) => line !== "" || (index > 0 && arr[index - 1] !== "")).join("\n");
196
230
  }
197
231
 
198
232
  function formatCursorUsageSummary(usage: CursorUsageSummary): string {
@@ -277,8 +311,11 @@ async function sendUsageSummary(platform: PlatformAdapter, chatId: string, tool:
277
311
  return;
278
312
  }
279
313
 
280
- const usage = await getCodexUsageSummary();
281
- const content = formatCodexUsageSummary(usage);
314
+ const [usage, chatGptSubscription] = await Promise.all([
315
+ getCodexUsageSummary(),
316
+ getChatGptSubscriptionStatus().catch(() => null),
317
+ ]);
318
+ const content = formatCodexUsageSummary(usage, chatGptSubscription);
282
319
  if (platform.kind === "wechat") {
283
320
  await platform.sendText(chatId, content).catch(() => {});
284
321
  } else if (platform.kind === "feishu") {
@@ -1,8 +1,8 @@
1
- export function cwdDisplayName(cwd: string): string {
2
- const trimmed = cwd.trim().replace(/[\\/]+$/, "");
3
- return trimmed.split(/[\\/]/).filter(Boolean).pop() || trimmed || "cwd";
4
- }
5
-
6
- export function sessionChatName(left: string, cwd: string): string {
7
- return `${left}-${cwdDisplayName(cwd)}`;
8
- }
1
+ export function cwdDisplayName(cwd: string): string {
2
+ const trimmed = cwd.trim().replace(/[\\/]+$/, "");
3
+ return trimmed.split(/[\\/]/).filter(Boolean).pop() || trimmed || "cwd";
4
+ }
5
+
6
+ export function sessionChatName(left: string, cwd: string): string {
7
+ return `${left}-${cwdDisplayName(cwd)}`;
8
+ }
package/src/session.ts CHANGED
@@ -955,12 +955,12 @@ export async function runAgentSession(
955
955
  const imSkillsCacheDir = join(USER_DATA_DIR, "im-skills");
956
956
  const skillVariables = {
957
957
  cwd,
958
- session_id: sessionId,
959
- im_skills_cache_dir: imSkillsCacheDir,
960
- delegate_task_url: `http://127.0.0.1:${CHATCCC_PORT}/api/agent/delegate-task`,
961
- send_image_url: `http://127.0.0.1:${CHATCCC_PORT}/api/agent/send-image`,
962
- send_file_url: `http://127.0.0.1:${CHATCCC_PORT}/api/agent/send-file`,
963
- send_image_script: join(feishuSkillDir, "send-image.mjs"),
958
+ session_id: sessionId,
959
+ im_skills_cache_dir: imSkillsCacheDir,
960
+ delegate_task_url: `http://127.0.0.1:${CHATCCC_PORT}/api/agent/delegate-task`,
961
+ send_image_url: `http://127.0.0.1:${CHATCCC_PORT}/api/agent/send-image`,
962
+ send_file_url: `http://127.0.0.1:${CHATCCC_PORT}/api/agent/send-file`,
963
+ send_image_script: join(feishuSkillDir, "send-image.mjs"),
964
964
  send_file_script: join(feishuSkillDir, "send-file.mjs"),
965
965
  download_video_script: join(feishuSkillDir, "download-video.mjs"),
966
966
  wechat_send_image_script: join(wechatImageSkillDir, "send-image.mjs"),
package/src/web-ui.ts CHANGED
@@ -29,6 +29,7 @@ const ILINK_AUTH_PATH = join(USER_DATA_DIR, "state", "ilink-auth.json");
29
29
  interface AppConfig {
30
30
  feishu?: { appId?: string; appSecret?: string };
31
31
  platforms?: { feishu?: { enabled?: boolean }; ilink?: { enabled?: boolean } };
32
+ chromeDevtools?: { enabled?: boolean; port?: number; chromePath?: string };
32
33
  port?: number;
33
34
  gitTimeoutSeconds?: number;
34
35
  claude?: { enabled?: boolean; defaultAgent?: boolean; model?: string; subagentModel?: string; effort?: string; apiKey?: string; baseUrl?: string; maxTurn?: number };
@@ -336,6 +337,15 @@ export function unflattenConfig(flat: Record<string, unknown>): Record<string, u
336
337
  result.platforms = result.platforms || {};
337
338
  (result.platforms as Record<string, unknown>).ilink = (result.platforms as Record<string, unknown>).ilink || {};
338
339
  ((result.platforms as Record<string, unknown>).ilink as Record<string, unknown>).enabled = val === true || val === "true";
340
+ } else if (key === "CHATCCC_CHROME_DEVTOOLS_ENABLED") {
341
+ result.chromeDevtools = result.chromeDevtools || {};
342
+ (result.chromeDevtools as Record<string, unknown>).enabled = val === true || val === "true";
343
+ } else if (key === "CHATCCC_CHROME_DEVTOOLS_PORT") {
344
+ result.chromeDevtools = result.chromeDevtools || {};
345
+ (result.chromeDevtools as Record<string, unknown>).port = parseInt(val as string, 10) || 15166;
346
+ } else if (key === "CHATCCC_CHROME_DEVTOOLS_PATH") {
347
+ result.chromeDevtools = result.chromeDevtools || {};
348
+ (result.chromeDevtools as Record<string, unknown>).chromePath = val;
339
349
  } else if (key === "CHATCCC_PORT") {
340
350
  result.port = parseInt(val as string, 10) || 18080;
341
351
  } else if (key === "CHATCCC_GIT_TIMEOUT_SECONDS") {
@@ -639,6 +649,32 @@ header .badge{font-size:13px;padding:4px 12px;border-radius:12px;font-weight:500
639
649
  </div>
640
650
  </div>
641
651
 
652
+ <!-- Chrome CDP -->
653
+ <div style="background:#f8fafc;border:1px solid #e2e8f0;border-radius:8px;padding:14px;margin-bottom:12px" id="chrome-devtools-block">
654
+ <div style="display:flex;align-items:center;justify-content:space-between">
655
+ <div>
656
+ <div style="font-weight:600;font-size:14px">常驻 Chrome CDP(选填)</div>
657
+ <div style="font-size:12px;color:#64748b">维护本机 Chrome DevTools Protocol 端口,用于 ChatGPT 订阅到期查询</div>
658
+ </div>
659
+ <input type="checkbox" class="agent-toggle" id="field-CHATCCC_CHROME_DEVTOOLS_ENABLED" onchange="toggleWizardChromeDevtoolsFields(this.checked)">
660
+ </div>
661
+ <div id="chrome-devtools-settings" style="margin-top:12px;padding-top:12px;border-top:1px solid #e2e8f0">
662
+ <div class="hint" style="margin-bottom:10px;line-height:1.6">
663
+ 依赖:本机已安装 Google Chrome;查询 ChatGPT 订阅到期时间时,需要在这个 CDP 专用 Chrome 窗口中登录 ChatGPT。
664
+ </div>
665
+ <div class="form-group" style="margin-bottom:10px">
666
+ <label>CDP 端口</label>
667
+ <input type="number" id="field-CHATCCC_CHROME_DEVTOOLS_PORT" min="1" max="65535" step="1" placeholder="15166">
668
+ <div class="hint">默认 15166;健康检查端点:http://127.0.0.1:15166/json/version</div>
669
+ </div>
670
+ <div class="form-group">
671
+ <label>Chrome 路径(选填)</label>
672
+ <input type="text" id="field-CHATCCC_CHROME_DEVTOOLS_PATH" placeholder="留空自动探测 chrome.exe">
673
+ <div class="hint">选填;留空时自动探测 Google Chrome。</div>
674
+ </div>
675
+ </div>
676
+ </div>
677
+
642
678
  <div class="btn-group" style="justify-content:flex-end">
643
679
  <button class="btn btn-primary" id="btn-step1-next" onclick="goStep1Next()">下一步</button>
644
680
  </div>
@@ -825,6 +861,17 @@ header .badge{font-size:13px;padding:4px 12px;border-radius:12px;font-weight:500
825
861
  </div>
826
862
  </details>
827
863
 
864
+ <details class="card config-section">
865
+ <summary>Chrome CDP(选填)</summary>
866
+ <div class="section-detail">
867
+ <div class="config-row"><span class="key">状态</span><span class="val" id="cfg-CHROME_DEVTOOLS_ENABLED">-</span></div>
868
+ <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>
869
+ <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>
870
+ <div class="hint" style="margin-top:6px;line-height:1.6">依赖:本机 Google Chrome;ChatGPT 订阅到期查询需要在该 CDP Chrome 中登录 ChatGPT。</div>
871
+ <button class="btn btn-outline" style="margin-top:8px" onclick="editSection('chromeDevtools')">编辑</button>
872
+ </div>
873
+ </details>
874
+
828
875
  <details class="card config-section" id="dash-claude">
829
876
  <summary>Claude Agent</summary>
830
877
  <div class="section-detail">
@@ -907,6 +954,7 @@ const AGENT_FIELDS = {
907
954
  codex: ['CHATCCC_CODEX_PATH','CHATCCC_CODEX_MODEL','CHATCCC_CODEX_EFFORT']
908
955
  };
909
956
  const FEISHU_FIELDS = ['CHATCCC_APP_ID','CHATCCC_APP_SECRET'];
957
+ const CHROME_DEVTOOLS_FIELDS = ['CHATCCC_CHROME_DEVTOOLS_ENABLED','CHATCCC_CHROME_DEVTOOLS_PORT','CHATCCC_CHROME_DEVTOOLS_PATH'];
910
958
 
911
959
  function cursorBatteryModeLabel(value) {
912
960
  return value === 'onDemandUse' ? 'On demand use 金额' : 'API 使用比例';
@@ -918,6 +966,18 @@ function onCursorBatteryModeChange(prefix, value) {
918
966
  if (row) row.style.display = value === 'onDemandUse' ? '' : 'none';
919
967
  }
920
968
 
969
+ function toggleWizardChromeDevtoolsFields(enabled) {
970
+ var settings = document.getElementById('chrome-devtools-settings');
971
+ if (settings) settings.style.display = enabled ? '' : 'none';
972
+ }
973
+
974
+ function toggleEditChromeDevtoolsFields(enabled) {
975
+ ['CHATCCC_CHROME_DEVTOOLS_PORT', 'CHATCCC_CHROME_DEVTOOLS_PATH'].forEach(function(key){
976
+ var row = document.getElementById('edit-row-' + key);
977
+ if (row) row.style.display = enabled ? '' : 'none';
978
+ });
979
+ }
980
+
921
981
  function onWizardPlatformToggle(platform, enabled) {
922
982
  state.platformsEnabled[platform] = enabled;
923
983
  // 飞书凭证字段跟随开关显示/隐藏
@@ -1133,6 +1193,12 @@ function renderStep1() {
1133
1193
  if (credFields) credFields.style.display = feishuEnabled ? '' : 'none';
1134
1194
  var ilToggle = document.getElementById('platform-enable-ilink');
1135
1195
  if (ilToggle) ilToggle.checked = ilinkEnabled;
1196
+ var cd = c.chromeDevtools || {};
1197
+ var cdpEnabled = document.getElementById('field-CHATCCC_CHROME_DEVTOOLS_ENABLED');
1198
+ if (cdpEnabled) cdpEnabled.checked = cd.enabled === true;
1199
+ prefillNested('field-CHATCCC_CHROME_DEVTOOLS_PORT', cd.port || 15166);
1200
+ prefillNested('field-CHATCCC_CHROME_DEVTOOLS_PATH', cd.chromePath);
1201
+ toggleWizardChromeDevtoolsFields(cd.enabled === true);
1136
1202
  }
1137
1203
 
1138
1204
  /**
@@ -1231,6 +1297,12 @@ function collectAllFields() {
1231
1297
  if (ptEl && ptEl.value.trim()) vars['CHATCCC_FEISHU_PLATFORM_TYPE'] = ptEl.value.trim();
1232
1298
  vars.CHATCCC_FEISHU_ENABLED = !!state.platformsEnabled.feishu;
1233
1299
  vars.CHATCCC_ILINK_ENABLED = !!state.platformsEnabled.ilink;
1300
+ var cdpEnabledEl = document.getElementById('field-CHATCCC_CHROME_DEVTOOLS_ENABLED');
1301
+ vars.CHATCCC_CHROME_DEVTOOLS_ENABLED = !!(cdpEnabledEl && cdpEnabledEl.checked);
1302
+ var cdpPortEl = document.getElementById('field-CHATCCC_CHROME_DEVTOOLS_PORT');
1303
+ vars.CHATCCC_CHROME_DEVTOOLS_PORT = (cdpPortEl && cdpPortEl.value.trim()) ? cdpPortEl.value.trim() : '15166';
1304
+ var cdpPathEl = document.getElementById('field-CHATCCC_CHROME_DEVTOOLS_PATH');
1305
+ if (cdpPathEl && cdpPathEl.value.trim()) vars.CHATCCC_CHROME_DEVTOOLS_PATH = cdpPathEl.value.trim();
1234
1306
  vars.CHATCCC_CLAUDE_ENABLED = !!state.agentsEnabled.claude;
1235
1307
  vars.CHATCCC_CURSOR_ENABLED = !!state.agentsEnabled.cursor;
1236
1308
  vars.CHATCCC_CODEX_ENABLED = !!state.agentsEnabled.codex;
@@ -1281,6 +1353,15 @@ function renderStep3() {
1281
1353
  lines.push('<div style="color:#ef4444;margin-top:8px">未启用任何平台</div>');
1282
1354
  }
1283
1355
 
1356
+ lines.push('<h3 style="margin:16px 0 8px">Chrome CDP</h3>');
1357
+ lines.push('<div class="config-row"><span class="key">状态</span><span class="val">' + (vars.CHATCCC_CHROME_DEVTOOLS_ENABLED ? '已启用' : '已禁用') + '</span></div>');
1358
+ if (vars.CHATCCC_CHROME_DEVTOOLS_ENABLED) {
1359
+ lines.push('<div class="config-row"><span class="key">CDP 端口</span><span class="val">' + (vars.CHATCCC_CHROME_DEVTOOLS_PORT || '15166') + '</span></div>');
1360
+ if (vars.CHATCCC_CHROME_DEVTOOLS_PATH) {
1361
+ lines.push('<div class="config-row"><span class="key">Chrome 路径</span><span class="val">' + vars.CHATCCC_CHROME_DEVTOOLS_PATH + '</span></div>');
1362
+ }
1363
+ }
1364
+
1284
1365
  lines.push('<h3 style="margin:16px 0 8px">已启用的 AI Agent</h3>');
1285
1366
  var enabledList = [];
1286
1367
  if (state.agentsEnabled.claude) enabledList.push('claude');
@@ -1471,6 +1552,16 @@ function updateDashboardUI() {
1471
1552
  ilinkForgetRow.style.display = (ilinkEnabled && state.ilinkAuthExists) ? '' : 'none';
1472
1553
  }
1473
1554
 
1555
+ var chromeDevtools = c.chromeDevtools || {};
1556
+ var cdpPort = chromeDevtools.port || 15166;
1557
+ document.getElementById('cfg-CHROME_DEVTOOLS_ENABLED').textContent = chromeDevtools.enabled ? '已启用' : '已禁用';
1558
+ document.getElementById('cfg-CHROME_DEVTOOLS_PORT').textContent = String(cdpPort);
1559
+ document.getElementById('cfg-CHROME_DEVTOOLS_PATH').textContent = chromeDevtools.chromePath || '(自动探测)';
1560
+ var cdpPortRow = document.getElementById('cfg-CHROME_DEVTOOLS_PORT_ROW');
1561
+ var cdpPathRow = document.getElementById('cfg-CHROME_DEVTOOLS_PATH_ROW');
1562
+ if (cdpPortRow) cdpPortRow.style.display = chromeDevtools.enabled ? '' : 'none';
1563
+ if (cdpPathRow) cdpPathRow.style.display = chromeDevtools.enabled ? '' : 'none';
1564
+
1474
1565
  document.getElementById('dash-claude').style.display = claudeOn ? '' : 'none';
1475
1566
  document.getElementById('dash-cursor').style.display = cursorOn ? '' : 'none';
1476
1567
  document.getElementById('dash-codex').style.display = codexOn ? '' : 'none';
@@ -1546,14 +1637,18 @@ function editSection(section) {
1546
1637
  editSectionType = section;
1547
1638
  var fields;
1548
1639
  if (section === 'feishu') fields = FEISHU_FIELDS;
1640
+ else if (section === 'chromeDevtools') fields = CHROME_DEVTOOLS_FIELDS;
1549
1641
  else fields = AGENT_FIELDS[section] || [];
1550
1642
 
1551
- var titleMap = { feishu: '飞书', claude: 'Claude Agent', cursor: 'Cursor Agent', codex: 'Codex Agent' };
1643
+ var titleMap = { feishu: '飞书', chromeDevtools: 'Chrome CDP', claude: 'Claude Agent', cursor: 'Cursor Agent', codex: 'Codex Agent' };
1552
1644
  document.getElementById('edit-modal-title').textContent = '编辑 ' + (titleMap[section] || section);
1553
1645
 
1554
1646
  var html = '';
1555
1647
  var labelMap = {
1556
1648
  'CHATCCC_APP_ID': 'App ID', 'CHATCCC_APP_SECRET': 'App Secret',
1649
+ 'CHATCCC_CHROME_DEVTOOLS_ENABLED': '启用常驻 Chrome CDP(选填)',
1650
+ 'CHATCCC_CHROME_DEVTOOLS_PORT': 'CDP 端口',
1651
+ 'CHATCCC_CHROME_DEVTOOLS_PATH': 'Chrome 路径(选填)',
1557
1652
  'CHATCCC_ANTHROPIC_MODEL': '模型', 'CHATCCC_ANTHROPIC_SUBAGENT_MODEL': 'Subagent 模型', 'CHATCCC_ANTHROPIC_EFFORT': 'Effort',
1558
1653
  'CHATCCC_ANTHROPIC_API_KEY': 'API Key', 'CHATCCC_ANTHROPIC_BASE_URL': 'Base URL', 'CHATCCC_ANTHROPIC_MAX_TURN': 'Max Turns (0=无限制)',
1559
1654
  'CHATCCC_CURSOR_PATH': 'CLI 路径', 'CHATCCC_CURSOR_MODEL': '模型',
@@ -1561,6 +1656,15 @@ function editSection(section) {
1561
1656
  'CHATCCC_CURSOR_ON_DEMAND_MONTHLY_BUDGET': '每月On demand use预算',
1562
1657
  'CHATCCC_CODEX_PATH': 'CLI 路径', 'CHATCCC_CODEX_MODEL': '模型', 'CHATCCC_CODEX_EFFORT': 'Effort'
1563
1658
  };
1659
+ var hintMap = {
1660
+ 'CHATCCC_CHROME_DEVTOOLS_ENABLED': '依赖:本机 Google Chrome;ChatGPT 订阅到期查询需要在该 CDP Chrome 中登录 ChatGPT。',
1661
+ 'CHATCCC_CHROME_DEVTOOLS_PORT': '默认 15166,健康检查端点为 http://127.0.0.1:15166/json/version。',
1662
+ 'CHATCCC_CHROME_DEVTOOLS_PATH': '选填。留空时自动探测 Google Chrome。'
1663
+ };
1664
+
1665
+ if (section === 'chromeDevtools') {
1666
+ html += '<div class="hint" style="margin-bottom:12px;line-height:1.6">常驻 Chrome CDP 用于维护本机 Chrome DevTools Protocol 端口。依赖:本机 Google Chrome;ChatGPT 订阅到期查询需要在该 CDP Chrome 中登录 ChatGPT。</div>';
1667
+ }
1564
1668
 
1565
1669
  fields.forEach(function(key){
1566
1670
  var val = state.config[key] || '';
@@ -1569,6 +1673,10 @@ function editSection(section) {
1569
1673
  if (section === 'feishu') {
1570
1674
  if (key === 'CHATCCC_APP_ID' && state.config.feishu) val = state.config.feishu.appId || '';
1571
1675
  else if (key === 'CHATCCC_APP_SECRET' && state.config.feishu) val = state.config.feishu.appSecret || '';
1676
+ } else if (section === 'chromeDevtools' && state.config.chromeDevtools) {
1677
+ if (key === 'CHATCCC_CHROME_DEVTOOLS_ENABLED') val = state.config.chromeDevtools.enabled === true ? 'true' : 'false';
1678
+ else if (key === 'CHATCCC_CHROME_DEVTOOLS_PORT') val = state.config.chromeDevtools.port != null ? String(state.config.chromeDevtools.port) : '15166';
1679
+ else if (key === 'CHATCCC_CHROME_DEVTOOLS_PATH') val = state.config.chromeDevtools.chromePath || '';
1572
1680
  } else if (section === 'claude' && state.config.claude) {
1573
1681
  if (key === 'CHATCCC_ANTHROPIC_MODEL') val = state.config.claude.model || '';
1574
1682
  else if (key === 'CHATCCC_ANTHROPIC_SUBAGENT_MODEL') val = state.config.claude.subagentModel || '';
@@ -1588,7 +1696,12 @@ function editSection(section) {
1588
1696
  }
1589
1697
  }
1590
1698
  var isSecret = key.includes('SECRET') || key.includes('API_KEY');
1591
- if (key === 'CHATCCC_CURSOR_AVATAR_BATTERY_MODE') {
1699
+ if (key === 'CHATCCC_CHROME_DEVTOOLS_ENABLED') {
1700
+ var checked = val === true || val === 'true';
1701
+ html += '<div class="form-group"><label style="display:flex;align-items:center;gap:8px"><input type="checkbox" id="edit-' + key + '"' + (checked ? ' checked' : '') + ' onchange="toggleEditChromeDevtoolsFields(this.checked)"> ' + (labelMap[key] || key) + '</label>';
1702
+ if (hintMap[key]) html += '<div class="hint" style="margin-top:6px;line-height:1.5">' + hintMap[key] + '</div>';
1703
+ html += '</div>';
1704
+ } else if (key === 'CHATCCC_CURSOR_AVATAR_BATTERY_MODE') {
1592
1705
  var modeVal = val || 'apiPercent';
1593
1706
  html += '<div class="form-group"><label>' + (labelMap[key] || key) + '</label>';
1594
1707
  html += '<select id="edit-' + key + '" onchange="onCursorBatteryModeChange(\\'edit-\\', this.value)" style="width:100%;padding:8px 12px;border:1px solid #cbd5e1;border-radius:8px;font-size:14px;outline:none">';
@@ -1596,11 +1709,19 @@ function editSection(section) {
1596
1709
  html += '<option value="onDemandUse"' + (modeVal === 'onDemandUse' ? ' selected' : '') + '>On demand use 金额</option>';
1597
1710
  html += '</select></div>';
1598
1711
  } else {
1599
- var rowId = key === 'CHATCCC_CURSOR_ON_DEMAND_MONTHLY_BUDGET' ? ' id="edit-cursor-on-demand-budget-row"' : '';
1600
- var inputType = key === 'CHATCCC_CURSOR_ON_DEMAND_MONTHLY_BUDGET' ? 'number' : (isSecret ? 'password' : 'text');
1601
- var attrs = key === 'CHATCCC_CURSOR_ON_DEMAND_MONTHLY_BUDGET' ? ' min="1" step="1"' : '';
1712
+ var rowId = key === 'CHATCCC_CURSOR_ON_DEMAND_MONTHLY_BUDGET'
1713
+ ? ' id="edit-cursor-on-demand-budget-row"'
1714
+ : (section === 'chromeDevtools' && key !== 'CHATCCC_CHROME_DEVTOOLS_ENABLED' ? ' id="edit-row-' + key + '"' : '');
1715
+ var isNumber = key === 'CHATCCC_CURSOR_ON_DEMAND_MONTHLY_BUDGET' || key === 'CHATCCC_CHROME_DEVTOOLS_PORT';
1716
+ var inputType = isNumber ? 'number' : (isSecret ? 'password' : 'text');
1717
+ var attrs = key === 'CHATCCC_CURSOR_ON_DEMAND_MONTHLY_BUDGET'
1718
+ ? ' min="1" step="1"'
1719
+ : key === 'CHATCCC_CHROME_DEVTOOLS_PORT'
1720
+ ? ' min="1" max="65535" step="1" placeholder="15166"'
1721
+ : '';
1602
1722
  html += '<div class="form-group"' + rowId + '><label>' + (labelMap[key] || key) + '</label>';
1603
1723
  html += '<input type="' + inputType + '" id="edit-' + key + '"' + attrs + ' value="' + String(val).replace(/"/g,'&quot;') + '">';
1724
+ if (hintMap[key]) html += '<div class="hint" style="margin-top:6px;line-height:1.5">' + hintMap[key] + '</div>';
1604
1725
  html += '</div>';
1605
1726
  }
1606
1727
  });
@@ -1617,6 +1738,9 @@ function editSection(section) {
1617
1738
  if (section === 'cursor') {
1618
1739
  var editModeEl = document.getElementById('edit-CHATCCC_CURSOR_AVATAR_BATTERY_MODE');
1619
1740
  onCursorBatteryModeChange('edit-', editModeEl ? editModeEl.value : 'apiPercent');
1741
+ } else if (section === 'chromeDevtools') {
1742
+ var editChromeDevtoolsEnabledEl = document.getElementById('edit-CHATCCC_CHROME_DEVTOOLS_ENABLED');
1743
+ toggleEditChromeDevtoolsFields(!!(editChromeDevtoolsEnabledEl && editChromeDevtoolsEnabledEl.checked));
1620
1744
  }
1621
1745
  document.getElementById('edit-modal').classList.remove('hidden');
1622
1746
  document.getElementById('edit-overlay').classList.remove('hidden');
@@ -1631,13 +1755,19 @@ function closeEditModal() {
1631
1755
  async function saveEdit() {
1632
1756
  var fields;
1633
1757
  if (editSectionType === 'feishu') fields = FEISHU_FIELDS;
1758
+ else if (editSectionType === 'chromeDevtools') fields = CHROME_DEVTOOLS_FIELDS;
1634
1759
  else fields = AGENT_FIELDS[editSectionType] || [];
1635
1760
 
1636
1761
  var vars = {};
1637
1762
  fields.forEach(function(key){
1638
1763
  var el = document.getElementById('edit-' + key);
1639
- if (el) vars[key] = el.value.trim();
1764
+ if (!el) return;
1765
+ if (key === 'CHATCCC_CHROME_DEVTOOLS_ENABLED') vars[key] = !!el.checked;
1766
+ else vars[key] = el.value.trim();
1640
1767
  });
1768
+ if (editSectionType === 'chromeDevtools' && !vars.CHATCCC_CHROME_DEVTOOLS_PORT) {
1769
+ vars.CHATCCC_CHROME_DEVTOOLS_PORT = '15166';
1770
+ }
1641
1771
  if (editSectionType === 'cursor' && vars.CHATCCC_CURSOR_AVATAR_BATTERY_MODE === 'onDemandUse' && !vars.CHATCCC_CURSOR_ON_DEMAND_MONTHLY_BUDGET) {
1642
1772
  vars.CHATCCC_CURSOR_ON_DEMAND_MONTHLY_BUDGET = '1000';
1643
1773
  }