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
@@ -0,0 +1,1367 @@
1
+ import { Hono } from 'hono';
2
+ import { authMiddleware } from '../middleware/auth.js';
3
+ import { GroupCreateSchema, GroupPatchSchema, GroupMemberAddSchema, ContainerEnvSchema, } from '../schemas.js';
4
+ import { checkGroupLimit } from '../billing.js';
5
+ import { DATA_DIR, GROUPS_DIR, isDockerAvailable } from '../config.js';
6
+ import { LAUNCH_CWD } from '../app-root.js';
7
+ import { enforceAgentExecutionMode, hasRuntimeBoundaryChange, normalizeAgentType, validateGroupRuntimeUpdate, } from '../group-runtime.js';
8
+ import { materializeHostWorkspaceDefaultCwd, validateHostWorkspaceCwd, } from '../host-workspace-cwd.js';
9
+ import { getModelPresets, normalizeModelPreset, normalizeReasoningEffortPreset, supportsReasoningEffort, } from '../runtime-command-registry.js';
10
+ import { getRuntimeBuildStatus, isRuntimeBuildStale, } from '../runtime-build.js';
11
+ import { isHostExecutionGroup, hasHostExecutionPermission, canAccessGroup, canModifyGroup, canDeleteGroup, canManageGroupMembers, MAX_GROUP_NAME_LEN, getWebDeps, } from '../web-context.js';
12
+ import { getRegisteredGroup, setRegisteredGroup, deleteRegisteredGroup, getAllRegisteredGroups, getAllChats, getJidsByFolder, updateChatName, deleteSession, deleteChatHistory, deleteGroupData, ensureChatExists, storeMessageDirect, getMessagesPage, getMessagesAfter, getMessagesPageMulti, getMessagesAfterMulti, addGroupMember, removeGroupMember, getGroupMembers, getGroupMemberRole, getUserById, getAgent, listUsers, listAgentsByJid, getGroupsByTargetAgent, getGroupsByTargetMainJid, getMessage, deleteMessage, getUserPinnedGroups, pinGroup, unpinGroup, } from '../db.js';
13
+ import { logger } from '../logger.js';
14
+ import { getContainerEnvConfig, saveContainerEnvConfig, toPublicContainerEnvConfig, } from '../runtime-config.js';
15
+ import { loadMountAllowlist, findAllowedRoot, matchesBlockedPattern, } from '../mount-security.js';
16
+ import crypto from 'node:crypto';
17
+ import { execFile } from 'node:child_process';
18
+ import { promisify } from 'node:util';
19
+ import fs from 'node:fs';
20
+ import fsp from 'node:fs/promises';
21
+ import path from 'node:path';
22
+ import net from 'node:net';
23
+ import { broadcastNewMessage, invalidateAllowedUserCache } from '../web.js';
24
+ import { getStreamingSession } from '../feishu-streaming-card.js';
25
+ const execFileAsync = promisify(execFile);
26
+ function normalizeOptionalRuntimeModel(agentType, rawValue) {
27
+ if (rawValue == null)
28
+ return null;
29
+ const trimmed = rawValue.trim();
30
+ if (!trimmed)
31
+ return null;
32
+ return normalizeModelPreset(agentType, trimmed);
33
+ }
34
+ function normalizeOptionalReasoningEffort(agentType, rawValue) {
35
+ if (!supportsReasoningEffort(agentType) || rawValue == null)
36
+ return null;
37
+ const trimmed = rawValue.trim();
38
+ if (!trimmed)
39
+ return null;
40
+ return normalizeReasoningEffortPreset(trimmed);
41
+ }
42
+ function readHistoryCursorQuery(c, prefix) {
43
+ const timestamp = c.req.query(prefix);
44
+ if (!timestamp)
45
+ return undefined;
46
+ const id = c.req.query(`${prefix}_id`);
47
+ const chatJid = c.req.query(`${prefix}_chat_jid`);
48
+ if (!id)
49
+ return timestamp;
50
+ return {
51
+ timestamp,
52
+ id,
53
+ ...(chatJid ? { chat_jid: chatJid } : {}),
54
+ };
55
+ }
56
+ /**
57
+ * 检查 hostname 是否为内网地址(SSRF 防护)。
58
+ * 拒绝 127.x, 10.x, 172.16-31.x, 192.168.x, 169.254.x, ::1, fd00::, fe80:: 等。
59
+ */
60
+ function isPrivateHostname(hostname) {
61
+ // localhost 变体
62
+ if (hostname === 'localhost' || hostname.endsWith('.localhost'))
63
+ return true;
64
+ // IPv6: 移除方括号
65
+ const cleaned = hostname.replace(/^\[|\]$/g, '');
66
+ if (net.isIPv6(cleaned)) {
67
+ const lower = cleaned.toLowerCase();
68
+ if (lower === '::1' || lower === '::')
69
+ return true;
70
+ // fd00::/8 (unique local) 和 fe80::/10 (link-local)
71
+ if (lower.startsWith('fd') || lower.startsWith('fe80'))
72
+ return true;
73
+ // ::ffff:127.0.0.1 等 IPv4-mapped IPv6
74
+ if (lower.startsWith('::ffff:')) {
75
+ const ipv4Part = lower.slice(7);
76
+ return isPrivateIPv4(ipv4Part);
77
+ }
78
+ return false;
79
+ }
80
+ if (net.isIPv4(cleaned)) {
81
+ return isPrivateIPv4(cleaned);
82
+ }
83
+ return false;
84
+ }
85
+ function isPrivateIPv4(ip) {
86
+ const parts = ip.split('.').map(Number);
87
+ if (parts.length !== 4 || parts.some((p) => isNaN(p)))
88
+ return false;
89
+ const [a, b] = parts;
90
+ // 127.0.0.0/8
91
+ if (a === 127)
92
+ return true;
93
+ // 10.0.0.0/8
94
+ if (a === 10)
95
+ return true;
96
+ // 172.16.0.0/12
97
+ if (a === 172 && b >= 16 && b <= 31)
98
+ return true;
99
+ // 192.168.0.0/16
100
+ if (a === 192 && b === 168)
101
+ return true;
102
+ // 169.254.0.0/16 (link-local)
103
+ if (a === 169 && b === 254)
104
+ return true;
105
+ // 0.0.0.0
106
+ if (a === 0)
107
+ return true;
108
+ return false;
109
+ }
110
+ const groupRoutes = new Hono();
111
+ // --- Helper functions ---
112
+ function normalizeGroupName(name) {
113
+ if (typeof name !== 'string')
114
+ return '';
115
+ return name.trim().slice(0, MAX_GROUP_NAME_LEN);
116
+ }
117
+ function buildGroupsPayload(user) {
118
+ const groups = getAllRegisteredGroups();
119
+ const chats = new Map(getAllChats().map((chat) => [chat.jid, chat]));
120
+ const isAdmin = hasHostExecutionPermission(user);
121
+ const isSharedAdminHomeGroup = (jid, group) => isAdmin && !!group.is_home && jid === 'web:main' && group.folder === 'main';
122
+ const homeFolders = new Set(Object.entries(groups)
123
+ .filter(([jid, group]) => jid.startsWith('web:') && !!group.is_home)
124
+ .map(([_, group]) => group.folder));
125
+ const result = {};
126
+ // 先过滤出要显示的群组 jid
127
+ const visibleEntries = [];
128
+ for (const [jid, group] of Object.entries(groups)) {
129
+ const isHome = !!group.is_home;
130
+ const isWeb = jid.startsWith('web:');
131
+ const isHost = isHostExecutionGroup(group);
132
+ const isSharedAdminHome = isSharedAdminHomeGroup(jid, group);
133
+ // Hide IM channels that belong to a home folder.
134
+ // These are merged into the home conversation in UI and message APIs.
135
+ if (!isWeb && !isHome && homeFolders.has(group.folder))
136
+ continue;
137
+ // Hide other users' home groups from the chat sidebar.
138
+ // Each user only sees their own home container.
139
+ if (isHome && group.created_by !== user.id && !isSharedAdminHome)
140
+ continue;
141
+ // Host execution groups require admin unless it's the user's own home group
142
+ if (isHost &&
143
+ !isAdmin &&
144
+ !(isHome && (group.created_by === user.id || isSharedAdminHome)))
145
+ continue;
146
+ // User isolation: all users only see their own groups + shared groups
147
+ if (!canAccessGroup({ id: user.id, role: user.role }, { ...group, jid }))
148
+ continue;
149
+ visibleEntries.push([jid, group]);
150
+ }
151
+ // 批量获取每个 jid 的最新消息(替代 N+1 逐个查询)
152
+ const visibleJids = visibleEntries.map(([jid]) => jid);
153
+ const latestByJid = new Map();
154
+ if (visibleJids.length > 0) {
155
+ // 用 multi 查询获取足够多的消息来覆盖所有 jid
156
+ const allLatest = getMessagesPageMulti(visibleJids, undefined, visibleJids.length * 3);
157
+ for (const msg of allLatest) {
158
+ if (!latestByJid.has(msg.chat_jid)) {
159
+ latestByJid.set(msg.chat_jid, {
160
+ content: msg.content,
161
+ timestamp: msg.timestamp,
162
+ });
163
+ }
164
+ }
165
+ }
166
+ // Fetch user's pinned groups
167
+ const pins = getUserPinnedGroups(user.id);
168
+ // Cache member info per folder (avoid repeated queries)
169
+ const memberCache = new Map();
170
+ function getMemberInfo(folder) {
171
+ let cached = memberCache.get(folder);
172
+ if (!cached) {
173
+ const members = getGroupMembers(folder);
174
+ const role = members.find((m) => m.user_id === user.id)?.role ?? null;
175
+ cached = { count: members.length, role };
176
+ memberCache.set(folder, cached);
177
+ }
178
+ return cached;
179
+ }
180
+ for (const [jid, group] of visibleEntries) {
181
+ const isHome = !!group.is_home;
182
+ const isWeb = jid.startsWith('web:');
183
+ const isSharedAdminHome = isSharedAdminHomeGroup(jid, group);
184
+ const latest = latestByJid.get(jid);
185
+ const memberInfo = !isHome ? getMemberInfo(group.folder) : null;
186
+ const isShared = memberInfo ? memberInfo.count > 1 : false;
187
+ result[jid] = {
188
+ name: group.name,
189
+ folder: group.folder,
190
+ added_at: group.added_at,
191
+ agent_type: group.agentType || 'claude',
192
+ model: group.model ?? null,
193
+ reasoning_effort: group.reasoningEffort ?? null,
194
+ kind: isHome ? 'home' : isWeb ? 'web' : 'feishu',
195
+ editable: isWeb,
196
+ deletable: isWeb && !isHome,
197
+ lastMessage: latest?.content,
198
+ lastMessageTime: latest?.timestamp ||
199
+ chats.get(jid)?.last_message_time ||
200
+ group.added_at,
201
+ execution_mode: group.executionMode || 'container',
202
+ custom_cwd: isAdmin ? group.customCwd : undefined,
203
+ is_home: isHome || undefined,
204
+ is_my_home: (isHome && (group.created_by === user.id || isSharedAdminHome)) ||
205
+ undefined,
206
+ is_shared: isShared || undefined,
207
+ member_role: memberInfo?.role ?? undefined,
208
+ member_count: isShared ? memberInfo?.count : undefined,
209
+ pinned_at: pins[jid] || undefined,
210
+ activation_mode: group.activation_mode ?? 'auto',
211
+ };
212
+ }
213
+ return result;
214
+ }
215
+ import { removeFlowArtifacts } from '../file-manager.js';
216
+ import { clearSessionJsonlFiles, resetWorkspaceRuntimeState, } from '../workspace-runtime-reset.js';
217
+ export { removeFlowArtifacts };
218
+ function resetWorkspaceForGroup(folder) {
219
+ // 1. 清除工作目录(Agent 文件、AGENTS.md、logs/ 等),然后重建空目录
220
+ const groupDir = path.join(GROUPS_DIR, folder);
221
+ fs.rmSync(groupDir, { recursive: true, force: true });
222
+ fs.mkdirSync(groupDir, { recursive: true });
223
+ // 2. 清除整个 Claude 会话目录(下次启动时 container-runner 会重建)
224
+ fs.rmSync(path.join(DATA_DIR, 'sessions', folder), {
225
+ recursive: true,
226
+ force: true,
227
+ });
228
+ // 3. 清除 IPC 残留并重建目录结构
229
+ const ipcDir = path.join(DATA_DIR, 'ipc', folder);
230
+ fs.rmSync(ipcDir, { recursive: true, force: true });
231
+ fs.mkdirSync(path.join(ipcDir, 'input'), { recursive: true });
232
+ fs.mkdirSync(path.join(ipcDir, 'messages'), { recursive: true });
233
+ fs.mkdirSync(path.join(ipcDir, 'tasks'), { recursive: true });
234
+ // 4. 清除日期记忆目录(~/.cli-claw/memory/{folder}/)
235
+ fs.rmSync(path.join(DATA_DIR, 'memory', folder), {
236
+ recursive: true,
237
+ force: true,
238
+ });
239
+ }
240
+ function toPublicContainerEnvForUser(config, user) {
241
+ const base = toPublicContainerEnvConfig(config);
242
+ if (user.role === 'admin' ||
243
+ (user.permissions && user.permissions.includes('manage_group_env'))) {
244
+ return base;
245
+ }
246
+ return {
247
+ ...base,
248
+ customEnv: {},
249
+ };
250
+ }
251
+ // --- Routes ---
252
+ // GET /api/groups - 获取群组列表
253
+ groupRoutes.get('/', authMiddleware, (c) => {
254
+ const user = c.get('user');
255
+ const groups = buildGroupsPayload(user);
256
+ return c.json({ groups });
257
+ });
258
+ // POST /api/groups - 创建新群组
259
+ groupRoutes.post('/', authMiddleware, async (c) => {
260
+ const deps = getWebDeps();
261
+ if (!deps)
262
+ return c.json({ error: 'Server not initialized' }, 500);
263
+ const body = await c.req.json().catch(() => ({}));
264
+ const validation = GroupCreateSchema.safeParse(body);
265
+ if (!validation.success) {
266
+ return c.json({ error: 'Invalid request body' }, 400);
267
+ }
268
+ const name = normalizeGroupName(validation.data.name);
269
+ if (!name) {
270
+ return c.json({ error: 'Group name is required' }, 400);
271
+ }
272
+ const agentType = normalizeAgentType(validation.data.agent_type);
273
+ const executionMode = validation.data.execution_mode ||
274
+ (agentType === 'codex'
275
+ ? 'host'
276
+ : (await isDockerAvailable())
277
+ ? 'container'
278
+ : 'host');
279
+ const model = normalizeOptionalRuntimeModel(agentType, validation.data.model);
280
+ if (validation.data.model && !model) {
281
+ return c.json({
282
+ error: `Unsupported ${agentType} model preset`,
283
+ presets: getModelPresets(agentType),
284
+ }, 400);
285
+ }
286
+ const reasoningEffort = normalizeOptionalReasoningEffort(agentType, validation.data.reasoning_effort);
287
+ if (validation.data.reasoning_effort && !reasoningEffort) {
288
+ return c.json({
289
+ error: supportsReasoningEffort(agentType)
290
+ ? 'Unsupported reasoning_effort preset'
291
+ : `${agentType} does not support reasoning_effort`,
292
+ }, 400);
293
+ }
294
+ const customCwd = validation.data.custom_cwd; // Schema already trims and converts empty to undefined
295
+ const initSourcePath = validation.data.init_source_path;
296
+ const initGitUrl = validation.data.init_git_url;
297
+ const authUser = c.get('user');
298
+ let normalizedCustomCwd;
299
+ const runtimeError = enforceAgentExecutionMode(agentType, executionMode);
300
+ if (runtimeError) {
301
+ return c.json({ error: runtimeError }, 400);
302
+ }
303
+ // Billing: check group limit
304
+ const groupLimit = checkGroupLimit(authUser.id, authUser.role);
305
+ if (!groupLimit.allowed) {
306
+ return c.json({ error: groupLimit.reason }, 403);
307
+ }
308
+ // 互斥校验:init_source_path 和 init_git_url 不能同时指定
309
+ if (initSourcePath && initGitUrl) {
310
+ return c.json({ error: 'init_source_path and init_git_url are mutually exclusive' }, 400);
311
+ }
312
+ // init_source_path / init_git_url 仅 container 模式可用
313
+ if (executionMode === 'host' && (initSourcePath || initGitUrl)) {
314
+ return c.json({
315
+ error: 'init_source_path and init_git_url are only valid for container mode',
316
+ }, 400);
317
+ }
318
+ if (executionMode === 'host') {
319
+ if (!hasHostExecutionPermission(authUser)) {
320
+ return c.json({ error: 'Insufficient permissions for host execution mode' }, 403);
321
+ }
322
+ if (customCwd) {
323
+ const validation = validateHostWorkspaceCwd(customCwd, {
324
+ fieldLabel: 'custom_cwd',
325
+ });
326
+ if ('error' in validation) {
327
+ return c.json({ error: validation.error }, validation.error.includes('under an allowed root') ? 403 : 400);
328
+ }
329
+ normalizedCustomCwd = validation.cwd;
330
+ }
331
+ }
332
+ else if (customCwd) {
333
+ return c.json({ error: 'custom_cwd is only valid for host mode' }, 400);
334
+ }
335
+ // 验证 init_source_path
336
+ if (initSourcePath) {
337
+ if (!hasHostExecutionPermission(authUser)) {
338
+ return c.json({ error: 'Insufficient permissions: init_source_path requires admin' }, 403);
339
+ }
340
+ if (!path.isAbsolute(initSourcePath)) {
341
+ return c.json({ error: 'init_source_path must be an absolute path' }, 400);
342
+ }
343
+ let realPath;
344
+ try {
345
+ const stat = fs.statSync(initSourcePath);
346
+ if (!stat.isDirectory()) {
347
+ return c.json({ error: 'init_source_path must be an existing directory' }, 400);
348
+ }
349
+ realPath = fs.realpathSync(initSourcePath);
350
+ }
351
+ catch {
352
+ return c.json({ error: 'init_source_path directory does not exist' }, 400);
353
+ }
354
+ // 白名单校验
355
+ const allowlist = loadMountAllowlist();
356
+ if (allowlist &&
357
+ allowlist.allowedRoots &&
358
+ allowlist.allowedRoots.length > 0) {
359
+ const allowedRoot = findAllowedRoot(realPath, allowlist.allowedRoots);
360
+ if (!allowedRoot) {
361
+ const allowedPaths = allowlist.allowedRoots
362
+ .map((r) => r.path)
363
+ .join(', ');
364
+ return c.json({
365
+ error: `init_source_path must be under an allowed root. Allowed roots: ${allowedPaths}. Check config/mount-allowlist.json`,
366
+ }, 403);
367
+ }
368
+ // 敏感路径过滤
369
+ const blockedMatch = matchesBlockedPattern(realPath, allowlist.blockedPatterns);
370
+ if (blockedMatch) {
371
+ return c.json({
372
+ error: `init_source_path matches blocked pattern "${blockedMatch}"`,
373
+ }, 403);
374
+ }
375
+ }
376
+ }
377
+ // 验证 init_git_url(SSRF 防护 + admin 权限)
378
+ if (initGitUrl) {
379
+ if (!hasHostExecutionPermission(authUser)) {
380
+ return c.json({ error: 'Insufficient permissions: init_git_url requires admin' }, 403);
381
+ }
382
+ if (initGitUrl.length > 2000) {
383
+ return c.json({ error: 'init_git_url is too long (max 2000 characters)' }, 400);
384
+ }
385
+ let gitUrl;
386
+ try {
387
+ gitUrl = new URL(initGitUrl);
388
+ }
389
+ catch {
390
+ return c.json({ error: 'init_git_url is not a valid URL' }, 400);
391
+ }
392
+ // 仅允许 https 协议(HTTP 明文传输存在中间人攻击风险)
393
+ if (gitUrl.protocol !== 'https:') {
394
+ return c.json({ error: 'init_git_url must use https protocol' }, 400);
395
+ }
396
+ // 阻止内网地址
397
+ if (isPrivateHostname(gitUrl.hostname)) {
398
+ return c.json({ error: 'init_git_url must not point to a private/internal address' }, 400);
399
+ }
400
+ }
401
+ const jid = `web:${crypto.randomUUID()}`;
402
+ const folder = `flow-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 6)}`;
403
+ const now = new Date().toISOString();
404
+ const group = {
405
+ name,
406
+ folder,
407
+ added_at: now,
408
+ agentType,
409
+ executionMode: executionMode,
410
+ model,
411
+ reasoningEffort,
412
+ customCwd: executionMode === 'host' ? normalizedCustomCwd : undefined,
413
+ initSourcePath: executionMode !== 'host' ? initSourcePath : undefined,
414
+ initGitUrl: executionMode !== 'host' ? initGitUrl : undefined,
415
+ created_by: authUser.id,
416
+ };
417
+ const materializedGroup = materializeHostWorkspaceDefaultCwd(group, {
418
+ launchCwd: LAUNCH_CWD,
419
+ fieldLabel: 'CLI launch cwd',
420
+ });
421
+ if ('error' in materializedGroup) {
422
+ return c.json({ error: materializedGroup.error }, 500);
423
+ }
424
+ setRegisteredGroup(jid, materializedGroup.group);
425
+ updateChatName(jid, name);
426
+ deps.getRegisteredGroups()[jid] = materializedGroup.group;
427
+ // Register creator as owner in group_members
428
+ addGroupMember(folder, authUser.id, 'owner', authUser.id);
429
+ // 工作区初始化
430
+ const groupDir = path.join(GROUPS_DIR, folder);
431
+ try {
432
+ if (initSourcePath) {
433
+ await fsp.mkdir(groupDir, { recursive: true });
434
+ await fsp.cp(initSourcePath, groupDir, { recursive: true });
435
+ logger.info({ folder, source: initSourcePath }, 'Workspace initialized from local directory');
436
+ }
437
+ if (initGitUrl) {
438
+ await execFileAsync('git', ['clone', '--depth', '1', initGitUrl, groupDir], {
439
+ timeout: 120_000,
440
+ });
441
+ logger.info({ folder, url: initGitUrl }, 'Workspace initialized from git clone');
442
+ }
443
+ }
444
+ catch (err) {
445
+ // 初始化失败时清理
446
+ logger.error({ folder, err }, 'Workspace initialization failed, cleaning up');
447
+ fs.rmSync(groupDir, { recursive: true, force: true });
448
+ deleteRegisteredGroup(jid);
449
+ deleteChatHistory(jid);
450
+ delete deps.getRegisteredGroups()[jid];
451
+ const errMsg = err instanceof Error ? err.message : String(err);
452
+ return c.json({ error: `Workspace initialization failed: ${errMsg}` }, 500);
453
+ }
454
+ // 容器模式工作区创建后立即启动容器预热,避免用户打开终端时还需等待
455
+ if (executionMode === 'container') {
456
+ deps.ensureTerminalContainerStarted(jid);
457
+ }
458
+ return c.json({
459
+ success: true,
460
+ jid,
461
+ group: {
462
+ name: group.name,
463
+ folder: group.folder,
464
+ added_at: group.added_at,
465
+ agent_type: group.agentType || 'claude',
466
+ execution_mode: group.executionMode || 'container',
467
+ model: group.model ?? null,
468
+ reasoning_effort: group.reasoningEffort ?? null,
469
+ custom_cwd: hasHostExecutionPermission(authUser)
470
+ ? materializedGroup.group.customCwd
471
+ : undefined,
472
+ kind: 'web',
473
+ editable: true,
474
+ deletable: true,
475
+ lastMessage: undefined,
476
+ lastMessageTime: now,
477
+ member_role: 'owner',
478
+ member_count: 1,
479
+ is_shared: false,
480
+ },
481
+ });
482
+ });
483
+ // PATCH /api/groups/:jid - 重命名群组
484
+ groupRoutes.patch('/:jid', authMiddleware, async (c) => {
485
+ const deps = getWebDeps();
486
+ if (!deps)
487
+ return c.json({ error: 'Server not initialized' }, 500);
488
+ const jid = c.req.param('jid');
489
+ const existing = getRegisteredGroup(jid);
490
+ if (!existing)
491
+ return c.json({ error: 'Group not found' }, 404);
492
+ const authUser = c.get('user');
493
+ const body = await c.req.json().catch(() => ({}));
494
+ const validation = GroupPatchSchema.safeParse(body);
495
+ if (!validation.success) {
496
+ return c.json({ error: 'Invalid request body' }, 400);
497
+ }
498
+ const { name: rawName, is_pinned, activation_mode, agent_type, execution_mode, model, reasoning_effort, } = validation.data;
499
+ const name = rawName ? normalizeGroupName(rawName) : undefined;
500
+ // 至少需要提供一个字段
501
+ if (!name &&
502
+ is_pinned === undefined &&
503
+ activation_mode === undefined &&
504
+ agent_type === undefined &&
505
+ execution_mode === undefined &&
506
+ model === undefined &&
507
+ reasoning_effort === undefined) {
508
+ return c.json({ error: 'No fields to update' }, 400);
509
+ }
510
+ // member 用户不允许使用 host 模式(安全限制)
511
+ if (execution_mode === 'host' && !hasHostExecutionPermission(authUser)) {
512
+ return c.json({ error: 'Insufficient permissions for host execution mode' }, 403);
513
+ }
514
+ // Pin/unpin only requires canAccessGroup (it's a per-user preference)
515
+ const isPinOnly = is_pinned !== undefined &&
516
+ !name &&
517
+ activation_mode === undefined &&
518
+ agent_type === undefined &&
519
+ execution_mode === undefined &&
520
+ model === undefined &&
521
+ reasoning_effort === undefined;
522
+ if (isPinOnly) {
523
+ if (!canAccessGroup({ id: authUser.id, role: authUser.role }, { ...existing, jid })) {
524
+ return c.json({ error: 'Group not found' }, 404);
525
+ }
526
+ }
527
+ else {
528
+ // Name/skills changes require canModifyGroup (owner only)
529
+ if (!canModifyGroup({ id: authUser.id, role: authUser.role }, { ...existing, jid })) {
530
+ return c.json({ error: 'Group not found' }, 404);
531
+ }
532
+ if (!jid.startsWith('web:') && authUser.role !== 'admin') {
533
+ return c.json({ error: 'This group cannot be edited' }, 403);
534
+ }
535
+ if (isHostExecutionGroup(existing) &&
536
+ !hasHostExecutionPermission(authUser)) {
537
+ return c.json({ error: 'Insufficient permissions for host execution mode' }, 403);
538
+ }
539
+ }
540
+ // Handle pin/unpin (per-user, separate table)
541
+ let pinned_at;
542
+ if (is_pinned === true) {
543
+ pinned_at = pinGroup(authUser.id, jid);
544
+ }
545
+ else if (is_pinned === false) {
546
+ unpinGroup(authUser.id, jid);
547
+ }
548
+ // Update registered group if any editable field changed
549
+ if (name ||
550
+ activation_mode !== undefined ||
551
+ agent_type !== undefined ||
552
+ execution_mode !== undefined ||
553
+ model !== undefined ||
554
+ reasoning_effort !== undefined) {
555
+ const nextAgentType = agent_type !== undefined
556
+ ? normalizeAgentType(agent_type)
557
+ : existing.agentType || 'claude';
558
+ const nextExecutionMode = execution_mode !== undefined
559
+ ? execution_mode
560
+ : existing.executionMode || 'container';
561
+ const nextModel = model !== undefined
562
+ ? normalizeOptionalRuntimeModel(nextAgentType, model)
563
+ : (existing.model ?? null);
564
+ if (model !== undefined && model !== null && !nextModel) {
565
+ return c.json({
566
+ error: `Unsupported ${nextAgentType} model preset`,
567
+ presets: getModelPresets(nextAgentType),
568
+ }, 400);
569
+ }
570
+ const nextReasoningEffort = reasoning_effort !== undefined
571
+ ? normalizeOptionalReasoningEffort(nextAgentType, reasoning_effort)
572
+ : (existing.reasoningEffort ?? null);
573
+ if (reasoning_effort !== undefined &&
574
+ reasoning_effort !== null &&
575
+ !nextReasoningEffort) {
576
+ return c.json({
577
+ error: supportsReasoningEffort(nextAgentType)
578
+ ? 'Unsupported reasoning_effort preset'
579
+ : `${nextAgentType} does not support reasoning_effort`,
580
+ }, 400);
581
+ }
582
+ const runtimeBoundaryChanged = hasRuntimeBoundaryChange({
583
+ currentAgentType: existing.agentType || 'claude',
584
+ currentExecutionMode: existing.executionMode || 'container',
585
+ nextAgentType,
586
+ nextExecutionMode,
587
+ });
588
+ const runtimeSettingsChanged = runtimeBoundaryChanged ||
589
+ (existing.model ?? null) !== nextModel ||
590
+ (existing.reasoningEffort ?? null) !== nextReasoningEffort;
591
+ const runtimeError = validateGroupRuntimeUpdate({
592
+ isHome: !!existing.is_home,
593
+ currentExecutionMode: existing.executionMode || 'container',
594
+ nextAgentType,
595
+ nextExecutionMode,
596
+ });
597
+ if (runtimeError) {
598
+ return c.json({ error: runtimeError }, runtimeError === 'Cannot change execution mode of home containers'
599
+ ? 403
600
+ : 400);
601
+ }
602
+ if (runtimeBoundaryChanged && isRuntimeBuildStale()) {
603
+ const buildStatus = getRuntimeBuildStatus();
604
+ logger.warn({
605
+ jid,
606
+ folder: existing.folder,
607
+ previousAgentType: existing.agentType || 'claude',
608
+ nextAgentType,
609
+ previousExecutionMode: existing.executionMode || 'container',
610
+ nextExecutionMode,
611
+ buildStatus,
612
+ }, 'Rejected workspace runtime change because backend process is stale');
613
+ return c.json({
614
+ error: 'Runtime change requires a backend restart because the current process is older than the on-disk build',
615
+ stale_build: true,
616
+ }, 409);
617
+ }
618
+ const updated = {
619
+ name: name || existing.name,
620
+ folder: existing.folder,
621
+ added_at: existing.added_at,
622
+ containerConfig: existing.containerConfig,
623
+ agentType: nextAgentType,
624
+ executionMode: execution_mode !== undefined
625
+ ? execution_mode
626
+ : existing.executionMode,
627
+ model: nextModel,
628
+ reasoningEffort: nextReasoningEffort,
629
+ customCwd: existing.customCwd,
630
+ initSourcePath: existing.initSourcePath,
631
+ initGitUrl: existing.initGitUrl,
632
+ created_by: existing.created_by,
633
+ is_home: existing.is_home,
634
+ target_agent_id: existing.target_agent_id,
635
+ target_main_jid: existing.target_main_jid,
636
+ reply_policy: existing.reply_policy,
637
+ require_mention: existing.require_mention,
638
+ activation_mode: activation_mode !== undefined
639
+ ? activation_mode
640
+ : existing.activation_mode,
641
+ };
642
+ const materializedGroup = materializeHostWorkspaceDefaultCwd(updated, {
643
+ launchCwd: LAUNCH_CWD,
644
+ fieldLabel: 'CLI launch cwd',
645
+ });
646
+ if ('error' in materializedGroup) {
647
+ return c.json({ error: materializedGroup.error }, 500);
648
+ }
649
+ const persistedGroup = materializedGroup.group;
650
+ setRegisteredGroup(jid, persistedGroup);
651
+ if (name)
652
+ updateChatName(jid, name);
653
+ deps.getRegisteredGroups()[jid] = persistedGroup;
654
+ if (runtimeSettingsChanged) {
655
+ try {
656
+ await resetWorkspaceRuntimeState(deps, jid, persistedGroup);
657
+ }
658
+ catch (err) {
659
+ logger.error({
660
+ jid,
661
+ folder: persistedGroup.folder,
662
+ previousAgentType: existing.agentType || 'claude',
663
+ nextAgentType,
664
+ previousExecutionMode: existing.executionMode || 'container',
665
+ nextExecutionMode,
666
+ previousModel: existing.model ?? null,
667
+ nextModel,
668
+ previousReasoningEffort: existing.reasoningEffort ?? null,
669
+ nextReasoningEffort,
670
+ err,
671
+ }, 'Workspace runtime changed but failed to reset active runners');
672
+ return c.json({
673
+ error: 'Workspace runtime updated, but failed to reset active sessions',
674
+ }, 500);
675
+ }
676
+ logger.info({
677
+ jid,
678
+ folder: persistedGroup.folder,
679
+ previousAgentType: existing.agentType || 'claude',
680
+ nextAgentType,
681
+ previousExecutionMode: existing.executionMode || 'container',
682
+ nextExecutionMode,
683
+ previousModel: existing.model ?? null,
684
+ nextModel,
685
+ previousReasoningEffort: existing.reasoningEffort ?? null,
686
+ nextReasoningEffort,
687
+ }, 'Workspace runtime changed, reset active runners and sessions');
688
+ }
689
+ }
690
+ return c.json({ success: true, pinned_at });
691
+ });
692
+ // DELETE /api/groups/:jid - 删除群组
693
+ groupRoutes.delete('/:jid', authMiddleware, async (c) => {
694
+ const deps = getWebDeps();
695
+ if (!deps)
696
+ return c.json({ error: 'Server not initialized' }, 500);
697
+ const jid = c.req.param('jid');
698
+ const existing = getRegisteredGroup(jid);
699
+ if (!existing)
700
+ return c.json({ error: 'Group not found' }, 404);
701
+ const authUser = c.get('user');
702
+ if (!canDeleteGroup({ id: authUser.id, role: authUser.role }, existing)) {
703
+ return c.json({ error: 'Group not found' }, 404);
704
+ }
705
+ if (!jid.startsWith('web:')) {
706
+ return c.json({ error: 'This group cannot be deleted' }, 403);
707
+ }
708
+ if (isHostExecutionGroup(existing) && !hasHostExecutionPermission(authUser)) {
709
+ return c.json({ error: 'Insufficient permissions for host execution mode' }, 403);
710
+ }
711
+ // Block deletion if any IM binding exists (agent or main conversation)
712
+ const agents = listAgentsByJid(jid);
713
+ const boundAgents = [];
714
+ for (const a of agents) {
715
+ if (a.kind === 'conversation') {
716
+ const linked = getGroupsByTargetAgent(a.id);
717
+ if (linked.length > 0) {
718
+ boundAgents.push({
719
+ agentId: a.id,
720
+ agentName: a.name,
721
+ imGroups: linked.map((l) => ({ jid: l.jid, name: l.group.name })),
722
+ });
723
+ }
724
+ }
725
+ }
726
+ // Search by actual JID; also check legacy folder-based format for backward compat
727
+ const mainBoundByJid = getGroupsByTargetMainJid(jid);
728
+ const legacyMainJid = `web:${existing.folder}`;
729
+ const mainBoundByFolder = legacyMainJid !== jid ? getGroupsByTargetMainJid(legacyMainJid) : [];
730
+ const mainBoundJids = new Set(mainBoundByJid.map((l) => l.jid));
731
+ const mainBound = [
732
+ ...mainBoundByJid,
733
+ ...mainBoundByFolder.filter((l) => !mainBoundJids.has(l.jid)),
734
+ ];
735
+ if (boundAgents.length > 0 || mainBound.length > 0) {
736
+ const mainImGroups = mainBound.map((l) => ({
737
+ jid: l.jid,
738
+ name: l.group.name,
739
+ }));
740
+ return c.json({
741
+ error: '该工作区绑定了 IM 群组,请先解绑后再删除。',
742
+ bound_agents: boundAgents,
743
+ bound_main_im_groups: mainImGroups,
744
+ }, 409);
745
+ }
746
+ // Wait for container to fully stop before cleaning up its files
747
+ try {
748
+ await deps.queue.stopGroup(jid);
749
+ }
750
+ catch (err) {
751
+ logger.error({ jid, err }, 'Failed to stop container before deleting group');
752
+ return c.json({ error: 'Failed to stop container, group not deleted' }, 500);
753
+ }
754
+ deleteGroupData(jid, existing.folder);
755
+ removeFlowArtifacts(existing.folder);
756
+ delete deps.getRegisteredGroups()[jid];
757
+ delete deps.getSessions()[existing.folder];
758
+ deps.setLastAgentTimestamp(jid, { timestamp: '', id: '' });
759
+ return c.json({ success: true });
760
+ });
761
+ // POST /api/groups/:jid/stop - 停止当前运行的容器/进程
762
+ groupRoutes.post('/:jid/stop', authMiddleware, async (c) => {
763
+ const deps = getWebDeps();
764
+ if (!deps)
765
+ return c.json({ error: 'Server not initialized' }, 500);
766
+ const jid = c.req.param('jid');
767
+ const group = getRegisteredGroup(jid);
768
+ if (!group)
769
+ return c.json({ error: 'Group not found' }, 404);
770
+ const authUser = c.get('user');
771
+ if (!canAccessGroup({ id: authUser.id, role: authUser.role }, group)) {
772
+ return c.json({ error: 'Group not found' }, 404);
773
+ }
774
+ try {
775
+ await deps.queue.stopGroup(jid);
776
+ return c.json({ success: true });
777
+ }
778
+ catch (err) {
779
+ logger.error({ jid, err }, 'Failed to stop group');
780
+ return c.json({ error: 'Failed to stop container' }, 500);
781
+ }
782
+ });
783
+ // POST /api/groups/:jid/interrupt - 中断当前查询(不杀容器)
784
+ groupRoutes.post('/:jid/interrupt', authMiddleware, async (c) => {
785
+ const deps = getWebDeps();
786
+ if (!deps)
787
+ return c.json({ error: 'Server not initialized' }, 500);
788
+ const rawJid = c.req.param('jid');
789
+ const jid = decodeURIComponent(rawJid);
790
+ // Support virtual JIDs for conversation agents: {jid}#agent:{agentId}
791
+ const agentSep = jid.indexOf('#agent:');
792
+ const baseJid = agentSep >= 0 ? jid.slice(0, agentSep) : jid;
793
+ const group = getRegisteredGroup(baseJid);
794
+ if (!group)
795
+ return c.json({ error: 'Group not found' }, 404);
796
+ const authUser = c.get('user');
797
+ if (!canAccessGroup({ id: authUser.id, role: authUser.role }, group)) {
798
+ return c.json({ error: 'Group not found' }, 404);
799
+ }
800
+ const interrupted = deps.queue.interruptQuery(jid);
801
+ if (interrupted) {
802
+ // ── 立即 abort 飞书流式卡片 ──
803
+ const session = getStreamingSession(jid);
804
+ if (session?.isActive()) {
805
+ session.abort('已中断').catch(() => { });
806
+ }
807
+ // Persist interrupt as a system marker so refresh/state-restore can
808
+ // deterministically clear waiting even when no assistant reply exists.
809
+ const messageId = crypto.randomUUID();
810
+ const timestamp = new Date().toISOString();
811
+ try {
812
+ ensureChatExists(jid);
813
+ storeMessageDirect(messageId, jid, '__system__', 'system', 'query_interrupted', timestamp, true);
814
+ broadcastNewMessage(jid, {
815
+ id: messageId,
816
+ chat_jid: jid,
817
+ sender: '__system__',
818
+ sender_name: 'system',
819
+ content: 'query_interrupted',
820
+ timestamp,
821
+ is_from_me: true,
822
+ });
823
+ }
824
+ catch (err) {
825
+ logger.warn({ jid, err }, 'Interrupt succeeded but failed to append system marker');
826
+ }
827
+ }
828
+ return c.json({ success: true, interrupted });
829
+ });
830
+ // POST /api/groups/:jid/reset-session - 重置会话上下文
831
+ // Optional body: { agentId?: string } — when provided, only reset that agent's session
832
+ groupRoutes.post('/:jid/reset-session', authMiddleware, async (c) => {
833
+ const deps = getWebDeps();
834
+ if (!deps)
835
+ return c.json({ error: 'Server not initialized' }, 500);
836
+ const jid = c.req.param('jid');
837
+ const group = getRegisteredGroup(jid);
838
+ if (!group)
839
+ return c.json({ error: 'Group not found' }, 404);
840
+ const authUser = c.get('user');
841
+ if (!canModifyGroup({ id: authUser.id, role: authUser.role }, { ...group, jid })) {
842
+ return c.json({ error: 'Group not found' }, 404);
843
+ }
844
+ if (isHostExecutionGroup(group) && !hasHostExecutionPermission(authUser)) {
845
+ return c.json({ error: 'Insufficient permissions for host execution mode' }, 403);
846
+ }
847
+ // Read optional agentId from request body
848
+ let agentId;
849
+ try {
850
+ const body = await c.req.json().catch(() => ({}));
851
+ if (body && typeof body.agentId === 'string' && body.agentId) {
852
+ agentId = body.agentId;
853
+ }
854
+ }
855
+ catch {
856
+ /* no body or invalid JSON — treat as main session reset */
857
+ }
858
+ // Validate agentId belongs to this group
859
+ if (agentId) {
860
+ const agent = getAgent(agentId);
861
+ if (!agent || agent.chat_jid !== jid) {
862
+ return c.json({ error: 'Agent not found' }, 404);
863
+ }
864
+ }
865
+ // 1. Stop running processes
866
+ try {
867
+ if (agentId) {
868
+ // Agent-specific: only stop the agent's virtual JID process
869
+ const virtualJid = `${jid}#agent:${agentId}`;
870
+ await deps.queue.stopGroup(virtualJid, { force: true });
871
+ }
872
+ else {
873
+ // Main session: stop ALL processes for this folder
874
+ const siblingJids = getJidsByFolder(group.folder);
875
+ await Promise.all(siblingJids.map((j) => deps.queue.stopGroup(j, { force: true })));
876
+ }
877
+ }
878
+ catch (err) {
879
+ logger.error({ jid, agentId, err }, 'Failed to stop containers before resetting session');
880
+ return c.json({ error: 'Failed to stop container, session not reset' }, 500);
881
+ }
882
+ // 2. Delete session JSONL files so Claude starts fresh.
883
+ try {
884
+ clearSessionJsonlFiles(group.folder, agentId);
885
+ }
886
+ catch (err) {
887
+ logger.error({ jid, folder: group.folder, agentId, err }, 'Failed to clear session files during reset');
888
+ return c.json({ error: 'Failed to clear session files, session not reset' }, 500);
889
+ }
890
+ // 3. Delete session from DB (and in-memory cache for main session).
891
+ try {
892
+ deleteSession(group.folder, agentId);
893
+ if (!agentId) {
894
+ delete deps.getSessions()[group.folder];
895
+ }
896
+ }
897
+ catch (err) {
898
+ logger.error({ jid, folder: group.folder, agentId, err }, 'Failed to clear session state during reset');
899
+ return c.json({ error: 'Failed to clear session state, session not reset' }, 500);
900
+ }
901
+ // 4. Insert system divider message into the correct JID (best-effort).
902
+ const targetJid = agentId ? `${jid}#agent:${agentId}` : jid;
903
+ const dividerMessageId = crypto.randomUUID();
904
+ const timestamp = new Date().toISOString();
905
+ try {
906
+ ensureChatExists(targetJid);
907
+ storeMessageDirect(dividerMessageId, targetJid, '__system__', 'system', 'context_reset', timestamp, true);
908
+ broadcastNewMessage(targetJid, {
909
+ id: dividerMessageId,
910
+ chat_jid: targetJid,
911
+ sender: '__system__',
912
+ sender_name: 'system',
913
+ content: 'context_reset',
914
+ timestamp,
915
+ is_from_me: true,
916
+ });
917
+ }
918
+ catch (err) {
919
+ logger.warn({ jid, agentId, err }, 'Session reset succeeded but failed to append divider message');
920
+ }
921
+ // 5. Advance lastAgentTimestamp so old messages before the reset are not
922
+ // re-sent to the next fresh agent session.
923
+ if (agentId) {
924
+ const virtualJid = `${jid}#agent:${agentId}`;
925
+ deps.setLastAgentTimestamp(virtualJid, { timestamp, id: dividerMessageId });
926
+ }
927
+ else {
928
+ // Main session: advance cursor for ALL sibling JIDs sharing this folder.
929
+ const siblingJids = getJidsByFolder(group.folder);
930
+ for (const siblingJid of siblingJids) {
931
+ deps.setLastAgentTimestamp(siblingJid, {
932
+ timestamp,
933
+ id: dividerMessageId,
934
+ });
935
+ }
936
+ }
937
+ logger.info({ jid, folder: group.folder, agentId }, 'Session reset: cleared session files and stopped containers');
938
+ return c.json({ success: true, dividerMessageId });
939
+ });
940
+ // POST /api/groups/:jid/clear-history - 清除聊天历史
941
+ groupRoutes.post('/:jid/clear-history', authMiddleware, async (c) => {
942
+ const deps = getWebDeps();
943
+ if (!deps)
944
+ return c.json({ error: 'Server not initialized' }, 500);
945
+ const jid = c.req.param('jid');
946
+ const group = getRegisteredGroup(jid);
947
+ if (!group)
948
+ return c.json({ error: 'Group not found' }, 404);
949
+ const authUser = c.get('user');
950
+ if (!canModifyGroup({ id: authUser.id, role: authUser.role }, { ...group, jid })) {
951
+ return c.json({ error: 'Group not found' }, 404);
952
+ }
953
+ if (isHostExecutionGroup(group) && !hasHostExecutionPermission(authUser)) {
954
+ return c.json({ error: 'Insufficient permissions for host execution mode' }, 403);
955
+ }
956
+ // Collect all JIDs sharing the same folder (e.g., web:main + feishu groups)
957
+ const siblingJids = getJidsByFolder(group.folder);
958
+ // 1. Stop ALL active processes for this folder first to avoid writes during cleanup.
959
+ try {
960
+ await Promise.all(siblingJids.map((j) => deps.queue.stopGroup(j, { force: true })));
961
+ }
962
+ catch (err) {
963
+ logger.error({ jid, siblingJids, err }, 'Failed to stop containers before clearing history');
964
+ return c.json({ error: 'Failed to stop container, history not cleared' }, 500);
965
+ }
966
+ // 2. Reset workspace: clear working directory, session files, and IPC artifacts.
967
+ try {
968
+ resetWorkspaceForGroup(group.folder);
969
+ }
970
+ catch (err) {
971
+ logger.error({ jid, folder: group.folder, err }, 'Failed to reset workspace while clearing history');
972
+ return c.json({ error: 'Failed to reset workspace, history not cleared' }, 500);
973
+ }
974
+ // 3. Clear session state and message history for ALL sibling JIDs.
975
+ try {
976
+ deleteSession(group.folder);
977
+ delete deps.getSessions()[group.folder];
978
+ for (const siblingJid of siblingJids) {
979
+ deleteChatHistory(siblingJid);
980
+ // Re-create the chats row so subsequent messages work properly
981
+ ensureChatExists(siblingJid);
982
+ deps.setLastAgentTimestamp(siblingJid, { timestamp: '', id: '' });
983
+ }
984
+ }
985
+ catch (err) {
986
+ logger.error({ jid, folder: group.folder, err }, 'Failed to clear history state');
987
+ return c.json({ error: 'Failed to clear history' }, 500);
988
+ }
989
+ logger.info({ jid, folder: group.folder, siblingJids }, 'Cleared workspace, context and chat history for group and all siblings');
990
+ return c.json({ success: true });
991
+ });
992
+ // GET /api/groups/:jid/messages - 获取消息历史
993
+ groupRoutes.get('/:jid/messages', authMiddleware, async (c) => {
994
+ const jid = c.req.param('jid');
995
+ const group = getRegisteredGroup(jid);
996
+ if (!group) {
997
+ return c.json({ error: 'Group not found' }, 404);
998
+ }
999
+ const authUser = c.get('user');
1000
+ if (!canAccessGroup({ id: authUser.id, role: authUser.role }, group)) {
1001
+ return c.json({ error: 'Group not found' }, 404);
1002
+ }
1003
+ if (isHostExecutionGroup(group) && !hasHostExecutionPermission(authUser)) {
1004
+ return c.json({ error: 'Insufficient permissions for host execution mode' }, 403);
1005
+ }
1006
+ const before = readHistoryCursorQuery(c, 'before');
1007
+ const after = readHistoryCursorQuery(c, 'after');
1008
+ const agentIdParam = c.req.query('agentId');
1009
+ const limitRaw = parseInt(c.req.query('limit') || '50', 10);
1010
+ const limit = Math.min(Number.isFinite(limitRaw) ? Math.max(1, limitRaw) : 50, 200);
1011
+ // Agent conversation: query messages from the virtual chat_jid
1012
+ if (agentIdParam) {
1013
+ const agent = getAgent(agentIdParam);
1014
+ if (!agent || agent.chat_jid !== jid) {
1015
+ return c.json({ error: 'Agent not found' }, 404);
1016
+ }
1017
+ const virtualJid = `${jid}#agent:${agentIdParam}`;
1018
+ if (after) {
1019
+ const messages = getMessagesAfter(virtualJid, after, limit);
1020
+ return c.json({ messages });
1021
+ }
1022
+ const rows = getMessagesPage(virtualJid, before, limit + 1);
1023
+ const hasMore = rows.length > limit;
1024
+ const messages = hasMore ? rows.slice(0, limit) : rows;
1025
+ return c.json({ messages, hasMore });
1026
+ }
1027
+ // is_home 群组合并查询:将同 folder 下所有 JID(web + feishu/telegram IM 通道)的消息合并展示
1028
+ // - admin: merge all siblings in the folder (shared admin home)
1029
+ // - member: merge only siblings with same owner to prevent cross-user leakage
1030
+ const queryJids = [jid];
1031
+ if (group.is_home) {
1032
+ const siblingJids = getJidsByFolder(group.folder);
1033
+ for (const siblingJid of siblingJids) {
1034
+ if (siblingJid === jid)
1035
+ continue;
1036
+ const siblingGroup = getRegisteredGroup(siblingJid);
1037
+ if (!siblingGroup)
1038
+ continue;
1039
+ // Merge siblings by ownership: same creator, or admin's own IM channels
1040
+ const ownerMatch = group.created_by && siblingGroup.created_by === group.created_by;
1041
+ const adminSelfMatch = authUser.role === 'admin' && siblingGroup.created_by === authUser.id;
1042
+ if (ownerMatch || adminSelfMatch) {
1043
+ queryJids.push(siblingJid);
1044
+ }
1045
+ }
1046
+ }
1047
+ if (queryJids.length === 1) {
1048
+ // 单 JID 走原路径
1049
+ if (after) {
1050
+ const messages = getMessagesAfter(jid, after, limit);
1051
+ return c.json({ messages });
1052
+ }
1053
+ const rows = getMessagesPage(jid, before, limit + 1);
1054
+ const hasMore = rows.length > limit;
1055
+ const messages = hasMore ? rows.slice(0, limit) : rows;
1056
+ return c.json({ messages, hasMore });
1057
+ }
1058
+ // 多 JID 合并查询
1059
+ if (after) {
1060
+ const messages = getMessagesAfterMulti(queryJids, after, limit);
1061
+ return c.json({ messages });
1062
+ }
1063
+ const rows = getMessagesPageMulti(queryJids, before, limit + 1);
1064
+ const hasMore = rows.length > limit;
1065
+ const messages = hasMore ? rows.slice(0, limit) : rows;
1066
+ return c.json({ messages, hasMore });
1067
+ });
1068
+ // DELETE /api/groups/:jid/messages/:messageId - 删除单条消息
1069
+ groupRoutes.delete('/:jid/messages/:messageId', authMiddleware, (c) => {
1070
+ const jid = c.req.param('jid');
1071
+ const messageId = c.req.param('messageId');
1072
+ const group = getRegisteredGroup(jid);
1073
+ if (!group) {
1074
+ return c.json({ error: 'Group not found' }, 404);
1075
+ }
1076
+ const authUser = c.get('user');
1077
+ if (!canAccessGroup({ id: authUser.id, role: authUser.role }, group)) {
1078
+ return c.json({ error: 'Group not found' }, 404);
1079
+ }
1080
+ // Ownership check: admin can delete any message, non-admin can only delete their own
1081
+ const msg = getMessage(jid, messageId);
1082
+ if (!msg) {
1083
+ return c.json({ error: 'Message not found' }, 404);
1084
+ }
1085
+ if (authUser.role !== 'admin') {
1086
+ // AI messages (is_from_me=1) cannot be deleted by non-admin
1087
+ // User messages can only be deleted by the sender
1088
+ if (msg.is_from_me === 1 || (msg.sender && msg.sender !== authUser.id)) {
1089
+ return c.json({ error: 'Permission denied' }, 403);
1090
+ }
1091
+ }
1092
+ const deleted = deleteMessage(jid, messageId);
1093
+ if (!deleted) {
1094
+ return c.json({ error: 'Message not found' }, 404);
1095
+ }
1096
+ return c.json({ success: true });
1097
+ });
1098
+ // GET /api/groups/:jid/env - 获取容器环境变量配置
1099
+ groupRoutes.get('/:jid/env', authMiddleware, (c) => {
1100
+ const jid = c.req.param('jid');
1101
+ const group = getRegisteredGroup(jid);
1102
+ if (!group)
1103
+ return c.json({ error: 'Group not found' }, 404);
1104
+ if ((group.agentType || 'claude') === 'codex') {
1105
+ return c.json({
1106
+ error: 'This workspace uses Codex and does not support Claude env overrides',
1107
+ }, 400);
1108
+ }
1109
+ const user = c.get('user');
1110
+ if (!canAccessGroup({ id: user.id, role: user.role }, group)) {
1111
+ return c.json({ error: 'Group not found' }, 404);
1112
+ }
1113
+ if (isHostExecutionGroup(group) && !hasHostExecutionPermission(user)) {
1114
+ return c.json({ error: 'Insufficient permissions for host execution mode' }, 403);
1115
+ }
1116
+ // Check permissions
1117
+ if (user.role !== 'admin' &&
1118
+ (!user.permissions || !user.permissions.includes('manage_group_env'))) {
1119
+ return c.json({ error: 'Insufficient permissions' }, 403);
1120
+ }
1121
+ const config = getContainerEnvConfig(group.folder);
1122
+ return c.json(toPublicContainerEnvForUser(config, user));
1123
+ });
1124
+ // PUT /api/groups/:jid/env - 更新容器环境变量配置
1125
+ groupRoutes.put('/:jid/env', authMiddleware, async (c) => {
1126
+ const jid = c.req.param('jid');
1127
+ const group = getRegisteredGroup(jid);
1128
+ if (!group)
1129
+ return c.json({ error: 'Group not found' }, 404);
1130
+ if ((group.agentType || 'claude') === 'codex') {
1131
+ return c.json({
1132
+ error: 'This workspace uses Codex and does not support Claude env overrides',
1133
+ }, 400);
1134
+ }
1135
+ const envUser = c.get('user');
1136
+ if (!canAccessGroup({ id: envUser.id, role: envUser.role }, group)) {
1137
+ return c.json({ error: 'Group not found' }, 404);
1138
+ }
1139
+ if (isHostExecutionGroup(group) && !hasHostExecutionPermission(envUser)) {
1140
+ return c.json({ error: 'Insufficient permissions for host execution mode' }, 403);
1141
+ }
1142
+ // Check permissions
1143
+ if (envUser.role !== 'admin' &&
1144
+ (!envUser.permissions || !envUser.permissions.includes('manage_group_env'))) {
1145
+ return c.json({ error: 'Insufficient permissions' }, 403);
1146
+ }
1147
+ const body = await c.req.json().catch(() => ({}));
1148
+ const validation = ContainerEnvSchema.safeParse(body);
1149
+ if (!validation.success) {
1150
+ return c.json({ error: 'Invalid request body' }, 400);
1151
+ }
1152
+ const data = validation.data;
1153
+ // Validate customEnv keys/values to prevent env injection
1154
+ if (data.customEnv) {
1155
+ const envKeyRe = /^[A-Za-z_][A-Za-z0-9_]*$/;
1156
+ for (const [key, value] of Object.entries(data.customEnv)) {
1157
+ if (!envKeyRe.test(key)) {
1158
+ return c.json({
1159
+ error: `Invalid env key: "${key}". Keys must match [A-Za-z_][A-Za-z0-9_]*`,
1160
+ }, 400);
1161
+ }
1162
+ if (/[\r\n\0]/.test(value)) {
1163
+ return c.json({
1164
+ error: `Env value for "${key}" contains invalid control characters`,
1165
+ }, 400);
1166
+ }
1167
+ }
1168
+ }
1169
+ const current = getContainerEnvConfig(group.folder);
1170
+ // Build updated config: only update fields that are explicitly provided
1171
+ const updated = { ...current };
1172
+ if (data.anthropicBaseUrl !== undefined)
1173
+ updated.anthropicBaseUrl = data.anthropicBaseUrl;
1174
+ if (data.anthropicAuthToken !== undefined)
1175
+ updated.anthropicAuthToken = data.anthropicAuthToken;
1176
+ if (data.anthropicApiKey !== undefined)
1177
+ updated.anthropicApiKey = data.anthropicApiKey;
1178
+ if (data.claudeCodeOauthToken !== undefined)
1179
+ updated.claudeCodeOauthToken = data.claudeCodeOauthToken;
1180
+ if (data.anthropicModel !== undefined)
1181
+ updated.anthropicModel = data.anthropicModel;
1182
+ if (data.customEnv !== undefined)
1183
+ updated.customEnv = data.customEnv;
1184
+ try {
1185
+ saveContainerEnvConfig(group.folder, updated);
1186
+ // Restart container so it picks up the new env immediately
1187
+ const deps = getWebDeps();
1188
+ if (deps) {
1189
+ await deps.queue.restartGroup(jid);
1190
+ logger.info({ jid, folder: group.folder }, 'Restarted container after env config update');
1191
+ }
1192
+ return c.json(toPublicContainerEnvConfig(updated));
1193
+ }
1194
+ catch (err) {
1195
+ logger.error({ err }, 'Failed to save container env config');
1196
+ return c.json({ error: 'Failed to save config' }, 500);
1197
+ }
1198
+ });
1199
+ // --- Member Management Routes ---
1200
+ // GET /api/groups/:jid/members - 列出成员
1201
+ groupRoutes.get('/:jid/members', authMiddleware, (c) => {
1202
+ const jid = c.req.param('jid');
1203
+ const group = getRegisteredGroup(jid);
1204
+ if (!group)
1205
+ return c.json({ error: 'Group not found' }, 404);
1206
+ const authUser = c.get('user');
1207
+ if (!canAccessGroup({ id: authUser.id, role: authUser.role }, group)) {
1208
+ return c.json({ error: 'Group not found' }, 404);
1209
+ }
1210
+ const members = getGroupMembers(group.folder);
1211
+ return c.json({ members });
1212
+ });
1213
+ // GET /api/groups/:jid/members/search?q=... - 搜索可添加的用户(owner/admin 权限)
1214
+ groupRoutes.get('/:jid/members/search', authMiddleware, (c) => {
1215
+ const jid = c.req.param('jid');
1216
+ const group = getRegisteredGroup(jid);
1217
+ if (!group)
1218
+ return c.json({ error: 'Group not found' }, 404);
1219
+ const authUser = c.get('user');
1220
+ if (!canManageGroupMembers({ id: authUser.id, role: authUser.role }, { ...group, jid })) {
1221
+ return c.json({ error: 'Forbidden' }, 403);
1222
+ }
1223
+ const q = c.req.query('q') || '';
1224
+ if (!q.trim())
1225
+ return c.json({ users: [] });
1226
+ const result = listUsers({ query: q.trim(), status: 'active', pageSize: 10 });
1227
+ const existingIds = new Set(getGroupMembers(group.folder).map((m) => m.user_id));
1228
+ const users = result.users
1229
+ .filter((u) => !existingIds.has(u.id))
1230
+ .map((u) => ({
1231
+ id: u.id,
1232
+ username: u.username,
1233
+ display_name: u.display_name,
1234
+ }));
1235
+ return c.json({ users });
1236
+ });
1237
+ // POST /api/groups/:jid/members - 添加成员
1238
+ groupRoutes.post('/:jid/members', authMiddleware, async (c) => {
1239
+ const jid = c.req.param('jid');
1240
+ const group = getRegisteredGroup(jid);
1241
+ if (!group)
1242
+ return c.json({ error: 'Group not found' }, 404);
1243
+ const authUser = c.get('user');
1244
+ if (!canManageGroupMembers({ id: authUser.id, role: authUser.role }, group)) {
1245
+ return c.json({ error: 'Insufficient permissions' }, 403);
1246
+ }
1247
+ if (group.is_home) {
1248
+ return c.json({ error: 'Cannot add members to home groups' }, 400);
1249
+ }
1250
+ const body = await c.req.json().catch(() => ({}));
1251
+ const validation = GroupMemberAddSchema.safeParse(body);
1252
+ if (!validation.success) {
1253
+ return c.json({ error: 'Invalid request body' }, 400);
1254
+ }
1255
+ const { user_id: targetUserId } = validation.data;
1256
+ // Check target user exists and is active
1257
+ const targetUser = getUserById(targetUserId);
1258
+ if (!targetUser || targetUser.status !== 'active') {
1259
+ return c.json({ error: 'User not found or inactive' }, 404);
1260
+ }
1261
+ // Check if already a member
1262
+ const existingRole = getGroupMemberRole(group.folder, targetUserId);
1263
+ if (existingRole !== null) {
1264
+ return c.json({ error: 'User is already a member' }, 409);
1265
+ }
1266
+ addGroupMember(group.folder, targetUserId, 'member', authUser.id);
1267
+ invalidateAllowedUserCache(jid);
1268
+ logger.info({ jid, folder: group.folder, targetUserId, addedBy: authUser.id }, 'Group member added');
1269
+ const members = getGroupMembers(group.folder);
1270
+ return c.json({ success: true, members });
1271
+ });
1272
+ // DELETE /api/groups/:jid/members/:userId - 移除成员
1273
+ groupRoutes.delete('/:jid/members/:userId', authMiddleware, (c) => {
1274
+ const jid = c.req.param('jid');
1275
+ const targetUserId = c.req.param('userId');
1276
+ const group = getRegisteredGroup(jid);
1277
+ if (!group)
1278
+ return c.json({ error: 'Group not found' }, 404);
1279
+ const authUser = c.get('user');
1280
+ // Self-removal: any member can leave
1281
+ const isSelfRemoval = targetUserId === authUser.id;
1282
+ if (!isSelfRemoval) {
1283
+ if (!canManageGroupMembers({ id: authUser.id, role: authUser.role }, group)) {
1284
+ return c.json({ error: 'Insufficient permissions' }, 403);
1285
+ }
1286
+ }
1287
+ // Check target is actually a member
1288
+ const targetRole = getGroupMemberRole(group.folder, targetUserId);
1289
+ if (targetRole === null) {
1290
+ return c.json({ error: 'User is not a member' }, 404);
1291
+ }
1292
+ // Owner cannot be removed
1293
+ if (targetRole === 'owner') {
1294
+ return c.json({ error: 'Cannot remove the owner' }, 400);
1295
+ }
1296
+ removeGroupMember(group.folder, targetUserId);
1297
+ invalidateAllowedUserCache(jid);
1298
+ logger.info({
1299
+ jid,
1300
+ folder: group.folder,
1301
+ targetUserId,
1302
+ removedBy: authUser.id,
1303
+ isSelfRemoval,
1304
+ }, 'Group member removed');
1305
+ const members = getGroupMembers(group.folder);
1306
+ return c.json({ success: true, members });
1307
+ });
1308
+ // --- MCP Configuration Routes ---
1309
+ // GET /api/groups/:jid/mcp - 获取工作区 MCP 配置
1310
+ groupRoutes.get('/:jid/mcp', authMiddleware, (c) => {
1311
+ const jid = c.req.param('jid');
1312
+ const group = getRegisteredGroup(jid);
1313
+ if (!group)
1314
+ return c.json({ error: 'Group not found' }, 404);
1315
+ const authUser = c.get('user');
1316
+ if (!canAccessGroup({ id: authUser.id, role: authUser.role }, group)) {
1317
+ return c.json({ error: 'Group not found' }, 404);
1318
+ }
1319
+ return c.json({
1320
+ mcp_mode: group.mcp_mode ?? 'inherit',
1321
+ selected_mcps: group.selected_mcps ?? null,
1322
+ });
1323
+ });
1324
+ // PUT /api/groups/:jid/mcp - 更新工作区 MCP 配置
1325
+ groupRoutes.put('/:jid/mcp', authMiddleware, async (c) => {
1326
+ const jid = c.req.param('jid');
1327
+ const group = getRegisteredGroup(jid);
1328
+ if (!group)
1329
+ return c.json({ error: 'Group not found' }, 404);
1330
+ const authUser = c.get('user');
1331
+ if (!canAccessGroup({ id: authUser.id, role: authUser.role }, group)) {
1332
+ return c.json({ error: 'Group not found' }, 404);
1333
+ }
1334
+ const body = await c.req.json().catch(() => ({}));
1335
+ const mcp_mode = body.mcp_mode;
1336
+ const selected_mcps = body.selected_mcps;
1337
+ // Validate mcp_mode
1338
+ if (mcp_mode !== undefined &&
1339
+ mcp_mode !== 'inherit' &&
1340
+ mcp_mode !== 'custom') {
1341
+ return c.json({ error: 'Invalid mcp_mode' }, 400);
1342
+ }
1343
+ // Validate selected_mcps
1344
+ if (selected_mcps !== undefined && selected_mcps !== null) {
1345
+ if (!Array.isArray(selected_mcps)) {
1346
+ return c.json({ error: 'selected_mcps must be an array' }, 400);
1347
+ }
1348
+ for (const mcp of selected_mcps) {
1349
+ if (typeof mcp !== 'string') {
1350
+ return c.json({ error: 'selected_mcps must contain strings' }, 400);
1351
+ }
1352
+ }
1353
+ }
1354
+ // Update the group
1355
+ const updatedGroup = {
1356
+ ...group,
1357
+ mcp_mode: mcp_mode ?? group.mcp_mode ?? 'inherit',
1358
+ selected_mcps: selected_mcps !== undefined ? selected_mcps : group.selected_mcps,
1359
+ };
1360
+ setRegisteredGroup(jid, updatedGroup);
1361
+ return c.json({
1362
+ success: true,
1363
+ mcp_mode: updatedGroup.mcp_mode,
1364
+ selected_mcps: updatedGroup.selected_mcps,
1365
+ });
1366
+ });
1367
+ export default groupRoutes;