chatccc 0.2.203 → 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/README.md +14 -10
- package/config.sample.json +8 -5
- package/package.json +1 -1
- package/src/__tests__/config-reload.test.ts +19 -6
- package/src/__tests__/config-sample.test.ts +10 -1
- package/src/__tests__/startup-lifecycle.test.ts +98 -0
- package/src/__tests__/web-ui.test.ts +59 -0
- package/src/config.ts +19 -11
- package/src/index.ts +59 -19
- package/src/orchestrator.ts +11 -4
- package/src/startup-lifecycle.ts +96 -0
- package/src/web-ui.ts +185 -127
package/README.md
CHANGED
|
@@ -133,7 +133,7 @@ Write-Host ''
|
|
|
133
133
|
chatccc
|
|
134
134
|
```
|
|
135
135
|
|
|
136
|
-
|
|
136
|
+
如果一切顺利,系统默认浏览器会自动打开 `http://localhost:18080/` 的 Web 配置页面。按页面提示填入飞书 App ID / App Secret,点击"保存并启动"即可。
|
|
137
137
|
|
|
138
138
|
> **只想装 ChatCCC 本体?** 如果你已经有 Node.js,直接 `npm install -g chatccc && chatccc` 即可,不需要跑上面的完整脚本。
|
|
139
139
|
|
|
@@ -151,7 +151,7 @@ chatccc
|
|
|
151
151
|
|
|
152
152
|
旧版本留在仓库或包目录下的 `config.json`、`logs/`、`state/` 会在首次启动时自动迁移到用户目录。
|
|
153
153
|
|
|
154
|
-
|
|
154
|
+
每次直接运行 `chatccc` 时,无论是否已经完成配置,ChatCCC 默认都会用系统默认浏览器打开本地 Web UI(默认 `http://localhost:18080/`,修改 `config.port` 后跟随实际端口)。可在首次配置向导或管理页的 **Web UI** 设置中关闭;关闭后从下一次直接启动起生效。由 `/restart`、`/update` 或 Web UI 发起的内部重启始终不会重复打开浏览器。Linux 服务器没有 `DISPLAY`/`WAYLAND_DISPLAY` 时会跳过打开,并在终端输出 SSH 隧道访问提示。主页顶部的 **Agent Team** 入口会跳转到独立的 `/agent-team` 页面。
|
|
155
155
|
|
|
156
156
|
#### 从源码运行
|
|
157
157
|
|
|
@@ -260,11 +260,14 @@ Codex 的默认模型和推理强度可继续由 `~/.codex/config.toml` 管理
|
|
|
260
260
|
"appId": "cli_xxxxxxxxxxxx",
|
|
261
261
|
"appSecret": "xxxxxxxxxxxxxxxxxxxxxxxxxx"
|
|
262
262
|
},
|
|
263
|
-
"platforms": {
|
|
264
|
-
"feishu": { "enabled": true, "platformType": "feishu" },
|
|
265
|
-
"ilink": { "enabled": true, "reuseTokenOnStart": true }
|
|
266
|
-
},
|
|
267
|
-
"
|
|
263
|
+
"platforms": {
|
|
264
|
+
"feishu": { "enabled": true, "platformType": "feishu" },
|
|
265
|
+
"ilink": { "enabled": true, "reuseTokenOnStart": true }
|
|
266
|
+
},
|
|
267
|
+
"webUi": {
|
|
268
|
+
"openOnStart": true
|
|
269
|
+
},
|
|
270
|
+
"chromeDevtools": {
|
|
268
271
|
"enabled": false,
|
|
269
272
|
"port": 15166,
|
|
270
273
|
"chromePath": ""
|
|
@@ -305,9 +308,10 @@ Codex 的默认模型和推理强度可继续由 `~/.codex/config.toml` 管理
|
|
|
305
308
|
| `feishu.appId` / `feishu.appSecret` | 飞书应用凭证 |
|
|
306
309
|
| `platforms.feishu.enabled` | 是否启用飞书 |
|
|
307
310
|
| `platforms.feishu.platformType` | 飞书平台类型,默认 `feishu` |
|
|
308
|
-
| `platforms.ilink.enabled` | 是否启用微信 iLink |
|
|
309
|
-
| `platforms.ilink.reuseTokenOnStart` | 启动时是否复用已有微信登录 token |
|
|
310
|
-
| `
|
|
311
|
+
| `platforms.ilink.enabled` | 是否启用微信 iLink |
|
|
312
|
+
| `platforms.ilink.reuseTokenOnStart` | 启动时是否复用已有微信登录 token |
|
|
313
|
+
| `webUi.openOnStart` | 直接启动时是否打开系统默认浏览器;默认 true,内部重启始终跳过 |
|
|
314
|
+
| `chromeDevtools.enabled` | 是否启用常驻 Chrome CDP;默认 false |
|
|
311
315
|
| `chromeDevtools.port` | Chrome CDP 端口;默认 15166 |
|
|
312
316
|
| `chromeDevtools.chromePath` | Chrome 可执行文件路径;留空时自动探测 |
|
|
313
317
|
| `port` | 本地 Web 配置面板和中继服务端口 |
|
package/config.sample.json
CHANGED
|
@@ -3,11 +3,14 @@
|
|
|
3
3
|
"appId": "",
|
|
4
4
|
"appSecret": ""
|
|
5
5
|
},
|
|
6
|
-
"platforms": {
|
|
7
|
-
"feishu": { "enabled": true, "platformType": "feishu" },
|
|
8
|
-
"ilink": { "enabled": true, "reuseTokenOnStart": true }
|
|
9
|
-
},
|
|
10
|
-
"
|
|
6
|
+
"platforms": {
|
|
7
|
+
"feishu": { "enabled": true, "platformType": "feishu" },
|
|
8
|
+
"ilink": { "enabled": true, "reuseTokenOnStart": true }
|
|
9
|
+
},
|
|
10
|
+
"webUi": {
|
|
11
|
+
"openOnStart": true
|
|
12
|
+
},
|
|
13
|
+
"chromeDevtools": {
|
|
11
14
|
"enabled": false,
|
|
12
15
|
"port": 15166,
|
|
13
16
|
"chromePath": ""
|
package/package.json
CHANGED
|
@@ -38,10 +38,11 @@ import {
|
|
|
38
38
|
// 原地切换复用同一 server,重新读端口只会引入混乱)
|
|
39
39
|
// ---------------------------------------------------------------------------
|
|
40
40
|
|
|
41
|
-
const baseAppConfig: AppConfig = {
|
|
42
|
-
feishu: { appId: "INITIAL_APP", appSecret: "INITIAL_SECRET" },
|
|
43
|
-
platforms: { feishu: { enabled: true }, ilink: { enabled: true } },
|
|
44
|
-
|
|
41
|
+
const baseAppConfig: AppConfig = {
|
|
42
|
+
feishu: { appId: "INITIAL_APP", appSecret: "INITIAL_SECRET" },
|
|
43
|
+
platforms: { feishu: { enabled: true }, ilink: { enabled: true } },
|
|
44
|
+
webUi: { openOnStart: true },
|
|
45
|
+
chromeDevtools: { enabled: false, port: 15166, chromePath: "" },
|
|
45
46
|
port: 18080,
|
|
46
47
|
gitTimeoutSeconds: 180,
|
|
47
48
|
allowInterrupt: false,
|
|
@@ -216,7 +217,7 @@ describe("applyLoadedConfig — config 对象引用契约", () => {
|
|
|
216
217
|
expect(config.codex.path).toBe("/refresh/codex");
|
|
217
218
|
});
|
|
218
219
|
|
|
219
|
-
it("刷新 chromeDevtools 配置但保持 config 引用", () => {
|
|
220
|
+
it("刷新 chromeDevtools 配置但保持 config 引用", () => {
|
|
220
221
|
const refBefore = config;
|
|
221
222
|
|
|
222
223
|
applyLoadedConfig({
|
|
@@ -226,7 +227,19 @@ describe("applyLoadedConfig — config 对象引用契约", () => {
|
|
|
226
227
|
|
|
227
228
|
expect(config).toBe(refBefore);
|
|
228
229
|
expect(config.chromeDevtools).toEqual({ enabled: true, port: 15166, chromePath: "C:/Chrome/chrome.exe" });
|
|
229
|
-
});
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it("刷新 webUi 启动开关但保持 config 引用", () => {
|
|
233
|
+
const refBefore = config;
|
|
234
|
+
|
|
235
|
+
applyLoadedConfig({
|
|
236
|
+
...structuredClone(baseAppConfig),
|
|
237
|
+
webUi: { openOnStart: false },
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
expect(config).toBe(refBefore);
|
|
241
|
+
expect(config.webUi).toEqual({ openOnStart: false });
|
|
242
|
+
});
|
|
230
243
|
|
|
231
244
|
it("空 feishu 凭证也能正确刷入(向导回滚到空的反向场景)", () => {
|
|
232
245
|
applyLoadedConfig({
|
|
@@ -59,7 +59,16 @@ describe("config.sample.json", () => {
|
|
|
59
59
|
expect(sample.chromeDevtools?.enabled).toBe(false);
|
|
60
60
|
expect(sample.chromeDevtools?.port).toBe(15166);
|
|
61
61
|
expect(sample.chromeDevtools?.chromePath).toBe("");
|
|
62
|
-
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("opens the Web UI on direct startup by default", () => {
|
|
65
|
+
const configSamplePath = join(process.cwd(), "config.sample.json");
|
|
66
|
+
const sample = JSON.parse(readFileSync(configSamplePath, "utf8")) as {
|
|
67
|
+
webUi?: { openOnStart?: unknown };
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
expect(sample.webUi?.openOnStart).toBe(true);
|
|
71
|
+
});
|
|
63
72
|
|
|
64
73
|
it("keeps raw stream logs disabled by default for every agent", () => {
|
|
65
74
|
const configSamplePath = join(process.cwd(), "config.sample.json");
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
INTERNAL_RESTART_ENV_VAR,
|
|
5
|
+
buildWebUiUrl,
|
|
6
|
+
createInternalRestartEnv,
|
|
7
|
+
openWebUiInDefaultBrowser,
|
|
8
|
+
shouldAutoOpenWebUi,
|
|
9
|
+
} from "../startup-lifecycle.ts";
|
|
10
|
+
|
|
11
|
+
describe("ChatCCC startup lifecycle", () => {
|
|
12
|
+
it("opens the Web UI by default for a direct start", () => {
|
|
13
|
+
expect(shouldAutoOpenWebUi({ env: {} })).toBe(true);
|
|
14
|
+
expect(shouldAutoOpenWebUi({ env: {}, openOnStart: true })).toBe(true);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("respects the persisted openOnStart preference", () => {
|
|
18
|
+
expect(shouldAutoOpenWebUi({ env: {}, openOnStart: false })).toBe(false);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("never opens the Web UI for an internal restart", () => {
|
|
22
|
+
expect(shouldAutoOpenWebUi({
|
|
23
|
+
env: { [INTERNAL_RESTART_ENV_VAR]: "1" },
|
|
24
|
+
openOnStart: true,
|
|
25
|
+
})).toBe(false);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("marks internal restart children without losing the inherited environment", () => {
|
|
29
|
+
expect(createInternalRestartEnv({ PATH: "test-path", CUSTOM: "value" })).toEqual({
|
|
30
|
+
PATH: "test-path",
|
|
31
|
+
CUSTOM: "value",
|
|
32
|
+
[INTERNAL_RESTART_ENV_VAR]: "1",
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("uses localhost and the configured port for the Web UI URL", () => {
|
|
37
|
+
expect(buildWebUiUrl(18080)).toBe("http://localhost:18080/");
|
|
38
|
+
expect(buildWebUiUrl(18081)).toBe("http://localhost:18081/");
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("opens the configured URL in the Windows default browser", () => {
|
|
42
|
+
const child = {
|
|
43
|
+
on: vi.fn().mockReturnThis(),
|
|
44
|
+
unref: vi.fn(),
|
|
45
|
+
};
|
|
46
|
+
const spawnImpl = vi.fn(() => child as never);
|
|
47
|
+
|
|
48
|
+
expect(openWebUiInDefaultBrowser(18081, {
|
|
49
|
+
platform: "win32",
|
|
50
|
+
spawnImpl,
|
|
51
|
+
})).toBe(true);
|
|
52
|
+
expect(spawnImpl).toHaveBeenCalledWith(
|
|
53
|
+
"cmd.exe",
|
|
54
|
+
["/c", "start", "", "http://localhost:18081/"],
|
|
55
|
+
{
|
|
56
|
+
detached: true,
|
|
57
|
+
stdio: "ignore",
|
|
58
|
+
windowsHide: true,
|
|
59
|
+
},
|
|
60
|
+
);
|
|
61
|
+
expect(child.on).toHaveBeenCalledWith("error", expect.any(Function));
|
|
62
|
+
expect(child.unref).toHaveBeenCalledOnce();
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("opens the configured URL with xdg-open on a Linux desktop", () => {
|
|
66
|
+
const child = {
|
|
67
|
+
on: vi.fn().mockReturnThis(),
|
|
68
|
+
unref: vi.fn(),
|
|
69
|
+
};
|
|
70
|
+
const spawnImpl = vi.fn(() => child as never);
|
|
71
|
+
|
|
72
|
+
expect(openWebUiInDefaultBrowser(18080, {
|
|
73
|
+
platform: "linux",
|
|
74
|
+
env: { DISPLAY: ":0" },
|
|
75
|
+
spawnImpl,
|
|
76
|
+
})).toBe(true);
|
|
77
|
+
expect(spawnImpl).toHaveBeenCalledWith(
|
|
78
|
+
"xdg-open",
|
|
79
|
+
["http://localhost:18080/"],
|
|
80
|
+
{ detached: true, stdio: "ignore" },
|
|
81
|
+
);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("skips xdg-open and prints an SSH tunnel hint on headless Linux", () => {
|
|
85
|
+
const spawnImpl = vi.fn();
|
|
86
|
+
const onInfo = vi.fn();
|
|
87
|
+
|
|
88
|
+
expect(openWebUiInDefaultBrowser(18080, {
|
|
89
|
+
platform: "linux",
|
|
90
|
+
env: {},
|
|
91
|
+
spawnImpl,
|
|
92
|
+
onInfo,
|
|
93
|
+
})).toBe(false);
|
|
94
|
+
expect(spawnImpl).not.toHaveBeenCalled();
|
|
95
|
+
expect(onInfo).toHaveBeenCalledWith(expect.stringContaining("ssh -L 18080:127.0.0.1:18080"));
|
|
96
|
+
expect(onInfo).toHaveBeenCalledWith(expect.stringContaining("http://localhost:18080/"));
|
|
97
|
+
});
|
|
98
|
+
});
|
|
@@ -1,11 +1,44 @@
|
|
|
1
|
+
import { createServer } from "node:http";
|
|
2
|
+
import type { AddressInfo } from "node:net";
|
|
1
3
|
import { describe, it, expect } from "vitest";
|
|
2
4
|
import {
|
|
5
|
+
AGENT_TEAM_PAGE_HTML,
|
|
3
6
|
PAGE_HTML,
|
|
4
7
|
chooseStartPath,
|
|
8
|
+
createUiRouter,
|
|
5
9
|
getRestartRequiredReasons,
|
|
6
10
|
unflattenConfig,
|
|
7
11
|
} from "../web-ui.ts";
|
|
8
12
|
|
|
13
|
+
describe("Agent Team page", () => {
|
|
14
|
+
it("adds a prominent global entry to the ChatCCC page", () => {
|
|
15
|
+
expect(PAGE_HTML).toContain('href="/agent-team"');
|
|
16
|
+
expect(PAGE_HTML).toContain('class="agent-team-entry"');
|
|
17
|
+
expect(PAGE_HTML).toContain("Agent Team");
|
|
18
|
+
expect(PAGE_HTML).toContain("linear-gradient");
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("contains only the Agent Team title as page content", () => {
|
|
22
|
+
expect(AGENT_TEAM_PAGE_HTML).toContain("<title>Agent Team</title>");
|
|
23
|
+
expect(AGENT_TEAM_PAGE_HTML).toContain("<h1>Agent Team</h1>");
|
|
24
|
+
expect(AGENT_TEAM_PAGE_HTML).not.toContain("dashboard-view");
|
|
25
|
+
expect(AGENT_TEAM_PAGE_HTML).not.toContain("wizard-view");
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("serves the Agent Team page from its own route", async () => {
|
|
29
|
+
const server = createServer(createUiRouter());
|
|
30
|
+
await new Promise<void>((resolve) => server.listen(0, "127.0.0.1", resolve));
|
|
31
|
+
const port = (server.address() as AddressInfo).port;
|
|
32
|
+
try {
|
|
33
|
+
const response = await fetch(`http://127.0.0.1:${port}/agent-team`);
|
|
34
|
+
expect(response.status).toBe(200);
|
|
35
|
+
expect(await response.text()).toBe(AGENT_TEAM_PAGE_HTML);
|
|
36
|
+
} finally {
|
|
37
|
+
await new Promise<void>((resolve, reject) => server.close((err) => err ? reject(err) : resolve()));
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
9
42
|
describe("unflattenConfig", () => {
|
|
10
43
|
it("maps Claude subagent model into claude.subagentModel", () => {
|
|
11
44
|
expect(
|
|
@@ -66,6 +99,18 @@ describe("unflattenConfig", () => {
|
|
|
66
99
|
},
|
|
67
100
|
});
|
|
68
101
|
});
|
|
102
|
+
|
|
103
|
+
it("maps the Web UI startup preference into webUi config", () => {
|
|
104
|
+
expect(
|
|
105
|
+
unflattenConfig({
|
|
106
|
+
CHATCCC_WEB_UI_OPEN_ON_START: false,
|
|
107
|
+
}),
|
|
108
|
+
).toEqual({
|
|
109
|
+
webUi: {
|
|
110
|
+
openOnStart: false,
|
|
111
|
+
},
|
|
112
|
+
});
|
|
113
|
+
});
|
|
69
114
|
});
|
|
70
115
|
|
|
71
116
|
describe("getRestartRequiredReasons", () => {
|
|
@@ -75,6 +120,7 @@ describe("getRestartRequiredReasons", () => {
|
|
|
75
120
|
feishu: { enabled: true, platformType: "feishu" },
|
|
76
121
|
ilink: { enabled: true, reuseTokenOnStart: true },
|
|
77
122
|
},
|
|
123
|
+
webUi: { openOnStart: true },
|
|
78
124
|
chromeDevtools: { enabled: false, port: 15166, chromePath: "" },
|
|
79
125
|
port: 18080,
|
|
80
126
|
claude: {
|
|
@@ -142,6 +188,12 @@ describe("getRestartRequiredReasons", () => {
|
|
|
142
188
|
});
|
|
143
189
|
|
|
144
190
|
describe("dashboard edit modal", () => {
|
|
191
|
+
it("keeps the embedded dashboard script syntactically valid", () => {
|
|
192
|
+
const script = PAGE_HTML.match(/<script>([\s\S]*)<\/script>/)?.[1];
|
|
193
|
+
expect(script).toBeTruthy();
|
|
194
|
+
expect(() => new Function(script ?? "")).not.toThrow();
|
|
195
|
+
});
|
|
196
|
+
|
|
145
197
|
it("shows the edit modal and overlay when a section edit button is clicked", () => {
|
|
146
198
|
expect(PAGE_HTML).toContain("function editSection(section)");
|
|
147
199
|
expect(PAGE_HTML).toContain("document.getElementById('edit-modal').classList.remove('hidden');");
|
|
@@ -158,6 +210,13 @@ describe("dashboard edit modal", () => {
|
|
|
158
210
|
expect(PAGE_HTML).toContain("生效范围:保存后下一条消息或下个新会话生效");
|
|
159
211
|
expect(PAGE_HTML).toContain("生效范围:飞书开关、App ID、App Secret 或平台类型变更需要重启 ChatCCC");
|
|
160
212
|
});
|
|
213
|
+
|
|
214
|
+
it("shows the Web UI startup switch in both the wizard and dashboard", () => {
|
|
215
|
+
expect(PAGE_HTML).toContain('id="field-CHATCCC_WEB_UI_OPEN_ON_START"');
|
|
216
|
+
expect(PAGE_HTML).toContain('id="cfg-WEB_UI_OPEN_ON_START"');
|
|
217
|
+
expect(PAGE_HTML).toContain("editSection('webUi')");
|
|
218
|
+
expect(PAGE_HTML).toContain("下次直接启动生效;内部重启始终不打开");
|
|
219
|
+
});
|
|
161
220
|
});
|
|
162
221
|
|
|
163
222
|
// ---------------------------------------------------------------------------
|
package/src/config.ts
CHANGED
|
@@ -150,10 +150,11 @@ export interface RawStreamLogsConfig {
|
|
|
150
150
|
ccc: RawStreamAgentLogConfig;
|
|
151
151
|
}
|
|
152
152
|
|
|
153
|
-
export interface AppConfig {
|
|
154
|
-
feishu: FeishuConfig;
|
|
155
|
-
platforms: PlatformsConfig;
|
|
156
|
-
|
|
153
|
+
export interface AppConfig {
|
|
154
|
+
feishu: FeishuConfig;
|
|
155
|
+
platforms: PlatformsConfig;
|
|
156
|
+
webUi: { openOnStart: boolean };
|
|
157
|
+
chromeDevtools: ChromeDevtoolsConfig;
|
|
157
158
|
port: number;
|
|
158
159
|
gitTimeoutSeconds: number;
|
|
159
160
|
/** 若为 false,AI 生成过程中用户发送消息不会打断,须先点「停止」再发送新消息 */
|
|
@@ -418,9 +419,10 @@ function normalizeRawStreamAgentLogConfig(raw: unknown): RawStreamAgentLogConfig
|
|
|
418
419
|
|
|
419
420
|
function loadConfig(): AppConfig {
|
|
420
421
|
const defaults: AppConfig = {
|
|
421
|
-
feishu: { appId: "", appSecret: "" },
|
|
422
|
-
platforms: { feishu: { enabled: true }, ilink: { enabled: true } },
|
|
423
|
-
|
|
422
|
+
feishu: { appId: "", appSecret: "" },
|
|
423
|
+
platforms: { feishu: { enabled: true }, ilink: { enabled: true } },
|
|
424
|
+
webUi: { openOnStart: true },
|
|
425
|
+
chromeDevtools: { enabled: false, port: 15166, chromePath: "" },
|
|
424
426
|
port: 18080,
|
|
425
427
|
gitTimeoutSeconds: 180,
|
|
426
428
|
allowInterrupt: false,
|
|
@@ -495,6 +497,7 @@ function loadConfig(): AppConfig {
|
|
|
495
497
|
};
|
|
496
498
|
codex?: { enabled?: unknown; defaultAgent?: unknown; path?: unknown; command?: unknown; model?: unknown; alternativeModel?: unknown; effort?: unknown };
|
|
497
499
|
ccc?: { DEEPSEEK_API_KEY?: unknown; DEEPSEEK_BASE_URL?: unknown; model?: unknown };
|
|
500
|
+
webUi?: { openOnStart?: unknown };
|
|
498
501
|
chromeDevtools?: { enabled?: unknown; port?: unknown; chromePath?: unknown };
|
|
499
502
|
rawStreamLogs?: unknown;
|
|
500
503
|
};
|
|
@@ -510,6 +513,7 @@ function loadConfig(): AppConfig {
|
|
|
510
513
|
const cursorRaw = (parsed.cursor ?? {}) as NonNullable<typeof parsed.cursor>;
|
|
511
514
|
const codexRaw = (parsed.codex ?? {}) as NonNullable<typeof parsed.codex>;
|
|
512
515
|
const cccRaw = (parsed.ccc ?? {}) as NonNullable<typeof parsed.ccc>;
|
|
516
|
+
const webUiRaw = (parsed.webUi ?? {}) as NonNullable<typeof parsed.webUi>;
|
|
513
517
|
const chromeDevtoolsRaw = (parsed.chromeDevtools ?? {}) as NonNullable<typeof parsed.chromeDevtools>;
|
|
514
518
|
const rawStreamLogsRaw = typeof parsed.rawStreamLogs === "object" && parsed.rawStreamLogs !== null
|
|
515
519
|
? parsed.rawStreamLogs as unknown as Record<string, unknown>
|
|
@@ -577,7 +581,7 @@ function loadConfig(): AppConfig {
|
|
|
577
581
|
appId: feishu.appId ?? "",
|
|
578
582
|
appSecret: feishu.appSecret ?? "",
|
|
579
583
|
},
|
|
580
|
-
platforms: {
|
|
584
|
+
platforms: {
|
|
581
585
|
feishu: {
|
|
582
586
|
enabled: typeof (parsed.platforms as unknown as Record<string, unknown> | undefined)?.feishu === "object"
|
|
583
587
|
? Boolean(((parsed.platforms as unknown as Record<string, unknown>).feishu as Record<string, unknown>).enabled ?? true)
|
|
@@ -595,9 +599,13 @@ function loadConfig(): AppConfig {
|
|
|
595
599
|
reuseTokenOnStart: typeof (parsed.platforms as unknown as Record<string, unknown> | undefined)?.ilink === "object"
|
|
596
600
|
? Boolean(((parsed.platforms as unknown as Record<string, unknown>).ilink as Record<string, unknown>).reuseTokenOnStart ?? true)
|
|
597
601
|
: true,
|
|
598
|
-
},
|
|
599
|
-
},
|
|
600
|
-
|
|
602
|
+
},
|
|
603
|
+
},
|
|
604
|
+
webUi: {
|
|
605
|
+
// 兼容升级前没有 webUi 字段的 config.json:缺省仍按“自动打开”处理。
|
|
606
|
+
openOnStart: typeof webUiRaw.openOnStart === "boolean" ? webUiRaw.openOnStart : true,
|
|
607
|
+
},
|
|
608
|
+
chromeDevtools: {
|
|
601
609
|
enabled: typeof chromeDevtoolsRaw.enabled === "boolean" ? chromeDevtoolsRaw.enabled : false,
|
|
602
610
|
port: Number.isInteger(chromeDevtoolsPort) && chromeDevtoolsPort >= 1 && chromeDevtoolsPort <= 65535
|
|
603
611
|
? chromeDevtoolsPort
|
package/src/index.ts
CHANGED
|
@@ -28,12 +28,18 @@ import { WSClient, EventDispatcher, Domain } from "@larksuiteoapi/node-sdk";
|
|
|
28
28
|
import WebSocket from "ws";
|
|
29
29
|
|
|
30
30
|
import { appendStartupTrace, attachRelayWebSocket, ensureSingleInstance, freeRelayListenPort, installCrashLogging, waitForPortFree } from "./shared.ts";
|
|
31
|
-
import { createUiRouter, setExtraApiHandler, setReloadConfigHook, startSetupMode } from "./web-ui.ts";
|
|
31
|
+
import { createUiRouter, setExtraApiHandler, setReloadConfigHook, startSetupMode } from "./web-ui.ts";
|
|
32
|
+
import {
|
|
33
|
+
buildWebUiUrl,
|
|
34
|
+
openWebUiInDefaultBrowser,
|
|
35
|
+
shouldAutoOpenWebUi,
|
|
36
|
+
} from "./startup-lifecycle.ts";
|
|
32
37
|
import { buildPlatformStartupPlan } from "./platform-startup.ts";
|
|
33
38
|
import { makeTraceId, logTrace } from "./trace.ts";
|
|
34
39
|
import {
|
|
35
|
-
CHATCCC_PORT,
|
|
36
|
-
|
|
40
|
+
CHATCCC_PORT,
|
|
41
|
+
config,
|
|
42
|
+
APP_ID,
|
|
37
43
|
APP_SECRET,
|
|
38
44
|
FEISHU_ENABLED,
|
|
39
45
|
FEISHU_PLATFORM_TYPE,
|
|
@@ -720,11 +726,17 @@ async function startConfiguredPlatforms(
|
|
|
720
726
|
// Main
|
|
721
727
|
// ---------------------------------------------------------------------------
|
|
722
728
|
|
|
723
|
-
async function main(): Promise<void> {
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
729
|
+
async function main(): Promise<void> {
|
|
730
|
+
// 用户直接运行 chatccc 时打开系统默认浏览器;由 `/restart`、`/update`
|
|
731
|
+
// 或 Web UI 拉起的替代进程会携带内部标记,不重复打扰用户。
|
|
732
|
+
const autoOpenWebUi = shouldAutoOpenWebUi({
|
|
733
|
+
openOnStart: config.webUi.openOnStart,
|
|
734
|
+
});
|
|
735
|
+
appendStartupTrace("main: entered", {
|
|
736
|
+
argv: process.argv.join(" ").slice(0, 400),
|
|
737
|
+
CHATCCC_PORT,
|
|
738
|
+
PROJECT_ROOT,
|
|
739
|
+
autoOpenWebUi,
|
|
728
740
|
});
|
|
729
741
|
|
|
730
742
|
// 黑匣子:所有未捕获异常 / 信号 / beforeExit 都同步写入 startup-trace.log(appendFileSync)。
|
|
@@ -780,9 +792,18 @@ async function main(): Promise<void> {
|
|
|
780
792
|
console.log(`${"=".repeat(60)}`);
|
|
781
793
|
console.log(` 发送消息: POST http://127.0.0.1:${SIM_PORT}/api/sim/inject-message`);
|
|
782
794
|
console.log(` 消息日志: ~/.chatccc/sim/messages.jsonl`);
|
|
783
|
-
console.log(`${"=".repeat(60)}\n`);
|
|
784
|
-
|
|
785
|
-
|
|
795
|
+
console.log(`${"=".repeat(60)}\n`);
|
|
796
|
+
|
|
797
|
+
if (autoOpenWebUi) {
|
|
798
|
+
const url = buildWebUiUrl(SIM_PORT);
|
|
799
|
+
const opened = openWebUiInDefaultBrowser(SIM_PORT);
|
|
800
|
+
appendStartupTrace(
|
|
801
|
+
opened ? "web-ui: opening simulate browser" : "web-ui: simulate browser unavailable",
|
|
802
|
+
{ url },
|
|
803
|
+
);
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
installShutdownHandlers(simServer);
|
|
786
807
|
return;
|
|
787
808
|
}
|
|
788
809
|
|
|
@@ -830,8 +851,9 @@ async function main(): Promise<void> {
|
|
|
830
851
|
if (!APP_ID.trim() || !APP_SECRET.trim()) {
|
|
831
852
|
// 凭证不全:进 setup 向导。注入 onActivate 回调让用户点"保存并启动"
|
|
832
853
|
// 时,原地(同进程)调用 startBotService,复用 setup HTTP server。
|
|
833
|
-
startSetupMode(CHATCCC_PORT, {
|
|
834
|
-
|
|
854
|
+
startSetupMode(CHATCCC_PORT, {
|
|
855
|
+
openBrowser: autoOpenWebUi,
|
|
856
|
+
onActivate: async (httpServer: Server) => {
|
|
835
857
|
reloadRuntimeConfig("setup-activate");
|
|
836
858
|
appendStartupTrace("setup-activate: reloaded config from disk", {
|
|
837
859
|
appIdMaskAfterReload: maskAppId(APP_ID),
|
|
@@ -867,17 +889,35 @@ async function main(): Promise<void> {
|
|
|
867
889
|
await waitForPortFree(CHATCCC_PORT);
|
|
868
890
|
appendStartupTrace("main: port free confirmed", { CHATCCC_PORT });
|
|
869
891
|
}
|
|
870
|
-
const httpServer = createServer(createUiRouter());
|
|
871
|
-
await listenWithRetry(httpServer, CHATCCC_PORT, "127.0.0.1").catch((err: NodeJS.ErrnoException) => {
|
|
892
|
+
const httpServer = createServer(createUiRouter());
|
|
893
|
+
await listenWithRetry(httpServer, CHATCCC_PORT, "127.0.0.1").catch((err: NodeJS.ErrnoException) => {
|
|
872
894
|
console.error(`\n[启动] 本地中继 WebSocket 监听失败:端口 ${CHATCCC_PORT}(${err.code ?? "?"} — ${err.message})`);
|
|
873
895
|
console.error(
|
|
874
896
|
" 处理建议: 关闭占用该端口的其它程序,或在 config.json 的 port 字段里改成其它未占用端口(如 18081)。"
|
|
875
897
|
);
|
|
876
898
|
printServiceDidNotStart(`本地中继端口 ${CHATCCC_PORT} 无法监听(${err.code ?? "?"} — ${err.message})`);
|
|
877
|
-
process.exit(1);
|
|
878
|
-
});
|
|
879
|
-
|
|
880
|
-
|
|
899
|
+
process.exit(1);
|
|
900
|
+
});
|
|
901
|
+
|
|
902
|
+
// 必须等 HTTP server 真正监听后再发起打开请求,避免浏览器先到一步看到
|
|
903
|
+
// ERR_CONNECTION_REFUSED。Chrome CDP 守护仍保持自己原有的独立行为。
|
|
904
|
+
if (autoOpenWebUi) {
|
|
905
|
+
const url = buildWebUiUrl(CHATCCC_PORT);
|
|
906
|
+
const opened = openWebUiInDefaultBrowser(CHATCCC_PORT);
|
|
907
|
+
if (opened) {
|
|
908
|
+
console.log(`[WEB-UI] 已请求系统默认浏览器打开: ${url}`);
|
|
909
|
+
appendStartupTrace("web-ui: opening default browser", { url });
|
|
910
|
+
} else {
|
|
911
|
+
appendStartupTrace("web-ui: default browser unavailable", { url });
|
|
912
|
+
}
|
|
913
|
+
} else {
|
|
914
|
+
appendStartupTrace("web-ui: default browser skipped by lifecycle or preference", {
|
|
915
|
+
url: buildWebUiUrl(CHATCCC_PORT),
|
|
916
|
+
openOnStart: config.webUi.openOnStart,
|
|
917
|
+
});
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
await startConfiguredPlatforms(httpServer, { failOnFeishuError: false });
|
|
881
921
|
|
|
882
922
|
installShutdownHandlers(httpServer);
|
|
883
923
|
}
|
package/src/orchestrator.ts
CHANGED
|
@@ -91,6 +91,7 @@ import { applySharedPrefix } from "./shared-prefix.ts";
|
|
|
91
91
|
import { cwdDisplayName, sessionChatName } from "./session-name.ts";
|
|
92
92
|
import { reloadRuntimeConfig } from "./runtime-reload.ts";
|
|
93
93
|
import { acquireUpdateCommandGuard } from "./update-command-guard.ts";
|
|
94
|
+
import { createInternalRestartEnv } from "./startup-lifecycle.ts";
|
|
94
95
|
export { type PlatformAdapter } from "./platform-adapter.ts";
|
|
95
96
|
import type { ChatAvatarUsageHints, PlatformAdapter } from "./platform-adapter.ts";
|
|
96
97
|
import type { CodexUsageSummary } from "./feishu-api.ts";
|
|
@@ -480,7 +481,12 @@ function syncUpdateAndRestart(): void {
|
|
|
480
481
|
|
|
481
482
|
// 3. spawn new chatccc
|
|
482
483
|
try {
|
|
483
|
-
const child = spawn(binPath, [], {
|
|
484
|
+
const child = spawn(binPath, [], {
|
|
485
|
+
detached: true,
|
|
486
|
+
stdio: "ignore",
|
|
487
|
+
shell: true,
|
|
488
|
+
env: createInternalRestartEnv(),
|
|
489
|
+
});
|
|
484
490
|
child.unref();
|
|
485
491
|
updLog(`spawn new chatccc OK, childPid=${child.pid}, bin=${binPath}`);
|
|
486
492
|
appendStartupTrace("update: spawn OK", { childPid: child.pid, binPath });
|
|
@@ -545,9 +551,10 @@ export async function handleCommand(
|
|
|
545
551
|
appendStartupTrace("restart: spawn begin", { fromPid: process.pid });
|
|
546
552
|
const child = spawn("npx", ["tsx", "src/index.ts"], {
|
|
547
553
|
cwd: PROJECT_ROOT,
|
|
548
|
-
detached: true,
|
|
549
|
-
stdio: "ignore",
|
|
550
|
-
shell: true,
|
|
554
|
+
detached: true,
|
|
555
|
+
stdio: "ignore",
|
|
556
|
+
shell: true,
|
|
557
|
+
env: createInternalRestartEnv(),
|
|
551
558
|
});
|
|
552
559
|
|
|
553
560
|
child.on("error", (err) => {
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { spawn, type ChildProcess } from "node:child_process";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* ChatCCC 自己拉起替代进程时使用的内部标记。
|
|
5
|
+
*
|
|
6
|
+
* 不能用“是否已有配置”判断是否打开控制台:首次配置和日常直接启动都应该
|
|
7
|
+
* 打开,而 `/restart`、`/update` 和 Web UI 重启都不应该打扰用户。环境变量
|
|
8
|
+
* 会自然穿过 cmd/bash/npx 这几层启动器,因此也适用于 Windows 与 Linux。
|
|
9
|
+
*/
|
|
10
|
+
export const INTERNAL_RESTART_ENV_VAR = "CHATCCC_INTERNAL_RESTART";
|
|
11
|
+
|
|
12
|
+
type Environment = Record<string, string | undefined>;
|
|
13
|
+
|
|
14
|
+
export function createInternalRestartEnv(
|
|
15
|
+
inherited: Environment = process.env,
|
|
16
|
+
): NodeJS.ProcessEnv {
|
|
17
|
+
return {
|
|
18
|
+
...inherited,
|
|
19
|
+
[INTERNAL_RESTART_ENV_VAR]: "1",
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** 用户直接启动时打开;ChatCCC 内部重启产生的替代进程不打开。 */
|
|
24
|
+
interface AutoOpenWebUiOptions {
|
|
25
|
+
env?: Environment;
|
|
26
|
+
openOnStart?: boolean;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function shouldAutoOpenWebUi(options: AutoOpenWebUiOptions = {}): boolean {
|
|
30
|
+
const env = options.env ?? process.env;
|
|
31
|
+
return options.openOnStart !== false && env[INTERNAL_RESTART_ENV_VAR] !== "1";
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Web UI 始终使用 localhost,并跟随实际配置端口。 */
|
|
35
|
+
export function buildWebUiUrl(port: number): string {
|
|
36
|
+
return `http://localhost:${port}/`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
interface OpenBrowserDeps {
|
|
40
|
+
platform?: NodeJS.Platform;
|
|
41
|
+
env?: Environment;
|
|
42
|
+
spawnImpl?: typeof spawn;
|
|
43
|
+
onError?: (message: string) => void;
|
|
44
|
+
onInfo?: (message: string) => void;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* 调用操作系统默认浏览器打开 Web UI,与 Chrome CDP 守护功能完全独立。
|
|
49
|
+
* 返回值仅表示打开请求是否成功发起;浏览器是否复用标签页由系统浏览器决定。
|
|
50
|
+
*/
|
|
51
|
+
export function openWebUiInDefaultBrowser(
|
|
52
|
+
port: number,
|
|
53
|
+
deps: OpenBrowserDeps = {},
|
|
54
|
+
): boolean {
|
|
55
|
+
const url = buildWebUiUrl(port);
|
|
56
|
+
const platform = deps.platform ?? process.platform;
|
|
57
|
+
const env = deps.env ?? process.env;
|
|
58
|
+
const spawnImpl = deps.spawnImpl ?? spawn;
|
|
59
|
+
const onError = deps.onError ?? ((message: string) => console.error(message));
|
|
60
|
+
const onInfo = deps.onInfo ?? ((message: string) => console.log(message));
|
|
61
|
+
|
|
62
|
+
// Linux 服务器通常没有图形会话。此时调用 xdg-open 只会制造噪音;
|
|
63
|
+
// 直接给出可复制的 SSH 隧道命令,让用户从自己的电脑访问本地 Web UI。
|
|
64
|
+
if (platform === "linux" && !env.DISPLAY && !env.WAYLAND_DISPLAY) {
|
|
65
|
+
onInfo(
|
|
66
|
+
`[WEB-UI] 未检测到 Linux 图形桌面,跳过自动打开浏览器。` +
|
|
67
|
+
`可在本机执行 ssh -L ${port}:127.0.0.1:${port} <user>@<server>,` +
|
|
68
|
+
`然后访问 ${url}`,
|
|
69
|
+
);
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
try {
|
|
74
|
+
let child: ChildProcess;
|
|
75
|
+
if (platform === "win32") {
|
|
76
|
+
// `start` 会把第一个带引号的参数当窗口标题,空字符串是必要占位符。
|
|
77
|
+
child = spawnImpl("cmd.exe", ["/c", "start", "", url], {
|
|
78
|
+
detached: true,
|
|
79
|
+
stdio: "ignore",
|
|
80
|
+
windowsHide: true,
|
|
81
|
+
});
|
|
82
|
+
} else if (platform === "darwin") {
|
|
83
|
+
child = spawnImpl("open", [url], { detached: true, stdio: "ignore" });
|
|
84
|
+
} else {
|
|
85
|
+
child = spawnImpl("xdg-open", [url], { detached: true, stdio: "ignore" });
|
|
86
|
+
}
|
|
87
|
+
child.on("error", (err) => {
|
|
88
|
+
onError(`[WEB-UI] 自动打开浏览器失败: ${err.message}`);
|
|
89
|
+
});
|
|
90
|
+
child.unref();
|
|
91
|
+
return true;
|
|
92
|
+
} catch (err) {
|
|
93
|
+
onError(`[WEB-UI] 自动打开浏览器失败: ${(err as Error).message}`);
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
}
|
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
|
+
}
|