chatccc 0.2.251 → 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 (277) 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/package.json +1 -1
  5. package/dist/deepccc-agent/src/cli.js +634 -0
  6. package/dist/deepccc-agent/src/config.js +76 -0
  7. package/dist/deepccc-agent/src/context.js +348 -0
  8. package/dist/deepccc-agent/src/file-log.js +34 -0
  9. package/dist/deepccc-agent/src/file-tools.js +1208 -0
  10. package/dist/deepccc-agent/src/index.js +571 -0
  11. package/dist/deepccc-agent/src/permissions.js +170 -0
  12. package/dist/deepccc-agent/src/privacy.js +124 -0
  13. package/dist/deepccc-agent/src/proc-tree-kill.js +60 -0
  14. package/dist/deepccc-agent/src/progress/cards-helpers.js +70 -0
  15. package/dist/deepccc-agent/src/progress/reducer.js +102 -0
  16. package/dist/deepccc-agent/src/progress/terminal-renderer.js +264 -0
  17. package/dist/deepccc-agent/src/progress/view.js +30 -0
  18. package/dist/deepccc-agent/src/raw-stream-log.js +106 -0
  19. package/dist/deepccc-agent/src/session-search.js +276 -0
  20. package/dist/deepccc-agent/src/session-select.js +23 -0
  21. package/dist/deepccc-agent/src/sigint.js +26 -0
  22. package/dist/deepccc-agent/src/skills.js +178 -0
  23. package/dist/deepccc-agent/src/web-tools.js +246 -0
  24. package/dist/src/adapters/adapter-interface.js +19 -0
  25. package/dist/src/adapters/ccc-adapter.js +112 -0
  26. package/dist/src/adapters/claude-adapter.js +497 -0
  27. package/dist/src/adapters/claude-session-meta-store.js +92 -0
  28. package/dist/src/adapters/codex-adapter.js +279 -0
  29. package/dist/src/adapters/codex-session-meta-store.js +94 -0
  30. package/dist/src/adapters/cursor-adapter.js +491 -0
  31. package/dist/src/adapters/cursor-session-meta-store.js +116 -0
  32. package/dist/src/adapters/jsonl-stream.js +104 -0
  33. package/{src/adapters/proc-tree-kill.ts → dist/src/adapters/proc-tree-kill.js} +94 -97
  34. package/dist/src/adapters/raw-stream-log.js +106 -0
  35. package/dist/src/adapters/resource-monitor.js +113 -0
  36. package/dist/src/agent-activity.js +133 -0
  37. package/dist/src/agent-delegate-task-rpc.js +129 -0
  38. package/dist/src/agent-delegate-task.js +48 -0
  39. package/dist/src/agent-file-rpc.js +152 -0
  40. package/dist/src/agent-image-rpc.js +148 -0
  41. package/dist/src/agent-platform-routing.js +13 -0
  42. package/dist/src/agent-reload-config-rpc.js +23 -0
  43. package/dist/src/agent-rpc-body.js +87 -0
  44. package/dist/src/agent-stop-stuck.js +110 -0
  45. package/dist/src/card-action-routing.js +7 -0
  46. package/dist/src/card-plain-text.js +101 -0
  47. package/dist/src/cardkit.js +158 -0
  48. package/dist/src/cards.js +573 -0
  49. package/dist/src/chatgpt-subscription-rpc.js +18 -0
  50. package/dist/src/chatgpt-subscription.js +199 -0
  51. package/dist/src/chrome-devtools-guard.js +238 -0
  52. package/dist/src/claude-sdk-installer.js +249 -0
  53. package/dist/src/codex-reset-actions.js +143 -0
  54. package/dist/src/config-utils.js +149 -0
  55. package/dist/src/config.js +804 -0
  56. package/dist/src/cursor-usage.js +77 -0
  57. package/dist/src/exit-banner.js +28 -0
  58. package/dist/src/feishu-api.js +1404 -0
  59. package/dist/src/feishu-message-ingress.js +137 -0
  60. package/dist/src/feishu-platform.js +97 -0
  61. package/dist/src/format-message.js +252 -0
  62. package/dist/src/git-command.js +155 -0
  63. package/dist/src/im-skills.js +121 -0
  64. package/dist/src/index.js +833 -0
  65. package/dist/src/litellm-proxy.js +300 -0
  66. package/dist/src/orchestrator.js +2078 -0
  67. package/dist/src/package-root.js +26 -0
  68. package/dist/src/platform-adapter.js +7 -0
  69. package/dist/src/platform-startup.js +6 -0
  70. package/dist/src/privacy.js +100 -0
  71. package/dist/src/progress/reducer.js +102 -0
  72. package/dist/src/progress/terminal-renderer.js +264 -0
  73. package/dist/src/progress/view.js +30 -0
  74. package/dist/src/response-stall.js +14 -0
  75. package/dist/src/runtime-entry.js +13 -0
  76. package/dist/src/runtime-reload.js +19 -0
  77. package/dist/src/session-chat-binding.js +183 -0
  78. package/dist/src/session-name.js +7 -0
  79. package/dist/src/session.js +2144 -0
  80. package/dist/src/shared-prefix.js +16 -0
  81. package/dist/src/shared.js +493 -0
  82. package/dist/src/sim-agent.js +105 -0
  83. package/dist/src/sim-platform.js +142 -0
  84. package/dist/src/sim-store.js +231 -0
  85. package/dist/src/simplify.js +99 -0
  86. package/dist/src/startup-lifecycle.js +209 -0
  87. package/dist/src/stream-state.js +141 -0
  88. package/dist/src/terminal-error.js +100 -0
  89. package/dist/src/trace.js +50 -0
  90. package/dist/src/turn-cards.js +92 -0
  91. package/dist/src/update-command-guard.js +114 -0
  92. package/{src/web-ui.ts → dist/src/web-ui.js} +749 -823
  93. package/dist/src/wechat-platform.js +545 -0
  94. package/package.json +7 -5
  95. package/deepccc-agent/LICENSE +0 -201
  96. package/deepccc-agent/bin/deepccc.mjs +0 -26
  97. package/deepccc-agent/docs/cache-hit-rate-1.jpg +0 -0
  98. package/deepccc-agent/docs/cache-hit-rate-2.jpg +0 -0
  99. package/deepccc-agent/package-lock.json +0 -2027
  100. package/deepccc-agent/src/__tests__/chat-session.test.ts +0 -852
  101. package/deepccc-agent/src/__tests__/cli-json.test.ts +0 -49
  102. package/deepccc-agent/src/__tests__/config.test.ts +0 -34
  103. package/deepccc-agent/src/__tests__/context.test.ts +0 -341
  104. package/deepccc-agent/src/__tests__/file-tools.test.ts +0 -240
  105. package/deepccc-agent/src/__tests__/permissions.test.ts +0 -199
  106. package/deepccc-agent/src/__tests__/privacy.test.ts +0 -318
  107. package/deepccc-agent/src/__tests__/progress-reducer.test.ts +0 -121
  108. package/deepccc-agent/src/__tests__/session-search.test.ts +0 -262
  109. package/deepccc-agent/src/__tests__/session-select.test.ts +0 -116
  110. package/deepccc-agent/src/__tests__/sigint.test.ts +0 -56
  111. package/deepccc-agent/src/__tests__/skills.test.ts +0 -284
  112. package/deepccc-agent/src/__tests__/terminal-renderer.test.ts +0 -247
  113. package/deepccc-agent/src/__tests__/web-tools.test.ts +0 -220
  114. package/deepccc-agent/src/cli.ts +0 -682
  115. package/deepccc-agent/src/config.ts +0 -101
  116. package/deepccc-agent/src/context.ts +0 -465
  117. package/deepccc-agent/src/file-log.ts +0 -38
  118. package/deepccc-agent/src/file-tools.ts +0 -1493
  119. package/deepccc-agent/src/index.ts +0 -676
  120. package/deepccc-agent/src/permissions.ts +0 -226
  121. package/deepccc-agent/src/privacy.ts +0 -141
  122. package/deepccc-agent/src/proc-tree-kill.ts +0 -61
  123. package/deepccc-agent/src/progress/cards-helpers.ts +0 -76
  124. package/deepccc-agent/src/progress/reducer.ts +0 -113
  125. package/deepccc-agent/src/progress/terminal-renderer.ts +0 -294
  126. package/deepccc-agent/src/progress/view.ts +0 -77
  127. package/deepccc-agent/src/raw-stream-log.ts +0 -124
  128. package/deepccc-agent/src/session-search.ts +0 -370
  129. package/deepccc-agent/src/session-select.ts +0 -48
  130. package/deepccc-agent/src/sigint.ts +0 -50
  131. package/deepccc-agent/src/skills.ts +0 -205
  132. package/deepccc-agent/src/web-tools.ts +0 -313
  133. package/deepccc-agent/tsconfig.build.json +0 -13
  134. package/deepccc-agent/tsconfig.json +0 -13
  135. package/deepccc-agent/vitest.config.ts +0 -7
  136. package/src/__tests__/adapter-interface.test.ts +0 -152
  137. package/src/__tests__/agent-activity.test.ts +0 -86
  138. package/src/__tests__/agent-delegate-task-rpc.test.ts +0 -165
  139. package/src/__tests__/agent-image-rpc.test.ts +0 -34
  140. package/src/__tests__/agent-platform-routing.test.ts +0 -26
  141. package/src/__tests__/agent-reload-config-rpc.test.ts +0 -99
  142. package/src/__tests__/agent-rpc-body.test.ts +0 -42
  143. package/src/__tests__/builtin-chat-session.test.ts +0 -532
  144. package/src/__tests__/builtin-cli-json.test.ts +0 -39
  145. package/src/__tests__/builtin-config.test.ts +0 -26
  146. package/src/__tests__/builtin-context.test.ts +0 -319
  147. package/src/__tests__/builtin-file-tools.test.ts +0 -240
  148. package/src/__tests__/builtin-permissions.test.ts +0 -219
  149. package/src/__tests__/builtin-session-search.test.ts +0 -262
  150. package/src/__tests__/builtin-session-select.test.ts +0 -116
  151. package/src/__tests__/builtin-sigint.test.ts +0 -56
  152. package/src/__tests__/builtin-skills.test.ts +0 -284
  153. package/src/__tests__/builtin-web-tools.test.ts +0 -220
  154. package/src/__tests__/card-action-routing.test.ts +0 -18
  155. package/src/__tests__/card-plain-text.test.ts +0 -45
  156. package/src/__tests__/cardkit.test.ts +0 -60
  157. package/src/__tests__/cards.test.ts +0 -607
  158. package/src/__tests__/ccc-adapter.test.ts +0 -194
  159. package/src/__tests__/chatgpt-subscription-rpc.test.ts +0 -89
  160. package/src/__tests__/chatgpt-subscription.test.ts +0 -135
  161. package/src/__tests__/chrome-devtools-guard.test.ts +0 -165
  162. package/src/__tests__/claude-adapter.test.ts +0 -614
  163. package/src/__tests__/claude-raw-stream-log.test.ts +0 -96
  164. package/src/__tests__/claude-sdk-installer.test.ts +0 -285
  165. package/src/__tests__/codex-adapter.test.ts +0 -331
  166. package/src/__tests__/codex-raw-stream-log.test.ts +0 -170
  167. package/src/__tests__/codex-reset-actions.test.ts +0 -146
  168. package/src/__tests__/config-reload.test.ts +0 -284
  169. package/src/__tests__/config-sample.test.ts +0 -97
  170. package/src/__tests__/config-utils.test.ts +0 -40
  171. package/src/__tests__/config.test.ts +0 -395
  172. package/src/__tests__/crash-logging.test.ts +0 -360
  173. package/src/__tests__/cursor-adapter.test.ts +0 -890
  174. package/src/__tests__/cursor-session-meta-store.test.ts +0 -212
  175. package/src/__tests__/feishu-api.test.ts +0 -60
  176. package/src/__tests__/feishu-avatar.test.ts +0 -504
  177. package/src/__tests__/feishu-message-ingress.test.ts +0 -138
  178. package/src/__tests__/feishu-platform.test.ts +0 -75
  179. package/src/__tests__/fixtures/codex_simple_text.jsonl +0 -4
  180. package/src/__tests__/fixtures/codex_with_tool.jsonl +0 -6
  181. package/src/__tests__/fixtures/cursor_partial_only.jsonl +0 -5
  182. package/src/__tests__/fixtures/cursor_partial_with_final.jsonl +0 -13
  183. package/src/__tests__/fixtures/cursor_with_tool_call.jsonl +0 -12
  184. package/src/__tests__/format-message.test.ts +0 -316
  185. package/src/__tests__/git-command.test.ts +0 -288
  186. package/src/__tests__/im-skills.test.ts +0 -125
  187. package/src/__tests__/jsonl-stream.test.ts +0 -79
  188. package/src/__tests__/orchestrator.test.ts +0 -1268
  189. package/src/__tests__/package-files.test.ts +0 -24
  190. package/src/__tests__/platform-startup.test.ts +0 -19
  191. package/src/__tests__/privacy.test.ts +0 -198
  192. package/src/__tests__/proc-tree-kill.test.ts +0 -108
  193. package/src/__tests__/progress-reducer.test.ts +0 -121
  194. package/src/__tests__/raw-stream-log.test.ts +0 -106
  195. package/src/__tests__/response-stall.test.ts +0 -49
  196. package/src/__tests__/restart.test.ts +0 -232
  197. package/src/__tests__/session-ccc-config.test.ts +0 -66
  198. package/src/__tests__/session.test.ts +0 -3004
  199. package/src/__tests__/shared-prefix.test.ts +0 -36
  200. package/src/__tests__/sim-agent.test.ts +0 -174
  201. package/src/__tests__/sim-platform.test.ts +0 -93
  202. package/src/__tests__/sim-store.test.ts +0 -214
  203. package/src/__tests__/simplify.test.ts +0 -283
  204. package/src/__tests__/startup-lifecycle.test.ts +0 -231
  205. package/src/__tests__/stop-session.test.ts +0 -162
  206. package/src/__tests__/stream-state.test.ts +0 -164
  207. package/src/__tests__/terminal-error.test.ts +0 -54
  208. package/src/__tests__/terminal-renderer.test.ts +0 -247
  209. package/src/__tests__/update-command-guard.test.ts +0 -144
  210. package/src/__tests__/web-ui.test.ts +0 -438
  211. package/src/__tests__/wechat-platform.test.ts +0 -111
  212. package/src/adapters/adapter-interface.ts +0 -217
  213. package/src/adapters/ccc-adapter.ts +0 -150
  214. package/src/adapters/claude-adapter.ts +0 -673
  215. package/src/adapters/claude-session-meta-store.ts +0 -120
  216. package/src/adapters/codex-adapter.ts +0 -426
  217. package/src/adapters/codex-session-meta-store.ts +0 -131
  218. package/src/adapters/cursor-adapter.ts +0 -681
  219. package/src/adapters/cursor-session-meta-store.ts +0 -154
  220. package/src/adapters/jsonl-stream.ts +0 -157
  221. package/src/adapters/raw-stream-log.ts +0 -124
  222. package/src/adapters/resource-monitor.ts +0 -141
  223. package/src/agent-activity.ts +0 -175
  224. package/src/agent-delegate-task-rpc.ts +0 -153
  225. package/src/agent-delegate-task.ts +0 -91
  226. package/src/agent-file-rpc.ts +0 -172
  227. package/src/agent-image-rpc.ts +0 -168
  228. package/src/agent-platform-routing.ts +0 -28
  229. package/src/agent-reload-config-rpc.ts +0 -34
  230. package/src/agent-rpc-body.ts +0 -92
  231. package/src/agent-stop-stuck.ts +0 -129
  232. package/src/card-action-routing.ts +0 -14
  233. package/src/card-plain-text.ts +0 -108
  234. package/src/cardkit.ts +0 -179
  235. package/src/cards.ts +0 -684
  236. package/src/chatgpt-subscription-rpc.ts +0 -27
  237. package/src/chatgpt-subscription.ts +0 -299
  238. package/src/chrome-devtools-guard.ts +0 -318
  239. package/src/claude-sdk-installer.ts +0 -324
  240. package/src/codex-reset-actions.ts +0 -184
  241. package/src/config-utils.ts +0 -211
  242. package/src/config.ts +0 -1063
  243. package/src/cursor-usage.ts +0 -128
  244. package/src/exit-banner.ts +0 -33
  245. package/src/feishu-api.ts +0 -1616
  246. package/src/feishu-message-ingress.ts +0 -195
  247. package/src/feishu-platform.ts +0 -159
  248. package/src/format-message.ts +0 -293
  249. package/src/git-command.ts +0 -202
  250. package/src/im-skills.ts +0 -149
  251. package/src/index.ts +0 -1089
  252. package/src/litellm-proxy.ts +0 -374
  253. package/src/orchestrator.ts +0 -2543
  254. package/src/platform-adapter.ts +0 -70
  255. package/src/platform-startup.ts +0 -16
  256. package/src/privacy.ts +0 -118
  257. package/src/progress/reducer.ts +0 -113
  258. package/src/progress/terminal-renderer.ts +0 -294
  259. package/src/progress/view.ts +0 -77
  260. package/src/response-stall.ts +0 -28
  261. package/src/runtime-reload.ts +0 -34
  262. package/src/session-chat-binding.ts +0 -292
  263. package/src/session-name.ts +0 -8
  264. package/src/session.ts +0 -2659
  265. package/src/shared-prefix.ts +0 -29
  266. package/src/shared.ts +0 -552
  267. package/src/sim-agent.ts +0 -167
  268. package/src/sim-platform.ts +0 -177
  269. package/src/sim-store.ts +0 -317
  270. package/src/simplify.ts +0 -120
  271. package/src/startup-lifecycle.ts +0 -250
  272. package/src/stream-state.ts +0 -177
  273. package/src/terminal-error.ts +0 -129
  274. package/src/trace.ts +0 -51
  275. package/src/turn-cards.ts +0 -118
  276. package/src/update-command-guard.ts +0 -165
  277. 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
+ });