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,695 @@
1
+ import crypto from 'node:crypto';
2
+ import { CronExpressionParser } from 'cron-parser';
3
+ import fs from 'fs';
4
+ import path from 'path';
5
+ import { DATA_DIR, GROUPS_DIR, SCHEDULER_POLL_INTERVAL, TIMEZONE, } from './config.js';
6
+ import { runDailySummaryIfNeeded } from './daily-summary.js';
7
+ import { getSystemSettings } from './runtime-config.js';
8
+ import { runContainerAgent, runHostAgent, writeTasksSnapshot, } from './container-runner.js';
9
+ import { addGroupMember, getAllTasks, cleanupOldTaskRunLogs, cleanupStaleRunningLogs, deleteGroupData, ensureChatExists, getDueTasks, getTaskById, getUserById, getUserHomeGroup, logTaskRun, logTaskRunStart, updateTaskRunLog, setRegisteredGroup, updateChatName, updateTaskAfterRun, updateTaskWorkspace, } from './db.js';
10
+ import { resolveEffectiveHostWorkspaceCwd } from './host-workspace-cwd.js';
11
+ import { logger } from './logger.js';
12
+ import { resolveTaskOwner } from './task-utils.js';
13
+ import { removeFlowArtifacts } from './file-manager.js';
14
+ import { hasScriptCapacity, runScript } from './script-runner.js';
15
+ import { checkBillingAccessFresh, isBillingEnabled } from './billing.js';
16
+ /**
17
+ * Resolve the actual group JID to send a task to.
18
+ * Falls back from the task's stored chat_jid to any group matching the same folder.
19
+ */
20
+ function resolveTargetGroupJid(task, groups) {
21
+ const directTarget = groups[task.chat_jid];
22
+ if (directTarget && directTarget.folder === task.group_folder) {
23
+ return task.chat_jid;
24
+ }
25
+ const sameFolder = Object.entries(groups).filter(([, g]) => g.folder === task.group_folder);
26
+ const preferred = sameFolder.find(([jid]) => jid.startsWith('web:')) || sameFolder[0];
27
+ return preferred?.[0] || '';
28
+ }
29
+ function resolveTaskSourceGroup(task, groups) {
30
+ const directSource = groups[task.chat_jid];
31
+ if (directSource && directSource.folder === task.group_folder) {
32
+ return directSource;
33
+ }
34
+ return (Object.values(groups).find((group) => group.folder === task.group_folder && group.is_home) ||
35
+ Object.values(groups).find((group) => group.folder === task.group_folder));
36
+ }
37
+ function findHomeSiblingGroup(group, groups) {
38
+ if (!group || group.is_home)
39
+ return undefined;
40
+ return Object.values(groups).find((candidate) => candidate.folder === group.folder && candidate.is_home);
41
+ }
42
+ function resolveTaskExecutionMode(task, deps) {
43
+ if (task.execution_mode === 'host' || task.execution_mode === 'container') {
44
+ return task.execution_mode;
45
+ }
46
+ // Legacy fallback: inherit from the original group
47
+ const groups = deps.registeredGroups();
48
+ const group = groups[task.chat_jid];
49
+ if (group) {
50
+ if (!group.is_home) {
51
+ const homeSibling = Object.values(groups).find((g) => g.folder === group.folder && g.is_home);
52
+ if (homeSibling)
53
+ return homeSibling.executionMode || 'container';
54
+ }
55
+ return group.executionMode || 'container';
56
+ }
57
+ return 'container';
58
+ }
59
+ function ensureTaskWorkspace(task, deps) {
60
+ // If workspace already exists and is registered, reuse it
61
+ if (task.workspace_jid && task.workspace_folder) {
62
+ const groups = deps.registeredGroups();
63
+ if (groups[task.workspace_jid]) {
64
+ return { jid: task.workspace_jid, folder: task.workspace_folder };
65
+ }
66
+ // Workspace was deleted externally — clean up orphaned filesystem directory before recreating
67
+ const oldDir = path.join(GROUPS_DIR, task.workspace_folder);
68
+ try {
69
+ fs.rmSync(oldDir, { recursive: true, force: true });
70
+ }
71
+ catch {
72
+ /* ignore if already gone */
73
+ }
74
+ }
75
+ const jid = `web:${crypto.randomUUID()}`;
76
+ // Strip existing 'task-' prefix from IPC-originated IDs to avoid 'task-task-...'
77
+ const idBase = task.id.startsWith('task-') ? task.id.slice(5) : task.id;
78
+ const folder = `task-${idBase.slice(0, 12)}`;
79
+ // 从 prompt 提取简短名称(取第一行前 12 个字符)
80
+ const firstLine = task.prompt.split('\n')[0].trim();
81
+ const shortName = firstLine.slice(0, 12).trim() || task.id.slice(0, 6);
82
+ const name = shortName;
83
+ const executionMode = resolveTaskExecutionMode(task, deps);
84
+ const sourceGroup = Object.values(deps.registeredGroups()).find((g) => g.folder === task.group_folder);
85
+ const ownerId = resolveTaskOwner(task, sourceGroup);
86
+ const sourceHomeGroup = findHomeSiblingGroup(sourceGroup, deps.registeredGroups());
87
+ const sourceWorkspaceCwd = sourceGroup
88
+ ? resolveEffectiveHostWorkspaceCwd(sourceGroup, sourceHomeGroup)
89
+ : undefined;
90
+ const group = {
91
+ name,
92
+ folder,
93
+ added_at: new Date().toISOString(),
94
+ executionMode,
95
+ customCwd: executionMode === 'host' ? sourceWorkspaceCwd : undefined,
96
+ created_by: ownerId,
97
+ };
98
+ setRegisteredGroup(jid, group);
99
+ ensureChatExists(jid);
100
+ updateChatName(jid, name);
101
+ if (ownerId) {
102
+ addGroupMember(folder, ownerId, 'owner', ownerId);
103
+ }
104
+ deps.registeredGroups()[jid] = group;
105
+ // Create filesystem directory
106
+ const groupDir = path.join(GROUPS_DIR, folder);
107
+ fs.mkdirSync(groupDir, { recursive: true });
108
+ // Persist workspace info back to the task record
109
+ updateTaskWorkspace(task.id, jid, folder);
110
+ // Also update the in-memory task object
111
+ task.workspace_jid = jid;
112
+ task.workspace_folder = folder;
113
+ logger.info({ taskId: task.id, folder, jid, executionMode, ownerId }, 'Created task workspace');
114
+ // Notify frontend via WebSocket so sidebar refreshes (scoped to task owner)
115
+ deps.onWorkspaceCreated?.(jid, folder, name, ownerId);
116
+ return { jid, folder };
117
+ }
118
+ const runningTaskIds = new Set();
119
+ export function getRunningTaskIds() {
120
+ return [...runningTaskIds];
121
+ }
122
+ function computeNextRun(task) {
123
+ if (task.schedule_type === 'cron') {
124
+ const interval = CronExpressionParser.parse(task.schedule_value, {
125
+ tz: TIMEZONE,
126
+ });
127
+ return interval.next().toISOString();
128
+ }
129
+ else if (task.schedule_type === 'interval') {
130
+ const ms = Number(task.schedule_value);
131
+ if (!Number.isFinite(ms) || ms <= 0)
132
+ return null;
133
+ const anchor = task.next_run
134
+ ? new Date(task.next_run).getTime()
135
+ : Date.now();
136
+ const now = Date.now();
137
+ const elapsed = now - anchor;
138
+ const periods = elapsed > 0 ? Math.ceil(elapsed / ms) : 1;
139
+ return new Date(anchor + periods * ms).toISOString();
140
+ }
141
+ // 'once' tasks have no next run
142
+ return null;
143
+ }
144
+ /**
145
+ * Re-check DB before running — task may have been cancelled/paused while queued.
146
+ * Returns true if the task is still active and should proceed.
147
+ */
148
+ function isTaskStillActive(taskId, label) {
149
+ const currentTask = getTaskById(taskId);
150
+ if (!currentTask || currentTask.status !== 'active') {
151
+ logger.info({ taskId }, `Skipping ${label ?? 'task'}: deleted or no longer active since enqueue`);
152
+ return false;
153
+ }
154
+ return true;
155
+ }
156
+ export async function runTask(staleTask, deps, options) {
157
+ if (!options?.manualRun && !isTaskStillActive(staleTask.id, 'task'))
158
+ return;
159
+ // Refresh task from DB to avoid stale closure data
160
+ const task = getTaskById(staleTask.id);
161
+ if (!task)
162
+ return;
163
+ runningTaskIds.add(task.id);
164
+ const startTime = Date.now();
165
+ const runLogId = logTaskRunStart(task.id);
166
+ // Ensure task has a dedicated workspace (Agent tasks only)
167
+ const workspace = ensureTaskWorkspace(task, deps);
168
+ const workspaceGroups = deps.registeredGroups();
169
+ const workspaceGroup = workspaceGroups[workspace.jid];
170
+ if (!workspaceGroup) {
171
+ logger.error({ taskId: task.id, workspaceJid: workspace.jid }, 'Workspace group not found after creation');
172
+ updateTaskRunLog(runLogId, {
173
+ duration_ms: Date.now() - startTime,
174
+ status: 'error',
175
+ result: null,
176
+ error: `Workspace group not found: ${workspace.jid}`,
177
+ });
178
+ runningTaskIds.delete(task.id);
179
+ return;
180
+ }
181
+ const effectiveJid = options?.taskRunId
182
+ ? `${workspace.jid}#task:${options.taskRunId}`
183
+ : workspace.jid;
184
+ const groupDir = path.join(GROUPS_DIR, workspace.folder);
185
+ fs.mkdirSync(groupDir, { recursive: true });
186
+ logger.info({ taskId: task.id, group: workspace.folder }, 'Running scheduled task');
187
+ // Billing quota check before running task
188
+ if (isBillingEnabled() && workspaceGroup.created_by) {
189
+ const owner = getUserById(workspaceGroup.created_by);
190
+ if (owner && owner.role !== 'admin') {
191
+ const accessResult = checkBillingAccessFresh(workspaceGroup.created_by, owner.role);
192
+ if (!accessResult.allowed) {
193
+ const reason = accessResult.reason || '当前账户不可用';
194
+ logger.info({
195
+ taskId: task.id,
196
+ userId: workspaceGroup.created_by,
197
+ reason,
198
+ blockType: accessResult.blockType,
199
+ }, 'Billing access denied, blocking scheduled task');
200
+ updateTaskRunLog(runLogId, {
201
+ duration_ms: Date.now() - startTime,
202
+ status: 'error',
203
+ result: null,
204
+ error: `计费限制: ${reason}`,
205
+ });
206
+ runningTaskIds.delete(task.id);
207
+ // Still compute next run so the task isn't stuck (but preserve for manual runs)
208
+ const nextRun = options?.manualRun
209
+ ? task.next_run
210
+ : computeNextRun(task);
211
+ updateTaskAfterRun(task.id, nextRun, `Error: 计费限制: ${reason}`);
212
+ return;
213
+ }
214
+ }
215
+ }
216
+ const sourceWorkspaceGroup = resolveTaskSourceGroup(task, workspaceGroups);
217
+ const sourceWorkspaceCwd = sourceWorkspaceGroup
218
+ ? resolveEffectiveHostWorkspaceCwd(sourceWorkspaceGroup, findHomeSiblingGroup(sourceWorkspaceGroup, workspaceGroups))
219
+ : undefined;
220
+ // Update tasks snapshot for container to read (filtered by group)
221
+ const isHome = false; // Task workspaces are never home
222
+ const isAdminHome = false;
223
+ const tasks = getAllTasks();
224
+ writeTasksSnapshot(workspace.folder, isAdminHome, tasks.map((t) => ({
225
+ id: t.id,
226
+ groupFolder: t.group_folder,
227
+ prompt: t.prompt,
228
+ schedule_type: t.schedule_type,
229
+ schedule_value: t.schedule_value,
230
+ status: t.status,
231
+ next_run: t.next_run,
232
+ })));
233
+ // Store task prompt as a user message in workspace chat so it's visible in conversation
234
+ if (deps.storePromptMessage) {
235
+ const owner = workspaceGroup.created_by
236
+ ? getUserById(workspaceGroup.created_by)
237
+ : null;
238
+ const senderName = owner?.display_name || owner?.username || '定时任务';
239
+ deps.storePromptMessage(workspace.jid, owner?.id || 'system', senderName, task.prompt);
240
+ }
241
+ let result = null;
242
+ let error = null;
243
+ // Track the time of last meaningful output from the agent.
244
+ // duration_ms should measure actual work time, not include idle wait.
245
+ let lastOutputTime = startTime;
246
+ let runLogFinalized = false;
247
+ const finalizeRunLog = () => {
248
+ if (runLogFinalized)
249
+ return;
250
+ runLogFinalized = true;
251
+ runningTaskIds.delete(task.id);
252
+ const durationMs = lastOutputTime - startTime;
253
+ updateTaskRunLog(runLogId, {
254
+ duration_ms: durationMs,
255
+ status: error ? 'error' : 'success',
256
+ result,
257
+ error,
258
+ });
259
+ // Send _close sentinel so the idle agent process exits promptly,
260
+ // freeing the queue slot for the next run.
261
+ if (idleTimer)
262
+ clearTimeout(idleTimer);
263
+ deps.queue.closeStdin(effectiveJid);
264
+ };
265
+ // Use persistent session for task workspace
266
+ const sessions = deps.getSessions();
267
+ const sessionId = sessions[workspace.folder];
268
+ // Idle timer: writes _close sentinel after idleTimeout of no output,
269
+ // so the container exits instead of hanging at waitForIpcMessage forever.
270
+ let idleTimer = null;
271
+ const resetIdleTimer = () => {
272
+ if (idleTimer)
273
+ clearTimeout(idleTimer);
274
+ idleTimer = setTimeout(() => {
275
+ logger.debug({ taskId: task.id }, 'Scheduled task idle timeout, closing container stdin');
276
+ deps.queue.closeStdin(effectiveJid);
277
+ }, getSystemSettings().idleTimeout);
278
+ };
279
+ try {
280
+ const executionMode = resolveTaskExecutionMode(task, deps);
281
+ const runAgent = executionMode === 'host' ? runHostAgent : runContainerAgent;
282
+ // Resolve owner's home folder for correct volume mounts (skills, memory, AGENTS.md)
283
+ const ownerHomeFolder = workspaceGroup.created_by
284
+ ? getUserHomeGroup(workspaceGroup.created_by)?.folder || workspace.folder
285
+ : workspace.folder;
286
+ const output = await runAgent(workspaceGroup, {
287
+ prompt: task.prompt,
288
+ sessionId,
289
+ groupFolder: workspace.folder,
290
+ chatJid: workspace.jid,
291
+ agentType: workspaceGroup.agentType || 'claude',
292
+ isMain: isAdminHome,
293
+ isHome,
294
+ isAdminHome,
295
+ isScheduledTask: true,
296
+ taskRunId: options?.taskRunId,
297
+ }, (proc, identifier) => deps.onProcess(effectiveJid, proc, executionMode === 'container' ? identifier : null, workspace.folder, identifier, options?.taskRunId), async (streamedOutput) => {
298
+ // Broadcast stream events to WebSocket clients viewing the task workspace
299
+ if (streamedOutput.status === 'stream' && streamedOutput.streamEvent) {
300
+ deps.broadcastStreamEvent?.(workspace.jid, streamedOutput.streamEvent);
301
+ }
302
+ if (streamedOutput.result) {
303
+ result = streamedOutput.result;
304
+ lastOutputTime = Date.now();
305
+ resetIdleTimer();
306
+ }
307
+ if (streamedOutput.status === 'error') {
308
+ error = streamedOutput.error || 'Unknown error';
309
+ lastOutputTime = Date.now();
310
+ }
311
+ // Finalize run log on first non-stream output (success/error/closed).
312
+ // Don't wait for the process to exit — idle timeout can be very long.
313
+ if (streamedOutput.status !== 'stream') {
314
+ finalizeRunLog();
315
+ }
316
+ }, ownerHomeFolder, sourceWorkspaceCwd ? { executionCwd: sourceWorkspaceCwd } : undefined);
317
+ if (idleTimer)
318
+ clearTimeout(idleTimer);
319
+ if (output.status === 'error') {
320
+ error = output.error || 'Unknown error';
321
+ lastOutputTime = Date.now();
322
+ }
323
+ else if (output.result) {
324
+ // Messages are sent via MCP tool (IPC), result text is just logged
325
+ result = output.result;
326
+ lastOutputTime = Date.now();
327
+ }
328
+ // Finalize if not already done by onOutput callback
329
+ finalizeRunLog();
330
+ logger.info({ taskId: task.id, durationMs: lastOutputTime - startTime }, 'Task completed');
331
+ }
332
+ catch (err) {
333
+ if (idleTimer)
334
+ clearTimeout(idleTimer);
335
+ error = err instanceof Error ? err.message : String(err);
336
+ lastOutputTime = Date.now();
337
+ logger.error({ taskId: task.id, error }, 'Task failed');
338
+ }
339
+ finally {
340
+ runningTaskIds.delete(task.id);
341
+ // Clean up isolated task IPC directory
342
+ if (options?.taskRunId) {
343
+ const taskRunDir = path.join(DATA_DIR, 'ipc', workspace.folder, 'tasks-run', options.taskRunId);
344
+ try {
345
+ fs.rmSync(taskRunDir, { recursive: true, force: true });
346
+ }
347
+ catch {
348
+ /* ignore */
349
+ }
350
+ }
351
+ // Safety net: finalize run log if not already done by onOutput callback
352
+ finalizeRunLog();
353
+ }
354
+ // manualRun: preserve original next_run schedule
355
+ const nextRun = options?.manualRun ? task.next_run : computeNextRun(task);
356
+ const resultSummary = error
357
+ ? `Error: ${error}`
358
+ : result
359
+ ? result.slice(0, 200)
360
+ : 'Completed';
361
+ updateTaskAfterRun(task.id, nextRun, resultSummary);
362
+ // Auto-cleanup once-task workspace after completion
363
+ if (task.schedule_type === 'once' &&
364
+ !options?.manualRun &&
365
+ task.workspace_jid &&
366
+ task.workspace_folder) {
367
+ setTimeout(() => {
368
+ try {
369
+ const groups = deps.registeredGroups();
370
+ if (groups[task.workspace_jid]) {
371
+ deleteGroupData(task.workspace_jid, task.workspace_folder);
372
+ delete groups[task.workspace_jid];
373
+ removeFlowArtifacts(task.workspace_folder);
374
+ logger.info({ taskId: task.id, folder: task.workspace_folder }, 'Cleaned up once-task workspace');
375
+ }
376
+ }
377
+ catch (err) {
378
+ logger.error({ taskId: task.id, err }, 'Failed to cleanup once-task workspace');
379
+ }
380
+ }, 60_000);
381
+ }
382
+ }
383
+ export async function runScriptTask(staleTask, deps, groupJid, manualRun = false) {
384
+ if (!manualRun && !isTaskStillActive(staleTask.id, 'script task'))
385
+ return;
386
+ // Refresh task from DB to avoid stale closure data
387
+ const task = getTaskById(staleTask.id);
388
+ if (!task)
389
+ return;
390
+ runningTaskIds.add(task.id);
391
+ const startTime = Date.now();
392
+ const runLogId = logTaskRunStart(task.id);
393
+ logger.info({ taskId: task.id, group: task.group_folder, executionType: 'script' }, 'Running script task');
394
+ // Billing quota check before running script task
395
+ if (isBillingEnabled() && task.group_folder) {
396
+ const groups = deps.registeredGroups();
397
+ const group = groups[groupJid];
398
+ if (group?.created_by) {
399
+ const owner = getUserById(group.created_by);
400
+ if (owner && owner.role !== 'admin') {
401
+ const accessResult = checkBillingAccessFresh(group.created_by, owner.role);
402
+ if (!accessResult.allowed) {
403
+ const reason = accessResult.reason || '当前账户不可用';
404
+ logger.info({
405
+ taskId: task.id,
406
+ userId: group.created_by,
407
+ reason,
408
+ blockType: accessResult.blockType,
409
+ }, 'Billing access denied, blocking script task');
410
+ updateTaskRunLog(runLogId, {
411
+ duration_ms: Date.now() - startTime,
412
+ status: 'error',
413
+ result: null,
414
+ error: `计费限制: ${reason}`,
415
+ });
416
+ runningTaskIds.delete(task.id);
417
+ const nextRun = manualRun ? task.next_run : computeNextRun(task);
418
+ updateTaskAfterRun(task.id, nextRun, `Error: 计费限制: ${reason}`);
419
+ return;
420
+ }
421
+ }
422
+ }
423
+ }
424
+ const sourceWorkspaceGroup = resolveTaskSourceGroup(task, deps.registeredGroups());
425
+ const sourceWorkspaceCwd = sourceWorkspaceGroup
426
+ ? resolveEffectiveHostWorkspaceCwd(sourceWorkspaceGroup, findHomeSiblingGroup(sourceWorkspaceGroup, deps.registeredGroups()))
427
+ : undefined;
428
+ const groupDir = path.join(GROUPS_DIR, task.group_folder);
429
+ fs.mkdirSync(groupDir, { recursive: true });
430
+ if (!task.script_command) {
431
+ logger.error({ taskId: task.id }, 'Script task has no script_command, skipping');
432
+ updateTaskRunLog(runLogId, {
433
+ duration_ms: Date.now() - startTime,
434
+ status: 'error',
435
+ result: null,
436
+ error: 'script_command is empty',
437
+ });
438
+ runningTaskIds.delete(task.id);
439
+ return;
440
+ }
441
+ let result = null;
442
+ let error = null;
443
+ try {
444
+ const scriptResult = await runScript(task.script_command, task.group_folder, sourceWorkspaceCwd);
445
+ if (scriptResult.timedOut) {
446
+ error = `脚本执行超时 (${Math.round(scriptResult.durationMs / 1000)}s)`;
447
+ }
448
+ else if (scriptResult.exitCode !== 0) {
449
+ error = scriptResult.stderr.trim() || `退出码: ${scriptResult.exitCode}`;
450
+ result = scriptResult.stdout.trim() || null;
451
+ }
452
+ else {
453
+ result = scriptResult.stdout.trim() || null;
454
+ }
455
+ // Send result to user (skip if no output and no error)
456
+ if (error || result) {
457
+ const text = error
458
+ ? `[脚本] 执行失败: ${error}${result ? `\n输出:\n${result.slice(0, 500)}` : ''}`
459
+ : `[脚本] ${result.slice(0, 1000)}`;
460
+ await deps.sendMessage(groupJid, `${deps.assistantName}: ${text}`, {
461
+ source: 'scheduled_task',
462
+ });
463
+ }
464
+ logger.info({
465
+ taskId: task.id,
466
+ durationMs: Date.now() - startTime,
467
+ exitCode: scriptResult.exitCode,
468
+ }, 'Script task completed');
469
+ }
470
+ catch (err) {
471
+ error = err instanceof Error ? err.message : String(err);
472
+ logger.error({ taskId: task.id, error }, 'Script task failed');
473
+ }
474
+ finally {
475
+ runningTaskIds.delete(task.id);
476
+ }
477
+ const durationMs = Date.now() - startTime;
478
+ updateTaskRunLog(runLogId, {
479
+ duration_ms: durationMs,
480
+ status: error ? 'error' : 'success',
481
+ result,
482
+ error,
483
+ });
484
+ // manualRun: preserve original next_run schedule
485
+ const nextRun = manualRun ? task.next_run : computeNextRun(task);
486
+ const resultSummary = error
487
+ ? `Error: ${error}`
488
+ : result
489
+ ? result.slice(0, 200)
490
+ : 'Completed';
491
+ updateTaskAfterRun(task.id, nextRun, resultSummary);
492
+ }
493
+ /**
494
+ * Group context mode: inject task prompt as a regular message into the source workspace.
495
+ * The message is processed by the existing message pipeline (IPC if running, new container if idle).
496
+ */
497
+ async function runGroupModeTask(task, deps, targetGroupJid, manualRun = false) {
498
+ const startTime = Date.now();
499
+ try {
500
+ // Resolve task owner for sender attribution
501
+ const owner = task.created_by ? getUserById(task.created_by) : null;
502
+ const senderName = owner?.display_name || owner?.username || '定时任务';
503
+ if (!deps.storePromptMessage) {
504
+ throw new Error('storePromptMessage dependency not available');
505
+ }
506
+ // Store prompt as a user message in the source workspace chat
507
+ deps.storePromptMessage(targetGroupJid, owner?.id || 'system', senderName, task.prompt);
508
+ // Trigger normal message processing for the source workspace
509
+ deps.queue.enqueueMessageCheck(targetGroupJid);
510
+ logger.info({ taskId: task.id, targetGroupJid, contextMode: 'group' }, 'Group-mode task injected into source workspace');
511
+ logTaskRun({
512
+ task_id: task.id,
513
+ run_at: new Date().toISOString(),
514
+ duration_ms: Date.now() - startTime,
515
+ status: 'success',
516
+ result: '已注入到源工作区',
517
+ error: null,
518
+ });
519
+ }
520
+ catch (err) {
521
+ const error = err instanceof Error ? err.message : String(err);
522
+ logger.error({ taskId: task.id, error }, 'Group-mode task injection failed');
523
+ logTaskRun({
524
+ task_id: task.id,
525
+ run_at: new Date().toISOString(),
526
+ duration_ms: Date.now() - startTime,
527
+ status: 'error',
528
+ result: null,
529
+ error,
530
+ });
531
+ }
532
+ // Update next_run (manualRun preserves original schedule)
533
+ const nextRun = manualRun ? task.next_run : computeNextRun(task);
534
+ const resultSummary = '已注入到源工作区';
535
+ updateTaskAfterRun(task.id, nextRun, resultSummary);
536
+ }
537
+ let schedulerRunning = false;
538
+ const CLEANUP_INTERVAL_MS = 24 * 60 * 60 * 1000; // 24 hours
539
+ let lastCleanupTime = 0;
540
+ export function startSchedulerLoop(deps) {
541
+ if (schedulerRunning) {
542
+ logger.debug('Scheduler loop already running, skipping duplicate start');
543
+ return;
544
+ }
545
+ schedulerRunning = true;
546
+ // Clean up stale state from previous process crash
547
+ runningTaskIds.clear();
548
+ try {
549
+ const cleaned = cleanupStaleRunningLogs();
550
+ if (cleaned > 0) {
551
+ logger.info({ cleaned }, 'Cleaned up stale running task logs from previous session');
552
+ }
553
+ }
554
+ catch (err) {
555
+ logger.error({ err }, 'Failed to cleanup stale running task logs');
556
+ }
557
+ // Clean up orphaned workspaces from completed once-tasks
558
+ // (covers the case where process restarted before setTimeout cleanup fired)
559
+ try {
560
+ const allTasks = getAllTasks();
561
+ const groups = deps.registeredGroups();
562
+ let cleaned = 0;
563
+ for (const t of allTasks) {
564
+ if (t.schedule_type === 'once' &&
565
+ t.status === 'completed' &&
566
+ t.workspace_jid &&
567
+ t.workspace_folder &&
568
+ groups[t.workspace_jid]) {
569
+ deleteGroupData(t.workspace_jid, t.workspace_folder);
570
+ delete groups[t.workspace_jid];
571
+ removeFlowArtifacts(t.workspace_folder);
572
+ cleaned++;
573
+ }
574
+ }
575
+ if (cleaned > 0) {
576
+ logger.info({ cleaned }, 'Cleaned up orphaned once-task workspaces from previous session');
577
+ }
578
+ }
579
+ catch (err) {
580
+ logger.error({ err }, 'Failed to cleanup orphaned once-task workspaces');
581
+ }
582
+ logger.info('Scheduler loop started');
583
+ const loop = async () => {
584
+ try {
585
+ // Periodic cleanup of old task run logs (every 24h)
586
+ const now = Date.now();
587
+ if (now - lastCleanupTime >= CLEANUP_INTERVAL_MS) {
588
+ lastCleanupTime = now;
589
+ try {
590
+ const deleted = cleanupOldTaskRunLogs();
591
+ if (deleted > 0) {
592
+ logger.info({ deleted }, 'Cleaned up old task run logs');
593
+ }
594
+ }
595
+ catch (err) {
596
+ logger.error({ err }, 'Failed to cleanup old task run logs');
597
+ }
598
+ }
599
+ // Daily summary generation (runs at most once per hour, 2-3 AM)
600
+ if (deps.dailySummaryDeps) {
601
+ try {
602
+ runDailySummaryIfNeeded(deps.dailySummaryDeps);
603
+ }
604
+ catch (err) {
605
+ logger.error({ err }, 'Daily summary check failed');
606
+ }
607
+ }
608
+ const dueTasks = getDueTasks();
609
+ if (dueTasks.length > 0) {
610
+ logger.info({ count: dueTasks.length }, 'Found due tasks');
611
+ }
612
+ for (const task of dueTasks) {
613
+ // Re-check task status in case it was paused/cancelled
614
+ const currentTask = getTaskById(task.id);
615
+ if (!currentTask || currentTask.status !== 'active') {
616
+ continue;
617
+ }
618
+ if (runningTaskIds.has(currentTask.id)) {
619
+ continue;
620
+ }
621
+ const groups = deps.registeredGroups();
622
+ const targetGroupJid = resolveTargetGroupJid(currentTask, groups);
623
+ if (!targetGroupJid) {
624
+ logger.error({ taskId: currentTask.id, groupFolder: currentTask.group_folder }, 'Target group not registered, skipping scheduled task');
625
+ continue;
626
+ }
627
+ if (currentTask.execution_type === 'script') {
628
+ if (!hasScriptCapacity()) {
629
+ logger.debug({ taskId: currentTask.id }, 'Script concurrency limit reached, skipping');
630
+ continue;
631
+ }
632
+ // Script tasks run directly, not through GroupQueue
633
+ runScriptTask(currentTask, deps, targetGroupJid).catch((err) => {
634
+ logger.error({ taskId: currentTask.id, err }, 'Unhandled error in runScriptTask');
635
+ });
636
+ }
637
+ else if (currentTask.context_mode === 'group') {
638
+ // Group mode: inject prompt into source workspace as a regular message
639
+ runGroupModeTask(currentTask, deps, targetGroupJid).catch((err) => {
640
+ logger.error({ taskId: currentTask.id, err }, 'Unhandled error in runGroupModeTask');
641
+ });
642
+ }
643
+ else {
644
+ // Isolated mode (default): each agent task has a dedicated workspace
645
+ const taskQueueJid = currentTask.workspace_jid
646
+ ? `${currentTask.workspace_jid}#task:${currentTask.id}`
647
+ : `${targetGroupJid}#task:${currentTask.id}`;
648
+ deps.queue.enqueueTask(taskQueueJid, currentTask.id, () => runTask(currentTask, deps, {
649
+ taskRunId: currentTask.id,
650
+ }));
651
+ }
652
+ }
653
+ }
654
+ catch (err) {
655
+ logger.error({ err }, 'Error in scheduler loop');
656
+ }
657
+ setTimeout(loop, SCHEDULER_POLL_INTERVAL);
658
+ };
659
+ loop();
660
+ }
661
+ /**
662
+ * Manually trigger a task to run now (fire-and-forget).
663
+ * Does not change next_run — the task continues its normal schedule.
664
+ */
665
+ export function triggerTaskNow(taskId, deps) {
666
+ const task = getTaskById(taskId);
667
+ if (!task)
668
+ return { success: false, error: 'Task not found' };
669
+ if (task.status === 'completed')
670
+ return { success: false, error: 'Task already completed' };
671
+ if (task.status === 'paused')
672
+ return { success: false, error: '任务已暂停,请先恢复后再运行' };
673
+ if (runningTaskIds.has(taskId))
674
+ return { success: false, error: 'Task is already running' };
675
+ const groups = deps.registeredGroups();
676
+ const targetGroupJid = resolveTargetGroupJid(task, groups);
677
+ if (!targetGroupJid)
678
+ return { success: false, error: 'Target group not registered' };
679
+ if (task.execution_type === 'script') {
680
+ if (!hasScriptCapacity())
681
+ return { success: false, error: 'Script concurrency limit reached' };
682
+ runScriptTask(task, deps, targetGroupJid, true).catch((err) => logger.error({ taskId, err }, 'Manual script task failed'));
683
+ }
684
+ else if (task.context_mode === 'group') {
685
+ runGroupModeTask(task, deps, targetGroupJid, true).catch((err) => logger.error({ taskId, err }, 'Manual group-mode task failed'));
686
+ }
687
+ else {
688
+ const opts = { manualRun: true, taskRunId: task.id };
689
+ const taskQueueJid = task.workspace_jid
690
+ ? `${task.workspace_jid}#task:${task.id}`
691
+ : `${targetGroupJid}#task:${task.id}`;
692
+ deps.queue.enqueueTask(taskQueueJid, task.id, () => runTask(task, deps, opts));
693
+ }
694
+ return { success: true };
695
+ }