chatccc 0.2.252 → 0.2.253

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 (280) hide show
  1. package/README.md +1 -1
  2. package/bin/cccagent.mjs +12 -3
  3. package/bin/chatccc.mjs +15 -6
  4. package/deepccc-agent/os-prompts/darwin.md +8 -8
  5. package/deepccc-agent/os-prompts/linux.md +8 -8
  6. package/deepccc-agent/os-prompts/win32.md +11 -11
  7. package/deepccc-agent/package.json +65 -65
  8. package/dist/deepccc-agent/src/cli.js +634 -0
  9. package/dist/deepccc-agent/src/config.js +76 -0
  10. package/dist/deepccc-agent/src/context.js +348 -0
  11. package/dist/deepccc-agent/src/file-log.js +34 -0
  12. package/dist/deepccc-agent/src/file-tools.js +1208 -0
  13. package/dist/deepccc-agent/src/index.js +571 -0
  14. package/dist/deepccc-agent/src/permissions.js +170 -0
  15. package/dist/deepccc-agent/src/privacy.js +124 -0
  16. package/dist/deepccc-agent/src/proc-tree-kill.js +60 -0
  17. package/dist/deepccc-agent/src/progress/cards-helpers.js +70 -0
  18. package/dist/deepccc-agent/src/progress/reducer.js +102 -0
  19. package/dist/deepccc-agent/src/progress/terminal-renderer.js +264 -0
  20. package/dist/deepccc-agent/src/progress/view.js +30 -0
  21. package/dist/deepccc-agent/src/raw-stream-log.js +106 -0
  22. package/dist/deepccc-agent/src/session-search.js +276 -0
  23. package/dist/deepccc-agent/src/session-select.js +23 -0
  24. package/dist/deepccc-agent/src/sigint.js +26 -0
  25. package/dist/deepccc-agent/src/skills.js +178 -0
  26. package/dist/deepccc-agent/src/web-tools.js +246 -0
  27. package/dist/src/adapters/adapter-interface.js +19 -0
  28. package/dist/src/adapters/ccc-adapter.js +112 -0
  29. package/dist/src/adapters/claude-adapter.js +497 -0
  30. package/dist/src/adapters/claude-session-meta-store.js +92 -0
  31. package/dist/src/adapters/codex-adapter.js +279 -0
  32. package/dist/src/adapters/codex-session-meta-store.js +94 -0
  33. package/dist/src/adapters/cursor-adapter.js +491 -0
  34. package/dist/src/adapters/cursor-session-meta-store.js +116 -0
  35. package/dist/src/adapters/jsonl-stream.js +104 -0
  36. package/{src/adapters/proc-tree-kill.ts → dist/src/adapters/proc-tree-kill.js} +94 -97
  37. package/dist/src/adapters/raw-stream-log.js +106 -0
  38. package/dist/src/adapters/resource-monitor.js +113 -0
  39. package/dist/src/agent-activity.js +133 -0
  40. package/dist/src/agent-delegate-task-rpc.js +129 -0
  41. package/dist/src/agent-delegate-task.js +48 -0
  42. package/dist/src/agent-file-rpc.js +152 -0
  43. package/dist/src/agent-image-rpc.js +148 -0
  44. package/dist/src/agent-platform-routing.js +13 -0
  45. package/dist/src/agent-reload-config-rpc.js +23 -0
  46. package/dist/src/agent-rpc-body.js +87 -0
  47. package/dist/src/agent-stop-stuck.js +110 -0
  48. package/dist/src/card-action-routing.js +7 -0
  49. package/dist/src/card-plain-text.js +101 -0
  50. package/dist/src/cardkit.js +158 -0
  51. package/dist/src/cards.js +573 -0
  52. package/dist/src/chatgpt-subscription-rpc.js +18 -0
  53. package/dist/src/chatgpt-subscription.js +199 -0
  54. package/dist/src/chrome-devtools-guard.js +238 -0
  55. package/dist/src/claude-sdk-installer.js +249 -0
  56. package/dist/src/codex-reset-actions.js +143 -0
  57. package/dist/src/config-utils.js +149 -0
  58. package/dist/src/config.js +804 -0
  59. package/dist/src/cursor-usage.js +77 -0
  60. package/dist/src/exit-banner.js +28 -0
  61. package/dist/src/feishu-api.js +1404 -0
  62. package/dist/src/feishu-message-ingress.js +137 -0
  63. package/dist/src/feishu-platform.js +97 -0
  64. package/dist/src/format-message.js +252 -0
  65. package/dist/src/git-command.js +155 -0
  66. package/dist/src/im-skills.js +121 -0
  67. package/dist/src/index.js +833 -0
  68. package/dist/src/litellm-proxy.js +300 -0
  69. package/dist/src/orchestrator.js +2078 -0
  70. package/dist/src/package-root.js +26 -0
  71. package/dist/src/platform-adapter.js +7 -0
  72. package/dist/src/platform-startup.js +6 -0
  73. package/dist/src/privacy.js +100 -0
  74. package/dist/src/progress/reducer.js +102 -0
  75. package/dist/src/progress/terminal-renderer.js +264 -0
  76. package/dist/src/progress/view.js +30 -0
  77. package/dist/src/response-stall.js +14 -0
  78. package/dist/src/runtime-entry.js +13 -0
  79. package/dist/src/runtime-reload.js +19 -0
  80. package/dist/src/session-chat-binding.js +183 -0
  81. package/dist/src/session-name.js +7 -0
  82. package/dist/src/session.js +2144 -0
  83. package/dist/src/shared-prefix.js +16 -0
  84. package/dist/src/shared.js +493 -0
  85. package/dist/src/sim-agent.js +105 -0
  86. package/dist/src/sim-platform.js +142 -0
  87. package/dist/src/sim-store.js +231 -0
  88. package/dist/src/simplify.js +99 -0
  89. package/dist/src/startup-lifecycle.js +209 -0
  90. package/dist/src/stream-state.js +141 -0
  91. package/dist/src/terminal-error.js +100 -0
  92. package/dist/src/trace.js +50 -0
  93. package/dist/src/turn-cards.js +92 -0
  94. package/dist/src/update-command-guard.js +114 -0
  95. package/{src/web-ui.ts → dist/src/web-ui.js} +749 -823
  96. package/dist/src/wechat-platform.js +545 -0
  97. package/package.json +76 -74
  98. package/deepccc-agent/LICENSE +0 -201
  99. package/deepccc-agent/bin/deepccc.mjs +0 -26
  100. package/deepccc-agent/docs/cache-hit-rate-1.jpg +0 -0
  101. package/deepccc-agent/docs/cache-hit-rate-2.jpg +0 -0
  102. package/deepccc-agent/package-lock.json +0 -2027
  103. package/deepccc-agent/src/__tests__/chat-session.test.ts +0 -877
  104. package/deepccc-agent/src/__tests__/cli-json.test.ts +0 -49
  105. package/deepccc-agent/src/__tests__/config.test.ts +0 -34
  106. package/deepccc-agent/src/__tests__/context.test.ts +0 -341
  107. package/deepccc-agent/src/__tests__/file-tools.test.ts +0 -240
  108. package/deepccc-agent/src/__tests__/permissions.test.ts +0 -199
  109. package/deepccc-agent/src/__tests__/privacy.test.ts +0 -318
  110. package/deepccc-agent/src/__tests__/progress-reducer.test.ts +0 -121
  111. package/deepccc-agent/src/__tests__/session-search.test.ts +0 -262
  112. package/deepccc-agent/src/__tests__/session-select.test.ts +0 -116
  113. package/deepccc-agent/src/__tests__/sigint.test.ts +0 -56
  114. package/deepccc-agent/src/__tests__/skills.test.ts +0 -284
  115. package/deepccc-agent/src/__tests__/terminal-renderer.test.ts +0 -247
  116. package/deepccc-agent/src/__tests__/web-tools.test.ts +0 -220
  117. package/deepccc-agent/src/cli.ts +0 -682
  118. package/deepccc-agent/src/config.ts +0 -101
  119. package/deepccc-agent/src/context.ts +0 -465
  120. package/deepccc-agent/src/file-log.ts +0 -38
  121. package/deepccc-agent/src/file-tools.ts +0 -1493
  122. package/deepccc-agent/src/index.ts +0 -710
  123. package/deepccc-agent/src/permissions.ts +0 -226
  124. package/deepccc-agent/src/privacy.ts +0 -141
  125. package/deepccc-agent/src/proc-tree-kill.ts +0 -61
  126. package/deepccc-agent/src/progress/cards-helpers.ts +0 -76
  127. package/deepccc-agent/src/progress/reducer.ts +0 -113
  128. package/deepccc-agent/src/progress/terminal-renderer.ts +0 -294
  129. package/deepccc-agent/src/progress/view.ts +0 -77
  130. package/deepccc-agent/src/raw-stream-log.ts +0 -124
  131. package/deepccc-agent/src/session-search.ts +0 -370
  132. package/deepccc-agent/src/session-select.ts +0 -48
  133. package/deepccc-agent/src/sigint.ts +0 -50
  134. package/deepccc-agent/src/skills.ts +0 -205
  135. package/deepccc-agent/src/web-tools.ts +0 -313
  136. package/deepccc-agent/tsconfig.build.json +0 -13
  137. package/deepccc-agent/tsconfig.json +0 -13
  138. package/deepccc-agent/vitest.config.ts +0 -7
  139. package/src/__tests__/adapter-interface.test.ts +0 -152
  140. package/src/__tests__/agent-activity.test.ts +0 -86
  141. package/src/__tests__/agent-delegate-task-rpc.test.ts +0 -165
  142. package/src/__tests__/agent-image-rpc.test.ts +0 -34
  143. package/src/__tests__/agent-platform-routing.test.ts +0 -26
  144. package/src/__tests__/agent-reload-config-rpc.test.ts +0 -99
  145. package/src/__tests__/agent-rpc-body.test.ts +0 -42
  146. package/src/__tests__/builtin-chat-session.test.ts +0 -532
  147. package/src/__tests__/builtin-cli-json.test.ts +0 -39
  148. package/src/__tests__/builtin-config.test.ts +0 -26
  149. package/src/__tests__/builtin-context.test.ts +0 -319
  150. package/src/__tests__/builtin-file-tools.test.ts +0 -240
  151. package/src/__tests__/builtin-permissions.test.ts +0 -219
  152. package/src/__tests__/builtin-session-search.test.ts +0 -262
  153. package/src/__tests__/builtin-session-select.test.ts +0 -116
  154. package/src/__tests__/builtin-sigint.test.ts +0 -56
  155. package/src/__tests__/builtin-skills.test.ts +0 -284
  156. package/src/__tests__/builtin-web-tools.test.ts +0 -220
  157. package/src/__tests__/card-action-routing.test.ts +0 -18
  158. package/src/__tests__/card-plain-text.test.ts +0 -45
  159. package/src/__tests__/cardkit.test.ts +0 -60
  160. package/src/__tests__/cards.test.ts +0 -607
  161. package/src/__tests__/ccc-adapter.test.ts +0 -194
  162. package/src/__tests__/chatgpt-subscription-rpc.test.ts +0 -89
  163. package/src/__tests__/chatgpt-subscription.test.ts +0 -135
  164. package/src/__tests__/chrome-devtools-guard.test.ts +0 -165
  165. package/src/__tests__/claude-adapter.test.ts +0 -614
  166. package/src/__tests__/claude-raw-stream-log.test.ts +0 -96
  167. package/src/__tests__/claude-sdk-installer.test.ts +0 -285
  168. package/src/__tests__/codex-adapter.test.ts +0 -331
  169. package/src/__tests__/codex-raw-stream-log.test.ts +0 -170
  170. package/src/__tests__/codex-reset-actions.test.ts +0 -146
  171. package/src/__tests__/config-reload.test.ts +0 -284
  172. package/src/__tests__/config-sample.test.ts +0 -97
  173. package/src/__tests__/config-utils.test.ts +0 -40
  174. package/src/__tests__/config.test.ts +0 -395
  175. package/src/__tests__/crash-logging.test.ts +0 -360
  176. package/src/__tests__/cursor-adapter.test.ts +0 -890
  177. package/src/__tests__/cursor-session-meta-store.test.ts +0 -212
  178. package/src/__tests__/feishu-api.test.ts +0 -60
  179. package/src/__tests__/feishu-avatar.test.ts +0 -504
  180. package/src/__tests__/feishu-message-ingress.test.ts +0 -138
  181. package/src/__tests__/feishu-platform.test.ts +0 -75
  182. package/src/__tests__/fixtures/codex_simple_text.jsonl +0 -4
  183. package/src/__tests__/fixtures/codex_with_tool.jsonl +0 -6
  184. package/src/__tests__/fixtures/cursor_partial_only.jsonl +0 -5
  185. package/src/__tests__/fixtures/cursor_partial_with_final.jsonl +0 -13
  186. package/src/__tests__/fixtures/cursor_with_tool_call.jsonl +0 -12
  187. package/src/__tests__/format-message.test.ts +0 -316
  188. package/src/__tests__/git-command.test.ts +0 -288
  189. package/src/__tests__/im-skills.test.ts +0 -125
  190. package/src/__tests__/jsonl-stream.test.ts +0 -79
  191. package/src/__tests__/orchestrator.test.ts +0 -1268
  192. package/src/__tests__/package-files.test.ts +0 -24
  193. package/src/__tests__/platform-startup.test.ts +0 -19
  194. package/src/__tests__/privacy.test.ts +0 -198
  195. package/src/__tests__/proc-tree-kill.test.ts +0 -108
  196. package/src/__tests__/progress-reducer.test.ts +0 -121
  197. package/src/__tests__/raw-stream-log.test.ts +0 -106
  198. package/src/__tests__/response-stall.test.ts +0 -49
  199. package/src/__tests__/restart.test.ts +0 -232
  200. package/src/__tests__/session-ccc-config.test.ts +0 -66
  201. package/src/__tests__/session.test.ts +0 -3004
  202. package/src/__tests__/shared-prefix.test.ts +0 -36
  203. package/src/__tests__/sim-agent.test.ts +0 -174
  204. package/src/__tests__/sim-platform.test.ts +0 -93
  205. package/src/__tests__/sim-store.test.ts +0 -214
  206. package/src/__tests__/simplify.test.ts +0 -283
  207. package/src/__tests__/startup-lifecycle.test.ts +0 -231
  208. package/src/__tests__/stop-session.test.ts +0 -162
  209. package/src/__tests__/stream-state.test.ts +0 -164
  210. package/src/__tests__/terminal-error.test.ts +0 -54
  211. package/src/__tests__/terminal-renderer.test.ts +0 -247
  212. package/src/__tests__/update-command-guard.test.ts +0 -144
  213. package/src/__tests__/web-ui.test.ts +0 -438
  214. package/src/__tests__/wechat-platform.test.ts +0 -111
  215. package/src/adapters/adapter-interface.ts +0 -217
  216. package/src/adapters/ccc-adapter.ts +0 -150
  217. package/src/adapters/claude-adapter.ts +0 -673
  218. package/src/adapters/claude-session-meta-store.ts +0 -120
  219. package/src/adapters/codex-adapter.ts +0 -426
  220. package/src/adapters/codex-session-meta-store.ts +0 -131
  221. package/src/adapters/cursor-adapter.ts +0 -681
  222. package/src/adapters/cursor-session-meta-store.ts +0 -154
  223. package/src/adapters/jsonl-stream.ts +0 -157
  224. package/src/adapters/raw-stream-log.ts +0 -124
  225. package/src/adapters/resource-monitor.ts +0 -141
  226. package/src/agent-activity.ts +0 -175
  227. package/src/agent-delegate-task-rpc.ts +0 -153
  228. package/src/agent-delegate-task.ts +0 -91
  229. package/src/agent-file-rpc.ts +0 -172
  230. package/src/agent-image-rpc.ts +0 -168
  231. package/src/agent-platform-routing.ts +0 -28
  232. package/src/agent-reload-config-rpc.ts +0 -34
  233. package/src/agent-rpc-body.ts +0 -92
  234. package/src/agent-stop-stuck.ts +0 -129
  235. package/src/card-action-routing.ts +0 -14
  236. package/src/card-plain-text.ts +0 -108
  237. package/src/cardkit.ts +0 -179
  238. package/src/cards.ts +0 -684
  239. package/src/chatgpt-subscription-rpc.ts +0 -27
  240. package/src/chatgpt-subscription.ts +0 -299
  241. package/src/chrome-devtools-guard.ts +0 -318
  242. package/src/claude-sdk-installer.ts +0 -324
  243. package/src/codex-reset-actions.ts +0 -184
  244. package/src/config-utils.ts +0 -211
  245. package/src/config.ts +0 -1063
  246. package/src/cursor-usage.ts +0 -128
  247. package/src/exit-banner.ts +0 -33
  248. package/src/feishu-api.ts +0 -1616
  249. package/src/feishu-message-ingress.ts +0 -195
  250. package/src/feishu-platform.ts +0 -159
  251. package/src/format-message.ts +0 -293
  252. package/src/git-command.ts +0 -202
  253. package/src/im-skills.ts +0 -149
  254. package/src/index.ts +0 -1089
  255. package/src/litellm-proxy.ts +0 -374
  256. package/src/orchestrator.ts +0 -2543
  257. package/src/platform-adapter.ts +0 -70
  258. package/src/platform-startup.ts +0 -16
  259. package/src/privacy.ts +0 -118
  260. package/src/progress/reducer.ts +0 -113
  261. package/src/progress/terminal-renderer.ts +0 -294
  262. package/src/progress/view.ts +0 -77
  263. package/src/response-stall.ts +0 -28
  264. package/src/runtime-reload.ts +0 -34
  265. package/src/session-chat-binding.ts +0 -292
  266. package/src/session-name.ts +0 -8
  267. package/src/session.ts +0 -2659
  268. package/src/shared-prefix.ts +0 -29
  269. package/src/shared.ts +0 -552
  270. package/src/sim-agent.ts +0 -167
  271. package/src/sim-platform.ts +0 -177
  272. package/src/sim-store.ts +0 -317
  273. package/src/simplify.ts +0 -120
  274. package/src/startup-lifecycle.ts +0 -250
  275. package/src/stream-state.ts +0 -177
  276. package/src/terminal-error.ts +0 -129
  277. package/src/trace.ts +0 -51
  278. package/src/turn-cards.ts +0 -118
  279. package/src/update-command-guard.ts +0 -165
  280. package/src/wechat-platform.ts +0 -680
@@ -0,0 +1,833 @@
1
+ /**
2
+ * ChatCCC — Feishu Bot Bridge for AI Coding Tools (TypeScript)
3
+ * =================================================================
4
+ * Supported tools: Claude Code, Cursor, Codex (OpenAI).
5
+ *
6
+ * When a user sends "/new [tool]" to the bot (omitting tool uses the configured default Agent):
7
+ * 1. Create an AI tool session via the corresponding adapter, get session ID
8
+ * 2. Create a new Feishu group chat and add the user
9
+ * 3. Rename the group (name + description) to the session ID
10
+ * 4. Reply to the new group with session info and welcome message
11
+ *
12
+ * Auto-resume: when any message is received in a session group
13
+ * (group description contains a tool-specific prefix), the bot extracts the
14
+ * session ID, resumes the session, sends the user's text, and
15
+ * streams the response to the session's jsonl file.
16
+ *
17
+ * Buttons: progress cards have a 停止 button; help messages have /new and /restart buttons.
18
+ *
19
+ * Usage:
20
+ * npm run dev
21
+ * npm run start
22
+ * npm run demo:create-group -- --local (local relay mode)
23
+ */
24
+ import { createServer } from "node:http";
25
+ import { WSClient, EventDispatcher, Domain } from "@larksuiteoapi/node-sdk";
26
+ import WebSocket from "ws";
27
+ import { appendStartupTrace, attachRelayWebSocket, ensureSingleInstance, freeRelayListenPort, installCrashLogging, installEpipeGuard, waitForPortFree } from "./shared.js";
28
+ import { createUiRouter, setExtraApiHandler, setReloadConfigHook, startSetupMode } from "./web-ui.js";
29
+ import { buildWebUiUrl, createServiceLifecycleGuard, INTERNAL_RESTART_ENV_VAR, openWebUiInDefaultBrowser, shouldAutoOpenWebUi, } from "./startup-lifecycle.js";
30
+ import { buildPlatformStartupPlan } from "./platform-startup.js";
31
+ import { makeTraceId, logTrace } from "./trace.js";
32
+ import { CHATCCC_PORT, config, APP_ID, APP_SECRET, FEISHU_ENABLED, FEISHU_PLATFORM_TYPE, ILINK_ENABLED, ILINK_REUSE_TOKEN_ON_START, BASE_URL, LOCAL_RELAY_URL, PID_FILE, PROJECT_ROOT, USE_LOCAL, USE_SIMULATE, appendChatLog, fileLog, reportEnvironmentVariableReadout, maskAppId, resolveDefaultAgentTool, toolDisplayName, ts, } from "./config.js";
33
+ import { printServiceDidNotStart, printServiceRunningHint } from "./exit-banner.js";
34
+ import { addReaction, createGroupChat, extractSessionInfo, formatDelayNotice, getChatInfo, getTenantAccessToken, recallMessage, sendCardReply, sendRawCard, sendTextReply, setChatAvatar, updateCardMessage, updateChatInfo, disbandChat, sendRestartCard, verifyAllPermissions, reportPermissionResults, setPlatform, consumeCodexRateLimitResetCredit, } from "./feishu-platform.js";
35
+ import { SimulatedPlatform, SIM_DEFAULT_CHAT_ID } from "./sim-platform.js";
36
+ import { setMessageHandler } from "./sim-store.js";
37
+ import { handleAgentImageRequest } from "./agent-image-rpc.js";
38
+ import { handleAgentFileRequest } from "./agent-file-rpc.js";
39
+ import { handleAgentDelegateTaskRequest } from "./agent-delegate-task-rpc.js";
40
+ import { handleAgentStopStuckRequest } from "./agent-stop-stuck.js";
41
+ import { handleAgentReloadConfigRequest } from "./agent-reload-config-rpc.js";
42
+ import { handleChatGptSubscriptionRequest } from "./chatgpt-subscription-rpc.js";
43
+ import { applyPrivacy } from "./privacy.js";
44
+ import { createCardKitCard, sendCardKitMessage, updateCardKitCard, } from "./cardkit.js";
45
+ import { loadSessionRegistryForBinding, rebuildBindingsFromRegistry, resetState, setSessionPlatform, startUnifiedDisplayLoop, } from "./session.js";
46
+ import { startChromeDevtoolsGuard, stopChromeDevtoolsGuard } from "./chrome-devtools-guard.js";
47
+ import { rebuildSessionChatsFromRegistry, setQueueConsumer, } from "./session-chat-binding.js";
48
+ import { fixStaleStreamStates } from "./stream-state.js";
49
+ import { handleCommand } from "./orchestrator.js";
50
+ import { createWechatAdapter, startWechatPlatform } from "./wechat-platform.js";
51
+ import { handleCodexResetCardAction } from "./codex-reset-actions.js";
52
+ import { resolveFeishuCardActionChatType } from "./card-action-routing.js";
53
+ import { reloadRuntimeConfig } from "./runtime-reload.js";
54
+ import { createAckFirstEventHandler, feishuMessageLedger, } from "./feishu-message-ingress.js";
55
+ // ---------------------------------------------------------------------------
56
+ // Feishu 平台适配器
57
+ // ---------------------------------------------------------------------------
58
+ function createFeishuAdapter() {
59
+ const auth = () => getTenantAccessToken();
60
+ return {
61
+ kind: "feishu",
62
+ // ---- 基础消息 ----
63
+ async sendText(chatId, text) {
64
+ return sendTextReply(await auth(), chatId, text);
65
+ },
66
+ async sendCard(chatId, title, content, template) {
67
+ return sendCardReply(await auth(), chatId, title, content, template);
68
+ },
69
+ async sendRawCard(chatId, cardJson) {
70
+ return sendRawCard(await auth(), chatId, cardJson);
71
+ },
72
+ // ---- 群聊管理 ----
73
+ async createGroup(name, userIds) {
74
+ return createGroupChat(await auth(), name, userIds);
75
+ },
76
+ async updateChatInfo(chatId, name, description) {
77
+ return updateChatInfo(await auth(), chatId, name, description);
78
+ },
79
+ async getChatInfo(chatId) {
80
+ return getChatInfo(await auth(), chatId);
81
+ },
82
+ async disbandChat(chatId) {
83
+ return disbandChat(await auth(), chatId);
84
+ },
85
+ async setChatAvatar(chatId, tool, status, usageHints) {
86
+ return setChatAvatar(await auth(), chatId, tool, status, usageHints);
87
+ },
88
+ extractSessionInfo(description) {
89
+ return extractSessionInfo(description);
90
+ },
91
+ // ---- 进度展示(CardKit 委托) ----
92
+ async cardCreate(cardJson) {
93
+ return createCardKitCard(await auth(), applyPrivacy(cardJson));
94
+ },
95
+ async cardSend(chatId, cardId) {
96
+ return sendCardKitMessage(await auth(), chatId, cardId);
97
+ },
98
+ async cardUpdate(cardId, cardJson, sequence) {
99
+ return updateCardKitCard(await auth(), cardId, applyPrivacy(cardJson), sequence);
100
+ },
101
+ };
102
+ }
103
+ const feishuPlatform = createFeishuAdapter();
104
+ const wechatPlatform = createWechatAdapter();
105
+ setSessionPlatform(feishuPlatform);
106
+ // 注册队列消费回调:session 生成完成后自动处理缓存消息
107
+ setQueueConsumer((platform, msg) => {
108
+ handleCommand(platform, msg.text, msg.chatId, msg.openId, msg.msgTimestamp, msg.chatType, msg.traceId).catch(err => console.error(`[${ts()}] Queue consume failed: ${err.message}`));
109
+ });
110
+ function getInnerEvent(data) {
111
+ return (data.event ?? data);
112
+ }
113
+ import { formatMessageContent } from "./format-message.js";
114
+ import { buildUpdateCommandId, extractFeishuEventId, } from "./update-command-guard.js";
115
+ function parseCardAction(data) {
116
+ const raw = data?.event ?? data;
117
+ const action = raw?.action;
118
+ if (!action?.value)
119
+ return null;
120
+ let cmd;
121
+ if (typeof action.value === "object" && action.value !== null) {
122
+ cmd = action.value.action;
123
+ }
124
+ else if (typeof action.value === "string") {
125
+ try {
126
+ let v = JSON.parse(action.value);
127
+ if (typeof v === "string")
128
+ v = JSON.parse(v);
129
+ cmd = v.cmd ?? v.action;
130
+ }
131
+ catch {
132
+ return null;
133
+ }
134
+ }
135
+ if (!cmd)
136
+ return null;
137
+ const CMD_MAP = { stop: "/stop", cancel: "/cancel", new: "/new", "new claude": "/new claude", "new cursor": "/new cursor", "new codex": "/new codex", restart: "/restart", update: "/update", state: "/state", cd: "/cd", sessions: "/sessions", newh: "/newh" };
138
+ let text = CMD_MAP[cmd] ?? "";
139
+ if (cmd === "cd" && typeof action.value === "object" && action.value !== null) {
140
+ const path = action.value.path;
141
+ if (path)
142
+ text = `/cd ${path}`;
143
+ }
144
+ // cmd 本身就是以 / 开头的完整指令时,直接使用(如 /model <name> 动态按钮)
145
+ if (!text && cmd.startsWith("/"))
146
+ text = cmd;
147
+ if (!text)
148
+ return null;
149
+ const chatId = raw.open_chat_id ??
150
+ raw.context?.open_chat_id ??
151
+ raw.message?.chat_id ??
152
+ "";
153
+ const openId = raw.operator?.open_id ??
154
+ "";
155
+ return {
156
+ text,
157
+ chatId,
158
+ openId,
159
+ commandId: buildUpdateCommandId("card", extractFeishuEventId(data)),
160
+ };
161
+ }
162
+ // ---------------------------------------------------------------------------
163
+ // WebSocket relay broadcast
164
+ // ---------------------------------------------------------------------------
165
+ let broadcastToRelay = () => { };
166
+ const wechatSignal = { stopped: false };
167
+ async function processFeishuMessageEvent(data) {
168
+ const traceId = makeTraceId();
169
+ try {
170
+ broadcastToRelay(data);
171
+ const event = getInnerEvent(data);
172
+ const message = event.message;
173
+ if (!message)
174
+ return;
175
+ const messageId = message.message_id;
176
+ const chatId = message.chat_id ?? "";
177
+ const chatType = message.chat_type ?? "group";
178
+ const msgTimestamp = parseInt(message.create_time ?? "0", 10) || Date.now();
179
+ const disposition = await feishuMessageLedger.accept({
180
+ messageId,
181
+ chatId,
182
+ createTime: msgTimestamp,
183
+ });
184
+ if (disposition === "duplicate") {
185
+ console.log(`[MSG] Duplicate message ignored: ${messageId ?? "(missing id)"}`);
186
+ return;
187
+ }
188
+ if (disposition === "stale") {
189
+ logTrace(traceId, "DONE", {
190
+ outcome: "skip_stale_feishu_message",
191
+ messageId,
192
+ chatId,
193
+ msgTimestamp,
194
+ });
195
+ console.log(`[${ts()}] [SKIP] Stale Feishu message ignored: messageId=${messageId ?? "(missing id)"} chatId=${chatId} createTime=${msgTimestamp}`);
196
+ return;
197
+ }
198
+ const text = await formatMessageContent(message);
199
+ const openId = event.sender?.sender_id?.open_id ?? "";
200
+ console.log(`[MSG] id=${messageId ?? "(missing)"} sender=${openId} chat=${chatId} type=${chatType} text="${text}"`);
201
+ appendChatLog(chatId, openId, text);
202
+ if (messageId) {
203
+ getTenantAccessToken().then((freshToken) => addReaction(freshToken, messageId).catch((err) => console.error(`[${ts()}] Reaction failed: ${err.message}`))).catch((err) => console.error(`[${ts()}] Reaction token failed: ${err.message}`));
204
+ }
205
+ if (!text)
206
+ return;
207
+ logTrace(traceId, "RECV", {
208
+ messageId,
209
+ chatId,
210
+ chatType,
211
+ text: text.slice(0, 100),
212
+ });
213
+ const delayNotice = formatDelayNotice(msgTimestamp, text);
214
+ if (delayNotice) {
215
+ const delayToken = await getTenantAccessToken();
216
+ await sendCardReply(delayToken, chatId, "延迟送达", delayNotice, "yellow").catch(() => { });
217
+ // 延迟送达的旧消息只发提醒、不转发给 agent 执行:
218
+ // 断线重连或补推时,早已过期的消息不应未经确认就触发新任务。
219
+ // 用户如需执行可自行重发。
220
+ logTrace(traceId, "DONE", {
221
+ outcome: "skip_delayed_feishu_message",
222
+ chatId,
223
+ msgTimestamp,
224
+ delayMinutes: Math.floor((Date.now() - msgTimestamp) / 60000),
225
+ });
226
+ console.log(`[${ts()}] [SKIP] Delayed Feishu message not forwarded to agent: messageId=${messageId ?? "(missing id)"} chatId=${chatId} createTime=${msgTimestamp}`);
227
+ return;
228
+ }
229
+ await handleCommand(feishuPlatform, text, chatId, openId, msgTimestamp, chatType, traceId, buildUpdateCommandId("message", messageId));
230
+ }
231
+ catch (err) {
232
+ logTrace(traceId, "ERROR", { message: err.message });
233
+ console.error(`[${ts()}] [FATAL] im.message.receive_v1 worker crashed: ${err.message}`);
234
+ }
235
+ }
236
+ const handleFeishuMessageEventAckFirst = createAckFirstEventHandler(processFeishuMessageEvent, (err) => console.error(`[${ts()}] [FATAL] Detached Feishu event worker failed: ${err.message}`));
237
+ // ---------------------------------------------------------------------------
238
+ // Simulate mode: inject message via HTTP
239
+ // ---------------------------------------------------------------------------
240
+ async function handleSimInjectMessage(req, res) {
241
+ const url = new URL(req.url ?? "/", "http://127.0.0.1");
242
+ if (url.pathname !== "/api/sim/inject-message")
243
+ return false;
244
+ if (req.method !== "POST") {
245
+ res.writeHead(405, { "Content-Type": "application/json; charset=utf-8" });
246
+ res.end(JSON.stringify({ ok: false, error: "Method not allowed, use POST" }));
247
+ return true;
248
+ }
249
+ const chunks = [];
250
+ for await (const chunk of req) {
251
+ chunks.push(Buffer.from(chunk));
252
+ }
253
+ const body = Buffer.concat(chunks).toString("utf-8");
254
+ let parsed;
255
+ try {
256
+ parsed = JSON.parse(body);
257
+ }
258
+ catch {
259
+ res.writeHead(400, { "Content-Type": "application/json; charset=utf-8" });
260
+ res.end(JSON.stringify({ ok: false, error: "Invalid JSON body" }));
261
+ return true;
262
+ }
263
+ const text = parsed.text;
264
+ if (!text) {
265
+ res.writeHead(400, { "Content-Type": "application/json; charset=utf-8" });
266
+ res.end(JSON.stringify({ ok: false, error: "Missing 'text' field" }));
267
+ return true;
268
+ }
269
+ const chatId = parsed.chat_id || SIM_DEFAULT_CHAT_ID;
270
+ const openId = parsed.open_id || "sim_user_001";
271
+ const chatType = parsed.chat_type || "group";
272
+ console.log(`[${ts()}] [SIM:INJECT] chat=${chatId} text="${text.slice(0, 80)}"`);
273
+ appendChatLog(chatId, openId, text);
274
+ // Fire and forget: process command, respond 202 immediately
275
+ handleCommand(feishuPlatform, text, chatId, openId, Date.now(), chatType).catch((err) => console.error(`[${ts()}] [SIM:INJECT] handleCommand error: ${err.message}`));
276
+ res.writeHead(202, { "Content-Type": "application/json; charset=utf-8" });
277
+ res.end(JSON.stringify({ ok: true, chat_id: chatId }));
278
+ return true;
279
+ }
280
+ async function startBotService(opts) {
281
+ const { httpServer, port } = opts;
282
+ console.log(`\n[启动 3/7] 在 http://127.0.0.1:${port} 上挂载本地 WebSocket 中继 …`);
283
+ appendStartupTrace("startBotService: before attachRelayWebSocket", { port });
284
+ const wsAttachment = attachRelayWebSocket(httpServer);
285
+ broadcastToRelay = wsAttachment.broadcast;
286
+ // UI router 在 setup 模式下已经挂在 httpServer 上;main 直入模式由 main() 负责挂
287
+ // —— 这里不再额外 attach,避免重复触发 createUiRouter。
288
+ console.log(" 完成。\n");
289
+ // setup-activate 模式下,token / permissions / wsClient 任一阶段失败都需要
290
+ // 回滚已挂的 WSServer,否则用户重试 onActivate 时会重复挂载导致句柄泄漏。
291
+ // 用 try / catch 统一 wrap 后续启动逻辑。
292
+ try {
293
+ await startBotServiceCore();
294
+ }
295
+ catch (err) {
296
+ wsAttachment.close();
297
+ broadcastToRelay = () => { };
298
+ throw err;
299
+ }
300
+ }
301
+ async function startBotServiceCore() {
302
+ const modeTag = USE_LOCAL ? " (local relay mode)" : "";
303
+ console.log(`${"=".repeat(60)}`);
304
+ console.log(` ChatCCC — Feishu Bot Bridge for Claude Code${modeTag}`);
305
+ console.log(`${"=".repeat(60)}`);
306
+ console.log(` Send "/new" to the bot to create a new group + ${toolDisplayName(resolveDefaultAgentTool())} session.`);
307
+ console.log(` In a session group, send any message to resume & prompt.`);
308
+ console.log(`${"=".repeat(60)}`);
309
+ console.log(`\n[启动 4/7] 向飞书开放平台申请 tenant_access_token …`);
310
+ let token;
311
+ try {
312
+ token = await getTenantAccessToken();
313
+ }
314
+ catch (err) {
315
+ const msg = err instanceof Error ? err.message : String(err);
316
+ console.error(" 失败:无法获取 tenant_access_token。");
317
+ console.error(` 接口: POST ${BASE_URL}/auth/v3/tenant_access_token/internal`);
318
+ const apiHost = BASE_URL.replace("https://", "").replace("/open-apis", "");
319
+ console.error(" 常见原因:");
320
+ console.error(` - 本机网络无法访问 ${apiHost}(可尝试:关闭系统/终端代理、检查防火墙;Windows 可管理员运行 netsh winsock reset 后重启)`);
321
+ console.error(" - App ID / App Secret 与开放平台「凭证与基础信息」不一致");
322
+ console.error(" - 自建应用尚未创建/发布可用版本");
323
+ console.error(` 详情: ${msg}`);
324
+ throw new Error(`无法从飞书开放平台获取 tenant_access_token: ${msg}`);
325
+ }
326
+ console.log(` 完成。当前 App ID 摘要: ${maskAppId(APP_ID)}\n`);
327
+ console.log(`\n[启动 5/7] 验证飞书应用权限(非破坏性探针)…`);
328
+ const permResults = await verifyAllPermissions(token);
329
+ const hasFailed = reportPermissionResults(permResults, (msg) => {
330
+ console.log(msg);
331
+ });
332
+ if (hasFailed) {
333
+ const failedScopes = permResults.filter((r) => !r.ok).map((r) => r.scope).join(", ");
334
+ appendStartupTrace("startBotService: permissions check failed", { failed: failedScopes });
335
+ throw new Error(`飞书权限不足: ${failedScopes}`);
336
+ }
337
+ appendStartupTrace("startBotService: permissions check ok");
338
+ console.log(` 完成。所有必需权限已验证通过。\n`);
339
+ console.log(`[${ts()}] [AUTH] Token obtained`);
340
+ const eventDispatcher = new EventDispatcher({});
341
+ eventDispatcher.register({
342
+ "im.message.receive_v1": handleFeishuMessageEventAckFirst,
343
+ "card.action.trigger": async (data) => {
344
+ try {
345
+ // 拦截关闭按钮:先置空内容,再尝试撤回(撤回结果不影响后续流程)
346
+ const raw2 = data.event ?? data;
347
+ const action2 = raw2?.action;
348
+ const actionVal2 = action2?.value;
349
+ if (actionVal2?.action === "close") {
350
+ console.log(`[${ts()}] [CLOSE] close button clicked, raw keys: ${Object.keys(raw2).join(", ")}`);
351
+ const messageId = raw2?.open_message_id
352
+ ?? raw2?.context?.open_message_id;
353
+ console.log(`[${ts()}] [CLOSE] open_message_id=${messageId ?? "MISSING"}`);
354
+ if (messageId) {
355
+ const closeToken = await getTenantAccessToken();
356
+ updateCardMessage(closeToken, messageId, JSON.stringify({
357
+ config: { wide_screen_mode: true },
358
+ elements: [{ tag: "markdown", content: " " }],
359
+ })).catch((err) => {
360
+ console.error(`[${ts()}] [CLOSE] updateCardMessage failed: ${err.message}`);
361
+ });
362
+ recallMessage(closeToken, messageId).then((recalled) => {
363
+ console.log(`[${ts()}] [CLOSE] recall result: ${recalled ? "OK" : "FAILED"}`);
364
+ }).catch((err) => {
365
+ console.error(`[${ts()}] [CLOSE] recall failed: ${err.message}`);
366
+ });
367
+ }
368
+ else {
369
+ console.error(`[${ts()}] [CLOSE] no open_message_id in event, cannot close`);
370
+ }
371
+ return;
372
+ }
373
+ const handledCodexReset = await handleCodexResetCardAction(data, {
374
+ getTenantAccessToken,
375
+ sendRawCard,
376
+ sendTextReply,
377
+ sendCardReply,
378
+ updateCardMessage,
379
+ recallMessage,
380
+ consumeCodexRateLimitResetCredit,
381
+ });
382
+ if (handledCodexReset)
383
+ return;
384
+ const result = parseCardAction(data);
385
+ if (!result)
386
+ return;
387
+ const registry = await loadSessionRegistryForBinding();
388
+ const chatType = resolveFeishuCardActionChatType(result.chatId, registry);
389
+ console.log(`[BTN] chat=${result.chatId} chatType=${chatType} text="${result.text}"`);
390
+ handleCommand(feishuPlatform, result.text, result.chatId, result.openId, Date.now(), chatType, undefined, result.commandId).catch((err) => console.error(`[${ts()}] [BTN] handleCommand failed: ${err.message}`));
391
+ }
392
+ catch (err) {
393
+ console.error(`[${ts()}] [FATAL] card.action.trigger handler crashed: ${err.message}`);
394
+ }
395
+ },
396
+ });
397
+ if (USE_LOCAL) {
398
+ console.log(`\n[启动 6/7] 本地区 relay 模式:正在连接 ${LOCAL_RELAY_URL} …`);
399
+ console.log(" 若失败:请先在 SDK 模式下启动主进程,或确认本机中继已在该地址监听。");
400
+ let localRelayOpened = false;
401
+ const ws = new WebSocket(LOCAL_RELAY_URL);
402
+ ws.on("open", () => {
403
+ localRelayOpened = true;
404
+ console.log("[WS] Connected to local relay");
405
+ console.log("[启动 7/7] 已连接本地中继,可接收转发事件。\n");
406
+ printServiceRunningHint("local", `http://127.0.0.1:${CHATCCC_PORT}`);
407
+ });
408
+ ws.on("message", async (raw) => {
409
+ try {
410
+ const data = JSON.parse(raw.toString());
411
+ const action = parseCardAction(data);
412
+ if (action) {
413
+ const registry = await loadSessionRegistryForBinding();
414
+ const chatType = resolveFeishuCardActionChatType(action.chatId, registry);
415
+ handleCommand(feishuPlatform, action.text, action.chatId, action.openId, Date.now(), chatType, undefined, action.commandId).catch((err) => console.error(`[${ts()}] [BTN] handleCommand failed: ${err.message}`));
416
+ return;
417
+ }
418
+ const event = getInnerEvent(data);
419
+ const message = event.message;
420
+ if (!message)
421
+ return;
422
+ const text = await formatMessageContent(message);
423
+ const openId = event.sender?.sender_id?.open_id ?? "";
424
+ const chatId = message.chat_id ?? "";
425
+ appendChatLog(chatId, openId, text);
426
+ if (text.toLowerCase() === "/new" && openId) {
427
+ console.log(`[MSG] /new from ${openId}, but local relay does not handle /new yet. Use SDK mode.`);
428
+ }
429
+ }
430
+ catch { /* ignore */ }
431
+ });
432
+ ws.on("close", () => { console.log("[WS] Local relay disconnected"); process.exit(0); });
433
+ ws.on("error", (err) => {
434
+ if (!localRelayOpened) {
435
+ console.error(`[启动 6/7] 失败:无法连接本地中继。`);
436
+ console.error(` ${err.message}`);
437
+ console.error(` 目标: ${LOCAL_RELAY_URL}`);
438
+ // 注意:local relay 模式下连接是异步的;此处 throw 出 startBotService
439
+ // 已经返回的事件循环外,调用方拿不到。约定:local 模式连接失败一律
440
+ // 直接退出进程(与 setup-activate 模式无关 —— setup 模式不会用 local relay)。
441
+ printServiceDidNotStart(`无法连接本地中继 ${LOCAL_RELAY_URL}`);
442
+ process.exit(1);
443
+ }
444
+ console.error(`[WS] Local relay error: ${err.message}`);
445
+ });
446
+ }
447
+ else {
448
+ // 进程首次启动:此时所有 Map 都是空的,resetState 主要是打个 LOG 标识"开始
449
+ // 干净状态"。修正残留的 running stream-state 并重建 session→chat 映射。
450
+ resetState();
451
+ await feishuMessageLedger.load();
452
+ startUnifiedDisplayLoop();
453
+ fixStaleStreamStates().then(async () => {
454
+ const registry = await loadSessionRegistryForBinding();
455
+ rebuildSessionChatsFromRegistry(registry);
456
+ }).catch((err) => console.error(`[${ts()}] Init bindings failed: ${err.message}`));
457
+ // ⚠️ 关键设计:onReady / onReconnected 只重建只读映射,**绝不**清空运行时
458
+ // 状态(activePrompts、sessionInfoMap、processedMessages)。
459
+ // SDK 重连只是底层 WebSocket 抖动,业务层不受影响:
460
+ // - 后台 generator 仍在跑、stream-state 仍在被写
461
+ // - display loop 仍在向群推送
462
+ // - 已处理消息的去重 set 必须保留,避免 SDK 重推老消息时 prompt 跑两遍
463
+ // 历史 bug:此处曾误调 resetState() 导致重连即让所有后台任务变孤儿,
464
+ // 同一 session 还可能双开 prompt(详见 session.ts::resetState 注释)。
465
+ const wsClient = new WSClient({
466
+ appId: APP_ID,
467
+ appSecret: APP_SECRET,
468
+ domain: FEISHU_PLATFORM_TYPE === "lark" ? Domain.Lark : Domain.Feishu,
469
+ onReady: async () => {
470
+ await rebuildBindingsFromRegistry().catch((err) => console.error(`[${ts()}] [SDK READY] rebuild bindings failed: ${err.message}`));
471
+ },
472
+ onReconnected: async () => {
473
+ await rebuildBindingsFromRegistry().catch((err) => console.error(`[${ts()}] [SDK RECONNECT] rebuild bindings failed: ${err.message}`));
474
+ },
475
+ });
476
+ console.log(`\n[启动 6/7] 飞书长连接:正在通过 SDK 建立 WebSocket …`);
477
+ try {
478
+ await wsClient.start({ eventDispatcher });
479
+ }
480
+ catch (err) {
481
+ const msg = err instanceof Error ? err.message : String(err);
482
+ console.error(" 失败:飞书 WebSocket 未能启动。");
483
+ console.error(" 常见原因: 应用权限未开通、事件订阅未配置、网络问题、或 SDK 内部错误。");
484
+ console.error(` 详情: ${msg}`);
485
+ throw new Error(`飞书 SDK WebSocket 未能建立: ${msg}`);
486
+ }
487
+ console.log("[WS] Feishu WebSocket connected (SDK)");
488
+ console.log("[启动 7/7] 服务已就绪,等待飞书消息(群聊 / 卡片回调)。\n");
489
+ printServiceRunningHint("sdk", `http://127.0.0.1:${CHATCCC_PORT}`);
490
+ sendRestartCard(token).catch((err) => console.error(`[${ts()}] [RESTART] sendRestartCard failed: ${err.message}`));
491
+ }
492
+ }
493
+ // ---------------------------------------------------------------------------
494
+ // WeChat iLink supervisor — 自动重连
495
+ // ---------------------------------------------------------------------------
496
+ async function startWechatSupervisor() {
497
+ if (!ILINK_ENABLED) {
498
+ console.log("[WX] 微信 iLink 未启用(platforms.ilink.enabled 不为 true),跳过。");
499
+ return;
500
+ }
501
+ console.log("\n[WX] 启动微信 iLink 平台...");
502
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
503
+ const MAX_QR_RETRIES = 3;
504
+ let qrTimeoutCount = 0;
505
+ while (!wechatSignal.stopped) {
506
+ let isQrTimeout = false;
507
+ try {
508
+ await startWechatPlatform((text, chatId, openId, msgTimestamp, chatType, traceId) => handleCommand(wechatPlatform, text, chatId, openId, msgTimestamp, chatType, traceId), wechatSignal, ILINK_REUSE_TOKEN_ON_START);
509
+ // 登录成功后重置计数
510
+ qrTimeoutCount = 0;
511
+ }
512
+ catch (err) {
513
+ const msg = err.message ?? "";
514
+ isQrTimeout = msg.includes("QR 登录超时");
515
+ if (isQrTimeout) {
516
+ qrTimeoutCount++;
517
+ console.error(`[WX] QR 登录超时 (${qrTimeoutCount}/${MAX_QR_RETRIES}): ${msg}`);
518
+ if (qrTimeoutCount >= MAX_QR_RETRIES) {
519
+ console.error(`[WX] 已连续 ${MAX_QR_RETRIES} 次 QR 登录超时,放弃重试。如需重新尝试请重启 ChatCCC。`);
520
+ break;
521
+ }
522
+ }
523
+ else {
524
+ console.error(`[WX] 微信 iLink 崩溃: ${msg}`);
525
+ }
526
+ }
527
+ if (wechatSignal.stopped)
528
+ break;
529
+ const delaySeconds = isQrTimeout ? 300 : 30;
530
+ const delayDesc = isQrTimeout ? "5 分钟" : "30 秒";
531
+ console.log(`[WX] ${delayDesc}后重试...`);
532
+ await sleep(delaySeconds * 1000);
533
+ }
534
+ console.log("[WX] 微信 iLink 平台已停止。");
535
+ }
536
+ async function startConfiguredPlatforms(httpServer, options) {
537
+ const plan = buildPlatformStartupPlan({
538
+ feishuEnabled: FEISHU_ENABLED,
539
+ ilinkEnabled: ILINK_ENABLED,
540
+ });
541
+ if (plan.startFeishu) {
542
+ try {
543
+ await startBotService({ httpServer, port: CHATCCC_PORT });
544
+ }
545
+ catch (err) {
546
+ if (options.failOnFeishuError)
547
+ throw err;
548
+ console.error(`\n[飞书] 启动失败: ${err.message}`);
549
+ console.error("[飞书] 微信等其他平台不受影响,将继续启动。\n");
550
+ }
551
+ }
552
+ else {
553
+ console.log("[飞书] 平台未启用,跳过飞书启动。");
554
+ }
555
+ if (plan.startIlink) {
556
+ startWechatSupervisor().catch((err) => console.error(`[WX] 微信 supervisor 异常退出: ${err.message}`));
557
+ }
558
+ else {
559
+ console.log("[WX] 微信 iLink 未启用,跳过微信启动。");
560
+ }
561
+ }
562
+ // ---------------------------------------------------------------------------
563
+ // Main
564
+ // ---------------------------------------------------------------------------
565
+ async function main() {
566
+ // 用户直接运行 chatccc 时打开系统默认浏览器;由 `/restart`、`/update`
567
+ // 或 Web UI 拉起的替代进程会携带内部标记,不重复打扰用户。
568
+ const autoOpenWebUi = shouldAutoOpenWebUi({
569
+ openOnStart: config.webUi.openOnStart,
570
+ });
571
+ appendStartupTrace("main: entered", {
572
+ argv: process.argv.join(" ").slice(0, 400),
573
+ CHATCCC_PORT,
574
+ PROJECT_ROOT,
575
+ autoOpenWebUi,
576
+ });
577
+ // ChatCCC 是常驻服务,不能把“当前刚好有没有 Agent session”当作进程生命周期。
578
+ // referenced timer 明确锚定服务进程;HTTP Server 接入后还会定期 ref/健康检查。
579
+ // `/restart`、`/update` 都使用 process.exit(),不会被 timer 阻止。
580
+ const serviceLifecycle = createServiceLifecycleGuard({ tracer: appendStartupTrace });
581
+ serviceLifecycle.start();
582
+ // 黑匣子:所有未捕获异常 / 信号 / beforeExit 都同步写入 startup-trace.log(appendFileSync)。
583
+ // 越早装越好——后续任何一行抛错都有兜底;它独立于 SIGINT 清理(见末尾的
584
+ // server.close)——只负责诊断与默认致命退出,不替代清理逻辑。
585
+ installCrashLogging({
586
+ flush: () => fileLog.flush(),
587
+ onBeforeExit: (code) => serviceLifecycle.handleBeforeExit(code),
588
+ });
589
+ // stdout/stderr 管道读端消失(如 /restart 旧进程退出后)时,第三方 SDK 写
590
+ // stderr 会 EPIPE;无监听会抛 uncaughtException 杀死整个服务。挂守卫把这类
591
+ // IO 错误降级为一条非致命 trace。
592
+ installEpipeGuard();
593
+ // 模拟模式:独立端口 18079,不与 SDK 实例冲突,不走飞书凭证/权限/WSClient
594
+ if (USE_SIMULATE) {
595
+ const SIM_PORT = 18079;
596
+ console.log("\n[Simulate] 模拟飞书环境模式");
597
+ setPlatform(SimulatedPlatform);
598
+ console.log(" 已切换到 SimulatedPlatform(零飞书依赖)");
599
+ appendStartupTrace("main: simulate mode", { port: SIM_PORT });
600
+ // 注册消息处理器,让 SimAgent.sendMessage() 能进程内触发 handleCommand
601
+ setMessageHandler((text, chatId, openId, _ts, chatType, traceId) => handleCommand(feishuPlatform, text, chatId, openId, Date.now(), chatType, traceId));
602
+ setExtraApiHandler(async (req, res) => {
603
+ const injected = await handleSimInjectMessage(req, res);
604
+ if (injected)
605
+ return true;
606
+ return (await handleAgentReloadConfigRequest(req, res))
607
+ || (await handleAgentImageRequest(req, res))
608
+ || (await handleAgentFileRequest(req, res))
609
+ || (await handleAgentDelegateTaskRequest(req, res, feishuPlatform))
610
+ || (await handleAgentStopStuckRequest(req, res))
611
+ || (await handleChatGptSubscriptionRequest(req, res));
612
+ });
613
+ const simServer = createServer(createUiRouter());
614
+ await new Promise((resolveListen, rejectListen) => {
615
+ const onError = (err) => {
616
+ simServer.removeListener("listening", onListening);
617
+ rejectListen(err);
618
+ };
619
+ const onListening = () => {
620
+ simServer.removeListener("error", onError);
621
+ resolveListen();
622
+ };
623
+ simServer.once("error", onError);
624
+ simServer.once("listening", onListening);
625
+ simServer.listen(SIM_PORT, "127.0.0.1");
626
+ }).catch((err) => {
627
+ console.error(`\n[启动] 监听失败:端口 ${SIM_PORT}(${err.code ?? "?"} — ${err.message})`);
628
+ process.exit(1);
629
+ });
630
+ serviceLifecycle.attachServer(simServer, () => recoverHttpServer(simServer, SIM_PORT));
631
+ console.log(`\n${"=".repeat(60)}`);
632
+ console.log(` ChatCCC — 模拟飞书环境模式`);
633
+ console.log(`${"=".repeat(60)}`);
634
+ console.log(` 发送消息: POST http://127.0.0.1:${SIM_PORT}/api/sim/inject-message`);
635
+ console.log(` 消息日志: ~/.chatccc/sim/messages.jsonl`);
636
+ console.log(`${"=".repeat(60)}\n`);
637
+ if (autoOpenWebUi) {
638
+ const url = buildWebUiUrl(SIM_PORT);
639
+ const opened = openWebUiInDefaultBrowser(SIM_PORT);
640
+ appendStartupTrace(opened ? "web-ui: opening simulate browser" : "web-ui: simulate browser unavailable", { url });
641
+ }
642
+ installShutdownHandlers(simServer, serviceLifecycle);
643
+ return;
644
+ }
645
+ if (Number.isNaN(CHATCCC_PORT) || CHATCCC_PORT < 1 || CHATCCC_PORT > 65535) {
646
+ console.error("\n[启动] 预检失败: config.json 的 port 字段不是有效端口号(1–65535)。");
647
+ console.error(` 当前配置: ${CHATCCC_PORT}`);
648
+ reportEnvironmentVariableReadout();
649
+ printServiceDidNotStart("config.json 的 port 字段配置无效(须为 1–65535 的整数)");
650
+ process.exit(1);
651
+ }
652
+ console.log(`\n[启动 1/7] 单实例:按 PID 文件清理旧 ChatCCC 进程`);
653
+ console.log(` PID 文件: ${PID_FILE}`);
654
+ appendStartupTrace("main: before ensureSingleInstance", { PID_FILE, CHATCCC_PORT });
655
+ ensureSingleInstance(PID_FILE);
656
+ appendStartupTrace("main: after ensureSingleInstance");
657
+ console.log(" 完成。\n");
658
+ // 注册 reload hook:dashboard 模式(或 setup 激活后再点向导)下用户点
659
+ // "保存并启动" 时,web-ui 会调用本回调,把磁盘上刚保存的 config.json
660
+ // 刷进进程内的 export let 常量(live binding 让 CLAUDE_MODEL 等下次创建
661
+ // 会话时自动看到新值)。setup 首次激活走 onActivate 路径,不依赖此 hook。
662
+ setReloadConfigHook(() => {
663
+ reloadRuntimeConfig("reload-from-ui");
664
+ });
665
+ setExtraApiHandler(async (req, res) => {
666
+ return (await handleAgentReloadConfigRequest(req, res))
667
+ || (await handleAgentImageRequest(req, res))
668
+ || (await handleAgentFileRequest(req, res))
669
+ || (await handleAgentDelegateTaskRequest(req, res, feishuPlatform))
670
+ || (await handleAgentStopStuckRequest(req, res))
671
+ || (await handleChatGptSubscriptionRequest(req, res));
672
+ });
673
+ console.log(`[启动 2/7] 环境与凭证检查`);
674
+ reportEnvironmentVariableReadout();
675
+ console.log(` 工作目录: ${process.cwd()}`);
676
+ console.log(` 包根目录: ${PROJECT_ROOT}`);
677
+ if (FEISHU_ENABLED) {
678
+ appendStartupTrace("main: before feishu credential check", {
679
+ hasAppId: Boolean(APP_ID.trim()),
680
+ hasAppSecret: Boolean(APP_SECRET.trim()),
681
+ });
682
+ if (!APP_ID.trim() || !APP_SECRET.trim()) {
683
+ // 凭证不全:进 setup 向导。注入 onActivate 回调让用户点"保存并启动"
684
+ // 时,原地(同进程)调用 startBotService,复用 setup HTTP server。
685
+ const setupServer = startSetupMode(CHATCCC_PORT, {
686
+ openBrowser: autoOpenWebUi,
687
+ onActivate: async (httpServer) => {
688
+ reloadRuntimeConfig("setup-activate");
689
+ appendStartupTrace("setup-activate: reloaded config from disk", {
690
+ appIdMaskAfterReload: maskAppId(APP_ID),
691
+ });
692
+ try {
693
+ await startConfiguredPlatforms(httpServer, { failOnFeishuError: true });
694
+ return { ok: true };
695
+ }
696
+ catch (err) {
697
+ appendStartupTrace("setup-activate: startConfiguredPlatforms failed", {
698
+ message: err.message,
699
+ });
700
+ return { ok: false, error: err.message };
701
+ }
702
+ },
703
+ });
704
+ setupServer.once("listening", () => {
705
+ serviceLifecycle.attachServer(setupServer, () => recoverHttpServer(setupServer, CHATCCC_PORT));
706
+ });
707
+ installShutdownHandlers(setupServer, serviceLifecycle);
708
+ return;
709
+ }
710
+ console.log(` 必填项校验通过(App ID 摘要: ${maskAppId(APP_ID)})。\n`);
711
+ appendStartupTrace("main: feishu credentials ok", { appIdMask: maskAppId(APP_ID) });
712
+ }
713
+ else {
714
+ console.log(" 飞书平台未启用(platforms.feishu.enabled = false),跳过飞书凭证检查。\n");
715
+ appendStartupTrace("main: feishu disabled", {});
716
+ }
717
+ startChromeDevtoolsGuard();
718
+ // 启动 HTTP server(同时挂 UI router,供 dashboard / setup / agent image/file 使用)
719
+ appendStartupTrace("main: before freeRelayListenPort", { CHATCCC_PORT });
720
+ const killed = freeRelayListenPort(CHATCCC_PORT);
721
+ const isInternalRestart = process.env[INTERNAL_RESTART_ENV_VAR] === "1";
722
+ appendStartupTrace("main: after freeRelayListenPort", { CHATCCC_PORT, killed, isInternalRestart });
723
+ // 内部自重启(/restart、/update)时父进程还占着端口(祖先链 PID 不会被杀):
724
+ // 无条件等待端口释放再 listen,避免子进程一上来就 EADDRINUSE 秒退
725
+ // (Windows 端口释放有额外延迟,重试窗口容易不够)。
726
+ if (killed > 0 || isInternalRestart) {
727
+ await waitForPortFree(CHATCCC_PORT, isInternalRestart ? 5000 : undefined);
728
+ appendStartupTrace("main: port free confirmed", { CHATCCC_PORT, isInternalRestart });
729
+ }
730
+ const httpServer = createServer(createUiRouter());
731
+ await listenWithRetry(httpServer, CHATCCC_PORT, "127.0.0.1").catch((err) => {
732
+ console.error(`\n[启动] 本地中继 WebSocket 监听失败:端口 ${CHATCCC_PORT}(${err.code ?? "?"} — ${err.message})`);
733
+ console.error(" 处理建议: 关闭占用该端口的其它程序,或在 config.json 的 port 字段里改成其它未占用端口(如 18081)。");
734
+ printServiceDidNotStart(`本地中继端口 ${CHATCCC_PORT} 无法监听(${err.code ?? "?"} — ${err.message})`);
735
+ process.exit(1);
736
+ });
737
+ serviceLifecycle.attachServer(httpServer, () => recoverHttpServer(httpServer, CHATCCC_PORT));
738
+ // 平台鉴权/长连接启动可能耗时;此时也必须允许 Ctrl+C / SIGTERM 正常退出,
739
+ // 不能只留下更早安装的信号日志 listener 把默认退出行为吞掉。
740
+ installShutdownHandlers(httpServer, serviceLifecycle);
741
+ // 必须等 HTTP server 真正监听后再发起打开请求,避免浏览器先到一步看到
742
+ // ERR_CONNECTION_REFUSED。Chrome CDP 守护仍保持自己原有的独立行为。
743
+ if (autoOpenWebUi) {
744
+ const url = buildWebUiUrl(CHATCCC_PORT);
745
+ const opened = openWebUiInDefaultBrowser(CHATCCC_PORT);
746
+ if (opened) {
747
+ console.log(`[WEB-UI] 已请求系统默认浏览器打开: ${url}`);
748
+ appendStartupTrace("web-ui: opening default browser", { url });
749
+ }
750
+ else {
751
+ appendStartupTrace("web-ui: default browser unavailable", { url });
752
+ }
753
+ }
754
+ else {
755
+ appendStartupTrace("web-ui: default browser skipped by lifecycle or preference", {
756
+ url: buildWebUiUrl(CHATCCC_PORT),
757
+ openOnStart: config.webUi.openOnStart,
758
+ });
759
+ }
760
+ await startConfiguredPlatforms(httpServer, { failOnFeishuError: false });
761
+ }
762
+ /**
763
+ * 生命周期健康检查发现 HTTP Server 已停止监听时,优先原地恢复同一个 Server。
764
+ * router、WebSocket upgrade listener 都挂在这个对象上,复用它能保留现有服务绑定;
765
+ * 恢复失败会被 lifecycle guard 记录,并在下一轮检查重试。
766
+ */
767
+ async function recoverHttpServer(httpServer, port) {
768
+ if (httpServer.listening) {
769
+ httpServer.ref();
770
+ return;
771
+ }
772
+ appendStartupTrace("service-lifecycle: HTTP recovery begin", { port });
773
+ await listenWithRetry(httpServer, port, "127.0.0.1");
774
+ httpServer.ref();
775
+ appendStartupTrace("service-lifecycle: HTTP recovery listen succeeded", { port });
776
+ }
777
+ /**
778
+ * 带重试的 server.listen,Windows 端口释放有延迟时自动重试。
779
+ */
780
+ async function listenWithRetry(server, port, host, maxRetries = 3) {
781
+ for (let i = 0; i < maxRetries; i++) {
782
+ try {
783
+ await new Promise((resolve, reject) => {
784
+ server.once("error", reject);
785
+ server.once("listening", resolve);
786
+ server.listen(port, host);
787
+ });
788
+ return;
789
+ }
790
+ catch (err) {
791
+ const e = err;
792
+ server.removeAllListeners("listening");
793
+ server.removeAllListeners("error");
794
+ if (e.code === "EADDRINUSE" && i < maxRetries - 1) {
795
+ await new Promise((r) => setTimeout(r, 500 * (i + 1)));
796
+ continue;
797
+ }
798
+ throw err;
799
+ }
800
+ }
801
+ }
802
+ /**
803
+ * 注册 SIGINT / SIGTERM 清理:把 relay/setup 共用的 httpServer 关掉再退出。
804
+ *
805
+ * Node EventEmitter 按注册顺序触发,installCrashLogging 装得更早 → 同步 trace
806
+ * 先写盘,再走这里。
807
+ */
808
+ function installShutdownHandlers(httpServer, serviceLifecycle) {
809
+ process.on("SIGINT", () => {
810
+ console.log("\nShutting down...");
811
+ serviceLifecycle.beginShutdown("SIGINT");
812
+ wechatSignal.stopped = true;
813
+ stopChromeDevtoolsGuard();
814
+ httpServer.close();
815
+ process.exit(0);
816
+ });
817
+ process.on("SIGTERM", () => {
818
+ serviceLifecycle.beginShutdown("SIGTERM");
819
+ wechatSignal.stopped = true;
820
+ stopChromeDevtoolsGuard();
821
+ httpServer.close();
822
+ process.exit(0);
823
+ });
824
+ }
825
+ main().catch((err) => {
826
+ appendStartupTrace("main: catch fatal", { message: err.message, stack: err.stack?.slice(0, 800) });
827
+ console.error("\n[启动] 未捕获的致命错误(main 异步链)");
828
+ console.error(` ${err.message}`);
829
+ if (err.stack)
830
+ console.error(err.stack);
831
+ printServiceDidNotStart(`main() 异常: ${err.message}`);
832
+ process.exit(1);
833
+ });