cli-claw-kit 0.0.1

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 (295) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +245 -0
  3. package/config/default-groups.json +1 -0
  4. package/config/global-agents-md.template.md +37 -0
  5. package/config/mount-allowlist.json +11 -0
  6. package/container/Dockerfile +160 -0
  7. package/container/agent-runner/dist/.tsbuildinfo +1 -0
  8. package/container/agent-runner/dist/agent-definitions.js +22 -0
  9. package/container/agent-runner/dist/channel-prefixes.js +16 -0
  10. package/container/agent-runner/dist/codex-config.js +29 -0
  11. package/container/agent-runner/dist/image-detector.js +96 -0
  12. package/container/agent-runner/dist/index.js +2587 -0
  13. package/container/agent-runner/dist/mcp-tools.js +1076 -0
  14. package/container/agent-runner/dist/stream-event.types.js +5 -0
  15. package/container/agent-runner/dist/stream-processor.js +867 -0
  16. package/container/agent-runner/dist/types.js +6 -0
  17. package/container/agent-runner/dist/utils.js +115 -0
  18. package/container/agent-runner/package.json +36 -0
  19. package/container/agent-runner/prompts/security-rules.md +31 -0
  20. package/container/agent-runner/src/agent-definitions.ts +27 -0
  21. package/container/agent-runner/src/channel-prefixes.ts +16 -0
  22. package/container/agent-runner/src/codex-config.ts +40 -0
  23. package/container/agent-runner/src/image-detector.ts +116 -0
  24. package/container/agent-runner/src/index.ts +3107 -0
  25. package/container/agent-runner/src/mcp-tools.ts +1295 -0
  26. package/container/agent-runner/src/stream-event.types.ts +10 -0
  27. package/container/agent-runner/src/stream-processor.ts +932 -0
  28. package/container/agent-runner/src/types.ts +75 -0
  29. package/container/agent-runner/src/utils.ts +114 -0
  30. package/container/agent-runner/tsconfig.json +17 -0
  31. package/container/build.sh +28 -0
  32. package/container/entrypoint.sh +64 -0
  33. package/container/skills/agent-browser/SKILL.md +159 -0
  34. package/container/skills/install-skill/SKILL.md +64 -0
  35. package/container/skills/post-test-cleanup/SKILL.md +121 -0
  36. package/dist/.tsbuildinfo +1 -0
  37. package/dist/agent-output-parser.js +459 -0
  38. package/dist/app-root.js +52 -0
  39. package/dist/assistant-meta-footer.js +1 -0
  40. package/dist/auth.js +91 -0
  41. package/dist/billing.js +694 -0
  42. package/dist/channel-prefixes.js +16 -0
  43. package/dist/cli.js +86 -0
  44. package/dist/commands.js +79 -0
  45. package/dist/config.js +120 -0
  46. package/dist/container-runner.js +981 -0
  47. package/dist/daily-summary.js +210 -0
  48. package/dist/db.js +3683 -0
  49. package/dist/dingtalk.js +1347 -0
  50. package/dist/feishu-markdown-style.js +97 -0
  51. package/dist/feishu-streaming-card.js +1875 -0
  52. package/dist/feishu.js +1628 -0
  53. package/dist/file-manager.js +270 -0
  54. package/dist/group-queue.js +1070 -0
  55. package/dist/group-runtime.js +35 -0
  56. package/dist/host-workspace-cwd.js +85 -0
  57. package/dist/im-channel.js +384 -0
  58. package/dist/im-command-utils.js +142 -0
  59. package/dist/im-downloader.js +45 -0
  60. package/dist/im-manager.js +527 -0
  61. package/dist/im-utils.js +53 -0
  62. package/dist/image-detector.js +96 -0
  63. package/dist/index.js +5828 -0
  64. package/dist/logger.js +22 -0
  65. package/dist/mcp-utils.js +66 -0
  66. package/dist/message-attachments.js +69 -0
  67. package/dist/message-notifier.js +36 -0
  68. package/dist/middleware/auth.js +85 -0
  69. package/dist/mount-security.js +315 -0
  70. package/dist/permissions.js +67 -0
  71. package/dist/project-memory.js +6 -0
  72. package/dist/provider-pool.js +189 -0
  73. package/dist/qq.js +826 -0
  74. package/dist/reset-admin.js +42 -0
  75. package/dist/routes/admin.js +543 -0
  76. package/dist/routes/agent-definitions.js +241 -0
  77. package/dist/routes/agents.js +533 -0
  78. package/dist/routes/auth.js +675 -0
  79. package/dist/routes/billing.js +490 -0
  80. package/dist/routes/browse.js +210 -0
  81. package/dist/routes/bug-report.js +387 -0
  82. package/dist/routes/config.js +1868 -0
  83. package/dist/routes/files.js +671 -0
  84. package/dist/routes/groups.js +1367 -0
  85. package/dist/routes/mcp-servers.js +320 -0
  86. package/dist/routes/memory.js +523 -0
  87. package/dist/routes/monitor.js +307 -0
  88. package/dist/routes/skills.js +777 -0
  89. package/dist/routes/tasks.js +509 -0
  90. package/dist/routes/usage.js +64 -0
  91. package/dist/routes/workspace-config.js +458 -0
  92. package/dist/runtime-build.js +112 -0
  93. package/dist/runtime-command-handler.js +189 -0
  94. package/dist/runtime-command-registry.js +1 -0
  95. package/dist/runtime-config.js +1777 -0
  96. package/dist/runtime-identity.js +52 -0
  97. package/dist/schemas.js +590 -0
  98. package/dist/script-runner.js +64 -0
  99. package/dist/sdk-query.js +82 -0
  100. package/dist/skill-utils.js +145 -0
  101. package/dist/sqlite-compat.js +19 -0
  102. package/dist/stream-event.types.js +5 -0
  103. package/dist/streaming-runtime-meta.js +29 -0
  104. package/dist/task-scheduler.js +695 -0
  105. package/dist/task-utils.js +13 -0
  106. package/dist/telegram-pairing.js +59 -0
  107. package/dist/telegram.js +897 -0
  108. package/dist/terminal-manager.js +307 -0
  109. package/dist/tool-step-display.js +1 -0
  110. package/dist/types.js +1 -0
  111. package/dist/utils.js +85 -0
  112. package/dist/web-context.js +161 -0
  113. package/dist/web.js +1377 -0
  114. package/dist/wechat-crypto.js +182 -0
  115. package/dist/wechat.js +589 -0
  116. package/dist/workspace-runtime-reset.js +35 -0
  117. package/package.json +107 -0
  118. package/shared/assistant-meta-footer.ts +127 -0
  119. package/shared/channel-prefixes.ts +16 -0
  120. package/shared/dist/assistant-meta-footer.d.ts +29 -0
  121. package/shared/dist/assistant-meta-footer.js +85 -0
  122. package/shared/dist/channel-prefixes.d.ts +4 -0
  123. package/shared/dist/channel-prefixes.js +16 -0
  124. package/shared/dist/image-detector.d.ts +20 -0
  125. package/shared/dist/image-detector.js +96 -0
  126. package/shared/dist/runtime-command-registry.d.ts +38 -0
  127. package/shared/dist/runtime-command-registry.js +185 -0
  128. package/shared/dist/stream-event.d.ts +65 -0
  129. package/shared/dist/stream-event.js +8 -0
  130. package/shared/dist/tool-step-display.d.ts +4 -0
  131. package/shared/dist/tool-step-display.js +11 -0
  132. package/shared/image-detector.ts +116 -0
  133. package/shared/runtime-command-registry.ts +252 -0
  134. package/shared/stream-event.ts +67 -0
  135. package/shared/tool-step-display.ts +21 -0
  136. package/shared/tsconfig.json +24 -0
  137. package/web/dist/assets/BillingPage-B1wBR_o-.js +52 -0
  138. package/web/dist/assets/ChatPage-6GBZ9nXN.css +32 -0
  139. package/web/dist/assets/ChatPage-BOJcXtaj.js +161 -0
  140. package/web/dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
  141. package/web/dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
  142. package/web/dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
  143. package/web/dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
  144. package/web/dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
  145. package/web/dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
  146. package/web/dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
  147. package/web/dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
  148. package/web/dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
  149. package/web/dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
  150. package/web/dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
  151. package/web/dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
  152. package/web/dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
  153. package/web/dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
  154. package/web/dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
  155. package/web/dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
  156. package/web/dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
  157. package/web/dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
  158. package/web/dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
  159. package/web/dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
  160. package/web/dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
  161. package/web/dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
  162. package/web/dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
  163. package/web/dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
  164. package/web/dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
  165. package/web/dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
  166. package/web/dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
  167. package/web/dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
  168. package/web/dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
  169. package/web/dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
  170. package/web/dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
  171. package/web/dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
  172. package/web/dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
  173. package/web/dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
  174. package/web/dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
  175. package/web/dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
  176. package/web/dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
  177. package/web/dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
  178. package/web/dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
  179. package/web/dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
  180. package/web/dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
  181. package/web/dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
  182. package/web/dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
  183. package/web/dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
  184. package/web/dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
  185. package/web/dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
  186. package/web/dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
  187. package/web/dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
  188. package/web/dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
  189. package/web/dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
  190. package/web/dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
  191. package/web/dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
  192. package/web/dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
  193. package/web/dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
  194. package/web/dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
  195. package/web/dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
  196. package/web/dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
  197. package/web/dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
  198. package/web/dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
  199. package/web/dist/assets/SettingsPage-DoY7FoZ_.js +153 -0
  200. package/web/dist/assets/ShareImageDialog-C1ga8b7l.js +22 -0
  201. package/web/dist/assets/TasksPage-CRivnNsx.js +14 -0
  202. package/web/dist/assets/_basePickBy-Bf-bSoS9.js +1 -0
  203. package/web/dist/assets/_baseUniq-zAOaCuKw.js +1 -0
  204. package/web/dist/assets/arc-Dm9mVQ9U.js +1 -0
  205. package/web/dist/assets/architectureDiagram-2XIMDMQ5-BLmzX1wr.js +36 -0
  206. package/web/dist/assets/band-CquvqAHh.js +1 -0
  207. package/web/dist/assets/blockDiagram-WCTKOSBZ-B9pcqm3j.js +132 -0
  208. package/web/dist/assets/c4Diagram-IC4MRINW-Cytx1q3b.js +10 -0
  209. package/web/dist/assets/channel-BOVj73LR.js +1 -0
  210. package/web/dist/assets/channel-meta-CQD0Pei-.js +41 -0
  211. package/web/dist/assets/chunk-4BX2VUAB-0ToDr6RE.js +1 -0
  212. package/web/dist/assets/chunk-55IACEB6-DQDjnXfS.js +1 -0
  213. package/web/dist/assets/chunk-FMBD7UC4-Di8ABm6c.js +15 -0
  214. package/web/dist/assets/chunk-JSJVCQXG-BZQN6rnX.js +1 -0
  215. package/web/dist/assets/chunk-KX2RTZJC-zBbcpaN_.js +1 -0
  216. package/web/dist/assets/chunk-NQ4KR5QH-BCrLoU88.js +220 -0
  217. package/web/dist/assets/chunk-QZHKN3VN-Bqk8juan.js +1 -0
  218. package/web/dist/assets/chunk-WL4C6EOR-D2YX-MHY.js +189 -0
  219. package/web/dist/assets/classDiagram-VBA2DB6C-DUUoMyaK.js +1 -0
  220. package/web/dist/assets/classDiagram-v2-RAHNMMFH-DUUoMyaK.js +1 -0
  221. package/web/dist/assets/clone-BmaCesfa.js +1 -0
  222. package/web/dist/assets/cose-bilkent-S5V4N54A-CTsv6qQA.js +1 -0
  223. package/web/dist/assets/cytoscape.esm-BQaXIfA_.js +331 -0
  224. package/web/dist/assets/dagre-KLK3FWXG-Ci4Jh9nu.js +4 -0
  225. package/web/dist/assets/defaultLocale-DX6XiGOO.js +1 -0
  226. package/web/dist/assets/diagram-E7M64L7V-BFRnfTI2.js +24 -0
  227. package/web/dist/assets/diagram-IFDJBPK2-B7Zhnp0b.js +43 -0
  228. package/web/dist/assets/diagram-P4PSJMXO-BVyP7nwq.js +24 -0
  229. package/web/dist/assets/erDiagram-INFDFZHY-NorKdTOF.js +70 -0
  230. package/web/dist/assets/error-CGD5mp5f.js +1 -0
  231. package/web/dist/assets/flowDiagram-PKNHOUZH-Ch97nABF.js +162 -0
  232. package/web/dist/assets/ganttDiagram-A5KZAMGK-BQ2pLWsy.js +292 -0
  233. package/web/dist/assets/gitGraphDiagram-K3NZZRJ6-bcvnBsD2.js +65 -0
  234. package/web/dist/assets/graph-CeAEckur.js +1 -0
  235. package/web/dist/assets/index-CPnL1_qC.js +768 -0
  236. package/web/dist/assets/index-DVevCbcO.css +10 -0
  237. package/web/dist/assets/infoDiagram-LFFYTUFH-CcsrFdj-.js +2 -0
  238. package/web/dist/assets/init-Dmth1JHB.js +1 -0
  239. package/web/dist/assets/ishikawaDiagram-PHBUUO56-1upyMfHN.js +70 -0
  240. package/web/dist/assets/journeyDiagram-4ABVD52K-CKUi-V0c.js +139 -0
  241. package/web/dist/assets/kanban-definition-K7BYSVSG-DOnQwXfL.js +89 -0
  242. package/web/dist/assets/layout-BmMMqTnJ.js +1 -0
  243. package/web/dist/assets/linear-DiaJloY5.js +1 -0
  244. package/web/dist/assets/mermaid.core-BWLV1B2v.js +254 -0
  245. package/web/dist/assets/mindmap-definition-YRQLILUH-BeAKHVWP.js +68 -0
  246. package/web/dist/assets/ordinal-DILIJJjt.js +1 -0
  247. package/web/dist/assets/pieDiagram-SKSYHLDU-DfiMSfWo.js +30 -0
  248. package/web/dist/assets/quadrantDiagram-337W2JSQ-wZxZOJxd.js +7 -0
  249. package/web/dist/assets/requirementDiagram-Z7DCOOCP-BK4HHm17.js +73 -0
  250. package/web/dist/assets/sankeyDiagram-WA2Y5GQK-BX6t2avX.js +10 -0
  251. package/web/dist/assets/sequenceDiagram-2WXFIKYE-BPQlkbAa.js +145 -0
  252. package/web/dist/assets/sheet-rI0FfB1g.js +6 -0
  253. package/web/dist/assets/sliders-horizontal-CuijWFNK.js +6 -0
  254. package/web/dist/assets/sparkles-BsMYXJoT.js +11 -0
  255. package/web/dist/assets/square-0CqMX1Q3.js +11 -0
  256. package/web/dist/assets/stateDiagram-RAJIS63D-DxkV0Vwd.js +1 -0
  257. package/web/dist/assets/stateDiagram-v2-FVOUBMTO-qLYoiOPe.js +1 -0
  258. package/web/dist/assets/step-D51IIHGA.js +1 -0
  259. package/web/dist/assets/tasks-D8JjBTwx.js +1 -0
  260. package/web/dist/assets/time-O8zIGux3.js +1 -0
  261. package/web/dist/assets/timeline-definition-YZTLITO2-kNp1DyFc.js +61 -0
  262. package/web/dist/assets/treemap-KZPCXAKY-CkrClVhk.js +162 -0
  263. package/web/dist/assets/utils-KGAn0XTg.js +11 -0
  264. package/web/dist/assets/vennDiagram-LZ73GAT5-CgdzEZz4.js +34 -0
  265. package/web/dist/assets/xychartDiagram-JWTSCODW-DfYGPfNB.js +7 -0
  266. package/web/dist/assets/zap-_hKJYy7J.js +6 -0
  267. package/web/dist/favicon.svg +332 -0
  268. package/web/dist/fonts/AlibabaPuHuiTi-3-55-Regular.woff2 +0 -0
  269. package/web/dist/fonts/AlibabaPuHuiTi-3-65-Medium.woff2 +0 -0
  270. package/web/dist/fonts/AlibabaPuHuiTi-3-75-SemiBold.woff2 +0 -0
  271. package/web/dist/fonts/DMSans-latin-ext.woff2 +0 -0
  272. package/web/dist/fonts/DMSans-latin.woff2 +0 -0
  273. package/web/dist/icons/README.md +20 -0
  274. package/web/dist/icons/apple-touch-icon-180.png +0 -0
  275. package/web/dist/icons/icon-128.png +0 -0
  276. package/web/dist/icons/icon-144.png +0 -0
  277. package/web/dist/icons/icon-152.png +0 -0
  278. package/web/dist/icons/icon-192.png +0 -0
  279. package/web/dist/icons/icon-192.svg +332 -0
  280. package/web/dist/icons/icon-384.png +0 -0
  281. package/web/dist/icons/icon-48.png +0 -0
  282. package/web/dist/icons/icon-512-maskable.png +0 -0
  283. package/web/dist/icons/icon-512.png +0 -0
  284. package/web/dist/icons/icon-512.svg +332 -0
  285. package/web/dist/icons/icon-72.png +0 -0
  286. package/web/dist/icons/icon-96.png +0 -0
  287. package/web/dist/icons/loading-logo.svg +332 -0
  288. package/web/dist/icons/logo-1024.png +0 -0
  289. package/web/dist/icons/logo-icon.svg +332 -0
  290. package/web/dist/icons/logo-text.svg +332 -0
  291. package/web/dist/index.html +30 -0
  292. package/web/dist/manifest.webmanifest +1 -0
  293. package/web/dist/registerSW.js +1 -0
  294. package/web/dist/sw.js +1 -0
  295. package/web/dist/workbox-08d6266a.js +1 -0
package/dist/web.js ADDED
@@ -0,0 +1,1377 @@
1
+ import { Hono } from 'hono';
2
+ import { cors } from 'hono/cors';
3
+ import { serve } from '@hono/node-server';
4
+ import { serveStatic } from '@hono/node-server/serve-static';
5
+ import { WebSocketServer, WebSocket } from 'ws';
6
+ import crypto from 'crypto';
7
+ import { TerminalManager } from './terminal-manager.js';
8
+ import { resolveAppPath } from './app-root.js';
9
+ // Web context and shared utilities
10
+ import { setWebDeps, wsClients, lastActiveCache, LAST_ACTIVE_DEBOUNCE_MS, parseCookie, isHostExecutionGroup, hasHostExecutionPermission, canAccessGroup, getCachedSessionWithUser, invalidateSessionCache, } from './web-context.js';
11
+ // Schemas
12
+ import { MessageCreateSchema, TerminalStartSchema, TerminalInputSchema, TerminalResizeSchema, TerminalStopSchema, } from './schemas.js';
13
+ // Middleware
14
+ import { authMiddleware } from './middleware/auth.js';
15
+ // Route modules
16
+ import authRoutes from './routes/auth.js';
17
+ import groupRoutes from './routes/groups.js';
18
+ import memoryRoutes from './routes/memory.js';
19
+ import configRoutes, { injectConfigDeps } from './routes/config.js';
20
+ import tasksRoutes from './routes/tasks.js';
21
+ import adminRoutes from './routes/admin.js';
22
+ import fileRoutes from './routes/files.js';
23
+ import monitorRoutes, { injectMonitorDeps } from './routes/monitor.js';
24
+ import skillsRoutes from './routes/skills.js';
25
+ import browseRoutes from './routes/browse.js';
26
+ import agentRoutes from './routes/agents.js';
27
+ import mcpServersRoutes from './routes/mcp-servers.js';
28
+ import workspaceConfigRoutes from './routes/workspace-config.js';
29
+ import agentDefinitionsRoutes from './routes/agent-definitions.js';
30
+ import { usage as usageRoutes } from './routes/usage.js';
31
+ import billingRoutes from './routes/billing.js';
32
+ import bugReportRoutes from './routes/bug-report.js';
33
+ import { checkBillingAccess, formatBillingAccessDeniedMessage, } from './billing.js';
34
+ // Database and types (only for handleWebUserMessage and broadcast)
35
+ import { ensureChatExists, getRegisteredGroup, getJidsByFolder, storeMessageDirect, setRegisteredGroup, deleteUserSession, updateSessionLastActive, getGroupMembers, getAgent, isGroupShared, getUserById, } from './db.js';
36
+ import { isSessionExpired } from './auth.js';
37
+ import { WEB_PORT, SESSION_COOKIE_NAME_SECURE, SESSION_COOKIE_NAME_PLAIN, ASSISTANT_NAME, } from './config.js';
38
+ import { logger } from './logger.js';
39
+ import { executeSessionReset } from './commands.js';
40
+ import { normalizeImageAttachments, toAgentImages, } from './message-attachments.js';
41
+ import { executeRuntimeWorkspaceCommand } from './runtime-command-handler.js';
42
+ import { parseRuntimeCommand } from './runtime-command-registry.js';
43
+ // --- App Setup ---
44
+ const app = new Hono();
45
+ const terminalManager = new TerminalManager();
46
+ const wsTerminals = new Map(); // ws → groupJid
47
+ const terminalOwners = new Map(); // groupJid → ws
48
+ function normalizeTerminalSize(value, fallback, min, max) {
49
+ if (typeof value !== 'number' || !Number.isFinite(value))
50
+ return fallback;
51
+ const intValue = Math.floor(value);
52
+ if (intValue < min)
53
+ return min;
54
+ if (intValue > max)
55
+ return max;
56
+ return intValue;
57
+ }
58
+ function releaseTerminalOwnership(ws, groupJid) {
59
+ if (wsTerminals.get(ws) === groupJid) {
60
+ wsTerminals.delete(ws);
61
+ }
62
+ if (terminalOwners.get(groupJid) === ws) {
63
+ terminalOwners.delete(groupJid);
64
+ }
65
+ }
66
+ // --- CORS Middleware ---
67
+ const CORS_ALLOWED_ORIGINS = process.env.CORS_ALLOWED_ORIGINS || '';
68
+ const CORS_ALLOW_LOCALHOST = process.env.CORS_ALLOW_LOCALHOST !== 'false'; // default: true
69
+ function isAllowedOrigin(origin) {
70
+ if (!origin)
71
+ return null; // same-origin requests
72
+ // 环境变量设为 '*' 时允许所有来源
73
+ if (CORS_ALLOWED_ORIGINS === '*')
74
+ return origin;
75
+ // 允许 localhost / 127.0.0.1 的任意端口(开发 & 自托管场景,可通过 CORS_ALLOW_LOCALHOST=false 关闭)
76
+ if (CORS_ALLOW_LOCALHOST) {
77
+ try {
78
+ const url = new URL(origin);
79
+ if (url.hostname === 'localhost' || url.hostname === '127.0.0.1')
80
+ return origin;
81
+ }
82
+ catch {
83
+ /* invalid origin */
84
+ }
85
+ }
86
+ // 自定义白名单(逗号分隔)
87
+ if (CORS_ALLOWED_ORIGINS) {
88
+ const allowed = CORS_ALLOWED_ORIGINS.split(',').map((s) => s.trim());
89
+ if (allowed.includes(origin))
90
+ return origin;
91
+ }
92
+ return null;
93
+ }
94
+ app.use('/api/*', cors({
95
+ origin: (origin) => isAllowedOrigin(origin),
96
+ credentials: true,
97
+ }));
98
+ // --- Global State ---
99
+ let deps = null;
100
+ // --- Route Mounting ---
101
+ app.route('/api/auth', authRoutes);
102
+ app.route('/api/groups', groupRoutes);
103
+ app.route('/api/groups', fileRoutes); // File routes also under /api/groups
104
+ app.route('/api/memory', memoryRoutes);
105
+ app.route('/api/config', configRoutes);
106
+ app.route('/api/tasks', tasksRoutes);
107
+ app.route('/api/skills', skillsRoutes);
108
+ app.route('/api/admin', adminRoutes);
109
+ app.route('/api/browse', browseRoutes);
110
+ app.route('/api/mcp-servers', mcpServersRoutes);
111
+ app.route('/api/agent-definitions', agentDefinitionsRoutes);
112
+ app.route('/api/groups', agentRoutes); // Agent routes under /api/groups/:jid/agents
113
+ app.route('/api/groups', workspaceConfigRoutes); // Workspace config under /api/groups/:jid/workspace-config
114
+ app.route('/api', monitorRoutes);
115
+ app.route('/api/usage', usageRoutes);
116
+ app.route('/api/billing', billingRoutes);
117
+ app.route('/api/bug-report', bugReportRoutes);
118
+ // --- POST /api/messages ---
119
+ app.post('/api/messages', authMiddleware, async (c) => {
120
+ const body = await c.req.json().catch(() => ({}));
121
+ const validation = MessageCreateSchema.safeParse(body);
122
+ if (!validation.success) {
123
+ return c.json({ error: 'Invalid request body', details: validation.error.format() }, 400);
124
+ }
125
+ const { chatJid, content, attachments } = validation.data;
126
+ const group = getRegisteredGroup(chatJid);
127
+ if (!group)
128
+ return c.json({ error: 'Group not found' }, 404);
129
+ const authUser = c.get('user');
130
+ if (!canAccessGroup(authUser, group)) {
131
+ return c.json({ error: 'Access denied' }, 403);
132
+ }
133
+ if (isHostExecutionGroup(group) && !hasHostExecutionPermission(authUser)) {
134
+ return c.json({ error: 'Insufficient permissions for host execution mode' }, 403);
135
+ }
136
+ const result = await handleWebUserMessage(chatJid, content.trim(), attachments, authUser.id, authUser.display_name || authUser.username);
137
+ if (!result.ok)
138
+ return c.json({ error: result.error }, result.status);
139
+ return c.json({
140
+ success: true,
141
+ messageId: result.messageId,
142
+ timestamp: result.timestamp,
143
+ });
144
+ });
145
+ function persistImmediateMessage(options) {
146
+ const messageId = crypto.randomUUID();
147
+ const timestamp = new Date().toISOString();
148
+ ensureChatExists(options.chatJid);
149
+ storeMessageDirect(messageId, options.chatJid, options.sender, options.senderName, options.content, timestamp, options.isFromMe, options.sourceKind
150
+ ? {
151
+ attachments: options.attachments,
152
+ meta: { sourceKind: options.sourceKind },
153
+ }
154
+ : { attachments: options.attachments });
155
+ broadcastNewMessage(options.chatJid, {
156
+ id: messageId,
157
+ chat_jid: options.chatJid,
158
+ sender: options.sender,
159
+ sender_name: options.senderName,
160
+ content: options.content,
161
+ timestamp,
162
+ is_from_me: options.isFromMe,
163
+ attachments: options.attachments,
164
+ ...(options.sourceKind ? { source_kind: options.sourceKind } : {}),
165
+ });
166
+ return { messageId, timestamp };
167
+ }
168
+ async function handleWebSlashCommand(options) {
169
+ if (!deps)
170
+ return { handled: false };
171
+ const parsed = parseRuntimeCommand(options.content);
172
+ if (!parsed)
173
+ return { handled: false };
174
+ const displayChatJid = options.agentId
175
+ ? `${options.chatJid}#agent:${options.agentId}`
176
+ : options.chatJid;
177
+ const normalizedAttachments = normalizeImageAttachments(options.attachments, {
178
+ onMimeMismatch: ({ declaredMime, detectedMime }) => {
179
+ logger.warn({
180
+ chatJid: displayChatJid,
181
+ declaredMime,
182
+ detectedMime,
183
+ }, 'Web command attachment MIME mismatch detected, using detected MIME');
184
+ },
185
+ });
186
+ const attachmentsStr = normalizedAttachments.length > 0
187
+ ? JSON.stringify(normalizedAttachments)
188
+ : undefined;
189
+ const { messageId, timestamp } = persistImmediateMessage({
190
+ chatJid: displayChatJid,
191
+ sender: options.userId,
192
+ senderName: options.displayName,
193
+ content: options.content.trim(),
194
+ isFromMe: false,
195
+ attachments: attachmentsStr,
196
+ sourceKind: 'user_command',
197
+ });
198
+ const replyWithText = (text) => {
199
+ persistImmediateMessage({
200
+ chatJid: displayChatJid,
201
+ sender: 'cli-claw-agent',
202
+ senderName: ASSISTANT_NAME,
203
+ content: text,
204
+ isFromMe: true,
205
+ });
206
+ };
207
+ if (parsed.name === 'sw') {
208
+ if (deps.handleSpawnCommand && parsed.argsText) {
209
+ try {
210
+ await deps.handleSpawnCommand(displayChatJid, parsed.argsText);
211
+ }
212
+ catch (err) {
213
+ logger.error({ chatJid: displayChatJid, err }, '/sw command failed');
214
+ replyWithText('并行任务创建失败,请稍后重试');
215
+ }
216
+ }
217
+ else {
218
+ replyWithText('用法: /sw <任务描述>');
219
+ }
220
+ return { handled: true, messageId, timestamp };
221
+ }
222
+ if (parsed.name === 'clear') {
223
+ const targetGroup = getRegisteredGroup(options.chatJid);
224
+ if (!targetGroup) {
225
+ replyWithText('未找到当前工作区');
226
+ return { handled: true, messageId, timestamp };
227
+ }
228
+ try {
229
+ await executeSessionReset(options.chatJid, targetGroup.folder, {
230
+ queue: deps.queue,
231
+ sessions: deps.getSessions(),
232
+ broadcast: broadcastNewMessage,
233
+ setLastAgentTimestamp: deps.setLastAgentTimestamp,
234
+ }, options.agentId);
235
+ }
236
+ catch (err) {
237
+ logger.error({ chatJid: displayChatJid, err }, '/clear command failed');
238
+ replyWithText('清除上下文失败,请稍后重试');
239
+ }
240
+ return { handled: true, messageId, timestamp };
241
+ }
242
+ const runtimeResult = await executeRuntimeWorkspaceCommand({
243
+ entrypoint: 'web',
244
+ chatJid: displayChatJid,
245
+ commandText: options.content,
246
+ deps: {
247
+ getGroup: (jid) => deps.getRegisteredGroups()[jid] ?? getRegisteredGroup(jid),
248
+ setGroup: (jid, group) => {
249
+ setRegisteredGroup(jid, group);
250
+ deps.getRegisteredGroups()[jid] = group;
251
+ },
252
+ getSiblingJids: getJidsByFolder,
253
+ getAgent,
254
+ queue: deps.queue,
255
+ getSessions: deps.getSessions,
256
+ },
257
+ });
258
+ if (runtimeResult.handled) {
259
+ if (runtimeResult.reply) {
260
+ replyWithText(runtimeResult.reply);
261
+ }
262
+ return { handled: true, messageId, timestamp };
263
+ }
264
+ replyWithText(`当前 Web 入口不支持 /${parsed.name},请使用 /help 查看当前可用命令`);
265
+ return { handled: true, messageId, timestamp };
266
+ }
267
+ // --- handleWebUserMessage ---
268
+ async function handleWebUserMessage(chatJid, content, attachments, userId = 'web-user', displayName = 'Web') {
269
+ if (!deps)
270
+ return { ok: false, status: 500, error: 'Server not initialized' };
271
+ let group = deps.getRegisteredGroups()[chatJid];
272
+ if (!group) {
273
+ // Group may exist in DB but not in memory cache (created via setup/registration after loadState)
274
+ const dbGroup = getRegisteredGroup(chatJid);
275
+ if (!dbGroup)
276
+ return { ok: false, status: 404, error: 'Group not found' };
277
+ group = dbGroup;
278
+ }
279
+ const commandResult = await handleWebSlashCommand({
280
+ chatJid,
281
+ content,
282
+ attachments,
283
+ userId,
284
+ displayName,
285
+ });
286
+ if (commandResult.handled) {
287
+ return {
288
+ ok: true,
289
+ messageId: commandResult.messageId,
290
+ timestamp: commandResult.timestamp,
291
+ };
292
+ }
293
+ ensureChatExists(chatJid);
294
+ const messageId = crypto.randomUUID();
295
+ const timestamp = new Date().toISOString();
296
+ const normalizedAttachments = normalizeImageAttachments(attachments, {
297
+ onMimeMismatch: ({ declaredMime, detectedMime }) => {
298
+ logger.warn({ chatJid, messageId, declaredMime, detectedMime }, 'Web attachment MIME mismatch detected, using detected MIME');
299
+ },
300
+ });
301
+ const attachmentsStr = normalizedAttachments.length > 0
302
+ ? JSON.stringify(normalizedAttachments)
303
+ : undefined;
304
+ storeMessageDirect(messageId, chatJid, userId, displayName, content, timestamp, false, { attachments: attachmentsStr });
305
+ broadcastNewMessage(chatJid, {
306
+ id: messageId,
307
+ chat_jid: chatJid,
308
+ sender: userId,
309
+ sender_name: displayName,
310
+ content,
311
+ timestamp,
312
+ is_from_me: false,
313
+ attachments: attachmentsStr,
314
+ });
315
+ if (group.created_by) {
316
+ const owner = getUserById(group.created_by);
317
+ if (owner && owner.role !== 'admin') {
318
+ const accessResult = checkBillingAccess(group.created_by, owner.role);
319
+ if (!accessResult.allowed) {
320
+ const sysMsg = formatBillingAccessDeniedMessage(accessResult);
321
+ const sysMsgId = `sys_quota_${Date.now()}`;
322
+ const sysTimestamp = new Date().toISOString();
323
+ storeMessageDirect(sysMsgId, chatJid, '__billing__', ASSISTANT_NAME, sysMsg, sysTimestamp, true);
324
+ broadcastNewMessage(chatJid, {
325
+ id: sysMsgId,
326
+ chat_jid: chatJid,
327
+ sender: '__billing__',
328
+ sender_name: ASSISTANT_NAME,
329
+ content: sysMsg,
330
+ timestamp: sysTimestamp,
331
+ is_from_me: true,
332
+ });
333
+ deps.setLastAgentTimestamp(chatJid, { timestamp, id: messageId });
334
+ deps.advanceGlobalCursor({ timestamp, id: messageId });
335
+ return { ok: true, messageId, timestamp };
336
+ }
337
+ }
338
+ }
339
+ const shared = !group.is_home && isGroupShared(group.folder);
340
+ const formatted = deps.formatMessages([
341
+ {
342
+ id: messageId,
343
+ chat_jid: chatJid,
344
+ sender: userId,
345
+ sender_name: displayName,
346
+ content,
347
+ timestamp,
348
+ },
349
+ ], shared);
350
+ // IPC-inject the message into the running agent process. For home groups,
351
+ // the reply route is dynamically updated via activeRouteUpdaters so we no
352
+ // longer need to kill and restart the process (#99).
353
+ let pipedToActive = false;
354
+ const images = toAgentImages(normalizedAttachments);
355
+ const updateRoute = deps.updateReplyRoute;
356
+ const sendResult = deps.queue.sendMessage(chatJid, formatted, images, () => {
357
+ // IPC write succeeded — update reply route for home groups.
358
+ // Web messages have no IM source, so clear the IM route.
359
+ updateRoute?.(group.folder, null);
360
+ });
361
+ if (sendResult === 'sent') {
362
+ pipedToActive = true;
363
+ }
364
+ else {
365
+ deps.queue.enqueueMessageCheck(chatJid);
366
+ }
367
+ // Only advance per-group cursor when we piped directly into a running container.
368
+ //
369
+ // When piped to active, we also mark the group as having pending IPC-injected
370
+ // messages. If the agent crashes without processing them, the close handler
371
+ // resets pendingMessages so drainGroup re-reads from DB.
372
+ if (pipedToActive) {
373
+ deps.setLastAgentTimestamp(chatJid, { timestamp, id: messageId });
374
+ deps.queue.markIpcInjectedMessage(chatJid);
375
+ }
376
+ deps.advanceGlobalCursor({ timestamp, id: messageId });
377
+ return { ok: true, messageId, timestamp };
378
+ }
379
+ // --- Agent Conversation Message Handler ---
380
+ async function handleAgentConversationMessage(chatJid, agentId, content, userId, displayName, attachments) {
381
+ if (!deps)
382
+ return;
383
+ const agent = getAgent(agentId);
384
+ if (!agent || agent.kind !== 'conversation' || agent.chat_jid !== chatJid) {
385
+ logger.warn({ chatJid, agentId }, 'Agent conversation message rejected: agent not found or not a conversation');
386
+ return;
387
+ }
388
+ const virtualChatJid = `${chatJid}#agent:${agentId}`;
389
+ // Store message with virtual chat_jid
390
+ const messageId = crypto.randomUUID();
391
+ const timestamp = new Date().toISOString();
392
+ const normalizedAttachments = normalizeImageAttachments(attachments, {
393
+ onMimeMismatch: ({ declaredMime, detectedMime }) => {
394
+ logger.warn({ chatJid, messageId, agentId, declaredMime, detectedMime }, 'Agent conversation attachment MIME mismatch detected, using detected MIME');
395
+ },
396
+ });
397
+ const attachmentsStr = normalizedAttachments.length > 0
398
+ ? JSON.stringify(normalizedAttachments)
399
+ : undefined;
400
+ ensureChatExists(virtualChatJid);
401
+ storeMessageDirect(messageId, virtualChatJid, userId, displayName, content, timestamp, false, { attachments: attachmentsStr });
402
+ // Broadcast new_message with agentId so frontend routes to agent tab
403
+ broadcastNewMessage(virtualChatJid, {
404
+ id: messageId,
405
+ chat_jid: virtualChatJid,
406
+ sender: userId,
407
+ sender_name: displayName,
408
+ content,
409
+ timestamp,
410
+ is_from_me: false,
411
+ attachments: attachmentsStr,
412
+ }, agentId);
413
+ // Format for agent
414
+ const shared = false; // agent conversations are not shared
415
+ const formatted = deps.formatMessages([
416
+ {
417
+ id: messageId,
418
+ chat_jid: virtualChatJid,
419
+ sender: userId,
420
+ sender_name: displayName,
421
+ content,
422
+ timestamp,
423
+ },
424
+ ], shared);
425
+ // Try to pipe into running agent process
426
+ const agentImages = toAgentImages(normalizedAttachments);
427
+ const agentSendResult = deps.queue.sendMessage(virtualChatJid, formatted, agentImages);
428
+ if (agentSendResult === 'no_active') {
429
+ // No running process — force close any stale state and start fresh.
430
+ // Mirrors the reliable IM path in buildOnAgentMessage() (#240).
431
+ deps.queue.closeStdin(virtualChatJid);
432
+ if (deps.processAgentConversation) {
433
+ const taskId = `agent-conv:${agentId}:${Date.now()}`;
434
+ deps.queue.enqueueTask(virtualChatJid, taskId, async () => {
435
+ await deps.processAgentConversation(chatJid, agentId);
436
+ });
437
+ }
438
+ }
439
+ // 'sent' needs no further action
440
+ }
441
+ // --- Static Files ---
442
+ const WEB_DIST_ROOT = resolveAppPath('web', 'dist');
443
+ // 带 content hash 的静态资源:长期不可变缓存
444
+ app.use('/assets/*', async (c, next) => {
445
+ await next();
446
+ if (c.res.status === 200) {
447
+ c.res.headers.set('Cache-Control', 'public, max-age=31536000, immutable');
448
+ }
449
+ }, serveStatic({ root: WEB_DIST_ROOT }));
450
+ // SPA fallback:index.html / sw.js 等必须每次验证
451
+ app.use('/*', async (c, next) => {
452
+ await next();
453
+ if (c.res.status === 200) {
454
+ const p = c.req.path;
455
+ // 非文件扩展名路径(SPA fallback → index.html)、SW 脚本、manifest 禁止缓存
456
+ if (!p.match(/\.\w+$/) ||
457
+ p === '/sw.js' ||
458
+ p === '/registerSW.js' ||
459
+ p === '/manifest.webmanifest') {
460
+ c.res.headers.set('Cache-Control', 'no-cache, no-store, must-revalidate');
461
+ }
462
+ }
463
+ }, serveStatic({
464
+ root: WEB_DIST_ROOT,
465
+ rewriteRequestPath: (p) => {
466
+ // SPA fallback
467
+ if (p.startsWith('/api') || p.startsWith('/ws'))
468
+ return p;
469
+ if (p.match(/\.\w+$/))
470
+ return p; // Has file extension
471
+ return '/index.html';
472
+ },
473
+ }));
474
+ // --- WebSocket ---
475
+ function setupWebSocket(server) {
476
+ const wss = new WebSocketServer({ noServer: true });
477
+ server.on('upgrade', (request, socket, head) => {
478
+ const { pathname } = new URL(request.url, `http://${request.headers.host}`);
479
+ if (pathname !== '/ws') {
480
+ socket.destroy();
481
+ return;
482
+ }
483
+ // Verify session cookie
484
+ const cookies = parseCookie(request.headers.cookie);
485
+ const token = cookies[SESSION_COOKIE_NAME_SECURE] || cookies[SESSION_COOKIE_NAME_PLAIN];
486
+ if (!token) {
487
+ socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n');
488
+ socket.destroy();
489
+ return;
490
+ }
491
+ const session = getCachedSessionWithUser(token);
492
+ if (!session) {
493
+ invalidateSessionCache(token);
494
+ socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n');
495
+ socket.destroy();
496
+ return;
497
+ }
498
+ if (isSessionExpired(session.expires_at)) {
499
+ deleteUserSession(token);
500
+ invalidateSessionCache(token);
501
+ socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n');
502
+ socket.destroy();
503
+ return;
504
+ }
505
+ if (session.status !== 'active') {
506
+ socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n');
507
+ socket.destroy();
508
+ return;
509
+ }
510
+ request.__cliClawSessionId = token;
511
+ wss.handleUpgrade(request, socket, head, (ws) => {
512
+ wss.emit('connection', ws, request);
513
+ });
514
+ });
515
+ wss.on('connection', (ws, request) => {
516
+ const sessionId = request?.__cliClawSessionId;
517
+ logger.info('WebSocket client connected');
518
+ const connSession = sessionId
519
+ ? getCachedSessionWithUser(sessionId)
520
+ : undefined;
521
+ wsClients.set(ws, {
522
+ sessionId: sessionId || '',
523
+ userId: connSession?.user_id || '',
524
+ role: (connSession?.role || 'member'),
525
+ });
526
+ // Push streaming snapshots for active groups this user can access
527
+ if (connSession && streamingSnapshots.size > 0) {
528
+ const userId = connSession.user_id;
529
+ for (const [jid, snap] of streamingSnapshots) {
530
+ // Skip stale snapshots (> 30 min)
531
+ // Extended from 5 min to 30 min to support long-running sub-agents.
532
+ // See GitHub issue #241.
533
+ if (Date.now() - snap.updatedAt > 30 * 60 * 1000) {
534
+ streamingSnapshots.delete(jid);
535
+ continue;
536
+ }
537
+ // Skip empty snapshots
538
+ if (!snap.partialText &&
539
+ snap.activeTools.length === 0 &&
540
+ snap.recentEvents.length === 0) {
541
+ continue;
542
+ }
543
+ // Strip #agent: suffix for ACL lookup (virtual JIDs not in registered_groups)
544
+ const baseJid = jid.includes('#agent:') ? jid.split('#agent:')[0] : jid;
545
+ const allowed = getGroupAllowedUserIds(baseJid);
546
+ if (allowed === null || !allowed.has(userId))
547
+ continue;
548
+ try {
549
+ ws.send(JSON.stringify({
550
+ type: 'stream_snapshot',
551
+ chatJid: jid,
552
+ snapshot: {
553
+ partialText: snap.partialText,
554
+ activeTools: snap.activeTools,
555
+ recentEvents: snap.recentEvents,
556
+ todos: snap.todos,
557
+ systemStatus: snap.systemStatus,
558
+ turnId: snap.turnId,
559
+ runtimeIdentity: snap.runtimeIdentity ?? null,
560
+ },
561
+ }));
562
+ }
563
+ catch {
564
+ /* client not ready */
565
+ }
566
+ }
567
+ }
568
+ // Push runner_state: 'running' for all active groups on WS connect.
569
+ // This prevents a race where a late-arriving new_message clears
570
+ // waiting=false after snapshot restore, blocking all subsequent
571
+ // stream events. The runner_state event resets waiting=true.
572
+ if (connSession && deps) {
573
+ const userId = connSession.user_id;
574
+ const queueStatus = deps.queue.getStatus();
575
+ for (const g of queueStatus.groups) {
576
+ if (!g.active)
577
+ continue;
578
+ const jid = normalizeHomeJid(g.jid);
579
+ const allowed = getGroupAllowedUserIds(g.jid);
580
+ if (allowed === null || !allowed.has(userId))
581
+ continue;
582
+ try {
583
+ ws.send(JSON.stringify({
584
+ type: 'runner_state',
585
+ chatJid: jid,
586
+ state: 'running',
587
+ }));
588
+ }
589
+ catch {
590
+ /* client not ready */
591
+ }
592
+ }
593
+ }
594
+ const cleanupTerminalForWs = () => {
595
+ const termJid = wsTerminals.get(ws);
596
+ if (!termJid)
597
+ return;
598
+ terminalManager.stop(termJid);
599
+ releaseTerminalOwnership(ws, termJid);
600
+ };
601
+ ws.on('message', async (data) => {
602
+ if (!deps)
603
+ return;
604
+ try {
605
+ if (!sessionId) {
606
+ ws.close(1008, 'Unauthorized');
607
+ return;
608
+ }
609
+ const session = getCachedSessionWithUser(sessionId);
610
+ if (!session ||
611
+ isSessionExpired(session.expires_at) ||
612
+ session.status !== 'active') {
613
+ if (session && isSessionExpired(session.expires_at)) {
614
+ deleteUserSession(sessionId);
615
+ }
616
+ invalidateSessionCache(sessionId);
617
+ ws.close(1008, 'Unauthorized');
618
+ return;
619
+ }
620
+ const now = Date.now();
621
+ const lastUpdate = lastActiveCache.get(sessionId) || 0;
622
+ if (now - lastUpdate > LAST_ACTIVE_DEBOUNCE_MS) {
623
+ lastActiveCache.set(sessionId, now);
624
+ try {
625
+ updateSessionLastActive(sessionId);
626
+ }
627
+ catch {
628
+ /* best effort */
629
+ }
630
+ }
631
+ const msg = JSON.parse(data.toString());
632
+ const sendWsError = (error, chatJid) => {
633
+ const msg = { type: 'ws_error', error, chatJid };
634
+ ws.send(JSON.stringify(msg));
635
+ };
636
+ if (msg.type === 'send_message') {
637
+ const wsValidation = MessageCreateSchema.safeParse({
638
+ chatJid: msg.chatJid,
639
+ content: msg.content,
640
+ attachments: msg.attachments,
641
+ });
642
+ if (!wsValidation.success) {
643
+ sendWsError('消息格式无效', msg.chatJid);
644
+ logger.warn({
645
+ chatJid: msg.chatJid,
646
+ issues: wsValidation.error.issues.map((i) => i.message),
647
+ }, 'WebSocket send_message validation failed');
648
+ return;
649
+ }
650
+ const { chatJid, content, attachments } = wsValidation.data;
651
+ const agentId = msg.agentId;
652
+ // 群组访问权限检查
653
+ const targetGroup = getRegisteredGroup(chatJid);
654
+ if (targetGroup) {
655
+ if (!canAccessGroup({ id: session.user_id, role: session.role }, targetGroup)) {
656
+ sendWsError('无权访问该群组', chatJid);
657
+ logger.warn({ chatJid, userId: session.user_id }, 'WebSocket send_message blocked: access denied');
658
+ return;
659
+ }
660
+ if (isHostExecutionGroup(targetGroup)) {
661
+ if (session.role !== 'admin') {
662
+ sendWsError('宿主机模式需要管理员权限', chatJid);
663
+ logger.warn({ chatJid, userId: session.user_id }, 'WebSocket send_message blocked: host mode requires admin');
664
+ return;
665
+ }
666
+ }
667
+ }
668
+ const commandResult = await handleWebSlashCommand({
669
+ chatJid,
670
+ content,
671
+ attachments,
672
+ userId: session.user_id,
673
+ displayName: session.display_name || session.username,
674
+ agentId,
675
+ });
676
+ if (commandResult.handled) {
677
+ return;
678
+ }
679
+ // Route to agent conversation handler if agentId is present
680
+ if (agentId && deps) {
681
+ await handleAgentConversationMessage(chatJid, agentId, content.trim(), session.user_id, session.display_name || session.username, attachments);
682
+ return;
683
+ }
684
+ const result = await handleWebUserMessage(chatJid, content.trim(), attachments, session.user_id, session.display_name || session.username);
685
+ if (!result.ok) {
686
+ logger.warn({ chatJid, status: result.status, error: result.error }, 'WebSocket message rejected');
687
+ }
688
+ }
689
+ else if (msg.type === 'terminal_start') {
690
+ try {
691
+ // Schema 验证
692
+ const startValidation = TerminalStartSchema.safeParse(msg);
693
+ if (!startValidation.success) {
694
+ ws.send(JSON.stringify({
695
+ type: 'terminal_error',
696
+ chatJid: msg.chatJid || '',
697
+ error: '终端启动参数无效',
698
+ }));
699
+ return;
700
+ }
701
+ const chatJid = startValidation.data.chatJid.trim();
702
+ if (!chatJid) {
703
+ ws.send(JSON.stringify({
704
+ type: 'terminal_error',
705
+ chatJid: '',
706
+ error: 'chatJid 无效',
707
+ }));
708
+ return;
709
+ }
710
+ const group = deps.getRegisteredGroups()[chatJid];
711
+ if (!group) {
712
+ ws.send(JSON.stringify({
713
+ type: 'terminal_error',
714
+ chatJid,
715
+ error: '群组不存在',
716
+ }));
717
+ return;
718
+ }
719
+ // Permission: user must be able to access the group
720
+ const groupWithJid = { ...group, jid: chatJid };
721
+ if (!canAccessGroup({ id: session.user_id, role: session.role }, groupWithJid)) {
722
+ ws.send(JSON.stringify({
723
+ type: 'terminal_error',
724
+ chatJid,
725
+ error: '无权访问该群组终端',
726
+ }));
727
+ return;
728
+ }
729
+ if ((group.executionMode || 'container') === 'host') {
730
+ ws.send(JSON.stringify({
731
+ type: 'terminal_error',
732
+ chatJid,
733
+ error: '宿主机模式不支持终端',
734
+ }));
735
+ return;
736
+ }
737
+ // 查找活跃的容器
738
+ const status = deps.queue.getStatus();
739
+ const groupStatus = status.groups.find((g) => g.jid === chatJid);
740
+ if (!groupStatus || !groupStatus.active) {
741
+ deps.ensureTerminalContainerStarted(chatJid);
742
+ ws.send(JSON.stringify({
743
+ type: 'terminal_error',
744
+ chatJid,
745
+ error: '工作区启动中,请稍后重试',
746
+ }));
747
+ return;
748
+ }
749
+ if (!groupStatus.containerName) {
750
+ ws.send(JSON.stringify({
751
+ type: 'terminal_error',
752
+ chatJid,
753
+ error: '工作区启动中,请稍后重试',
754
+ }));
755
+ return;
756
+ }
757
+ const cols = normalizeTerminalSize(msg.cols, 80, 20, 300);
758
+ const rows = normalizeTerminalSize(msg.rows, 24, 8, 120);
759
+ // 停止该 ws 之前的终端
760
+ const prevJid = wsTerminals.get(ws);
761
+ if (prevJid && prevJid !== chatJid) {
762
+ terminalManager.stop(prevJid);
763
+ releaseTerminalOwnership(ws, prevJid);
764
+ }
765
+ // 若该 group 已被其它 ws 占用,先释放旧 owner,防止后续 close 误杀新会话
766
+ const existingOwner = terminalOwners.get(chatJid);
767
+ if (existingOwner && existingOwner !== ws) {
768
+ terminalManager.stop(chatJid);
769
+ releaseTerminalOwnership(existingOwner, chatJid);
770
+ if (existingOwner.readyState === WebSocket.OPEN) {
771
+ existingOwner.send(JSON.stringify({
772
+ type: 'terminal_stopped',
773
+ chatJid,
774
+ reason: '终端被其他连接接管',
775
+ }));
776
+ }
777
+ }
778
+ terminalManager.start(chatJid, groupStatus.containerName, cols, rows, (data) => {
779
+ if (ws.readyState === WebSocket.OPEN) {
780
+ ws.send(JSON.stringify({ type: 'terminal_output', chatJid, data }));
781
+ }
782
+ }, (_exitCode, _signal) => {
783
+ if (terminalOwners.get(chatJid) === ws) {
784
+ releaseTerminalOwnership(ws, chatJid);
785
+ }
786
+ if (ws.readyState === WebSocket.OPEN) {
787
+ ws.send(JSON.stringify({
788
+ type: 'terminal_stopped',
789
+ chatJid,
790
+ reason: '终端进程已退出',
791
+ }));
792
+ }
793
+ });
794
+ wsTerminals.set(ws, chatJid);
795
+ terminalOwners.set(chatJid, ws);
796
+ ws.send(JSON.stringify({ type: 'terminal_started', chatJid }));
797
+ }
798
+ catch (err) {
799
+ logger.error({ err, chatJid: msg.chatJid }, 'Error starting terminal');
800
+ const detail = err instanceof Error && err.message
801
+ ? err.message.slice(0, 160)
802
+ : 'unknown';
803
+ ws.send(JSON.stringify({
804
+ type: 'terminal_error',
805
+ chatJid: msg.chatJid,
806
+ error: `启动终端失败 (${detail})`,
807
+ }));
808
+ }
809
+ }
810
+ else if (msg.type === 'terminal_input') {
811
+ const inputValidation = TerminalInputSchema.safeParse(msg);
812
+ if (!inputValidation.success) {
813
+ ws.send(JSON.stringify({
814
+ type: 'terminal_error',
815
+ chatJid: msg.chatJid || '',
816
+ error: '终端输入参数无效',
817
+ }));
818
+ return;
819
+ }
820
+ const ownerJid = wsTerminals.get(ws);
821
+ if (ownerJid !== inputValidation.data.chatJid ||
822
+ terminalOwners.get(inputValidation.data.chatJid) !== ws) {
823
+ ws.send(JSON.stringify({
824
+ type: 'terminal_error',
825
+ chatJid: inputValidation.data.chatJid,
826
+ error: '终端会话已失效',
827
+ }));
828
+ return;
829
+ }
830
+ terminalManager.write(inputValidation.data.chatJid, inputValidation.data.data);
831
+ }
832
+ else if (msg.type === 'terminal_resize') {
833
+ const resizeValidation = TerminalResizeSchema.safeParse(msg);
834
+ if (!resizeValidation.success) {
835
+ ws.send(JSON.stringify({
836
+ type: 'terminal_error',
837
+ chatJid: msg.chatJid || '',
838
+ error: '终端调整参数无效',
839
+ }));
840
+ return;
841
+ }
842
+ const ownerJid = wsTerminals.get(ws);
843
+ if (ownerJid !== resizeValidation.data.chatJid ||
844
+ terminalOwners.get(resizeValidation.data.chatJid) !== ws) {
845
+ ws.send(JSON.stringify({
846
+ type: 'terminal_error',
847
+ chatJid: resizeValidation.data.chatJid,
848
+ error: '终端会话已失效',
849
+ }));
850
+ return;
851
+ }
852
+ const cols = normalizeTerminalSize(resizeValidation.data.cols, 80, 20, 300);
853
+ const rows = normalizeTerminalSize(resizeValidation.data.rows, 24, 8, 120);
854
+ terminalManager.resize(resizeValidation.data.chatJid, cols, rows);
855
+ }
856
+ else if (msg.type === 'terminal_stop') {
857
+ const stopValidation = TerminalStopSchema.safeParse(msg);
858
+ if (!stopValidation.success) {
859
+ return;
860
+ }
861
+ const ownerJid = wsTerminals.get(ws);
862
+ if (ownerJid !== stopValidation.data.chatJid ||
863
+ terminalOwners.get(stopValidation.data.chatJid) !== ws) {
864
+ return;
865
+ }
866
+ terminalManager.stop(stopValidation.data.chatJid);
867
+ releaseTerminalOwnership(ws, stopValidation.data.chatJid);
868
+ ws.send(JSON.stringify({
869
+ type: 'terminal_stopped',
870
+ chatJid: stopValidation.data.chatJid,
871
+ reason: '用户关闭终端',
872
+ }));
873
+ }
874
+ }
875
+ catch (err) {
876
+ logger.error({ err }, 'Error handling WebSocket message');
877
+ }
878
+ });
879
+ ws.on('close', () => {
880
+ logger.info('WebSocket client disconnected');
881
+ wsClients.delete(ws);
882
+ cleanupTerminalForWs();
883
+ });
884
+ ws.on('error', (err) => {
885
+ logger.error({ err }, 'WebSocket error');
886
+ wsClients.delete(ws);
887
+ cleanupTerminalForWs();
888
+ });
889
+ });
890
+ return wss;
891
+ }
892
+ // --- Broadcast Functions ---
893
+ /**
894
+ * Broadcast to all connected WebSocket clients.
895
+ * If adminOnly is true, only send to clients whose session belongs to an admin user.
896
+ * If ownerUserId is provided, only send to that user and admins (for group isolation).
897
+ */
898
+ /**
899
+ * Broadcast a WebSocket message with access control filtering.
900
+ *
901
+ * @param msg - The message to broadcast
902
+ * @param adminOnly - If true, only admin users receive the message
903
+ * @param allowedUserIds - Group access filtering:
904
+ * - undefined: no user-level filtering (e.g. system-wide admin broadcasts)
905
+ * - null: ownership unresolvable → default-deny, only admin can see
906
+ * - Set<string>: only these users + admin can see
907
+ */
908
+ function safeBroadcast(msg, adminOnly = false, allowedUserIds) {
909
+ const data = JSON.stringify(msg);
910
+ for (const [client, clientInfo] of wsClients) {
911
+ if (client.readyState !== WebSocket.OPEN) {
912
+ wsClients.delete(client);
913
+ continue;
914
+ }
915
+ if (!clientInfo.sessionId) {
916
+ wsClients.delete(client);
917
+ try {
918
+ client.close(1008, 'Unauthorized');
919
+ }
920
+ catch {
921
+ /* ignore */
922
+ }
923
+ continue;
924
+ }
925
+ const session = getCachedSessionWithUser(clientInfo.sessionId);
926
+ const expired = !!session && isSessionExpired(session.expires_at);
927
+ const invalid = !session || expired || session.status !== 'active';
928
+ if (invalid) {
929
+ if (expired) {
930
+ deleteUserSession(clientInfo.sessionId);
931
+ }
932
+ invalidateSessionCache(clientInfo.sessionId);
933
+ wsClients.delete(client);
934
+ try {
935
+ client.close(1008, 'Unauthorized');
936
+ }
937
+ catch {
938
+ /* ignore */
939
+ }
940
+ continue;
941
+ }
942
+ if (adminOnly && session.role !== 'admin') {
943
+ continue;
944
+ }
945
+ // Group isolation: only allowed users (owner + shared members) can see this group's events
946
+ // allowedUserIds === null means ownership unresolvable → default-deny (admin-only)
947
+ if (allowedUserIds !== undefined) {
948
+ if (allowedUserIds === null || !allowedUserIds.has(session.user_id)) {
949
+ continue;
950
+ }
951
+ }
952
+ try {
953
+ client.send(data);
954
+ }
955
+ catch {
956
+ wsClients.delete(client);
957
+ }
958
+ }
959
+ }
960
+ /**
961
+ * Get the set of user IDs allowed to receive broadcasts for a group.
962
+ * Includes the owner and all shared members. Admin is NOT automatically included
963
+ * — they must be the owner or a shared member to receive broadcasts.
964
+ *
965
+ * Returns:
966
+ * - Set<string>: allowed user IDs (owner + shared members)
967
+ * - null: ownership unresolvable → default-deny (admin-only)
968
+ */
969
+ const allowedUserIdsCache = new Map();
970
+ const ALLOWED_CACHE_TTL = 10_000; // 10 seconds
971
+ function getGroupAllowedUserIds(chatJid) {
972
+ const now = Date.now();
973
+ const cached = allowedUserIdsCache.get(chatJid);
974
+ if (cached && cached.expiry > now)
975
+ return cached.ids;
976
+ const result = computeGroupAllowedUserIds(chatJid);
977
+ allowedUserIdsCache.set(chatJid, {
978
+ ids: result,
979
+ expiry: now + ALLOWED_CACHE_TTL,
980
+ });
981
+ return result;
982
+ }
983
+ /** Invalidate the allowed-user cache for a group and all sibling JIDs sharing the same folder. */
984
+ export function invalidateAllowedUserCache(chatJid) {
985
+ allowedUserIdsCache.delete(chatJid);
986
+ // Also clear cache for sibling JIDs sharing the same folder,
987
+ // since membership is per-folder, not per-JID.
988
+ const group = getRegisteredGroup(chatJid);
989
+ if (group) {
990
+ const siblingJids = getJidsByFolder(group.folder);
991
+ for (const jid of siblingJids) {
992
+ allowedUserIdsCache.delete(jid);
993
+ }
994
+ }
995
+ }
996
+ function computeGroupAllowedUserIds(chatJid) {
997
+ const group = getRegisteredGroup(chatJid);
998
+ if (!group)
999
+ return null; // Unknown group → deny by default
1000
+ const allowed = new Set();
1001
+ // Add owner
1002
+ let ownerId = group.created_by ?? null;
1003
+ // Legacy fallback: IM group without created_by, resolve by sibling home group.
1004
+ if (!ownerId && !chatJid.startsWith('web:')) {
1005
+ const siblingJids = getJidsByFolder(group.folder);
1006
+ for (const siblingJid of siblingJids) {
1007
+ if (!siblingJid.startsWith('web:'))
1008
+ continue;
1009
+ const siblingGroup = getRegisteredGroup(siblingJid);
1010
+ if (siblingGroup?.is_home && siblingGroup.created_by) {
1011
+ ownerId = siblingGroup.created_by;
1012
+ break;
1013
+ }
1014
+ }
1015
+ }
1016
+ if (!ownerId) {
1017
+ if (group.is_home)
1018
+ return null;
1019
+ if (group.folder === 'main')
1020
+ return null;
1021
+ return null; // Unresolvable → deny by default
1022
+ }
1023
+ allowed.add(ownerId);
1024
+ // For non-home groups, include shared members
1025
+ if (!group.is_home) {
1026
+ const members = getGroupMembers(group.folder);
1027
+ for (const m of members) {
1028
+ allowed.add(m.user_id);
1029
+ }
1030
+ }
1031
+ return allowed;
1032
+ }
1033
+ /** Check if a chatJid belongs to a host-mode group (for broadcast filtering) */
1034
+ function isHostGroupJid(chatJid) {
1035
+ const group = getRegisteredGroup(chatJid);
1036
+ return !!group && isHostExecutionGroup(group);
1037
+ }
1038
+ /**
1039
+ * Normalize chatJid for WebSocket broadcasts.
1040
+ * IM groups (Feishu/Telegram) that share a folder with an is_home group are mapped
1041
+ * to that home group's web JID so the frontend can match all home-session events.
1042
+ */
1043
+ function normalizeHomeJid(chatJid) {
1044
+ if (chatJid.startsWith('web:'))
1045
+ return chatJid;
1046
+ const group = getRegisteredGroup(chatJid);
1047
+ if (!group)
1048
+ return chatJid;
1049
+ // Find the web: JID that shares this folder (typically the is_home group)
1050
+ const jids = getJidsByFolder(group.folder);
1051
+ for (const jid of jids) {
1052
+ if (jid.startsWith('web:')) {
1053
+ return jid;
1054
+ }
1055
+ }
1056
+ return chatJid;
1057
+ }
1058
+ export function broadcastToWebClients(chatJid, text) {
1059
+ const timestamp = new Date().toISOString();
1060
+ const jid = normalizeHomeJid(chatJid);
1061
+ const allowedUserIds = getGroupAllowedUserIds(chatJid);
1062
+ safeBroadcast({ type: 'agent_reply', chatJid: jid, text, timestamp }, isHostGroupJid(chatJid), allowedUserIds);
1063
+ }
1064
+ export function broadcastNewMessage(chatJid, msg, agentId, source) {
1065
+ // For virtual JIDs like "web:xxx#agent:yyy", extract base JID and agentId
1066
+ let baseChatJid = chatJid;
1067
+ let effectiveAgentId = agentId;
1068
+ if (chatJid.includes('#agent:')) {
1069
+ const parts = chatJid.split('#agent:');
1070
+ baseChatJid = parts[0];
1071
+ if (!effectiveAgentId)
1072
+ effectiveAgentId = parts[1];
1073
+ }
1074
+ const jid = normalizeHomeJid(baseChatJid);
1075
+ const allowedUserIds = getGroupAllowedUserIds(baseChatJid);
1076
+ const wsMsg = {
1077
+ type: 'new_message',
1078
+ chatJid: jid,
1079
+ message: { ...msg, is_from_me: msg.is_from_me ?? false },
1080
+ ...(effectiveAgentId ? { agentId: effectiveAgentId } : {}),
1081
+ ...(source ? { source } : {}),
1082
+ };
1083
+ safeBroadcast(wsMsg, isHostGroupJid(baseChatJid), allowedUserIds);
1084
+ }
1085
+ export function broadcastTyping(chatJid, isTyping) {
1086
+ const jid = normalizeHomeJid(chatJid);
1087
+ const allowedUserIds = getGroupAllowedUserIds(chatJid);
1088
+ safeBroadcast({ type: 'typing', chatJid: jid, isTyping }, isHostGroupJid(chatJid), allowedUserIds);
1089
+ }
1090
+ const streamingSnapshots = new Map();
1091
+ /** Accumulates full (non-truncated) text per group for shutdown persistence & disk buffer. */
1092
+ const streamingFullTexts = new Map();
1093
+ const MAX_SNAPSHOT_TEXT = 4000;
1094
+ const MAX_SNAPSHOT_EVENTS = 20;
1095
+ /** Push a recent event entry and truncate to MAX_SNAPSHOT_EVENTS. */
1096
+ function pushRecentEvent(snap, event) {
1097
+ snap.recentEvents.push(event);
1098
+ if (snap.recentEvents.length > MAX_SNAPSHOT_EVENTS) {
1099
+ snap.recentEvents = snap.recentEvents.slice(-MAX_SNAPSHOT_EVENTS);
1100
+ }
1101
+ }
1102
+ function updateStreamingSnapshot(normalizedJid, event) {
1103
+ let snap = streamingSnapshots.get(normalizedJid);
1104
+ // Reset on new turn
1105
+ if (snap?.turnId && event.turnId && snap.turnId !== event.turnId) {
1106
+ snap = undefined;
1107
+ streamingFullTexts.delete(normalizedJid);
1108
+ }
1109
+ if (!snap) {
1110
+ snap = {
1111
+ partialText: '',
1112
+ activeTools: [],
1113
+ recentEvents: [],
1114
+ systemStatus: null,
1115
+ turnId: event.turnId,
1116
+ updatedAt: Date.now(),
1117
+ };
1118
+ }
1119
+ snap.updatedAt = Date.now();
1120
+ if (event.turnId)
1121
+ snap.turnId = event.turnId;
1122
+ if (event.runtimeIdentity)
1123
+ snap.runtimeIdentity = event.runtimeIdentity;
1124
+ switch (event.eventType) {
1125
+ case 'text_delta':
1126
+ if (event.text) {
1127
+ snap.partialText += event.text;
1128
+ if (snap.partialText.length > MAX_SNAPSHOT_TEXT) {
1129
+ snap.partialText = snap.partialText.slice(-MAX_SNAPSHOT_TEXT);
1130
+ }
1131
+ // Accumulate full (non-truncated) text for shutdown persistence
1132
+ streamingFullTexts.set(normalizedJid, (streamingFullTexts.get(normalizedJid) || '') + event.text);
1133
+ }
1134
+ break;
1135
+ case 'tool_use_start':
1136
+ if (event.toolUseId && event.toolName) {
1137
+ snap.activeTools.push({
1138
+ toolName: event.toolName,
1139
+ toolUseId: event.toolUseId,
1140
+ startTime: Date.now(),
1141
+ toolInputSummary: event.toolInputSummary,
1142
+ parentToolUseId: event.parentToolUseId,
1143
+ });
1144
+ pushRecentEvent(snap, {
1145
+ id: event.toolUseId,
1146
+ timestamp: Date.now(),
1147
+ text: event.skillName || event.toolName,
1148
+ kind: event.skillName ? 'skill' : 'tool',
1149
+ });
1150
+ }
1151
+ break;
1152
+ case 'tool_use_end':
1153
+ if (event.toolUseId) {
1154
+ snap.activeTools = snap.activeTools.filter((t) => t.toolUseId !== event.toolUseId);
1155
+ }
1156
+ break;
1157
+ case 'tool_progress':
1158
+ if (event.toolUseId) {
1159
+ const tool = snap.activeTools.find((t) => t.toolUseId === event.toolUseId);
1160
+ if (tool) {
1161
+ if (event.toolInputSummary)
1162
+ tool.toolInputSummary = event.toolInputSummary;
1163
+ }
1164
+ }
1165
+ break;
1166
+ case 'status':
1167
+ snap.systemStatus = event.statusText || null;
1168
+ if (event.statusText) {
1169
+ pushRecentEvent(snap, {
1170
+ id: `status-${Date.now()}`,
1171
+ timestamp: Date.now(),
1172
+ text: event.statusText,
1173
+ kind: 'status',
1174
+ });
1175
+ }
1176
+ break;
1177
+ case 'hook_started':
1178
+ if (event.hookName) {
1179
+ pushRecentEvent(snap, {
1180
+ id: `hook-${Date.now()}`,
1181
+ timestamp: Date.now(),
1182
+ text: `${event.hookName} (${event.hookEvent || ''})`,
1183
+ kind: 'hook',
1184
+ });
1185
+ }
1186
+ break;
1187
+ case 'todo_update':
1188
+ if (event.todos) {
1189
+ snap.todos = event.todos.map((t) => ({
1190
+ id: t.id,
1191
+ content: t.content,
1192
+ status: t.status,
1193
+ }));
1194
+ }
1195
+ break;
1196
+ }
1197
+ streamingSnapshots.set(normalizedJid, snap);
1198
+ }
1199
+ export function clearStreamingSnapshot(chatJid) {
1200
+ const jid = normalizeHomeJid(chatJid);
1201
+ streamingSnapshots.delete(jid);
1202
+ streamingFullTexts.delete(jid);
1203
+ }
1204
+ /**
1205
+ * Return all active streaming texts with non-empty content.
1206
+ * Uses the full (non-truncated) text accumulator for shutdown persistence & disk buffer.
1207
+ */
1208
+ export function getActiveStreamingTexts() {
1209
+ const result = new Map();
1210
+ for (const [jid, fullText] of streamingFullTexts) {
1211
+ const text = fullText.trim();
1212
+ if (text) {
1213
+ result.set(jid, text);
1214
+ }
1215
+ }
1216
+ return result;
1217
+ }
1218
+ export function broadcastStreamEvent(chatJid, event, agentId) {
1219
+ const jid = normalizeHomeJid(chatJid);
1220
+ const allowedUserIds = getGroupAllowedUserIds(chatJid);
1221
+ const msg = agentId
1222
+ ? { type: 'stream_event', chatJid: jid, event, agentId }
1223
+ : { type: 'stream_event', chatJid: jid, event };
1224
+ safeBroadcast(msg, isHostGroupJid(chatJid), allowedUserIds);
1225
+ // Accumulate snapshot for both main and agent streams.
1226
+ // Agent streams use virtual JID format (jid#agent:agentId) as the key.
1227
+ const snapshotJid = agentId ? `${jid}#agent:${agentId}` : jid;
1228
+ updateStreamingSnapshot(snapshotJid, event);
1229
+ }
1230
+ export function broadcastGroupCreated(jid, folder, name, userId) {
1231
+ const allowedUserIds = userId ? new Set([userId]) : undefined;
1232
+ safeBroadcast({ type: 'group_created', jid, folder, name }, false, allowedUserIds);
1233
+ }
1234
+ export function broadcastBillingUpdate(userId, usage) {
1235
+ const msg = {
1236
+ type: 'billing_update',
1237
+ userId,
1238
+ usage,
1239
+ };
1240
+ // Send only to the specific user
1241
+ const allowedUserIds = new Set([userId]);
1242
+ safeBroadcast(msg, false, allowedUserIds);
1243
+ }
1244
+ export function broadcastAgentStatus(chatJid, agentId, status, name, prompt, resultSummary, kind) {
1245
+ const jid = normalizeHomeJid(chatJid);
1246
+ const allowedUserIds = getGroupAllowedUserIds(chatJid);
1247
+ // Resolve kind from DB if not provided
1248
+ const resolvedKind = kind || getAgent(agentId)?.kind;
1249
+ const msg = {
1250
+ type: 'agent_status',
1251
+ chatJid: jid,
1252
+ agentId,
1253
+ status,
1254
+ kind: resolvedKind,
1255
+ name,
1256
+ prompt,
1257
+ resultSummary,
1258
+ };
1259
+ safeBroadcast(msg, isHostGroupJid(chatJid), allowedUserIds);
1260
+ }
1261
+ export function broadcastRunnerState(chatJid, state) {
1262
+ const jid = normalizeHomeJid(chatJid);
1263
+ const allowedUserIds = getGroupAllowedUserIds(chatJid);
1264
+ const msg = {
1265
+ type: 'runner_state',
1266
+ chatJid: jid,
1267
+ state,
1268
+ };
1269
+ safeBroadcast(msg, isHostGroupJid(chatJid), allowedUserIds);
1270
+ // Clear streaming snapshots when runner goes idle (main + all agent snapshots)
1271
+ if (state === 'idle') {
1272
+ streamingSnapshots.delete(jid);
1273
+ streamingFullTexts.delete(jid);
1274
+ // Collect keys first, then delete (avoid mutating Map during iteration)
1275
+ const agentPrefix = jid + '#agent:';
1276
+ const snapshotKeysToDelete = [...streamingSnapshots.keys()].filter((k) => k.startsWith(agentPrefix));
1277
+ const fullTextKeysToDelete = [...streamingFullTexts.keys()].filter((k) => k.startsWith(agentPrefix));
1278
+ for (const key of snapshotKeysToDelete)
1279
+ streamingSnapshots.delete(key);
1280
+ for (const key of fullTextKeysToDelete)
1281
+ streamingFullTexts.delete(key);
1282
+ }
1283
+ }
1284
+ export function broadcastDockerBuildLog(line) {
1285
+ safeBroadcast({ type: 'docker_build_log', line }, true);
1286
+ }
1287
+ export function broadcastDockerBuildComplete(success, error) {
1288
+ safeBroadcast({ type: 'docker_build_complete', success, error }, true);
1289
+ }
1290
+ function broadcastStatus() {
1291
+ if (!deps)
1292
+ return;
1293
+ const queueStatus = deps.queue.getStatus();
1294
+ // Broadcast aggregate system metrics only to admin users.
1295
+ // Non-admin users get per-user filtered metrics via REST /api/status.
1296
+ safeBroadcast({
1297
+ type: 'status_update',
1298
+ activeContainers: queueStatus.activeContainerCount,
1299
+ activeHostProcesses: queueStatus.activeHostProcessCount,
1300
+ activeTotal: queueStatus.activeCount,
1301
+ queueLength: queueStatus.waitingCount,
1302
+ },
1303
+ /* adminOnly */ true);
1304
+ }
1305
+ // --- Server Startup ---
1306
+ let statusInterval = null;
1307
+ let httpServer = null;
1308
+ let wss = null;
1309
+ export function startWebServer(webDeps) {
1310
+ deps = webDeps;
1311
+ setWebDeps(webDeps);
1312
+ injectConfigDeps(webDeps);
1313
+ injectMonitorDeps({
1314
+ broadcastDockerBuildLog,
1315
+ broadcastDockerBuildComplete,
1316
+ });
1317
+ httpServer = serve({
1318
+ fetch: app.fetch,
1319
+ port: WEB_PORT,
1320
+ }, (info) => {
1321
+ logger.info({ port: info.port }, 'Web server started');
1322
+ });
1323
+ wss = setupWebSocket(httpServer);
1324
+ // Register container exit callback for terminal cleanup
1325
+ webDeps.queue.setOnContainerExit((groupJid) => {
1326
+ if (terminalManager.has(groupJid)) {
1327
+ const ownerWs = terminalOwners.get(groupJid);
1328
+ terminalManager.stop(groupJid);
1329
+ if (ownerWs) {
1330
+ releaseTerminalOwnership(ownerWs, groupJid);
1331
+ if (ownerWs.readyState === WebSocket.OPEN) {
1332
+ ownerWs.send(JSON.stringify({
1333
+ type: 'terminal_stopped',
1334
+ chatJid: groupJid,
1335
+ reason: '工作区已停止',
1336
+ }));
1337
+ }
1338
+ }
1339
+ }
1340
+ });
1341
+ // Register runner state change callback for sidebar indicators
1342
+ webDeps.queue.setOnRunnerStateChange(broadcastRunnerState);
1343
+ // Broadcast status every 5 seconds
1344
+ if (statusInterval)
1345
+ clearInterval(statusInterval);
1346
+ statusInterval = setInterval(broadcastStatus, 5000);
1347
+ }
1348
+ // --- Exports ---
1349
+ export function shutdownTerminals() {
1350
+ terminalManager.shutdown();
1351
+ }
1352
+ export async function shutdownWebServer() {
1353
+ if (statusInterval) {
1354
+ clearInterval(statusInterval);
1355
+ statusInterval = null;
1356
+ }
1357
+ // Close all WebSocket connections
1358
+ for (const client of wsClients.keys()) {
1359
+ try {
1360
+ client.close(1001, 'Server shutting down');
1361
+ }
1362
+ catch {
1363
+ /* ignore */
1364
+ }
1365
+ }
1366
+ wsClients.clear();
1367
+ // Close WebSocket server
1368
+ if (wss) {
1369
+ wss.close();
1370
+ wss = null;
1371
+ }
1372
+ // Close HTTP server
1373
+ if (httpServer) {
1374
+ httpServer.close();
1375
+ httpServer = null;
1376
+ }
1377
+ }