chatccc 0.2.252 → 0.2.254

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (296) hide show
  1. package/README.md +10 -1
  2. package/bin/cccagent.mjs +12 -3
  3. package/bin/chatccc.mjs +15 -6
  4. package/config.sample.json +2 -1
  5. package/deepccc-agent/README.md +40 -35
  6. package/deepccc-agent/os-prompts/darwin.md +8 -8
  7. package/deepccc-agent/os-prompts/linux.md +8 -8
  8. package/deepccc-agent/os-prompts/win32.md +11 -11
  9. package/deepccc-agent/package.json +65 -65
  10. package/dist/deepccc-agent/src/cli.js +634 -0
  11. package/dist/deepccc-agent/src/config.js +81 -0
  12. package/dist/deepccc-agent/src/context.js +348 -0
  13. package/dist/deepccc-agent/src/file-log.js +34 -0
  14. package/dist/deepccc-agent/src/file-tools.js +1208 -0
  15. package/dist/deepccc-agent/src/index.js +601 -0
  16. package/dist/deepccc-agent/src/permissions.js +170 -0
  17. package/dist/deepccc-agent/src/privacy.js +124 -0
  18. package/dist/deepccc-agent/src/proc-tree-kill.js +60 -0
  19. package/dist/deepccc-agent/src/progress/cards-helpers.js +70 -0
  20. package/dist/deepccc-agent/src/progress/reducer.js +102 -0
  21. package/dist/deepccc-agent/src/progress/terminal-renderer.js +264 -0
  22. package/dist/deepccc-agent/src/progress/view.js +30 -0
  23. package/dist/deepccc-agent/src/raw-stream-log.js +106 -0
  24. package/dist/deepccc-agent/src/session-search.js +276 -0
  25. package/dist/deepccc-agent/src/session-select.js +23 -0
  26. package/dist/deepccc-agent/src/sigint.js +26 -0
  27. package/dist/deepccc-agent/src/skills.js +178 -0
  28. package/dist/deepccc-agent/src/web-tools.js +246 -0
  29. package/dist/src/adapters/adapter-interface.js +19 -0
  30. package/dist/src/adapters/ccc-adapter.js +112 -0
  31. package/dist/src/adapters/claude-adapter.js +497 -0
  32. package/dist/src/adapters/claude-session-meta-store.js +92 -0
  33. package/dist/src/adapters/codex-adapter.js +279 -0
  34. package/dist/src/adapters/codex-session-meta-store.js +94 -0
  35. package/dist/src/adapters/cursor-adapter.js +491 -0
  36. package/dist/src/adapters/cursor-session-meta-store.js +116 -0
  37. package/dist/src/adapters/jsonl-stream.js +104 -0
  38. package/{src/adapters/proc-tree-kill.ts → dist/src/adapters/proc-tree-kill.js} +94 -97
  39. package/dist/src/adapters/raw-stream-log.js +106 -0
  40. package/dist/src/adapters/resource-monitor.js +113 -0
  41. package/dist/src/agent-activity.js +133 -0
  42. package/dist/src/agent-delegate-task-rpc.js +129 -0
  43. package/dist/src/agent-delegate-task.js +48 -0
  44. package/dist/src/agent-file-rpc.js +152 -0
  45. package/dist/src/agent-image-rpc.js +148 -0
  46. package/dist/src/agent-platform-routing.js +13 -0
  47. package/dist/src/agent-reload-config-rpc.js +23 -0
  48. package/dist/src/agent-rpc-body.js +87 -0
  49. package/dist/src/agent-stop-stuck.js +110 -0
  50. package/dist/src/agent-team/application/board-service.js +142 -0
  51. package/dist/src/agent-team/application/main-agent-service.js +151 -0
  52. package/dist/src/agent-team/domain/board.js +69 -0
  53. package/dist/src/agent-team/http/board-routes.js +213 -0
  54. package/dist/src/agent-team/infrastructure/filesystem-browser.js +112 -0
  55. package/dist/src/agent-team/infrastructure/main-agent-session-runtime.js +30 -0
  56. package/dist/src/agent-team/main-agent-bootstrap.js +15 -0
  57. package/dist/src/agent-team/repositories/board-repository.js +10 -0
  58. package/dist/src/agent-team/repositories/feishu-p2p-contact-store.js +64 -0
  59. package/dist/src/agent-team/repositories/json-board-repository.js +209 -0
  60. package/dist/src/agent-team/repositories/main-agent-binding-repository.js +84 -0
  61. package/dist/src/agent-team/sync/board-sync-adapter.js +1 -0
  62. package/dist/src/agent-team/web/agent-team-page.js +219 -0
  63. package/dist/src/agent-tool.js +10 -0
  64. package/dist/src/card-action-routing.js +7 -0
  65. package/dist/src/card-plain-text.js +101 -0
  66. package/dist/src/cardkit.js +158 -0
  67. package/dist/src/cards.js +573 -0
  68. package/dist/src/chatgpt-subscription-rpc.js +18 -0
  69. package/dist/src/chatgpt-subscription.js +199 -0
  70. package/dist/src/chrome-devtools-guard.js +238 -0
  71. package/dist/src/claude-sdk-installer.js +249 -0
  72. package/dist/src/codex-reset-actions.js +143 -0
  73. package/dist/src/config-utils.js +149 -0
  74. package/dist/src/config.js +808 -0
  75. package/dist/src/cursor-usage.js +77 -0
  76. package/dist/src/exit-banner.js +28 -0
  77. package/dist/src/feishu-api.js +1404 -0
  78. package/dist/src/feishu-message-ingress.js +137 -0
  79. package/dist/src/feishu-platform.js +97 -0
  80. package/dist/src/format-message.js +252 -0
  81. package/dist/src/git-command.js +155 -0
  82. package/dist/src/im-skills.js +121 -0
  83. package/dist/src/index.js +835 -0
  84. package/dist/src/litellm-proxy.js +300 -0
  85. package/dist/src/orchestrator.js +2096 -0
  86. package/dist/src/package-root.js +26 -0
  87. package/dist/src/platform-adapter.js +7 -0
  88. package/dist/src/platform-startup.js +6 -0
  89. package/dist/src/privacy.js +100 -0
  90. package/dist/src/progress/reducer.js +102 -0
  91. package/dist/src/progress/terminal-renderer.js +264 -0
  92. package/dist/src/progress/view.js +30 -0
  93. package/dist/src/response-stall.js +14 -0
  94. package/dist/src/runtime-entry.js +13 -0
  95. package/dist/src/runtime-reload.js +19 -0
  96. package/dist/src/session-chat-binding.js +183 -0
  97. package/dist/src/session-name.js +7 -0
  98. package/dist/src/session.js +2147 -0
  99. package/dist/src/shared-prefix.js +16 -0
  100. package/dist/src/shared.js +493 -0
  101. package/dist/src/sim-agent.js +105 -0
  102. package/dist/src/sim-platform.js +142 -0
  103. package/dist/src/sim-store.js +231 -0
  104. package/dist/src/simplify.js +99 -0
  105. package/dist/src/startup-lifecycle.js +209 -0
  106. package/dist/src/stream-state.js +141 -0
  107. package/dist/src/terminal-error.js +100 -0
  108. package/dist/src/trace.js +50 -0
  109. package/dist/src/turn-cards.js +92 -0
  110. package/dist/src/update-command-guard.js +114 -0
  111. package/{src/web-ui.ts → dist/src/web-ui.js} +753 -839
  112. package/dist/src/wechat-platform.js +545 -0
  113. package/package.json +76 -74
  114. package/deepccc-agent/LICENSE +0 -201
  115. package/deepccc-agent/bin/deepccc.mjs +0 -26
  116. package/deepccc-agent/docs/cache-hit-rate-1.jpg +0 -0
  117. package/deepccc-agent/docs/cache-hit-rate-2.jpg +0 -0
  118. package/deepccc-agent/package-lock.json +0 -2027
  119. package/deepccc-agent/src/__tests__/chat-session.test.ts +0 -877
  120. package/deepccc-agent/src/__tests__/cli-json.test.ts +0 -49
  121. package/deepccc-agent/src/__tests__/config.test.ts +0 -34
  122. package/deepccc-agent/src/__tests__/context.test.ts +0 -341
  123. package/deepccc-agent/src/__tests__/file-tools.test.ts +0 -240
  124. package/deepccc-agent/src/__tests__/permissions.test.ts +0 -199
  125. package/deepccc-agent/src/__tests__/privacy.test.ts +0 -318
  126. package/deepccc-agent/src/__tests__/progress-reducer.test.ts +0 -121
  127. package/deepccc-agent/src/__tests__/session-search.test.ts +0 -262
  128. package/deepccc-agent/src/__tests__/session-select.test.ts +0 -116
  129. package/deepccc-agent/src/__tests__/sigint.test.ts +0 -56
  130. package/deepccc-agent/src/__tests__/skills.test.ts +0 -284
  131. package/deepccc-agent/src/__tests__/terminal-renderer.test.ts +0 -247
  132. package/deepccc-agent/src/__tests__/web-tools.test.ts +0 -220
  133. package/deepccc-agent/src/cli.ts +0 -682
  134. package/deepccc-agent/src/config.ts +0 -101
  135. package/deepccc-agent/src/context.ts +0 -465
  136. package/deepccc-agent/src/file-log.ts +0 -38
  137. package/deepccc-agent/src/file-tools.ts +0 -1493
  138. package/deepccc-agent/src/index.ts +0 -710
  139. package/deepccc-agent/src/permissions.ts +0 -226
  140. package/deepccc-agent/src/privacy.ts +0 -141
  141. package/deepccc-agent/src/proc-tree-kill.ts +0 -61
  142. package/deepccc-agent/src/progress/cards-helpers.ts +0 -76
  143. package/deepccc-agent/src/progress/reducer.ts +0 -113
  144. package/deepccc-agent/src/progress/terminal-renderer.ts +0 -294
  145. package/deepccc-agent/src/progress/view.ts +0 -77
  146. package/deepccc-agent/src/raw-stream-log.ts +0 -124
  147. package/deepccc-agent/src/session-search.ts +0 -370
  148. package/deepccc-agent/src/session-select.ts +0 -48
  149. package/deepccc-agent/src/sigint.ts +0 -50
  150. package/deepccc-agent/src/skills.ts +0 -205
  151. package/deepccc-agent/src/web-tools.ts +0 -313
  152. package/deepccc-agent/tsconfig.build.json +0 -13
  153. package/deepccc-agent/tsconfig.json +0 -13
  154. package/deepccc-agent/vitest.config.ts +0 -7
  155. package/src/__tests__/adapter-interface.test.ts +0 -152
  156. package/src/__tests__/agent-activity.test.ts +0 -86
  157. package/src/__tests__/agent-delegate-task-rpc.test.ts +0 -165
  158. package/src/__tests__/agent-image-rpc.test.ts +0 -34
  159. package/src/__tests__/agent-platform-routing.test.ts +0 -26
  160. package/src/__tests__/agent-reload-config-rpc.test.ts +0 -99
  161. package/src/__tests__/agent-rpc-body.test.ts +0 -42
  162. package/src/__tests__/builtin-chat-session.test.ts +0 -532
  163. package/src/__tests__/builtin-cli-json.test.ts +0 -39
  164. package/src/__tests__/builtin-config.test.ts +0 -26
  165. package/src/__tests__/builtin-context.test.ts +0 -319
  166. package/src/__tests__/builtin-file-tools.test.ts +0 -240
  167. package/src/__tests__/builtin-permissions.test.ts +0 -219
  168. package/src/__tests__/builtin-session-search.test.ts +0 -262
  169. package/src/__tests__/builtin-session-select.test.ts +0 -116
  170. package/src/__tests__/builtin-sigint.test.ts +0 -56
  171. package/src/__tests__/builtin-skills.test.ts +0 -284
  172. package/src/__tests__/builtin-web-tools.test.ts +0 -220
  173. package/src/__tests__/card-action-routing.test.ts +0 -18
  174. package/src/__tests__/card-plain-text.test.ts +0 -45
  175. package/src/__tests__/cardkit.test.ts +0 -60
  176. package/src/__tests__/cards.test.ts +0 -607
  177. package/src/__tests__/ccc-adapter.test.ts +0 -194
  178. package/src/__tests__/chatgpt-subscription-rpc.test.ts +0 -89
  179. package/src/__tests__/chatgpt-subscription.test.ts +0 -135
  180. package/src/__tests__/chrome-devtools-guard.test.ts +0 -165
  181. package/src/__tests__/claude-adapter.test.ts +0 -614
  182. package/src/__tests__/claude-raw-stream-log.test.ts +0 -96
  183. package/src/__tests__/claude-sdk-installer.test.ts +0 -285
  184. package/src/__tests__/codex-adapter.test.ts +0 -331
  185. package/src/__tests__/codex-raw-stream-log.test.ts +0 -170
  186. package/src/__tests__/codex-reset-actions.test.ts +0 -146
  187. package/src/__tests__/config-reload.test.ts +0 -284
  188. package/src/__tests__/config-sample.test.ts +0 -97
  189. package/src/__tests__/config-utils.test.ts +0 -40
  190. package/src/__tests__/config.test.ts +0 -395
  191. package/src/__tests__/crash-logging.test.ts +0 -360
  192. package/src/__tests__/cursor-adapter.test.ts +0 -890
  193. package/src/__tests__/cursor-session-meta-store.test.ts +0 -212
  194. package/src/__tests__/feishu-api.test.ts +0 -60
  195. package/src/__tests__/feishu-avatar.test.ts +0 -504
  196. package/src/__tests__/feishu-message-ingress.test.ts +0 -138
  197. package/src/__tests__/feishu-platform.test.ts +0 -75
  198. package/src/__tests__/fixtures/codex_simple_text.jsonl +0 -4
  199. package/src/__tests__/fixtures/codex_with_tool.jsonl +0 -6
  200. package/src/__tests__/fixtures/cursor_partial_only.jsonl +0 -5
  201. package/src/__tests__/fixtures/cursor_partial_with_final.jsonl +0 -13
  202. package/src/__tests__/fixtures/cursor_with_tool_call.jsonl +0 -12
  203. package/src/__tests__/format-message.test.ts +0 -316
  204. package/src/__tests__/git-command.test.ts +0 -288
  205. package/src/__tests__/im-skills.test.ts +0 -125
  206. package/src/__tests__/jsonl-stream.test.ts +0 -79
  207. package/src/__tests__/orchestrator.test.ts +0 -1268
  208. package/src/__tests__/package-files.test.ts +0 -24
  209. package/src/__tests__/platform-startup.test.ts +0 -19
  210. package/src/__tests__/privacy.test.ts +0 -198
  211. package/src/__tests__/proc-tree-kill.test.ts +0 -108
  212. package/src/__tests__/progress-reducer.test.ts +0 -121
  213. package/src/__tests__/raw-stream-log.test.ts +0 -106
  214. package/src/__tests__/response-stall.test.ts +0 -49
  215. package/src/__tests__/restart.test.ts +0 -232
  216. package/src/__tests__/session-ccc-config.test.ts +0 -66
  217. package/src/__tests__/session.test.ts +0 -3004
  218. package/src/__tests__/shared-prefix.test.ts +0 -36
  219. package/src/__tests__/sim-agent.test.ts +0 -174
  220. package/src/__tests__/sim-platform.test.ts +0 -93
  221. package/src/__tests__/sim-store.test.ts +0 -214
  222. package/src/__tests__/simplify.test.ts +0 -283
  223. package/src/__tests__/startup-lifecycle.test.ts +0 -231
  224. package/src/__tests__/stop-session.test.ts +0 -162
  225. package/src/__tests__/stream-state.test.ts +0 -164
  226. package/src/__tests__/terminal-error.test.ts +0 -54
  227. package/src/__tests__/terminal-renderer.test.ts +0 -247
  228. package/src/__tests__/update-command-guard.test.ts +0 -144
  229. package/src/__tests__/web-ui.test.ts +0 -438
  230. package/src/__tests__/wechat-platform.test.ts +0 -111
  231. package/src/adapters/adapter-interface.ts +0 -217
  232. package/src/adapters/ccc-adapter.ts +0 -150
  233. package/src/adapters/claude-adapter.ts +0 -673
  234. package/src/adapters/claude-session-meta-store.ts +0 -120
  235. package/src/adapters/codex-adapter.ts +0 -426
  236. package/src/adapters/codex-session-meta-store.ts +0 -131
  237. package/src/adapters/cursor-adapter.ts +0 -681
  238. package/src/adapters/cursor-session-meta-store.ts +0 -154
  239. package/src/adapters/jsonl-stream.ts +0 -157
  240. package/src/adapters/raw-stream-log.ts +0 -124
  241. package/src/adapters/resource-monitor.ts +0 -141
  242. package/src/agent-activity.ts +0 -175
  243. package/src/agent-delegate-task-rpc.ts +0 -153
  244. package/src/agent-delegate-task.ts +0 -91
  245. package/src/agent-file-rpc.ts +0 -172
  246. package/src/agent-image-rpc.ts +0 -168
  247. package/src/agent-platform-routing.ts +0 -28
  248. package/src/agent-reload-config-rpc.ts +0 -34
  249. package/src/agent-rpc-body.ts +0 -92
  250. package/src/agent-stop-stuck.ts +0 -129
  251. package/src/card-action-routing.ts +0 -14
  252. package/src/card-plain-text.ts +0 -108
  253. package/src/cardkit.ts +0 -179
  254. package/src/cards.ts +0 -684
  255. package/src/chatgpt-subscription-rpc.ts +0 -27
  256. package/src/chatgpt-subscription.ts +0 -299
  257. package/src/chrome-devtools-guard.ts +0 -318
  258. package/src/claude-sdk-installer.ts +0 -324
  259. package/src/codex-reset-actions.ts +0 -184
  260. package/src/config-utils.ts +0 -211
  261. package/src/config.ts +0 -1063
  262. package/src/cursor-usage.ts +0 -128
  263. package/src/exit-banner.ts +0 -33
  264. package/src/feishu-api.ts +0 -1616
  265. package/src/feishu-message-ingress.ts +0 -195
  266. package/src/feishu-platform.ts +0 -159
  267. package/src/format-message.ts +0 -293
  268. package/src/git-command.ts +0 -202
  269. package/src/im-skills.ts +0 -149
  270. package/src/index.ts +0 -1089
  271. package/src/litellm-proxy.ts +0 -374
  272. package/src/orchestrator.ts +0 -2543
  273. package/src/platform-adapter.ts +0 -70
  274. package/src/platform-startup.ts +0 -16
  275. package/src/privacy.ts +0 -118
  276. package/src/progress/reducer.ts +0 -113
  277. package/src/progress/terminal-renderer.ts +0 -294
  278. package/src/progress/view.ts +0 -77
  279. package/src/response-stall.ts +0 -28
  280. package/src/runtime-reload.ts +0 -34
  281. package/src/session-chat-binding.ts +0 -292
  282. package/src/session-name.ts +0 -8
  283. package/src/session.ts +0 -2659
  284. package/src/shared-prefix.ts +0 -29
  285. package/src/shared.ts +0 -552
  286. package/src/sim-agent.ts +0 -167
  287. package/src/sim-platform.ts +0 -177
  288. package/src/sim-store.ts +0 -317
  289. package/src/simplify.ts +0 -120
  290. package/src/startup-lifecycle.ts +0 -250
  291. package/src/stream-state.ts +0 -177
  292. package/src/terminal-error.ts +0 -129
  293. package/src/trace.ts +0 -51
  294. package/src/turn-cards.ts +0 -118
  295. package/src/update-command-guard.ts +0 -165
  296. package/src/wechat-platform.ts +0 -680
@@ -1,700 +1,637 @@
1
- // =============================================================================
2
- // web-ui.ts — Setup Wizard & Management Dashboard HTTP Server
3
- // =============================================================================
4
- // Serves on the same port as the WebSocket relay (18080 default).
5
- // - Setup mode: no config.json → show setup wizard, skip Feishu connection
6
- // - Dashboard mode: config.json exists → serve management page alongside WS relay
7
- // =============================================================================
8
-
9
- import { createServer, IncomingMessage, ServerResponse } from "node:http";
10
- import { existsSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
11
- import { readFile, writeFile, stat } from "node:fs/promises";
12
- import { homedir } from "node:os";
13
- import { join, dirname } from "node:path";
14
- import { fileURLToPath } from "node:url";
15
- import { spawn, execSync } from "node:child_process";
16
- import {
17
- buildWebUiUrl,
18
- createInternalRestartEnv,
19
- openWebUiInDefaultBrowser,
20
- } from "./startup-lifecycle.ts";
21
- import {
22
- getClaudeSdkInstalledVersion,
23
- getLastInstallProgress,
24
- installClaudeSdk,
25
- isClaudeSdkInstalled,
26
- isInstallRunning,
27
- } from "./claude-sdk-installer.ts";
28
-
29
- const __dirname = dirname(fileURLToPath(import.meta.url));
30
- const PROJECT_ROOT = join(__dirname, "..");
31
- const USER_DATA_DIR = join(homedir(), ".chatccc");
32
- const CONFIG_FILE = join(USER_DATA_DIR, "config.json");
33
- const CONFIG_SAMPLE_FILE = join(PROJECT_ROOT, "config.sample.json");
34
- const PID_FILE = join(USER_DATA_DIR, "state", "runtime.pid");
35
- const ILINK_AUTH_PATH = join(USER_DATA_DIR, "state", "ilink-auth.json");
36
-
37
- // ---------------------------------------------------------------------------
38
- // Helpers — config.json parsing & generation
39
- // ---------------------------------------------------------------------------
40
-
41
- interface AppConfig {
42
- feishu?: { appId?: string; appSecret?: string };
43
- platforms?: {
44
- feishu?: { enabled?: boolean; platformType?: string };
45
- ilink?: { enabled?: boolean; reuseTokenOnStart?: boolean };
46
- };
47
- webUi?: { openOnStart?: boolean };
48
- chromeDevtools?: { enabled?: boolean; port?: number; chromePath?: string };
49
- port?: number;
50
- gitTimeoutSeconds?: number;
51
- claude?: { enabled?: boolean; defaultAgent?: boolean; model?: string; subagentModel?: string; effort?: string; apiKey?: string; baseUrl?: string; maxTurn?: number };
52
- // `command` 是已废弃的旧字段名,保留只读以兼容升级前的 config.json
53
- cursor?: {
54
- enabled?: boolean;
55
- defaultAgent?: boolean;
56
- path?: string;
57
- command?: string;
58
- model?: string;
59
- alternativeModel?: string;
60
- avatarBatteryMode?: string;
61
- onDemandMonthlyBudget?: number;
62
- };
63
- codex?: { enabled?: boolean; defaultAgent?: boolean; path?: string; command?: string; model?: string; alternativeModel?: string; effort?: string; fastMode?: boolean };
64
- ccc?: {
65
- enabled?: boolean;
66
- defaultAgent?: boolean;
67
- DEEPSEEK_API_KEY?: string;
68
- DEEPSEEK_BASE_URL?: string;
69
- model?: string;
70
- alternativeModel?: string;
71
- effort?: string;
72
- /** 留空("")= 不 override,跟随 DeepCCC 内核配置(~/.deepccc/config.json 或 DEEPCCC_PROVIDER) */
73
- provider?: "" | "openai" | "anthropic";
74
- };
75
- }
76
-
77
- // ---------------------------------------------------------------------------
78
- // /api/start 路径选择(纯函数,便于单测护栏)
79
- // ---------------------------------------------------------------------------
80
- //
81
- // 三种语义:
82
- // - "inplace" : setup 模式,原地启动飞书 service(同进程,不动 PID 文件)
83
- // - "spawn" : dashboard 模式 + service 未运行,spawn 子进程(旧 service 退出后场景)
84
- // - "reload" : dashboard 模式 + service 已经在跑(通常就是当前进程自己)→
85
- // 仅调用 reloadConfigFromDisk() 刷新 export let 常量,**不真正重启**。
86
- // 用户的设计意图:"让新 config 生效就行,不用走 spawn+exit 的真重启"
87
- //
88
- // 关键契约:只要 setup 模式注册了 onActivate 回调,无条件走 inplace —— 因为
89
- // setup 进程**总是**占着 PID 文件、isServiceRunning() 永远为 true,再判 PID
90
- // 会陷入"自己挡自己"的死循环(用户点保存并启动只会得到 already running)。
91
- //
92
- // 历史:曾经在 service 已运行时返回 "reject-already-running",但 dashboard 的
93
- // UI 本身就在跑着的进程内,service 必然在跑——重新跑向导改完配置点"保存并启动"
94
- // 100% 会撞到这条 reject 路径,造成"自己挡自己"的死循环。改 reload 后即可
95
- // 让常量热更新(API 来源、模型、effort、CLI 路径等下次创建会话即生效)。
96
- // 已建立的飞书 WSClient 仍持有旧 APP_ID/APP_SECRET 句柄——如改了飞书凭证,
97
- // 仍需重启 chatccc 进程才能让 WS 长连接换 token;但这是用户少见路径,文档说明即可。
98
-
99
- export type StartPath = "inplace" | "spawn" | "reload";
100
-
101
- export function chooseStartPath(input: {
102
- hasInplaceActivateHook: boolean;
103
- isServiceRunning: boolean;
104
- }): StartPath {
105
- if (input.hasInplaceActivateHook) return "inplace";
106
- if (input.isServiceRunning) return "reload";
107
- return "spawn";
108
- }
109
-
110
- /** 读取 cursor / codex 的 CLI 路径,优先新字段 path,回退旧字段 command */
111
- function readToolPath(tool?: { path?: string; command?: string }): string {
112
- if (!tool) return "";
113
- if (tool.path && tool.path.trim()) return tool.path;
114
- if (tool.command && tool.command.trim()) return tool.command;
115
- return "";
116
- }
117
-
118
- function loadConfig(): AppConfig {
119
- if (!existsSync(CONFIG_FILE)) return {};
120
- try {
121
- let raw = readFileSync(CONFIG_FILE, "utf8");
122
- // 移除可能意外写入的 UTF-8 BOM,避免 JSON.parse 失败导致返回空对象、
123
- // UI 显示所有配置为空。
124
- if (raw.charCodeAt(0) === 0xfeff) raw = raw.slice(1);
125
- return JSON.parse(raw);
126
- } catch {
127
- return {};
128
- }
129
- }
130
-
131
- function saveConfig(cfg: AppConfig): void {
132
- writeFileSync(CONFIG_FILE, JSON.stringify(cfg, null, 2), "utf8");
133
- }
134
-
135
- type ConfigApplyMode = "saved" | "reload" | "restart-required";
136
-
137
- interface ConfigApplyResult {
138
- mode: ConfigApplyMode;
139
- restartRequired: boolean;
140
- restartReasons: string[];
141
- }
142
-
143
- function stringValue(value: unknown): string {
144
- return typeof value === "string" ? value.trim() : "";
145
- }
146
-
147
- function portValue(cfg: AppConfig): number {
148
- return Number.isInteger(cfg.port) && cfg.port! >= 1 && cfg.port! <= 65535 ? cfg.port! : 18080;
149
- }
150
-
151
- function platformEnabled(cfg: AppConfig, platform: "feishu" | "ilink"): boolean {
152
- return cfg.platforms?.[platform]?.enabled !== false;
153
- }
154
-
155
- function feishuPlatformType(cfg: AppConfig): string {
156
- return cfg.platforms?.feishu?.platformType === "lark" ? "lark" : "feishu";
157
- }
158
-
159
- function ilinkReuseTokenOnStart(cfg: AppConfig): boolean {
160
- return cfg.platforms?.ilink?.reuseTokenOnStart !== false;
161
- }
162
-
163
- export function getRestartRequiredReasons(before: AppConfig, after: AppConfig): string[] {
164
- const reasons: string[] = [];
165
-
166
- if (portValue(before) !== portValue(after)) reasons.push("port");
167
- if (stringValue(before.feishu?.appId) !== stringValue(after.feishu?.appId)) reasons.push("feishu.appId");
168
- if (stringValue(before.feishu?.appSecret) !== stringValue(after.feishu?.appSecret)) reasons.push("feishu.appSecret");
169
- if (feishuPlatformType(before) !== feishuPlatformType(after)) reasons.push("platforms.feishu.platformType");
170
- if (platformEnabled(before, "feishu") !== platformEnabled(after, "feishu")) reasons.push("platforms.feishu.enabled");
171
- if (platformEnabled(before, "ilink") !== platformEnabled(after, "ilink")) reasons.push("platforms.ilink.enabled");
172
- if (ilinkReuseTokenOnStart(before) !== ilinkReuseTokenOnStart(after)) {
173
- reasons.push("platforms.ilink.reuseTokenOnStart");
174
- }
175
-
176
- return reasons;
177
- }
178
-
179
- async function applySavedConfigIfPossible(before: AppConfig, after: AppConfig): Promise<ConfigApplyResult> {
180
- const setupMode = Boolean(setupActivateHook && setupHttpServer);
181
- if (setupMode) {
182
- return { mode: "saved", restartRequired: false, restartReasons: [] };
183
- }
184
-
185
- const running = isServiceRunning();
186
- if (!running) {
187
- return { mode: "saved", restartRequired: false, restartReasons: [] };
188
- }
189
-
190
- const restartReasons = getRestartRequiredReasons(before, after);
191
- if (restartReasons.length > 0) {
192
- return { mode: "restart-required", restartRequired: true, restartReasons };
193
- }
194
- if (!reloadConfigHook) {
195
- return { mode: "saved", restartRequired: false, restartReasons: [] };
196
- }
197
-
198
- await reloadConfigHook();
199
- return { mode: "reload", restartRequired: false, restartReasons: [] };
200
- }
201
-
202
- function maskSecret(value: string | undefined): string {
203
- if (!value) return "(未设置)";
204
- if (value.length <= 8) return "***";
205
- return value.slice(0, 4) + "***" + value.slice(-4);
206
- }
207
-
208
- // ---------------------------------------------------------------------------
209
- // Helpers — process management
210
- // ---------------------------------------------------------------------------
211
-
212
- function isServiceRunning(): boolean {
213
- if (!existsSync(PID_FILE)) return false;
214
- try {
215
- const raw = readFileSync(PID_FILE, "utf8").trim();
216
- const pid = parseInt(raw, 10);
217
- if (isNaN(pid)) return false;
218
- process.kill(pid, 0);
219
- return true;
220
- } catch {
221
- return false;
222
- }
223
- }
224
-
225
- function getServicePid(): number | null {
226
- if (!existsSync(PID_FILE)) return null;
227
- try {
228
- const pid = parseInt(readFileSync(PID_FILE, "utf8").trim(), 10);
229
- return isNaN(pid) ? null : pid;
230
- } catch {
231
- return null;
232
- }
233
- }
234
-
235
- function getServiceUptime(): number | null {
236
- try {
237
- const s = statSync(PID_FILE);
238
- return Math.floor((Date.now() - s.mtimeMs) / 1000);
239
- } catch {
240
- return null;
241
- }
242
- }
243
-
244
- import { statSync } from "node:fs";
245
-
246
- function spawnService(): { ok: boolean; pid?: number; error?: string } {
247
- const indexPath = join(PROJECT_ROOT, "src", "index.ts");
248
- if (!existsSync(indexPath)) {
249
- return { ok: false, error: `Entry not found: ${indexPath}` };
250
- }
251
- try {
252
- const child = spawn("npx", ["tsx", "src/index.ts"], {
253
- cwd: PROJECT_ROOT,
254
- detached: true,
255
- stdio: "ignore",
256
- shell: true,
257
- env: createInternalRestartEnv(),
258
- });
259
- child.unref();
260
- return { ok: true, pid: child.pid ?? undefined };
261
- } catch (err) {
262
- return { ok: false, error: (err as Error).message };
263
- }
264
- }
265
-
266
- /**
267
- * 安排一次真正的进程重启:先回复 HTTP 请求,再 spawn 一个延迟启动的子进程,
268
- * 然后退出当前进程。延迟是为了让当前进程完全退出并释放端口。
269
- */
270
- function scheduleRestart(): void {
271
- const indexPath = join(PROJECT_ROOT, "src", "index.ts");
272
- if (!existsSync(indexPath)) return;
273
- if (process.platform === "win32") {
274
- // Windows: ping 作为 sleep(ping 自己 3 次 ≈ 2 秒延迟)
275
- spawn("cmd.exe", ["/c", "ping -n 3 127.0.0.1 > nul && npx tsx src/index.ts"], {
276
- cwd: PROJECT_ROOT,
277
- detached: true,
278
- stdio: "ignore",
279
- windowsHide: true,
280
- env: createInternalRestartEnv(),
281
- }).unref();
282
- } else {
283
- spawn("bash", ["-c", "sleep 2 && npx tsx src/index.ts"], {
284
- cwd: PROJECT_ROOT,
285
- detached: true,
286
- stdio: "ignore",
287
- env: createInternalRestartEnv(),
288
- }).unref();
289
- }
290
- }
291
-
292
- function stopService(): { ok: boolean; error?: string } {
293
- const pid = getServicePid();
294
- if (!pid) return { ok: false, error: "No PID file found" };
295
- try {
296
- if (process.platform === "win32") {
297
- execSync(`taskkill /PID ${pid} /F /T`, { stdio: "pipe", windowsHide: true });
298
- } else {
299
- process.kill(pid, "SIGTERM");
300
- }
301
- return { ok: true };
302
- } catch (err) {
303
- return { ok: false, error: (err as Error).message };
304
- }
305
- }
306
-
307
- function validateCli(tool: string): { ok: boolean; path?: string; error?: string } {
308
- const cfg = loadConfig();
309
- let cmd: string;
310
- if (tool === "cursor") {
311
- cmd = readToolPath(cfg.cursor) || detectCursorAgentPath();
312
- } else {
313
- cmd = readToolPath(cfg.codex) || "codex";
314
- }
315
- try {
316
- const out = execSync(`"${cmd}" --version`, { encoding: "utf8", timeout: 10000, windowsHide: true }).trim();
317
- return { ok: true, path: cmd, error: out.slice(0, 200) };
318
- } catch (err) {
319
- return { ok: false, path: cmd, error: (err as Error).message };
320
- }
321
- }
322
-
323
- function detectCursorAgentPath(): string {
324
- const localAppData = process.env.LOCALAPPDATA;
325
- if (localAppData) {
326
- const defaultPath = join(localAppData, "cursor-agent", "agent.cmd");
327
- if (existsSync(defaultPath)) return defaultPath;
328
- }
329
- return "agent";
330
- }
331
-
332
- // ---------------------------------------------------------------------------
333
- // Helpers — HTTP
334
- // ---------------------------------------------------------------------------
335
-
336
- function jsonReply(res: ServerResponse, code: number, data: unknown): void {
337
- res.writeHead(code, { "Content-Type": "application/json; charset=utf-8" });
338
- res.end(JSON.stringify(data));
339
- }
340
-
341
- function readRequestBody(req: IncomingMessage): Promise<string> {
342
- return new Promise((resolve) => {
343
- let body = "";
344
- req.on("data", (chunk: Buffer) => { body += chunk.toString(); });
345
- req.on("end", () => resolve(body));
346
- });
347
- }
348
-
349
- // ---------------------------------------------------------------------------
350
- // API route handlers
351
- // ---------------------------------------------------------------------------
352
-
353
- async function handleApiCheck(_req: IncomingMessage, res: ServerResponse): Promise<void> {
354
- const hasConfig = existsSync(CONFIG_FILE);
355
- let hasCreds = false;
356
- if (hasConfig) {
357
- const c = loadConfig();
358
- const feishuEnabled = c.platforms?.feishu?.enabled !== false; // 默认 true(向后兼容)
359
- const feishuOk = feishuEnabled && Boolean(c.feishu?.appId?.trim() && c.feishu?.appSecret?.trim());
360
- hasCreds = feishuOk || !feishuEnabled;
361
- }
362
- jsonReply(res, 200, { hasConfig, hasCreds, configPath: CONFIG_FILE });
363
- }
364
-
365
- async function handleGetConfig(_req: IncomingMessage, res: ServerResponse): Promise<void> {
366
- const running = isServiceRunning();
367
- const pid = getServicePid();
368
- const vars = loadConfig();
369
- let ilinkAuthExists = false;
370
- if (existsSync(ILINK_AUTH_PATH)) {
371
- try {
372
- const auth = JSON.parse(readFileSync(ILINK_AUTH_PATH, "utf8"));
373
- ilinkAuthExists = Boolean(auth.token);
374
- } catch { /* ignore parse errors */ }
375
- }
376
- jsonReply(res, 200, { vars, running, pid, ilinkAuthExists });
377
- }
378
-
379
- async function handlePostConfig(req: IncomingMessage, res: ServerResponse): Promise<void> {
380
- const body = await readRequestBody(req);
381
- let updates: Record<string, unknown>;
382
- try {
383
- const parsed = JSON.parse(body);
384
- updates = parsed.vars ?? {};
385
- } catch {
386
- jsonReply(res, 400, { ok: false, error: "Invalid JSON" });
387
- return;
388
- }
389
- const existing = loadConfig();
390
- // AppConfig 是闭合接口(无 index signature),但运行时本质就是 JSON 对象,
391
- // 这里通过 unknown 桥接到 Record 让 deepMerge 能复用;写回前断言回 AppConfig。
392
- const merged = deepMerge(
393
- existing as unknown as Record<string, unknown>,
394
- unflattenConfig(updates),
395
- ) as AppConfig;
396
- try {
397
- saveConfig(merged);
398
- const applyResult = await applySavedConfigIfPossible(existing, merged);
399
- jsonReply(res, 200, { ok: true, ...applyResult });
400
- } catch (err) {
401
- jsonReply(res, 500, { ok: false, saved: true, error: (err as Error).message });
402
- }
403
- }
404
-
405
- function deepMerge(target: Record<string, unknown>, source: Record<string, unknown>): Record<string, unknown> {
406
- const result = { ...target };
407
- for (const [key, val] of Object.entries(source)) {
408
- if (val !== null && typeof val === "object" && !Array.isArray(val) && typeof result[key] === "object" && result[key] !== null && !Array.isArray(result[key])) {
409
- result[key] = deepMerge(result[key] as Record<string, unknown>, val as Record<string, unknown>);
410
- } else {
411
- result[key] = val;
412
- }
413
- }
414
- return result;
415
- }
416
-
417
- // Convert flat key-value pairs to nested config structure
418
- export function unflattenConfig(flat: Record<string, unknown>): Record<string, unknown> {
419
- const result: Record<string, unknown> = {};
420
- for (const [key, val] of Object.entries(flat)) {
421
- if (key === "CHATCCC_APP_ID") {
422
- result.feishu = result.feishu || {};
423
- (result.feishu as Record<string, unknown>).appId = val;
424
- } else if (key === "CHATCCC_APP_SECRET") {
425
- result.feishu = result.feishu || {};
426
- (result.feishu as Record<string, unknown>).appSecret = val;
427
- } else if (key === "CHATCCC_FEISHU_ENABLED") {
428
- result.platforms = result.platforms || {};
429
- (result.platforms as Record<string, unknown>).feishu = (result.platforms as Record<string, unknown>).feishu || {};
430
- ((result.platforms as Record<string, unknown>).feishu as Record<string, unknown>).enabled = val === true || val === "true";
431
- } else if (key === "CHATCCC_FEISHU_PLATFORM_TYPE") {
432
- result.platforms = result.platforms || {};
433
- (result.platforms as Record<string, unknown>).feishu = (result.platforms as Record<string, unknown>).feishu || {};
434
- ((result.platforms as Record<string, unknown>).feishu as Record<string, unknown>).platformType = val;
435
- } else if (key === "CHATCCC_ILINK_ENABLED") {
436
- result.platforms = result.platforms || {};
437
- (result.platforms as Record<string, unknown>).ilink = (result.platforms as Record<string, unknown>).ilink || {};
438
- ((result.platforms as Record<string, unknown>).ilink as Record<string, unknown>).enabled = val === true || val === "true";
439
- } else if (key === "CHATCCC_WEB_UI_OPEN_ON_START") {
440
- result.webUi = result.webUi || {};
441
- (result.webUi as Record<string, unknown>).openOnStart = val === true || val === "true";
442
- } else if (key === "CHATCCC_CHROME_DEVTOOLS_ENABLED") {
443
- result.chromeDevtools = result.chromeDevtools || {};
444
- (result.chromeDevtools as Record<string, unknown>).enabled = val === true || val === "true";
445
- } else if (key === "CHATCCC_CHROME_DEVTOOLS_PORT") {
446
- result.chromeDevtools = result.chromeDevtools || {};
447
- (result.chromeDevtools as Record<string, unknown>).port = parseInt(val as string, 10) || 15166;
448
- } else if (key === "CHATCCC_CHROME_DEVTOOLS_PATH") {
449
- result.chromeDevtools = result.chromeDevtools || {};
450
- (result.chromeDevtools as Record<string, unknown>).chromePath = val;
451
- } else if (key === "CHATCCC_PORT") {
452
- result.port = parseInt(val as string, 10) || 18080;
453
- } else if (key === "CHATCCC_GIT_TIMEOUT_SECONDS") {
454
- result.gitTimeoutSeconds = parseInt(val as string, 10) || 180;
455
- } else if (key === "CHATCCC_ANTHROPIC_MODEL") {
456
- result.claude = result.claude || {};
457
- (result.claude as Record<string, unknown>).model = val;
458
- } else if (key === "CHATCCC_ANTHROPIC_SUBAGENT_MODEL") {
459
- result.claude = result.claude || {};
460
- (result.claude as Record<string, unknown>).subagentModel = val;
461
- } else if (key === "CHATCCC_ANTHROPIC_EFFORT") {
462
- result.claude = result.claude || {};
463
- (result.claude as Record<string, unknown>).effort = val;
464
- } else if (key === "CHATCCC_ANTHROPIC_API_KEY") {
465
- result.claude = result.claude || {};
466
- (result.claude as Record<string, unknown>).apiKey = val;
467
- } else if (key === "CHATCCC_ANTHROPIC_BASE_URL") {
468
- result.claude = result.claude || {};
469
- (result.claude as Record<string, unknown>).baseUrl = val;
470
- } else if (key === "CHATCCC_ANTHROPIC_MAX_TURN") {
471
- result.claude = result.claude || {};
472
- (result.claude as Record<string, unknown>).maxTurn = (function(v){ var n = parseInt(v, 10); return isNaN(n) ? 0 : n; })(val as string);
473
- } else if (key === "CHATCCC_CLAUDE_ENABLED") {
474
- result.claude = result.claude || {};
475
- (result.claude as Record<string, unknown>).enabled = val === true || val === "true";
476
- } else if (key === "CHATCCC_CLAUDE_DEFAULT_AGENT") {
477
- result.claude = result.claude || {};
478
- (result.claude as Record<string, unknown>).defaultAgent = val === true || val === "true";
479
- } else if (key === "CHATCCC_CURSOR_PATH") {
480
- result.cursor = result.cursor || {};
481
- (result.cursor as Record<string, unknown>).path = val;
482
- } else if (key === "CHATCCC_CURSOR_MODEL") {
483
- result.cursor = result.cursor || {};
484
- (result.cursor as Record<string, unknown>).model = val;
485
- } else if (key === "CHATCCC_CURSOR_ALTERNATIVE_MODEL") {
486
- result.cursor = result.cursor || {};
487
- (result.cursor as Record<string, unknown>).alternativeModel = val;
488
- } else if (key === "CHATCCC_CURSOR_AVATAR_BATTERY_MODE") {
489
- result.cursor = result.cursor || {};
490
- (result.cursor as Record<string, unknown>).avatarBatteryMode = val;
491
- } else if (key === "CHATCCC_CURSOR_ON_DEMAND_MONTHLY_BUDGET") {
492
- result.cursor = result.cursor || {};
493
- (result.cursor as Record<string, unknown>).onDemandMonthlyBudget = Number(val);
494
- } else if (key === "CHATCCC_CURSOR_ENABLED") {
495
- result.cursor = result.cursor || {};
496
- (result.cursor as Record<string, unknown>).enabled = val === true || val === "true";
497
- } else if (key === "CHATCCC_CURSOR_DEFAULT_AGENT") {
498
- result.cursor = result.cursor || {};
499
- (result.cursor as Record<string, unknown>).defaultAgent = val === true || val === "true";
500
- } else if (key === "CHATCCC_CODEX_PATH") {
501
- result.codex = result.codex || {};
502
- (result.codex as Record<string, unknown>).path = val;
503
- } else if (key === "CHATCCC_CODEX_MODEL") {
504
- result.codex = result.codex || {};
505
- (result.codex as Record<string, unknown>).model = val;
506
- } else if (key === "CHATCCC_CODEX_ALTERNATIVE_MODEL") {
507
- result.codex = result.codex || {};
508
- (result.codex as Record<string, unknown>).alternativeModel = val;
509
- } else if (key === "CHATCCC_CODEX_EFFORT") {
510
- result.codex = result.codex || {};
511
- (result.codex as Record<string, unknown>).effort = val;
512
- } else if (key === "CHATCCC_CODEX_FAST_MODE") {
513
- result.codex = result.codex || {};
514
- (result.codex as Record<string, unknown>).fastMode = val === true || val === "true";
515
- } else if (key === "CHATCCC_CODEX_ENABLED") {
516
- result.codex = result.codex || {};
517
- (result.codex as Record<string, unknown>).enabled = val === true || val === "true";
518
- } else if (key === "CHATCCC_CODEX_DEFAULT_AGENT") {
519
- result.codex = result.codex || {};
520
- (result.codex as Record<string, unknown>).defaultAgent = val === true || val === "true";
521
- } else if (key === "CHATCCC_CCC_API_KEY") {
522
- result.ccc = result.ccc || {};
523
- (result.ccc as Record<string, unknown>).DEEPSEEK_API_KEY = val;
524
- } else if (key === "CHATCCC_CCC_BASE_URL") {
525
- result.ccc = result.ccc || {};
526
- (result.ccc as Record<string, unknown>).DEEPSEEK_BASE_URL = val;
527
- } else if (key === "CHATCCC_CCC_MODEL") {
528
- result.ccc = result.ccc || {};
529
- (result.ccc as Record<string, unknown>).model = val;
530
- } else if (key === "CHATCCC_CCC_ALTERNATIVE_MODEL") {
531
- result.ccc = result.ccc || {};
532
- (result.ccc as Record<string, unknown>).alternativeModel = val;
533
- } else if (key === "CHATCCC_CCC_EFFORT") {
534
- result.ccc = result.ccc || {};
535
- (result.ccc as Record<string, unknown>).effort = val;
536
- } else if (key === "CHATCCC_CCC_PROVIDER") {
537
- result.ccc = result.ccc || {};
538
- (result.ccc as Record<string, unknown>).provider = val;
539
- } else if (key === "CHATCCC_CCC_ENABLED") {
540
- result.ccc = result.ccc || {};
541
- (result.ccc as Record<string, unknown>).enabled = val === true || val === "true";
542
- } else if (key === "CHATCCC_CCC_DEFAULT_AGENT") {
543
- result.ccc = result.ccc || {};
544
- (result.ccc as Record<string, unknown>).defaultAgent = val === true || val === "true";
545
- }
546
- }
547
- return result;
548
- }
549
-
550
- async function handleGetStatus(_req: IncomingMessage, res: ServerResponse): Promise<void> {
551
- const running = isServiceRunning();
552
- const pid = getServicePid();
553
- const uptime = running ? getServiceUptime() : null;
554
- jsonReply(res, 200, { running, pid, uptime });
555
- }
556
-
557
- async function handleStartService(_req: IncomingMessage, res: ServerResponse): Promise<void> {
558
- const path = chooseStartPath({
559
- hasInplaceActivateHook: Boolean(setupActivateHook && setupHttpServer),
560
- isServiceRunning: isServiceRunning(),
561
- });
562
-
563
- if (path === "inplace") {
564
- // setup 模式:原地启动飞书 service,复用 setup HTTP server,不动 PID 文件。
565
- // 注意 hook setupHttpServer chooseStartPath() 之后**绝不会被并发清空**
566
- // —— init 时一次性赋值,且当前函数是 onActivate 唯一调用点;非 null 断言安全。
567
- const hook = setupActivateHook!;
568
- const server = setupHttpServer!;
569
- const result = await hook(server);
570
- if (result.ok) {
571
- // 切换成功:清掉 hook 防止再次走 inplace 路径(service 已经在跑了)。
572
- // setupHttpServer 不清 —— 它已经接管为 service server,dashboard 仍要用。
573
- setupActivateHook = null;
574
- jsonReply(res, 200, { ok: true, pid: process.pid, mode: "inplace" });
575
- } else {
576
- jsonReply(res, 500, { ok: false, error: result.error });
577
- }
578
- return;
579
- }
580
-
581
- if (path === "reload") {
582
- // service 已经在跑:只刷新进程内配置,让后续消息/新会话使用新值。
583
- if (!reloadConfigHook) {
584
- jsonReply(res, 200, { ok: true, pid: process.pid, mode: "saved" });
585
- return;
586
- }
587
- try {
588
- await reloadConfigHook();
589
- jsonReply(res, 200, { ok: true, pid: process.pid, mode: "reload" });
590
- } catch (err) {
591
- jsonReply(res, 500, { ok: false, error: (err as Error).message });
592
- }
593
- return;
594
- }
595
-
596
- const result = spawnService();
597
- if (result.ok) {
598
- await new Promise((r) => setTimeout(r, 1000));
599
- jsonReply(res, 200, { ok: true, pid: result.pid, mode: "spawn" });
600
- } else {
601
- jsonReply(res, 500, result);
602
- }
603
- }
604
-
605
- async function handleStopService(req: IncomingMessage, res: ServerResponse): Promise<void> {
606
- const running = isServiceRunning();
607
- if (!running) {
608
- jsonReply(res, 200, { ok: false, error: "Service is not running" });
609
- return;
610
- }
611
- jsonReply(res, 200, { ok: true });
612
- setImmediate(() => {
613
- stopService();
614
- });
615
- }
616
-
617
- async function handleRestartService(_req: IncomingMessage, res: ServerResponse): Promise<void> {
618
- jsonReply(res, 200, { ok: true, message: "Restarting..." });
619
- scheduleRestart();
620
- process.exit(0);
621
- }
622
-
623
- async function handleValidate(req: IncomingMessage, res: ServerResponse): Promise<void> {
624
- const body = await readRequestBody(req);
625
- let tool: string;
626
- try { tool = JSON.parse(body).tool; } catch {
627
- jsonReply(res, 400, { ok: false, error: "Missing tool" });
628
- return;
629
- }
630
- const result = validateCli(tool);
631
- jsonReply(res, 200, result);
632
- }
633
-
634
- async function handleForgetIlink(_req: IncomingMessage, res: ServerResponse): Promise<void> {
635
- try {
636
- if (existsSync(ILINK_AUTH_PATH)) {
637
- unlinkSync(ILINK_AUTH_PATH);
638
- }
639
- jsonReply(res, 200, { ok: true });
640
- } catch (err) {
641
- jsonReply(res, 500, { ok: false, error: (err as Error).message });
642
- }
643
- }
644
-
645
- async function handleClaudeSdkStatus(_req: IncomingMessage, res: ServerResponse): Promise<void> {
646
- const progress = getLastInstallProgress();
647
- jsonReply(res, 200, {
648
- installed: isClaudeSdkInstalled(),
649
- version: getClaudeSdkInstalledVersion(),
650
- running: isInstallRunning(),
651
- // 展开到顶层:前端轮询 JS 读顶层 phase/message/percent/error(历史契约),
652
- // 若只放在 progress 嵌套里,前端 s.phase 恒为 undefined → 进度条永不渲染。
653
- ...progress,
654
- // 保留嵌套结构,兼容其它调用方。
655
- progress,
656
- });
657
- }
658
-
659
- async function handleClaudeSdkInstall(_req: IncomingMessage, res: ServerResponse): Promise<void> {
660
- if (isInstallRunning()) {
661
- jsonReply(res, 200, { ok: true, alreadyRunning: true });
662
- return;
663
- }
664
- try {
665
- // 后台执行,不阻塞响应;前端通过 /api/claude-sdk/status 轮询进度
666
- installClaudeSdk().catch((err: unknown) => {
667
- console.error(`[WEB-UI] Claude SDK 安装失败: ${(err as Error).message}`);
668
- });
669
- jsonReply(res, 200, { ok: true });
670
- } catch (err) {
671
- jsonReply(res, 500, { ok: false, error: (err as Error).message });
672
- }
673
- }
674
-
675
- // ---------------------------------------------------------------------------
676
- // HTML page (embedded template)
677
- // ---------------------------------------------------------------------------
678
-
679
- /** Agent Team 的首个占位页面:按产品约定只展示标题,不提前放入功能内容。 */
680
- export const AGENT_TEAM_PAGE_HTML = `<!DOCTYPE html>
681
- <html lang="zh-CN">
682
- <head>
683
- <meta charset="UTF-8">
684
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
685
- <title>Agent Team</title>
686
- <style>
687
- *,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
688
- 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}
689
- h1{font-size:clamp(40px,8vw,88px);font-weight:750;letter-spacing:-.04em;text-shadow:0 12px 42px rgba(129,140,248,.45)}
690
- </style>
691
- </head>
692
- <body>
693
- <h1>Agent Team</h1>
694
- </body>
695
- </html>`;
696
-
697
- export const PAGE_HTML = `<!DOCTYPE html>
1
+ // =============================================================================
2
+ // web-ui.ts — Setup Wizard & Management Dashboard HTTP Server
3
+ // =============================================================================
4
+ // Serves on the same port as the WebSocket relay (18080 default).
5
+ // - Setup mode: no config.json → show setup wizard, skip Feishu connection
6
+ // - Dashboard mode: config.json exists → serve management page alongside WS relay
7
+ // =============================================================================
8
+ import { createServer } from "node:http";
9
+ import { existsSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
10
+ import { homedir } from "node:os";
11
+ import { join } from "node:path";
12
+ import { spawn, execSync } from "node:child_process";
13
+ import { CHATCCC_PACKAGE_ROOT } from "./package-root.js";
14
+ import { resolveChatCccRuntimeSpawnSpec } from "./runtime-entry.js";
15
+ import { handleAgentTeamRequest } from "./agent-team/http/board-routes.js";
16
+ import { AGENT_TEAM_PAGE_HTML } from "./agent-team/web/agent-team-page.js";
17
+ export { AGENT_TEAM_PAGE_HTML } from "./agent-team/web/agent-team-page.js";
18
+ import { buildWebUiUrl, createInternalRestartEnv, openWebUiInDefaultBrowser, } from "./startup-lifecycle.js";
19
+ import { getClaudeSdkInstalledVersion, getLastInstallProgress, installClaudeSdk, isClaudeSdkInstalled, isInstallRunning, } from "./claude-sdk-installer.js";
20
+ const PROJECT_ROOT = CHATCCC_PACKAGE_ROOT;
21
+ const USER_DATA_DIR = join(homedir(), ".chatccc");
22
+ const CONFIG_FILE = join(USER_DATA_DIR, "config.json");
23
+ const CONFIG_SAMPLE_FILE = join(PROJECT_ROOT, "config.sample.json");
24
+ const PID_FILE = join(USER_DATA_DIR, "state", "runtime.pid");
25
+ const ILINK_AUTH_PATH = join(USER_DATA_DIR, "state", "ilink-auth.json");
26
+ export function chooseStartPath(input) {
27
+ if (input.hasInplaceActivateHook)
28
+ return "inplace";
29
+ if (input.isServiceRunning)
30
+ return "reload";
31
+ return "spawn";
32
+ }
33
+ /** 读取 cursor / codex 的 CLI 路径,优先新字段 path,回退旧字段 command */
34
+ function readToolPath(tool) {
35
+ if (!tool)
36
+ return "";
37
+ if (tool.path && tool.path.trim())
38
+ return tool.path;
39
+ if (tool.command && tool.command.trim())
40
+ return tool.command;
41
+ return "";
42
+ }
43
+ function loadConfig() {
44
+ if (!existsSync(CONFIG_FILE))
45
+ return {};
46
+ try {
47
+ let raw = readFileSync(CONFIG_FILE, "utf8");
48
+ // 移除可能意外写入的 UTF-8 BOM,避免 JSON.parse 失败导致返回空对象、
49
+ // UI 显示所有配置为空。
50
+ if (raw.charCodeAt(0) === 0xfeff)
51
+ raw = raw.slice(1);
52
+ return JSON.parse(raw);
53
+ }
54
+ catch {
55
+ return {};
56
+ }
57
+ }
58
+ function saveConfig(cfg) {
59
+ writeFileSync(CONFIG_FILE, JSON.stringify(cfg, null, 2), "utf8");
60
+ }
61
+ function stringValue(value) {
62
+ return typeof value === "string" ? value.trim() : "";
63
+ }
64
+ function portValue(cfg) {
65
+ return Number.isInteger(cfg.port) && cfg.port >= 1 && cfg.port <= 65535 ? cfg.port : 18080;
66
+ }
67
+ function platformEnabled(cfg, platform) {
68
+ return cfg.platforms?.[platform]?.enabled !== false;
69
+ }
70
+ function feishuPlatformType(cfg) {
71
+ return cfg.platforms?.feishu?.platformType === "lark" ? "lark" : "feishu";
72
+ }
73
+ function ilinkReuseTokenOnStart(cfg) {
74
+ return cfg.platforms?.ilink?.reuseTokenOnStart !== false;
75
+ }
76
+ export function getRestartRequiredReasons(before, after) {
77
+ const reasons = [];
78
+ if (portValue(before) !== portValue(after))
79
+ reasons.push("port");
80
+ if (stringValue(before.feishu?.appId) !== stringValue(after.feishu?.appId))
81
+ reasons.push("feishu.appId");
82
+ if (stringValue(before.feishu?.appSecret) !== stringValue(after.feishu?.appSecret))
83
+ reasons.push("feishu.appSecret");
84
+ if (feishuPlatformType(before) !== feishuPlatformType(after))
85
+ reasons.push("platforms.feishu.platformType");
86
+ if (platformEnabled(before, "feishu") !== platformEnabled(after, "feishu"))
87
+ reasons.push("platforms.feishu.enabled");
88
+ if (platformEnabled(before, "ilink") !== platformEnabled(after, "ilink"))
89
+ reasons.push("platforms.ilink.enabled");
90
+ if (ilinkReuseTokenOnStart(before) !== ilinkReuseTokenOnStart(after)) {
91
+ reasons.push("platforms.ilink.reuseTokenOnStart");
92
+ }
93
+ return reasons;
94
+ }
95
+ async function applySavedConfigIfPossible(before, after) {
96
+ const setupMode = Boolean(setupActivateHook && setupHttpServer);
97
+ if (setupMode) {
98
+ return { mode: "saved", restartRequired: false, restartReasons: [] };
99
+ }
100
+ const running = isServiceRunning();
101
+ if (!running) {
102
+ return { mode: "saved", restartRequired: false, restartReasons: [] };
103
+ }
104
+ const restartReasons = getRestartRequiredReasons(before, after);
105
+ if (restartReasons.length > 0) {
106
+ return { mode: "restart-required", restartRequired: true, restartReasons };
107
+ }
108
+ if (!reloadConfigHook) {
109
+ return { mode: "saved", restartRequired: false, restartReasons: [] };
110
+ }
111
+ await reloadConfigHook();
112
+ return { mode: "reload", restartRequired: false, restartReasons: [] };
113
+ }
114
+ function maskSecret(value) {
115
+ if (!value)
116
+ return "(未设置)";
117
+ if (value.length <= 8)
118
+ return "***";
119
+ return value.slice(0, 4) + "***" + value.slice(-4);
120
+ }
121
+ // ---------------------------------------------------------------------------
122
+ // Helpers process management
123
+ // ---------------------------------------------------------------------------
124
+ function isServiceRunning() {
125
+ if (!existsSync(PID_FILE))
126
+ return false;
127
+ try {
128
+ const raw = readFileSync(PID_FILE, "utf8").trim();
129
+ const pid = parseInt(raw, 10);
130
+ if (isNaN(pid))
131
+ return false;
132
+ process.kill(pid, 0);
133
+ return true;
134
+ }
135
+ catch {
136
+ return false;
137
+ }
138
+ }
139
+ function getServicePid() {
140
+ if (!existsSync(PID_FILE))
141
+ return null;
142
+ try {
143
+ const pid = parseInt(readFileSync(PID_FILE, "utf8").trim(), 10);
144
+ return isNaN(pid) ? null : pid;
145
+ }
146
+ catch {
147
+ return null;
148
+ }
149
+ }
150
+ function getServiceUptime() {
151
+ try {
152
+ const s = statSync(PID_FILE);
153
+ return Math.floor((Date.now() - s.mtimeMs) / 1000);
154
+ }
155
+ catch {
156
+ return null;
157
+ }
158
+ }
159
+ import { statSync } from "node:fs";
160
+ function spawnService() {
161
+ const spec = resolveChatCccRuntimeSpawnSpec(PROJECT_ROOT);
162
+ if (!existsSync(spec.args[0]))
163
+ return { ok: false, error: `Entry not found: ${spec.args[0]}` };
164
+ try {
165
+ const child = spawn(spec.command, spec.args, {
166
+ cwd: PROJECT_ROOT,
167
+ detached: true,
168
+ stdio: "ignore",
169
+ shell: false,
170
+ env: createInternalRestartEnv(),
171
+ });
172
+ child.unref();
173
+ return { ok: true, pid: child.pid ?? undefined };
174
+ }
175
+ catch (err) {
176
+ return { ok: false, error: err.message };
177
+ }
178
+ }
179
+ /**
180
+ * 安排一次真正的进程重启:先回复 HTTP 请求,再 spawn 一个延迟启动的子进程,
181
+ * 然后退出当前进程。延迟是为了让当前进程完全退出并释放端口。
182
+ */
183
+ function scheduleRestart() {
184
+ const spec = resolveChatCccRuntimeSpawnSpec(PROJECT_ROOT);
185
+ if (!existsSync(spec.args[0]))
186
+ return;
187
+ const delayedLauncher = [
188
+ "const { spawn } = require('node:child_process');",
189
+ `const command = ${JSON.stringify(spec.command)};`,
190
+ `const args = ${JSON.stringify(spec.args)};`,
191
+ `const cwd = ${JSON.stringify(PROJECT_ROOT)};`,
192
+ "setTimeout(() => {",
193
+ " const child = spawn(command, args, { cwd, detached: true, stdio: 'ignore', shell: false, env: process.env });",
194
+ " child.unref();",
195
+ "}, 2000);",
196
+ ].join("\n");
197
+ if (process.platform === "win32") {
198
+ // Windows keeps the helper window hidden; the helper itself provides the delay.
199
+ spawn(process.execPath, ["-e", delayedLauncher], {
200
+ cwd: PROJECT_ROOT,
201
+ detached: true,
202
+ stdio: "ignore",
203
+ windowsHide: true,
204
+ env: createInternalRestartEnv(),
205
+ }).unref();
206
+ }
207
+ else {
208
+ spawn(process.execPath, ["-e", delayedLauncher], {
209
+ cwd: PROJECT_ROOT,
210
+ detached: true,
211
+ stdio: "ignore",
212
+ env: createInternalRestartEnv(),
213
+ }).unref();
214
+ }
215
+ }
216
+ function stopService() {
217
+ const pid = getServicePid();
218
+ if (!pid)
219
+ return { ok: false, error: "No PID file found" };
220
+ try {
221
+ if (process.platform === "win32") {
222
+ execSync(`taskkill /PID ${pid} /F /T`, { stdio: "pipe", windowsHide: true });
223
+ }
224
+ else {
225
+ process.kill(pid, "SIGTERM");
226
+ }
227
+ return { ok: true };
228
+ }
229
+ catch (err) {
230
+ return { ok: false, error: err.message };
231
+ }
232
+ }
233
+ function validateCli(tool) {
234
+ const cfg = loadConfig();
235
+ let cmd;
236
+ if (tool === "cursor") {
237
+ cmd = readToolPath(cfg.cursor) || detectCursorAgentPath();
238
+ }
239
+ else {
240
+ cmd = readToolPath(cfg.codex) || "codex";
241
+ }
242
+ try {
243
+ const out = execSync(`"${cmd}" --version`, { encoding: "utf8", timeout: 10000, windowsHide: true }).trim();
244
+ return { ok: true, path: cmd, error: out.slice(0, 200) };
245
+ }
246
+ catch (err) {
247
+ return { ok: false, path: cmd, error: err.message };
248
+ }
249
+ }
250
+ function detectCursorAgentPath() {
251
+ const localAppData = process.env.LOCALAPPDATA;
252
+ if (localAppData) {
253
+ const defaultPath = join(localAppData, "cursor-agent", "agent.cmd");
254
+ if (existsSync(defaultPath))
255
+ return defaultPath;
256
+ }
257
+ return "agent";
258
+ }
259
+ // ---------------------------------------------------------------------------
260
+ // Helpers HTTP
261
+ // ---------------------------------------------------------------------------
262
+ function jsonReply(res, code, data) {
263
+ res.writeHead(code, { "Content-Type": "application/json; charset=utf-8" });
264
+ res.end(JSON.stringify(data));
265
+ }
266
+ function readRequestBody(req) {
267
+ return new Promise((resolve) => {
268
+ let body = "";
269
+ req.on("data", (chunk) => { body += chunk.toString(); });
270
+ req.on("end", () => resolve(body));
271
+ });
272
+ }
273
+ // ---------------------------------------------------------------------------
274
+ // API route handlers
275
+ // ---------------------------------------------------------------------------
276
+ async function handleApiCheck(_req, res) {
277
+ const hasConfig = existsSync(CONFIG_FILE);
278
+ let hasCreds = false;
279
+ if (hasConfig) {
280
+ const c = loadConfig();
281
+ const feishuEnabled = c.platforms?.feishu?.enabled !== false; // 默认 true(向后兼容)
282
+ const feishuOk = feishuEnabled && Boolean(c.feishu?.appId?.trim() && c.feishu?.appSecret?.trim());
283
+ hasCreds = feishuOk || !feishuEnabled;
284
+ }
285
+ jsonReply(res, 200, { hasConfig, hasCreds, configPath: CONFIG_FILE });
286
+ }
287
+ async function handleGetConfig(_req, res) {
288
+ const running = isServiceRunning();
289
+ const pid = getServicePid();
290
+ const vars = loadConfig();
291
+ let ilinkAuthExists = false;
292
+ if (existsSync(ILINK_AUTH_PATH)) {
293
+ try {
294
+ const auth = JSON.parse(readFileSync(ILINK_AUTH_PATH, "utf8"));
295
+ ilinkAuthExists = Boolean(auth.token);
296
+ }
297
+ catch { /* ignore parse errors */ }
298
+ }
299
+ jsonReply(res, 200, { vars, running, pid, ilinkAuthExists });
300
+ }
301
+ async function handlePostConfig(req, res) {
302
+ const body = await readRequestBody(req);
303
+ let updates;
304
+ try {
305
+ const parsed = JSON.parse(body);
306
+ updates = parsed.vars ?? {};
307
+ }
308
+ catch {
309
+ jsonReply(res, 400, { ok: false, error: "Invalid JSON" });
310
+ return;
311
+ }
312
+ const existing = loadConfig();
313
+ // AppConfig 是闭合接口(无 index signature),但运行时本质就是 JSON 对象,
314
+ // 这里通过 unknown 桥接到 Record 让 deepMerge 能复用;写回前断言回 AppConfig。
315
+ const merged = deepMerge(existing, unflattenConfig(updates));
316
+ try {
317
+ saveConfig(merged);
318
+ const applyResult = await applySavedConfigIfPossible(existing, merged);
319
+ jsonReply(res, 200, { ok: true, ...applyResult });
320
+ }
321
+ catch (err) {
322
+ jsonReply(res, 500, { ok: false, saved: true, error: err.message });
323
+ }
324
+ }
325
+ function deepMerge(target, source) {
326
+ const result = { ...target };
327
+ for (const [key, val] of Object.entries(source)) {
328
+ if (val !== null && typeof val === "object" && !Array.isArray(val) && typeof result[key] === "object" && result[key] !== null && !Array.isArray(result[key])) {
329
+ result[key] = deepMerge(result[key], val);
330
+ }
331
+ else {
332
+ result[key] = val;
333
+ }
334
+ }
335
+ return result;
336
+ }
337
+ // Convert flat key-value pairs to nested config structure
338
+ export function unflattenConfig(flat) {
339
+ const result = {};
340
+ for (const [key, val] of Object.entries(flat)) {
341
+ if (key === "CHATCCC_APP_ID") {
342
+ result.feishu = result.feishu || {};
343
+ result.feishu.appId = val;
344
+ }
345
+ else if (key === "CHATCCC_APP_SECRET") {
346
+ result.feishu = result.feishu || {};
347
+ result.feishu.appSecret = val;
348
+ }
349
+ else if (key === "CHATCCC_FEISHU_ENABLED") {
350
+ result.platforms = result.platforms || {};
351
+ result.platforms.feishu = result.platforms.feishu || {};
352
+ result.platforms.feishu.enabled = val === true || val === "true";
353
+ }
354
+ else if (key === "CHATCCC_FEISHU_PLATFORM_TYPE") {
355
+ result.platforms = result.platforms || {};
356
+ result.platforms.feishu = result.platforms.feishu || {};
357
+ result.platforms.feishu.platformType = val;
358
+ }
359
+ else if (key === "CHATCCC_ILINK_ENABLED") {
360
+ result.platforms = result.platforms || {};
361
+ result.platforms.ilink = result.platforms.ilink || {};
362
+ result.platforms.ilink.enabled = val === true || val === "true";
363
+ }
364
+ else if (key === "CHATCCC_WEB_UI_OPEN_ON_START") {
365
+ result.webUi = result.webUi || {};
366
+ result.webUi.openOnStart = val === true || val === "true";
367
+ }
368
+ else if (key === "CHATCCC_CHROME_DEVTOOLS_ENABLED") {
369
+ result.chromeDevtools = result.chromeDevtools || {};
370
+ result.chromeDevtools.enabled = val === true || val === "true";
371
+ }
372
+ else if (key === "CHATCCC_CHROME_DEVTOOLS_PORT") {
373
+ result.chromeDevtools = result.chromeDevtools || {};
374
+ result.chromeDevtools.port = parseInt(val, 10) || 15166;
375
+ }
376
+ else if (key === "CHATCCC_CHROME_DEVTOOLS_PATH") {
377
+ result.chromeDevtools = result.chromeDevtools || {};
378
+ result.chromeDevtools.chromePath = val;
379
+ }
380
+ else if (key === "CHATCCC_PORT") {
381
+ result.port = parseInt(val, 10) || 18080;
382
+ }
383
+ else if (key === "CHATCCC_GIT_TIMEOUT_SECONDS") {
384
+ result.gitTimeoutSeconds = parseInt(val, 10) || 180;
385
+ }
386
+ else if (key === "CHATCCC_ANTHROPIC_MODEL") {
387
+ result.claude = result.claude || {};
388
+ result.claude.model = val;
389
+ }
390
+ else if (key === "CHATCCC_ANTHROPIC_SUBAGENT_MODEL") {
391
+ result.claude = result.claude || {};
392
+ result.claude.subagentModel = val;
393
+ }
394
+ else if (key === "CHATCCC_ANTHROPIC_EFFORT") {
395
+ result.claude = result.claude || {};
396
+ result.claude.effort = val;
397
+ }
398
+ else if (key === "CHATCCC_ANTHROPIC_API_KEY") {
399
+ result.claude = result.claude || {};
400
+ result.claude.apiKey = val;
401
+ }
402
+ else if (key === "CHATCCC_ANTHROPIC_BASE_URL") {
403
+ result.claude = result.claude || {};
404
+ result.claude.baseUrl = val;
405
+ }
406
+ else if (key === "CHATCCC_ANTHROPIC_MAX_TURN") {
407
+ result.claude = result.claude || {};
408
+ result.claude.maxTurn = (function (v) { var n = parseInt(v, 10); return isNaN(n) ? 0 : n; })(val);
409
+ }
410
+ else if (key === "CHATCCC_CLAUDE_ENABLED") {
411
+ result.claude = result.claude || {};
412
+ result.claude.enabled = val === true || val === "true";
413
+ }
414
+ else if (key === "CHATCCC_CLAUDE_DEFAULT_AGENT") {
415
+ result.claude = result.claude || {};
416
+ result.claude.defaultAgent = val === true || val === "true";
417
+ }
418
+ else if (key === "CHATCCC_CURSOR_PATH") {
419
+ result.cursor = result.cursor || {};
420
+ result.cursor.path = val;
421
+ }
422
+ else if (key === "CHATCCC_CURSOR_MODEL") {
423
+ result.cursor = result.cursor || {};
424
+ result.cursor.model = val;
425
+ }
426
+ else if (key === "CHATCCC_CURSOR_ALTERNATIVE_MODEL") {
427
+ result.cursor = result.cursor || {};
428
+ result.cursor.alternativeModel = val;
429
+ }
430
+ else if (key === "CHATCCC_CURSOR_AVATAR_BATTERY_MODE") {
431
+ result.cursor = result.cursor || {};
432
+ result.cursor.avatarBatteryMode = val;
433
+ }
434
+ else if (key === "CHATCCC_CURSOR_ON_DEMAND_MONTHLY_BUDGET") {
435
+ result.cursor = result.cursor || {};
436
+ result.cursor.onDemandMonthlyBudget = Number(val);
437
+ }
438
+ else if (key === "CHATCCC_CURSOR_ENABLED") {
439
+ result.cursor = result.cursor || {};
440
+ result.cursor.enabled = val === true || val === "true";
441
+ }
442
+ else if (key === "CHATCCC_CURSOR_DEFAULT_AGENT") {
443
+ result.cursor = result.cursor || {};
444
+ result.cursor.defaultAgent = val === true || val === "true";
445
+ }
446
+ else if (key === "CHATCCC_CODEX_PATH") {
447
+ result.codex = result.codex || {};
448
+ result.codex.path = val;
449
+ }
450
+ else if (key === "CHATCCC_CODEX_MODEL") {
451
+ result.codex = result.codex || {};
452
+ result.codex.model = val;
453
+ }
454
+ else if (key === "CHATCCC_CODEX_ALTERNATIVE_MODEL") {
455
+ result.codex = result.codex || {};
456
+ result.codex.alternativeModel = val;
457
+ }
458
+ else if (key === "CHATCCC_CODEX_EFFORT") {
459
+ result.codex = result.codex || {};
460
+ result.codex.effort = val;
461
+ }
462
+ else if (key === "CHATCCC_CODEX_FAST_MODE") {
463
+ result.codex = result.codex || {};
464
+ result.codex.fastMode = val === true || val === "true";
465
+ }
466
+ else if (key === "CHATCCC_CODEX_ENABLED") {
467
+ result.codex = result.codex || {};
468
+ result.codex.enabled = val === true || val === "true";
469
+ }
470
+ else if (key === "CHATCCC_CODEX_DEFAULT_AGENT") {
471
+ result.codex = result.codex || {};
472
+ result.codex.defaultAgent = val === true || val === "true";
473
+ }
474
+ else if (key === "CHATCCC_CCC_API_KEY") {
475
+ result.ccc = result.ccc || {};
476
+ result.ccc.DEEPSEEK_API_KEY = val;
477
+ }
478
+ else if (key === "CHATCCC_CCC_BASE_URL") {
479
+ result.ccc = result.ccc || {};
480
+ result.ccc.DEEPSEEK_BASE_URL = val;
481
+ }
482
+ else if (key === "CHATCCC_CCC_MODEL") {
483
+ result.ccc = result.ccc || {};
484
+ result.ccc.model = val;
485
+ }
486
+ else if (key === "CHATCCC_CCC_ALTERNATIVE_MODEL") {
487
+ result.ccc = result.ccc || {};
488
+ result.ccc.alternativeModel = val;
489
+ }
490
+ else if (key === "CHATCCC_CCC_EFFORT") {
491
+ result.ccc = result.ccc || {};
492
+ result.ccc.effort = val;
493
+ }
494
+ else if (key === "CHATCCC_CCC_PROVIDER") {
495
+ result.ccc = result.ccc || {};
496
+ result.ccc.provider = val;
497
+ }
498
+ else if (key === "CHATCCC_CCC_ENABLED") {
499
+ result.ccc = result.ccc || {};
500
+ result.ccc.enabled = val === true || val === "true";
501
+ }
502
+ else if (key === "CHATCCC_CCC_DEFAULT_AGENT") {
503
+ result.ccc = result.ccc || {};
504
+ result.ccc.defaultAgent = val === true || val === "true";
505
+ }
506
+ }
507
+ return result;
508
+ }
509
+ async function handleGetStatus(_req, res) {
510
+ const running = isServiceRunning();
511
+ const pid = getServicePid();
512
+ const uptime = running ? getServiceUptime() : null;
513
+ jsonReply(res, 200, { running, pid, uptime });
514
+ }
515
+ async function handleStartService(_req, res) {
516
+ const path = chooseStartPath({
517
+ hasInplaceActivateHook: Boolean(setupActivateHook && setupHttpServer),
518
+ isServiceRunning: isServiceRunning(),
519
+ });
520
+ if (path === "inplace") {
521
+ // setup 模式:原地启动飞书 service,复用 setup HTTP server,不动 PID 文件。
522
+ // 注意 hook setupHttpServer 在 chooseStartPath() 之后**绝不会被并发清空**
523
+ // —— init 时一次性赋值,且当前函数是 onActivate 唯一调用点;非 null 断言安全。
524
+ const hook = setupActivateHook;
525
+ const server = setupHttpServer;
526
+ const result = await hook(server);
527
+ if (result.ok) {
528
+ // 切换成功:清掉 hook 防止再次走 inplace 路径(service 已经在跑了)。
529
+ // setupHttpServer 不清 —— 它已经接管为 service server,dashboard 仍要用。
530
+ setupActivateHook = null;
531
+ jsonReply(res, 200, { ok: true, pid: process.pid, mode: "inplace" });
532
+ }
533
+ else {
534
+ jsonReply(res, 500, { ok: false, error: result.error });
535
+ }
536
+ return;
537
+ }
538
+ if (path === "reload") {
539
+ // service 已经在跑:只刷新进程内配置,让后续消息/新会话使用新值。
540
+ if (!reloadConfigHook) {
541
+ jsonReply(res, 200, { ok: true, pid: process.pid, mode: "saved" });
542
+ return;
543
+ }
544
+ try {
545
+ await reloadConfigHook();
546
+ jsonReply(res, 200, { ok: true, pid: process.pid, mode: "reload" });
547
+ }
548
+ catch (err) {
549
+ jsonReply(res, 500, { ok: false, error: err.message });
550
+ }
551
+ return;
552
+ }
553
+ const result = spawnService();
554
+ if (result.ok) {
555
+ await new Promise((r) => setTimeout(r, 1000));
556
+ jsonReply(res, 200, { ok: true, pid: result.pid, mode: "spawn" });
557
+ }
558
+ else {
559
+ jsonReply(res, 500, result);
560
+ }
561
+ }
562
+ async function handleStopService(req, res) {
563
+ const running = isServiceRunning();
564
+ if (!running) {
565
+ jsonReply(res, 200, { ok: false, error: "Service is not running" });
566
+ return;
567
+ }
568
+ jsonReply(res, 200, { ok: true });
569
+ setImmediate(() => {
570
+ stopService();
571
+ });
572
+ }
573
+ async function handleRestartService(_req, res) {
574
+ jsonReply(res, 200, { ok: true, message: "Restarting..." });
575
+ scheduleRestart();
576
+ process.exit(0);
577
+ }
578
+ async function handleValidate(req, res) {
579
+ const body = await readRequestBody(req);
580
+ let tool;
581
+ try {
582
+ tool = JSON.parse(body).tool;
583
+ }
584
+ catch {
585
+ jsonReply(res, 400, { ok: false, error: "Missing tool" });
586
+ return;
587
+ }
588
+ const result = validateCli(tool);
589
+ jsonReply(res, 200, result);
590
+ }
591
+ async function handleForgetIlink(_req, res) {
592
+ try {
593
+ if (existsSync(ILINK_AUTH_PATH)) {
594
+ unlinkSync(ILINK_AUTH_PATH);
595
+ }
596
+ jsonReply(res, 200, { ok: true });
597
+ }
598
+ catch (err) {
599
+ jsonReply(res, 500, { ok: false, error: err.message });
600
+ }
601
+ }
602
+ async function handleClaudeSdkStatus(_req, res) {
603
+ const progress = getLastInstallProgress();
604
+ jsonReply(res, 200, {
605
+ installed: isClaudeSdkInstalled(),
606
+ version: getClaudeSdkInstalledVersion(),
607
+ running: isInstallRunning(),
608
+ // 展开到顶层:前端轮询 JS 读顶层 phase/message/percent/error(历史契约),
609
+ // 若只放在 progress 嵌套里,前端 s.phase 恒为 undefined → 进度条永不渲染。
610
+ ...progress,
611
+ // 保留嵌套结构,兼容其它调用方。
612
+ progress,
613
+ });
614
+ }
615
+ async function handleClaudeSdkInstall(_req, res) {
616
+ if (isInstallRunning()) {
617
+ jsonReply(res, 200, { ok: true, alreadyRunning: true });
618
+ return;
619
+ }
620
+ try {
621
+ // 后台执行,不阻塞响应;前端通过 /api/claude-sdk/status 轮询进度
622
+ installClaudeSdk().catch((err) => {
623
+ console.error(`[WEB-UI] Claude SDK 安装失败: ${err.message}`);
624
+ });
625
+ jsonReply(res, 200, { ok: true });
626
+ }
627
+ catch (err) {
628
+ jsonReply(res, 500, { ok: false, error: err.message });
629
+ }
630
+ }
631
+ // ---------------------------------------------------------------------------
632
+ // HTML page (embedded template)
633
+ // ---------------------------------------------------------------------------
634
+ export const PAGE_HTML = `<!DOCTYPE html>
698
635
  <html lang="zh-CN">
699
636
  <head>
700
637
  <meta charset="UTF-8">
@@ -2386,145 +2323,122 @@ init();
2386
2323
  setTimeout(claudeEngineRefreshStatus, 300);
2387
2324
  </script>
2388
2325
  </body>
2389
- </html>`;
2390
-
2391
- // ---------------------------------------------------------------------------
2392
- // Router
2393
- // ---------------------------------------------------------------------------
2394
-
2395
- async function handleRequest(req: IncomingMessage, res: ServerResponse): Promise<void> {
2396
- const url = req.url ?? "/";
2397
- const method = req.method ?? "GET";
2398
- const pathname = url.split("?", 1)[0];
2399
-
2400
- if (extraApiHandler && await extraApiHandler(req, res)) return;
2401
-
2402
- // API routes
2403
- if (url === "/api/check" && method === "GET") return handleApiCheck(req, res);
2404
- if (url === "/api/config" && method === "GET") return handleGetConfig(req, res);
2405
- if (url === "/api/config" && method === "POST") return handlePostConfig(req, res);
2406
- if (url === "/api/status" && method === "GET") return handleGetStatus(req, res);
2407
- if (url === "/api/start" && method === "POST") return handleStartService(req, res);
2408
- if (url === "/api/stop" && method === "POST") return handleStopService(req, res);
2409
- if (url === "/api/restart" && method === "POST") return handleRestartService(req, res);
2410
- if (url === "/api/validate" && method === "POST") return handleValidate(req, res);
2411
- if (url === "/api/ilink/forget" && method === "POST") return handleForgetIlink(req, res);
2412
- if (url === "/api/claude-sdk/status" && method === "GET") return handleClaudeSdkStatus(req, res);
2413
- if (url === "/api/claude-sdk/install" && method === "POST") return handleClaudeSdkInstall(req, res);
2414
-
2415
- if (method === "GET" && (pathname === "/agent-team" || pathname === "/agent-team/")) {
2416
- res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
2417
- res.end(AGENT_TEAM_PAGE_HTML);
2418
- return;
2419
- }
2420
-
2421
- // Serve HTML page for all other GET requests
2422
- if (method === "GET") {
2423
- res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
2424
- res.end(PAGE_HTML);
2425
- return;
2426
- }
2427
-
2428
- jsonReply(res, 404, { error: "Not found" });
2429
- }
2430
-
2431
- export function createUiRouter(): (req: IncomingMessage, res: ServerResponse) => void {
2432
- return (req, res) => {
2433
- handleRequest(req, res).catch((err) => {
2434
- console.error(`[WEB-UI] Unhandled error: ${(err as Error).message}`);
2435
- if (!res.headersSent) jsonReply(res, 500, { error: "Internal error" });
2436
- });
2437
- };
2438
- }
2439
-
2440
- // ---------------------------------------------------------------------------
2441
- // Setup mode entry point — called from index.ts when no credentials
2442
- // ---------------------------------------------------------------------------
2443
-
2444
- /**
2445
- * setup → service「在线切换」回调签名:
2446
- * - 入参 httpServer:setup 模式当前监听的 HTTP server,会被复用为 service
2447
- * relay server(避免 close + recreate 的端口竞态)。
2448
- * - 返回 { ok: true } 表示原地启动成功,前端会刷新进 dashboard。
2449
- * - 返回 { ok: false, error } 表示启动失败,前端会 toast 错误。
2450
- * setup HTTP server **必须仍然可用**,让用户改完 config 再试一次。
2451
- */
2452
- export type SetupActivateHook = (
2453
- httpServer: ReturnType<typeof createServer>,
2454
- ) => Promise<{ ok: true } | { ok: false; error: string }>;
2455
-
2456
- export interface StartSetupModeOptions {
2457
- onActivate?: SetupActivateHook;
2458
- /** 内部重启进入 setup 模式时设为 false,避免重复弹出系统浏览器。 */
2459
- openBrowser?: boolean;
2460
- }
2461
-
2462
- // setup HTTP server + onActivate 回调通过模块级变量暴露给 handleStartService。
2463
- // 一旦 onActivate 成功执行,setupActivateHook 会被清空——避免 dashboard 模式下
2464
- // 用户再点"启动"时还走 inplace 路径(service 已经在跑了)。
2465
- let setupHttpServer: ReturnType<typeof createServer> | null = null;
2466
- let setupActivateHook: SetupActivateHook | null = null;
2467
-
2468
- // reload-config 回调:dashboard 模式下用户点"保存并启动"时,service 已经在跑,
2469
- // 仅需要把磁盘上刚保存的 config.json 刷进进程内的 export let 常量(live binding)。
2470
- // index.ts 注入,因为 web-ui.ts 自身**不应**直接 import config.ts——后者顶层
2471
- // loadConfig 副作用,被 web-ui.ts 间接 import 会污染所有依赖 web-ui.ts 的单测。
2472
- let reloadConfigHook: (() => void) | null = null;
2473
- type ExtraApiHandler = (req: IncomingMessage, res: ServerResponse) => Promise<boolean> | boolean;
2474
- let extraApiHandler: ExtraApiHandler | null = null;
2475
-
2476
- /**
2477
- * 注册"reload config"回调。约定:
2478
- * - 由 index.ts 在 main() 中一次性调用,传入 () => reloadConfigFromDisk()。
2479
- * - handleStartService 在 path="reload" 分支会 await 调用一次;hook 抛错会被
2480
- * 捕获并以 500 回前端,避免 service 死锁。
2481
- */
2482
- export function setReloadConfigHook(hook: () => void | Promise<void>): void {
2483
- reloadConfigHook = hook;
2484
- }
2485
-
2486
- export function setExtraApiHandler(handler: ExtraApiHandler): void {
2487
- extraApiHandler = handler;
2488
- }
2489
-
2490
- export function startSetupMode(
2491
- port: number,
2492
- options: StartSetupModeOptions = {},
2493
- ): ReturnType<typeof createServer> {
2494
- const router = createUiRouter();
2495
- const server = createServer(router);
2496
- setupHttpServer = server;
2497
- setupActivateHook = options.onActivate ?? null;
2498
-
2499
- server.on("error", (err: NodeJS.ErrnoException) => {
2500
- if (err.code === "EADDRINUSE") {
2501
- console.error(`\n[WEB-UI] 端口 ${port} 已被占用。请检查是否有其他 ChatCCC 实例在运行。`);
2502
- console.error(" 可以先停止旧进程,或修改 config.json 中的 port 为其他端口。");
2503
- } else {
2504
- console.error(`\n[WEB-UI] HTTP 服务器错误: ${err.message}`);
2505
- }
2506
- process.exit(1);
2507
- });
2508
-
2509
- server.listen(port, "127.0.0.1", () => {
2510
- const url = buildWebUiUrl(port);
2511
- console.log("");
2512
- console.log("=".repeat(60));
2513
- console.log(" ChatCCC — 首次配置向导");
2514
- console.log("=".repeat(60));
2515
- console.log(" 未检测到已配置的飞书凭证,已启动配置界面。");
2516
- if (options.openBrowser !== false) {
2517
- console.log(` 已启用启动时自动打开浏览器: ${url}`);
2518
- } else {
2519
- console.log(` 内部重启不会自动打开浏览器,请按需访问: ${url}`);
2520
- }
2521
- console.log(" 若浏览器未自动弹出,请手动访问上面的地址。");
2522
- console.log("");
2523
- console.log(" 在向导里填好 App ID / App Secret 后点「保存并启动」,");
2524
- console.log(" 服务会在当前进程内直接激活,不需要重新运行 chatccc。");
2525
- console.log("=".repeat(60));
2526
- console.log("");
2527
- if (options.openBrowser !== false) openWebUiInDefaultBrowser(port);
2528
- });
2529
- return server;
2530
- }
2326
+ </html>`;
2327
+ // ---------------------------------------------------------------------------
2328
+ // Router
2329
+ // ---------------------------------------------------------------------------
2330
+ async function handleRequest(req, res) {
2331
+ const url = req.url ?? "/";
2332
+ const method = req.method ?? "GET";
2333
+ const pathname = url.split("?", 1)[0];
2334
+ if (extraApiHandler && await extraApiHandler(req, res))
2335
+ return;
2336
+ if (await handleAgentTeamRequest(req, res))
2337
+ return;
2338
+ // API routes
2339
+ if (url === "/api/check" && method === "GET")
2340
+ return handleApiCheck(req, res);
2341
+ if (url === "/api/config" && method === "GET")
2342
+ return handleGetConfig(req, res);
2343
+ if (url === "/api/config" && method === "POST")
2344
+ return handlePostConfig(req, res);
2345
+ if (url === "/api/status" && method === "GET")
2346
+ return handleGetStatus(req, res);
2347
+ if (url === "/api/start" && method === "POST")
2348
+ return handleStartService(req, res);
2349
+ if (url === "/api/stop" && method === "POST")
2350
+ return handleStopService(req, res);
2351
+ if (url === "/api/restart" && method === "POST")
2352
+ return handleRestartService(req, res);
2353
+ if (url === "/api/validate" && method === "POST")
2354
+ return handleValidate(req, res);
2355
+ if (url === "/api/ilink/forget" && method === "POST")
2356
+ return handleForgetIlink(req, res);
2357
+ if (url === "/api/claude-sdk/status" && method === "GET")
2358
+ return handleClaudeSdkStatus(req, res);
2359
+ if (url === "/api/claude-sdk/install" && method === "POST")
2360
+ return handleClaudeSdkInstall(req, res);
2361
+ if (method === "GET" && (pathname === "/agent-team" || pathname === "/agent-team/")) {
2362
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
2363
+ res.end(AGENT_TEAM_PAGE_HTML);
2364
+ return;
2365
+ }
2366
+ // Serve HTML page for all other GET requests
2367
+ if (method === "GET") {
2368
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
2369
+ res.end(PAGE_HTML);
2370
+ return;
2371
+ }
2372
+ jsonReply(res, 404, { error: "Not found" });
2373
+ }
2374
+ export function createUiRouter() {
2375
+ return (req, res) => {
2376
+ handleRequest(req, res).catch((err) => {
2377
+ console.error(`[WEB-UI] Unhandled error: ${err.message}`);
2378
+ if (!res.headersSent)
2379
+ jsonReply(res, 500, { error: "Internal error" });
2380
+ });
2381
+ };
2382
+ }
2383
+ // setup HTTP server + onActivate 回调通过模块级变量暴露给 handleStartService。
2384
+ // 一旦 onActivate 成功执行,setupActivateHook 会被清空——避免 dashboard 模式下
2385
+ // 用户再点"启动"时还走 inplace 路径(service 已经在跑了)。
2386
+ let setupHttpServer = null;
2387
+ let setupActivateHook = null;
2388
+ // reload-config 回调:dashboard 模式下用户点"保存并启动"时,service 已经在跑,
2389
+ // 仅需要把磁盘上刚保存的 config.json 刷进进程内的 export let 常量(live binding)。
2390
+ // index.ts 注入,因为 web-ui.ts 自身**不应**直接 import config.ts——后者顶层
2391
+ // loadConfig 副作用,被 web-ui.ts 间接 import 会污染所有依赖 web-ui.ts 的单测。
2392
+ let reloadConfigHook = null;
2393
+ let extraApiHandler = null;
2394
+ /**
2395
+ * 注册"reload config"回调。约定:
2396
+ * - 由 index.ts 在 main() 中一次性调用,传入 () => reloadConfigFromDisk()。
2397
+ * - handleStartService 在 path="reload" 分支会 await 调用一次;hook 抛错会被
2398
+ * 捕获并以 500 回前端,避免 service 死锁。
2399
+ */
2400
+ export function setReloadConfigHook(hook) {
2401
+ reloadConfigHook = hook;
2402
+ }
2403
+ export function setExtraApiHandler(handler) {
2404
+ extraApiHandler = handler;
2405
+ }
2406
+ export function startSetupMode(port, options = {}) {
2407
+ const router = createUiRouter();
2408
+ const server = createServer(router);
2409
+ setupHttpServer = server;
2410
+ setupActivateHook = options.onActivate ?? null;
2411
+ server.on("error", (err) => {
2412
+ if (err.code === "EADDRINUSE") {
2413
+ console.error(`\n[WEB-UI] 端口 ${port} 已被占用。请检查是否有其他 ChatCCC 实例在运行。`);
2414
+ console.error(" 可以先停止旧进程,或修改 config.json 中的 port 为其他端口。");
2415
+ }
2416
+ else {
2417
+ console.error(`\n[WEB-UI] HTTP 服务器错误: ${err.message}`);
2418
+ }
2419
+ process.exit(1);
2420
+ });
2421
+ server.listen(port, "127.0.0.1", () => {
2422
+ const url = buildWebUiUrl(port);
2423
+ console.log("");
2424
+ console.log("=".repeat(60));
2425
+ console.log(" ChatCCC — 首次配置向导");
2426
+ console.log("=".repeat(60));
2427
+ console.log(" 未检测到已配置的飞书凭证,已启动配置界面。");
2428
+ if (options.openBrowser !== false) {
2429
+ console.log(` 已启用启动时自动打开浏览器: ${url}`);
2430
+ }
2431
+ else {
2432
+ console.log(` 内部重启不会自动打开浏览器,请按需访问: ${url}`);
2433
+ }
2434
+ console.log(" 若浏览器未自动弹出,请手动访问上面的地址。");
2435
+ console.log("");
2436
+ console.log(" 在向导里填好 App ID / App Secret 后点「保存并启动」,");
2437
+ console.log(" 服务会在当前进程内直接激活,不需要重新运行 chatccc。");
2438
+ console.log("=".repeat(60));
2439
+ console.log("");
2440
+ if (options.openBrowser !== false)
2441
+ openWebUiInDefaultBrowser(port);
2442
+ });
2443
+ return server;
2444
+ }