chatccc 0.2.203 → 0.2.205
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -10
- package/config.sample.json +8 -5
- package/package.json +1 -1
- package/src/__tests__/agent-activity.test.ts +76 -0
- package/src/__tests__/codex-adapter.test.ts +7 -7
- package/src/__tests__/config-reload.test.ts +19 -6
- package/src/__tests__/config-sample.test.ts +10 -1
- package/src/__tests__/cursor-adapter.test.ts +5 -5
- package/src/__tests__/session.test.ts +97 -17
- package/src/__tests__/startup-lifecycle.test.ts +98 -0
- package/src/__tests__/web-ui.test.ts +59 -0
- package/src/adapters/codex-adapter.ts +1 -0
- package/src/adapters/cursor-adapter.ts +1 -0
- package/src/agent-activity.ts +170 -0
- package/src/config.ts +19 -11
- package/src/index.ts +59 -19
- package/src/orchestrator.ts +11 -4
- package/src/session-chat-binding.ts +6 -4
- package/src/session.ts +89 -60
- package/src/startup-lifecycle.ts +96 -0
- package/src/stream-state.ts +13 -7
- package/src/web-ui.ts +185 -127
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 === "
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
<
|
|
678
|
-
|
|
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
|
-
<!--
|
|
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
|
|
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
|
|
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
|
|
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">
|
|
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
|
|
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 === '
|
|
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
|
-
'
|
|
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
|
-
'
|
|
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 === '
|
|
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
|
-
|
|
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 === '
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
2111
|
-
});
|
|
2112
|
-
}
|
|
2168
|
+
if (options.openBrowser !== false) openWebUiInDefaultBrowser(port);
|
|
2169
|
+
});
|
|
2170
|
+
}
|