chatccc 0.2.202 → 0.2.204

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/web-ui.ts CHANGED
@@ -12,7 +12,12 @@ import { readFile, writeFile, stat } from "node:fs/promises";
12
12
  import { homedir } from "node:os";
13
13
  import { join, dirname } from "node:path";
14
14
  import { fileURLToPath } from "node:url";
15
- import { spawn, execSync } from "node:child_process";
15
+ import { spawn, execSync } from "node:child_process";
16
+ import {
17
+ buildWebUiUrl,
18
+ createInternalRestartEnv,
19
+ openWebUiInDefaultBrowser,
20
+ } from "./startup-lifecycle.ts";
16
21
 
17
22
  const __dirname = dirname(fileURLToPath(import.meta.url));
18
23
  const PROJECT_ROOT = join(__dirname, "..");
@@ -32,6 +37,7 @@ interface AppConfig {
32
37
  feishu?: { enabled?: boolean; platformType?: string };
33
38
  ilink?: { enabled?: boolean; reuseTokenOnStart?: boolean };
34
39
  };
40
+ webUi?: { openOnStart?: boolean };
35
41
  chromeDevtools?: { enabled?: boolean; port?: number; chromePath?: string };
36
42
  port?: number;
37
43
  gitTimeoutSeconds?: number;
@@ -225,11 +231,12 @@ function spawnService(): { ok: boolean; pid?: number; error?: string } {
225
231
  return { ok: false, error: `Entry not found: ${indexPath}` };
226
232
  }
227
233
  try {
228
- const child = spawn("npx", ["tsx", "src/index.ts"], {
229
- cwd: PROJECT_ROOT,
230
- detached: true,
231
- stdio: "ignore",
232
- shell: true,
234
+ const child = spawn("npx", ["tsx", "src/index.ts"], {
235
+ cwd: PROJECT_ROOT,
236
+ detached: true,
237
+ stdio: "ignore",
238
+ shell: true,
239
+ env: createInternalRestartEnv(),
233
240
  });
234
241
  child.unref();
235
242
  return { ok: true, pid: child.pid ?? undefined };
@@ -247,17 +254,19 @@ function scheduleRestart(): void {
247
254
  if (!existsSync(indexPath)) return;
248
255
  if (process.platform === "win32") {
249
256
  // Windows: ping 作为 sleep(ping 自己 3 次 ≈ 2 秒延迟)
250
- spawn("cmd.exe", ["/c", "ping -n 3 127.0.0.1 > nul && npx tsx src/index.ts"], {
251
- cwd: PROJECT_ROOT,
252
- detached: true,
253
- stdio: "ignore",
254
- windowsHide: true,
257
+ spawn("cmd.exe", ["/c", "ping -n 3 127.0.0.1 > nul && npx tsx src/index.ts"], {
258
+ cwd: PROJECT_ROOT,
259
+ detached: true,
260
+ stdio: "ignore",
261
+ windowsHide: true,
262
+ env: createInternalRestartEnv(),
255
263
  }).unref();
256
264
  } else {
257
- spawn("bash", ["-c", "sleep 2 && npx tsx src/index.ts"], {
258
- cwd: PROJECT_ROOT,
259
- detached: true,
260
- stdio: "ignore",
265
+ spawn("bash", ["-c", "sleep 2 && npx tsx src/index.ts"], {
266
+ cwd: PROJECT_ROOT,
267
+ detached: true,
268
+ stdio: "ignore",
269
+ env: createInternalRestartEnv(),
261
270
  }).unref();
262
271
  }
263
272
  }
@@ -405,11 +414,14 @@ export function unflattenConfig(flat: Record<string, unknown>): Record<string, u
405
414
  result.platforms = result.platforms || {};
406
415
  (result.platforms as Record<string, unknown>).feishu = (result.platforms as Record<string, unknown>).feishu || {};
407
416
  ((result.platforms as Record<string, unknown>).feishu as Record<string, unknown>).platformType = val;
408
- } else if (key === "CHATCCC_ILINK_ENABLED") {
409
- result.platforms = result.platforms || {};
410
- (result.platforms as Record<string, unknown>).ilink = (result.platforms as Record<string, unknown>).ilink || {};
411
- ((result.platforms as Record<string, unknown>).ilink as Record<string, unknown>).enabled = val === true || val === "true";
412
- } else if (key === "CHATCCC_CHROME_DEVTOOLS_ENABLED") {
417
+ } else if (key === "CHATCCC_ILINK_ENABLED") {
418
+ result.platforms = result.platforms || {};
419
+ (result.platforms as Record<string, unknown>).ilink = (result.platforms as Record<string, unknown>).ilink || {};
420
+ ((result.platforms as Record<string, unknown>).ilink as Record<string, unknown>).enabled = val === true || val === "true";
421
+ } else if (key === "CHATCCC_WEB_UI_OPEN_ON_START") {
422
+ result.webUi = result.webUi || {};
423
+ (result.webUi as Record<string, unknown>).openOnStart = val === true || val === "true";
424
+ } else if (key === "CHATCCC_CHROME_DEVTOOLS_ENABLED") {
413
425
  result.chromeDevtools = result.chromeDevtools || {};
414
426
  (result.chromeDevtools as Record<string, unknown>).enabled = val === true || val === "true";
415
427
  } else if (key === "CHATCCC_CHROME_DEVTOOLS_PORT") {
@@ -585,11 +597,29 @@ async function handleForgetIlink(_req: IncomingMessage, res: ServerResponse): Pr
585
597
  }
586
598
  }
587
599
 
588
- // ---------------------------------------------------------------------------
589
- // HTML page (embedded template)
590
- // ---------------------------------------------------------------------------
591
-
592
- export const PAGE_HTML = `<!DOCTYPE html>
600
+ // ---------------------------------------------------------------------------
601
+ // HTML page (embedded template)
602
+ // ---------------------------------------------------------------------------
603
+
604
+ /** Agent Team 的首个占位页面:按产品约定只展示标题,不提前放入功能内容。 */
605
+ export const AGENT_TEAM_PAGE_HTML = `<!DOCTYPE html>
606
+ <html lang="zh-CN">
607
+ <head>
608
+ <meta charset="UTF-8">
609
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
610
+ <title>Agent Team</title>
611
+ <style>
612
+ *,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
613
+ body{min-height:100vh;display:grid;place-items:center;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;background:radial-gradient(circle at 50% 20%,#312e81 0,#111827 42%,#020617 100%);color:#fff}
614
+ h1{font-size:clamp(40px,8vw,88px);font-weight:750;letter-spacing:-.04em;text-shadow:0 12px 42px rgba(129,140,248,.45)}
615
+ </style>
616
+ </head>
617
+ <body>
618
+ <h1>Agent Team</h1>
619
+ </body>
620
+ </html>`;
621
+
622
+ export const PAGE_HTML = `<!DOCTYPE html>
593
623
  <html lang="zh-CN">
594
624
  <head>
595
625
  <meta charset="UTF-8">
@@ -598,9 +628,14 @@ export const PAGE_HTML = `<!DOCTYPE html>
598
628
  <style>
599
629
  *,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
600
630
  body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;background:#f1f5f9;color:#1e293b;line-height:1.6}
601
- header{background:#0f172a;color:#fff;padding:16px 24px;display:flex;align-items:center;justify-content:space-between}
602
- header h1{font-size:20px;font-weight:600}
603
- header .badge{font-size:13px;padding:4px 12px;border-radius:12px;font-weight:500}
631
+ header{background:#0f172a;color:#fff;padding:16px 24px;display:flex;align-items:center;justify-content:space-between}
632
+ header h1{font-size:20px;font-weight:600}
633
+ header .badge{font-size:13px;padding:4px 12px;border-radius:12px;font-weight:500}
634
+ .header-actions{display:flex;align-items:center;gap:12px}
635
+ .agent-team-entry{display:inline-flex;align-items:center;gap:8px;padding:9px 16px;border:1px solid rgba(255,255,255,.2);border-radius:999px;background:linear-gradient(135deg,#6366f1,#8b5cf6 55%,#d946ef);color:#fff;text-decoration:none;font-size:14px;font-weight:700;letter-spacing:.01em;box-shadow:0 8px 24px rgba(99,102,241,.38);transition:transform .18s ease,box-shadow .18s ease,filter .18s ease}
636
+ .agent-team-entry:hover{transform:translateY(-2px);box-shadow:0 12px 30px rgba(139,92,246,.52);filter:saturate(1.16)}
637
+ .agent-team-entry:focus-visible{outline:3px solid rgba(196,181,253,.65);outline-offset:3px}
638
+ .agent-team-entry .agent-team-icon{font-size:16px;line-height:1}
604
639
  .badge-running{background:#16a34a;color:#fff}
605
640
  .badge-stopped{background:#94a3b8;color:#fff}
606
641
  /* container 完全不限宽、不留左右内边距 —— step-2 是三列卡片需要尽量利用屏幕;
@@ -668,14 +703,18 @@ header .badge{font-size:13px;padding:4px 12px;border-radius:12px;font-weight:500
668
703
  .toast-error{background:#ef4444}
669
704
  @keyframes slideIn{from{transform:translateX(100%);opacity:0}to{transform:translateX(0);opacity:1}}
670
705
  .spinner{display:inline-block;width:14px;height:14px;border:2px solid rgba(255,255,255,.3);border-top-color:#fff;border-radius:50%;animation:spin .6s linear infinite}
671
- @keyframes spin{to{transform:rotate(360deg)}}
672
- </style>
706
+ @keyframes spin{to{transform:rotate(360deg)}}
707
+ @media(max-width:520px){header{padding:12px 14px}.header-actions{gap:8px}.agent-team-entry{padding:8px 11px;font-size:13px}header .badge{padding:4px 8px}}
708
+ </style>
673
709
  </head>
674
710
  <body>
675
- <header>
676
- <h1>ChatCCC</h1>
677
- <span id="header-badge" class="badge badge-stopped">未启动</span>
678
- </header>
711
+ <header>
712
+ <h1>ChatCCC</h1>
713
+ <div class="header-actions">
714
+ <a href="/agent-team" class="agent-team-entry"><span class="agent-team-icon" aria-hidden="true">✦</span>Agent Team <span aria-hidden="true">→</span></a>
715
+ <span id="header-badge" class="badge badge-stopped">未启动</span>
716
+ </div>
717
+ </header>
679
718
  <div class="container">
680
719
 
681
720
  <!-- ===== Setup Wizard ===== -->
@@ -723,7 +762,7 @@ header .badge{font-size:13px;padding:4px 12px;border-radius:12px;font-weight:500
723
762
  </div>
724
763
  </div>
725
764
 
726
- <!-- 微信 iLink -->
765
+ <!-- 微信 iLink -->
727
766
  <div style="background:#f8fafc;border:1px solid #e2e8f0;border-radius:8px;padding:14px;margin-bottom:12px" id="platform-block-ilink">
728
767
  <div style="display:flex;align-items:center;justify-content:space-between">
729
768
  <div>
@@ -732,9 +771,20 @@ header .badge{font-size:13px;padding:4px 12px;border-radius:12px;font-weight:500
732
771
  </div>
733
772
  <input type="checkbox" class="agent-toggle" id="platform-enable-ilink" checked onchange="onWizardPlatformToggle('ilink', this.checked)">
734
773
  </div>
735
- </div>
736
-
737
- <!-- Chrome CDP -->
774
+ </div>
775
+
776
+ <!-- Web UI 启动行为 -->
777
+ <div style="background:#f8fafc;border:1px solid #e2e8f0;border-radius:8px;padding:14px;margin-bottom:12px" id="web-ui-startup-block">
778
+ <div style="display:flex;align-items:center;justify-content:space-between;gap:16px">
779
+ <div>
780
+ <div style="font-weight:600;font-size:14px">启动时打开 Web UI</div>
781
+ <div style="font-size:12px;color:#64748b">直接运行 ChatCCC 时用系统默认浏览器打开管理页面;内部重启始终不打开</div>
782
+ </div>
783
+ <input type="checkbox" class="agent-toggle" id="field-CHATCCC_WEB_UI_OPEN_ON_START" checked>
784
+ </div>
785
+ </div>
786
+
787
+ <!-- Chrome CDP -->
738
788
  <div style="background:#f8fafc;border:1px solid #e2e8f0;border-radius:8px;padding:14px;margin-bottom:12px" id="chrome-devtools-block">
739
789
  <div style="display:flex;align-items:center;justify-content:space-between">
740
790
  <div>
@@ -938,8 +988,8 @@ header .badge{font-size:13px;padding:4px 12px;border-radius:12px;font-weight:500
938
988
  </div>
939
989
  </details>
940
990
 
941
- <details class="card config-section">
942
- <summary>微信 iLink</summary>
991
+ <details class="card config-section">
992
+ <summary>微信 iLink</summary>
943
993
  <div class="section-detail">
944
994
  <div class="config-row">
945
995
  <span class="key">状态</span>
@@ -955,8 +1005,17 @@ header .badge{font-size:13px;padding:4px 12px;border-radius:12px;font-weight:500
955
1005
  <div class="hint" style="margin-top:6px;line-height:1.6">生效范围:微信 iLink 开关变更需要重启 ChatCCC。</div>
956
1006
  </div>
957
1007
  </details>
958
-
959
- <details class="card config-section">
1008
+
1009
+ <details class="card config-section">
1010
+ <summary>Web UI</summary>
1011
+ <div class="section-detail">
1012
+ <div class="config-row"><span class="key">直接启动时自动打开</span><span class="val" id="cfg-WEB_UI_OPEN_ON_START">-</span></div>
1013
+ <div class="hint" style="margin-top:6px;line-height:1.6">生效范围:下次直接启动生效;内部重启始终不打开。Linux 无图形桌面时会跳过打开并输出 SSH 隧道提示。</div>
1014
+ <button class="btn btn-outline" style="margin-top:8px" onclick="editSection('webUi')">编辑</button>
1015
+ </div>
1016
+ </details>
1017
+
1018
+ <details class="card config-section">
960
1019
  <summary>Chrome CDP(选填)</summary>
961
1020
  <div class="section-detail">
962
1021
  <div class="config-row"><span class="key">状态</span><span class="val" id="cfg-CHROME_DEVTOOLS_ENABLED">-</span></div>
@@ -1054,8 +1113,9 @@ const AGENT_FIELDS = {
1054
1113
  cursor: ['CHATCCC_CURSOR_PATH','CHATCCC_CURSOR_MODEL','CHATCCC_CURSOR_ALTERNATIVE_MODEL','CHATCCC_CURSOR_AVATAR_BATTERY_MODE','CHATCCC_CURSOR_ON_DEMAND_MONTHLY_BUDGET'],
1055
1114
  codex: ['CHATCCC_CODEX_PATH','CHATCCC_CODEX_MODEL','CHATCCC_CODEX_ALTERNATIVE_MODEL','CHATCCC_CODEX_EFFORT']
1056
1115
  };
1057
- const FEISHU_FIELDS = ['CHATCCC_APP_ID','CHATCCC_APP_SECRET'];
1058
- const CHROME_DEVTOOLS_FIELDS = ['CHATCCC_CHROME_DEVTOOLS_ENABLED','CHATCCC_CHROME_DEVTOOLS_PORT','CHATCCC_CHROME_DEVTOOLS_PATH'];
1116
+ const FEISHU_FIELDS = ['CHATCCC_APP_ID','CHATCCC_APP_SECRET'];
1117
+ const WEB_UI_FIELDS = ['CHATCCC_WEB_UI_OPEN_ON_START'];
1118
+ const CHROME_DEVTOOLS_FIELDS = ['CHATCCC_CHROME_DEVTOOLS_ENABLED','CHATCCC_CHROME_DEVTOOLS_PORT','CHATCCC_CHROME_DEVTOOLS_PATH'];
1059
1119
 
1060
1120
  function cursorBatteryModeLabel(value) {
1061
1121
  return value === 'onDemandUse' ? 'On demand use 金额' : 'API 使用比例';
@@ -1063,6 +1123,7 @@ function cursorBatteryModeLabel(value) {
1063
1123
 
1064
1124
  function configEffectHint(section) {
1065
1125
  if (section === 'feishu') return '生效范围:App ID、App Secret、平台类型或飞书开关变更需要重启 ChatCCC;其它未变更项仅保存。';
1126
+ if (section === 'webUi') return '生效范围:下次直接启动生效;内部重启始终不打开。';
1066
1127
  if (section === 'chromeDevtools') return '生效范围:保存后立即应用到 Chrome CDP 守护进程。';
1067
1128
  if (section === 'claude') return '生效范围:保存后下一条消息或下个新会话生效,当前生成不中断。';
1068
1129
  if (section === 'cursor') return '生效范围:保存后下一条消息或下个新会话生效,当前生成不中断。';
@@ -1312,9 +1373,11 @@ function renderStep1() {
1312
1373
  // 飞书凭证字段初始显隐
1313
1374
  var credFields = document.getElementById('feishu-cred-fields');
1314
1375
  if (credFields) credFields.style.display = feishuEnabled ? '' : 'none';
1315
- var ilToggle = document.getElementById('platform-enable-ilink');
1316
- if (ilToggle) ilToggle.checked = ilinkEnabled;
1317
- var cd = c.chromeDevtools || {};
1376
+ var ilToggle = document.getElementById('platform-enable-ilink');
1377
+ if (ilToggle) ilToggle.checked = ilinkEnabled;
1378
+ var webUiOpenOnStart = document.getElementById('field-CHATCCC_WEB_UI_OPEN_ON_START');
1379
+ if (webUiOpenOnStart) webUiOpenOnStart.checked = !c.webUi || c.webUi.openOnStart !== false;
1380
+ var cd = c.chromeDevtools || {};
1318
1381
  var cdpEnabled = document.getElementById('field-CHATCCC_CHROME_DEVTOOLS_ENABLED');
1319
1382
  if (cdpEnabled) cdpEnabled.checked = cd.enabled === true;
1320
1383
  prefillNested('field-CHATCCC_CHROME_DEVTOOLS_PORT', cd.port || 15166);
@@ -1418,9 +1481,11 @@ function collectAllFields() {
1418
1481
  // 平台类型(下拉选择框,始终发送以保持与服务端同步)
1419
1482
  var ptEl = document.getElementById('field-CHATCCC_FEISHU_PLATFORM_TYPE');
1420
1483
  if (ptEl && ptEl.value.trim()) vars['CHATCCC_FEISHU_PLATFORM_TYPE'] = ptEl.value.trim();
1421
- vars.CHATCCC_FEISHU_ENABLED = !!state.platformsEnabled.feishu;
1422
- vars.CHATCCC_ILINK_ENABLED = !!state.platformsEnabled.ilink;
1423
- var cdpEnabledEl = document.getElementById('field-CHATCCC_CHROME_DEVTOOLS_ENABLED');
1484
+ vars.CHATCCC_FEISHU_ENABLED = !!state.platformsEnabled.feishu;
1485
+ vars.CHATCCC_ILINK_ENABLED = !!state.platformsEnabled.ilink;
1486
+ var webUiOpenOnStartEl = document.getElementById('field-CHATCCC_WEB_UI_OPEN_ON_START');
1487
+ vars.CHATCCC_WEB_UI_OPEN_ON_START = !webUiOpenOnStartEl || !!webUiOpenOnStartEl.checked;
1488
+ var cdpEnabledEl = document.getElementById('field-CHATCCC_CHROME_DEVTOOLS_ENABLED');
1424
1489
  vars.CHATCCC_CHROME_DEVTOOLS_ENABLED = !!(cdpEnabledEl && cdpEnabledEl.checked);
1425
1490
  var cdpPortEl = document.getElementById('field-CHATCCC_CHROME_DEVTOOLS_PORT');
1426
1491
  vars.CHATCCC_CHROME_DEVTOOLS_PORT = (cdpPortEl && cdpPortEl.value.trim()) ? cdpPortEl.value.trim() : '15166';
@@ -1469,14 +1534,17 @@ function renderStep3() {
1469
1534
  lines.push('<div class="config-row"><span class="key">平台类型</span><span class="val">' + ptLabel + '</span></div>');
1470
1535
  }
1471
1536
 
1472
- lines.push('<h3 style="margin:16px 0 8px">微信 iLink</h3>');
1473
- lines.push('<div class="config-row"><span class="key">状态</span><span class="val">' + (state.platformsEnabled.ilink ? '已启用' : '已禁用') + '</span></div>');
1537
+ lines.push('<h3 style="margin:16px 0 8px">微信 iLink</h3>');
1538
+ lines.push('<div class="config-row"><span class="key">状态</span><span class="val">' + (state.platformsEnabled.ilink ? '已启用' : '已禁用') + '</span></div>');
1474
1539
 
1475
1540
  if (!state.platformsEnabled.feishu && !state.platformsEnabled.ilink) {
1476
- lines.push('<div style="color:#ef4444;margin-top:8px">未启用任何平台</div>');
1477
- }
1478
-
1479
- lines.push('<h3 style="margin:16px 0 8px">Chrome CDP</h3>');
1541
+ lines.push('<div style="color:#ef4444;margin-top:8px">未启用任何平台</div>');
1542
+ }
1543
+
1544
+ lines.push('<h3 style="margin:16px 0 8px">Web UI</h3>');
1545
+ lines.push('<div class="config-row"><span class="key">直接启动时自动打开</span><span class="val">' + (vars.CHATCCC_WEB_UI_OPEN_ON_START ? '已启用' : '已禁用') + '</span></div>');
1546
+
1547
+ lines.push('<h3 style="margin:16px 0 8px">Chrome CDP</h3>');
1480
1548
  lines.push('<div class="config-row"><span class="key">状态</span><span class="val">' + (vars.CHATCCC_CHROME_DEVTOOLS_ENABLED ? '已启用' : '已禁用') + '</span></div>');
1481
1549
  if (vars.CHATCCC_CHROME_DEVTOOLS_ENABLED) {
1482
1550
  lines.push('<div class="config-row"><span class="key">CDP 端口</span><span class="val">' + (vars.CHATCCC_CHROME_DEVTOOLS_PORT || '15166') + '</span></div>');
@@ -1684,11 +1752,14 @@ function updateDashboardUI() {
1684
1752
 
1685
1753
  // 微信 iLink "忘记扫码" 按钮:仅在 ilink 已启用且有已保存 token 时显示
1686
1754
  var ilinkForgetRow = document.getElementById('ilink-forget-row');
1687
- if (ilinkForgetRow) {
1688
- ilinkForgetRow.style.display = (ilinkEnabled && state.ilinkAuthExists) ? '' : 'none';
1689
- }
1690
-
1691
- var chromeDevtools = c.chromeDevtools || {};
1755
+ if (ilinkForgetRow) {
1756
+ ilinkForgetRow.style.display = (ilinkEnabled && state.ilinkAuthExists) ? '' : 'none';
1757
+ }
1758
+
1759
+ var webUi = c.webUi || {};
1760
+ document.getElementById('cfg-WEB_UI_OPEN_ON_START').textContent = webUi.openOnStart !== false ? '已启用' : '已禁用';
1761
+
1762
+ var chromeDevtools = c.chromeDevtools || {};
1692
1763
  var cdpPort = chromeDevtools.port || 15166;
1693
1764
  document.getElementById('cfg-CHROME_DEVTOOLS_ENABLED').textContent = chromeDevtools.enabled ? '已启用' : '已禁用';
1694
1765
  document.getElementById('cfg-CHROME_DEVTOOLS_PORT').textContent = String(cdpPort);
@@ -1771,22 +1842,24 @@ async function pollUntilRunning() {
1771
1842
  // ---- Edit Modal ----
1772
1843
  var editSectionType = null;
1773
1844
 
1774
- function editSection(section) {
1845
+ function editSection(section) {
1775
1846
  editSectionType = section;
1776
- var fields;
1777
- if (section === 'feishu') fields = FEISHU_FIELDS;
1778
- else if (section === 'chromeDevtools') fields = CHROME_DEVTOOLS_FIELDS;
1847
+ var fields;
1848
+ if (section === 'feishu') fields = FEISHU_FIELDS;
1849
+ else if (section === 'webUi') fields = WEB_UI_FIELDS;
1850
+ else if (section === 'chromeDevtools') fields = CHROME_DEVTOOLS_FIELDS;
1779
1851
  else fields = AGENT_FIELDS[section] || [];
1780
1852
 
1781
- var titleMap = { feishu: '飞书', chromeDevtools: 'Chrome CDP', claude: 'Claude Agent', cursor: 'Cursor Agent', codex: 'Codex Agent' };
1853
+ var titleMap = { feishu: '飞书', webUi: 'Web UI', chromeDevtools: 'Chrome CDP', claude: 'Claude Agent', cursor: 'Cursor Agent', codex: 'Codex Agent' };
1782
1854
  document.getElementById('edit-modal-title').textContent = '编辑 ' + (titleMap[section] || section);
1783
1855
 
1784
1856
  document.getElementById('edit-modal-effect').textContent = configEffectHint(section);
1785
1857
 
1786
1858
  var html = '';
1787
- var labelMap = {
1788
- 'CHATCCC_APP_ID': 'App ID', 'CHATCCC_APP_SECRET': 'App Secret',
1789
- 'CHATCCC_CHROME_DEVTOOLS_ENABLED': '启用常驻 Chrome CDP(选填)',
1859
+ var labelMap = {
1860
+ 'CHATCCC_APP_ID': 'App ID', 'CHATCCC_APP_SECRET': 'App Secret',
1861
+ 'CHATCCC_WEB_UI_OPEN_ON_START': '直接启动 ChatCCC 时自动打开 Web UI',
1862
+ 'CHATCCC_CHROME_DEVTOOLS_ENABLED': '启用常驻 Chrome CDP(选填)',
1790
1863
  'CHATCCC_CHROME_DEVTOOLS_PORT': 'CDP 端口',
1791
1864
  'CHATCCC_CHROME_DEVTOOLS_PATH': 'Chrome 路径(选填)',
1792
1865
  'CHATCCC_ANTHROPIC_MODEL': '模型', 'CHATCCC_ANTHROPIC_SUBAGENT_MODEL': 'Subagent 模型', 'CHATCCC_ANTHROPIC_EFFORT': 'Effort',
@@ -1796,8 +1869,9 @@ function editSection(section) {
1796
1869
  'CHATCCC_CURSOR_ON_DEMAND_MONTHLY_BUDGET': '每月On demand use预算',
1797
1870
  'CHATCCC_CODEX_PATH': 'CLI 路径', 'CHATCCC_CODEX_MODEL': '模型', 'CHATCCC_CODEX_ALTERNATIVE_MODEL': '备选模型', 'CHATCCC_CODEX_EFFORT': 'Effort'
1798
1871
  };
1799
- var hintMap = {
1800
- 'CHATCCC_CHROME_DEVTOOLS_ENABLED': '依赖:本机 Google Chrome;ChatGPT 订阅到期查询需要在该 CDP Chrome 中登录 ChatGPT。',
1872
+ var hintMap = {
1873
+ 'CHATCCC_WEB_UI_OPEN_ON_START': '关闭后可继续手动访问 http://localhost:<端口>/;/restart、/update Web UI 重启无论此项为何值都不会自动打开。',
1874
+ 'CHATCCC_CHROME_DEVTOOLS_ENABLED': '依赖:本机 Google Chrome;ChatGPT 订阅到期查询需要在该 CDP Chrome 中登录 ChatGPT。',
1801
1875
  'CHATCCC_CHROME_DEVTOOLS_PORT': '默认 15166,健康检查端点为 http://127.0.0.1:15166/json/version。',
1802
1876
  'CHATCCC_CHROME_DEVTOOLS_PATH': '选填。留空时自动探测 Google Chrome。'
1803
1877
  };
@@ -1810,10 +1884,12 @@ function editSection(section) {
1810
1884
  var val = state.config[key] || '';
1811
1885
  // Also check nested config
1812
1886
  if (!val) {
1813
- if (section === 'feishu') {
1887
+ if (section === 'feishu') {
1814
1888
  if (key === 'CHATCCC_APP_ID' && state.config.feishu) val = state.config.feishu.appId || '';
1815
1889
  else if (key === 'CHATCCC_APP_SECRET' && state.config.feishu) val = state.config.feishu.appSecret || '';
1816
- } else if (section === 'chromeDevtools' && state.config.chromeDevtools) {
1890
+ } else if (section === 'webUi') {
1891
+ val = !state.config.webUi || state.config.webUi.openOnStart !== false ? 'true' : 'false';
1892
+ } else if (section === 'chromeDevtools' && state.config.chromeDevtools) {
1817
1893
  if (key === 'CHATCCC_CHROME_DEVTOOLS_ENABLED') val = state.config.chromeDevtools.enabled === true ? 'true' : 'false';
1818
1894
  else if (key === 'CHATCCC_CHROME_DEVTOOLS_PORT') val = state.config.chromeDevtools.port != null ? String(state.config.chromeDevtools.port) : '15166';
1819
1895
  else if (key === 'CHATCCC_CHROME_DEVTOOLS_PATH') val = state.config.chromeDevtools.chromePath || '';
@@ -1838,9 +1914,10 @@ function editSection(section) {
1838
1914
  }
1839
1915
  }
1840
1916
  var isSecret = key.includes('SECRET') || key.includes('API_KEY');
1841
- if (key === 'CHATCCC_CHROME_DEVTOOLS_ENABLED') {
1842
- var checked = val === true || val === 'true';
1843
- 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>';
1917
+ if (key === 'CHATCCC_WEB_UI_OPEN_ON_START' || key === 'CHATCCC_CHROME_DEVTOOLS_ENABLED') {
1918
+ var checked = val === true || val === 'true';
1919
+ var changeHandler = key === 'CHATCCC_CHROME_DEVTOOLS_ENABLED' ? ' onchange="toggleEditChromeDevtoolsFields(this.checked)"' : '';
1920
+ html += '<div class="form-group"><label style="display:flex;align-items:center;gap:8px"><input type="checkbox" id="edit-' + key + '"' + (checked ? ' checked' : '') + changeHandler + '> ' + (labelMap[key] || key) + '</label>';
1844
1921
  if (hintMap[key]) html += '<div class="hint" style="margin-top:6px;line-height:1.5">' + hintMap[key] + '</div>';
1845
1922
  html += '</div>';
1846
1923
  } else if (key === 'CHATCCC_CURSOR_AVATAR_BATTERY_MODE') {
@@ -1894,17 +1971,18 @@ function closeEditModal() {
1894
1971
  editSectionType = null;
1895
1972
  }
1896
1973
 
1897
- async function saveEdit() {
1898
- var fields;
1899
- if (editSectionType === 'feishu') fields = FEISHU_FIELDS;
1900
- else if (editSectionType === 'chromeDevtools') fields = CHROME_DEVTOOLS_FIELDS;
1974
+ async function saveEdit() {
1975
+ var fields;
1976
+ if (editSectionType === 'feishu') fields = FEISHU_FIELDS;
1977
+ else if (editSectionType === 'webUi') fields = WEB_UI_FIELDS;
1978
+ else if (editSectionType === 'chromeDevtools') fields = CHROME_DEVTOOLS_FIELDS;
1901
1979
  else fields = AGENT_FIELDS[editSectionType] || [];
1902
1980
 
1903
1981
  var vars = {};
1904
1982
  fields.forEach(function(key){
1905
1983
  var el = document.getElementById('edit-' + key);
1906
1984
  if (!el) return;
1907
- if (key === 'CHATCCC_CHROME_DEVTOOLS_ENABLED') vars[key] = !!el.checked;
1985
+ if (key === 'CHATCCC_WEB_UI_OPEN_ON_START' || key === 'CHATCCC_CHROME_DEVTOOLS_ENABLED') vars[key] = !!el.checked;
1908
1986
  else vars[key] = el.value.trim();
1909
1987
  });
1910
1988
  if (editSectionType === 'chromeDevtools' && !vars.CHATCCC_CHROME_DEVTOOLS_PORT) {
@@ -1960,9 +2038,10 @@ init();
1960
2038
  // Router
1961
2039
  // ---------------------------------------------------------------------------
1962
2040
 
1963
- async function handleRequest(req: IncomingMessage, res: ServerResponse): Promise<void> {
1964
- const url = req.url ?? "/";
1965
- const method = req.method ?? "GET";
2041
+ async function handleRequest(req: IncomingMessage, res: ServerResponse): Promise<void> {
2042
+ const url = req.url ?? "/";
2043
+ const method = req.method ?? "GET";
2044
+ const pathname = url.split("?", 1)[0];
1966
2045
 
1967
2046
  if (extraApiHandler && await extraApiHandler(req, res)) return;
1968
2047
 
@@ -1975,7 +2054,13 @@ async function handleRequest(req: IncomingMessage, res: ServerResponse): Promise
1975
2054
  if (url === "/api/stop" && method === "POST") return handleStopService(req, res);
1976
2055
  if (url === "/api/restart" && method === "POST") return handleRestartService(req, res);
1977
2056
  if (url === "/api/validate" && method === "POST") return handleValidate(req, res);
1978
- if (url === "/api/ilink/forget" && method === "POST") return handleForgetIlink(req, res);
2057
+ if (url === "/api/ilink/forget" && method === "POST") return handleForgetIlink(req, res);
2058
+
2059
+ if (method === "GET" && (pathname === "/agent-team" || pathname === "/agent-team/")) {
2060
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
2061
+ res.end(AGENT_TEAM_PAGE_HTML);
2062
+ return;
2063
+ }
1979
2064
 
1980
2065
  // Serve HTML page for all other GET requests
1981
2066
  if (method === "GET") {
@@ -2000,40 +2085,7 @@ export function createUiRouter(): (req: IncomingMessage, res: ServerResponse) =>
2000
2085
  // Setup mode entry point — called from index.ts when no credentials
2001
2086
  // ---------------------------------------------------------------------------
2002
2087
 
2003
- /**
2004
- * 跨平台调起本地浏览器。
2005
- * - Windows:cmd /c start "" <url>。注意 start 把第一个被引号包裹的参数当成"窗口标题",
2006
- * 所以这里需要传一个空标题占位,否则当 url 被引号包住时它会被错误地当成标题。
2007
- * - macOS:open <url>
2008
- * - Linux:xdg-open <url>
2009
- *
2010
- * 任何失败都不抛——主流程不依赖浏览器是否真的弹起来,console 已经打印了 url。
2011
- */
2012
- function openInBrowser(url: string): void {
2013
- try {
2014
- if (process.platform === "win32") {
2015
- const child = spawn("cmd.exe", ["/c", "start", "", url], {
2016
- detached: true,
2017
- stdio: "ignore",
2018
- windowsHide: true,
2019
- });
2020
- child.on("error", () => { /* 浏览器未弹起来不影响主流程 */ });
2021
- child.unref();
2022
- } else if (process.platform === "darwin") {
2023
- const child = spawn("open", [url], { detached: true, stdio: "ignore" });
2024
- child.on("error", () => {});
2025
- child.unref();
2026
- } else {
2027
- const child = spawn("xdg-open", [url], { detached: true, stdio: "ignore" });
2028
- child.on("error", () => {});
2029
- child.unref();
2030
- }
2031
- } catch (err) {
2032
- console.error(`[WEB-UI] 自动打开浏览器失败: ${(err as Error).message}`);
2033
- }
2034
- }
2035
-
2036
- /**
2088
+ /**
2037
2089
  * setup → service「在线切换」回调签名:
2038
2090
  * - 入参 httpServer:setup 模式当前监听的 HTTP server,会被复用为 service 的
2039
2091
  * relay server(避免 close + recreate 的端口竞态)。
@@ -2045,8 +2097,10 @@ export type SetupActivateHook = (
2045
2097
  httpServer: ReturnType<typeof createServer>,
2046
2098
  ) => Promise<{ ok: true } | { ok: false; error: string }>;
2047
2099
 
2048
- export interface StartSetupModeOptions {
2049
- onActivate?: SetupActivateHook;
2100
+ export interface StartSetupModeOptions {
2101
+ onActivate?: SetupActivateHook;
2102
+ /** 内部重启进入 setup 模式时设为 false,避免重复弹出系统浏览器。 */
2103
+ openBrowser?: boolean;
2050
2104
  }
2051
2105
 
2052
2106
  // setup HTTP server + onActivate 回调通过模块级变量暴露给 handleStartService。
@@ -2093,20 +2147,24 @@ export function startSetupMode(port: number, options: StartSetupModeOptions = {}
2093
2147
  process.exit(1);
2094
2148
  });
2095
2149
 
2096
- server.listen(port, "127.0.0.1", () => {
2097
- const url = `http://127.0.0.1:${port}`;
2150
+ server.listen(port, "127.0.0.1", () => {
2151
+ const url = buildWebUiUrl(port);
2098
2152
  console.log("");
2099
2153
  console.log("=".repeat(60));
2100
2154
  console.log(" ChatCCC — 首次配置向导");
2101
2155
  console.log("=".repeat(60));
2102
2156
  console.log(" 未检测到已配置的飞书凭证,已启动配置界面。");
2103
- console.log(` 正在自动打开浏览器: ${url}`);
2157
+ if (options.openBrowser !== false) {
2158
+ console.log(` 已启用启动时自动打开浏览器: ${url}`);
2159
+ } else {
2160
+ console.log(` 内部重启不会自动打开浏览器,请按需访问: ${url}`);
2161
+ }
2104
2162
  console.log(" 若浏览器未自动弹出,请手动访问上面的地址。");
2105
2163
  console.log("");
2106
2164
  console.log(" 在向导里填好 App ID / App Secret 后点「保存并启动」,");
2107
2165
  console.log(" 服务会在当前进程内直接激活,不需要重新运行 chatccc。");
2108
2166
  console.log("=".repeat(60));
2109
2167
  console.log("");
2110
- openInBrowser(url);
2111
- });
2112
- }
2168
+ if (options.openBrowser !== false) openWebUiInDefaultBrowser(port);
2169
+ });
2170
+ }