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,509 @@
1
+ // Task management routes
2
+ import { Hono } from 'hono';
3
+ import * as crypto from 'node:crypto';
4
+ import { CronExpressionParser } from 'cron-parser';
5
+ import { sdkQuery } from '../sdk-query.js';
6
+ import { removeFlowArtifacts } from '../file-manager.js';
7
+ import { authMiddleware } from '../middleware/auth.js';
8
+ import { TaskCreateSchema, TaskPatchSchema } from '../schemas.js';
9
+ import { logger } from '../logger.js';
10
+ import { getAllTasks, getTaskById, createTask, updateTask, deleteTask, getTaskRunLogs, getRegisteredGroup, getAllRegisteredGroups, getUserHomeGroup, deleteGroupData, } from '../db.js';
11
+ import { TIMEZONE } from '../config.js';
12
+ import { isHostExecutionGroup, hasHostExecutionPermission, canAccessGroup, getWebDeps, } from '../web-context.js';
13
+ import { getRunningTaskIds } from '../task-scheduler.js';
14
+ const tasksRoutes = new Hono();
15
+ // --- Routes ---
16
+ tasksRoutes.get('/', authMiddleware, (c) => {
17
+ const authUser = c.get('user');
18
+ const allGroups = getAllRegisteredGroups();
19
+ const tasks = getAllTasks().filter((task) => {
20
+ // Host-mode tasks are only visible to admin
21
+ if (task.execution_mode === 'host' && authUser.role !== 'admin') {
22
+ return false;
23
+ }
24
+ const group = allGroups[task.chat_jid];
25
+ // Conservative: if group can't be resolved, only admin can see (may be orphaned task)
26
+ if (!group)
27
+ return authUser.role === 'admin';
28
+ if (!canAccessGroup({ id: authUser.id, role: authUser.role }, { ...group, jid: task.chat_jid }))
29
+ return false;
30
+ if (isHostExecutionGroup(group) && !hasHostExecutionPermission(authUser))
31
+ return false;
32
+ return true;
33
+ });
34
+ const visibleTaskIds = new Set(tasks.map((t) => t.id));
35
+ const filteredRunningIds = getRunningTaskIds().filter((id) => visibleTaskIds.has(id));
36
+ return c.json({ tasks, runningTaskIds: filteredRunningIds });
37
+ });
38
+ tasksRoutes.post('/', authMiddleware, async (c) => {
39
+ const body = await c.req.json().catch(() => ({}));
40
+ const validation = TaskCreateSchema.safeParse(body);
41
+ if (!validation.success) {
42
+ return c.json({ error: 'Invalid request body', details: validation.error.format() }, 400);
43
+ }
44
+ const { prompt, schedule_type, schedule_value, execution_type, script_command, notify_channels, } = validation.data;
45
+ const authUser = c.get('user');
46
+ // Auto-resolve group_folder/chat_jid from user's home group if not provided
47
+ let groupFolder = validation.data.group_folder;
48
+ let chatJid = validation.data.chat_jid;
49
+ if (!groupFolder || !chatJid) {
50
+ const homeGroup = getUserHomeGroup(authUser.id);
51
+ if (!homeGroup) {
52
+ return c.json({ error: 'User has no home group' }, 400);
53
+ }
54
+ groupFolder = groupFolder || homeGroup.folder;
55
+ chatJid = chatJid || homeGroup.jid;
56
+ }
57
+ const group = getRegisteredGroup(chatJid);
58
+ if (!group)
59
+ return c.json({ error: 'Group not found' }, 404);
60
+ if (group.folder !== groupFolder) {
61
+ return c.json({ error: 'group_folder does not match chat_jid group folder' }, 400);
62
+ }
63
+ if (!canAccessGroup({ id: authUser.id, role: authUser.role }, group)) {
64
+ return c.json({ error: 'Group not found' }, 404);
65
+ }
66
+ if (isHostExecutionGroup(group) && !hasHostExecutionPermission(authUser)) {
67
+ return c.json({ error: 'Insufficient permissions for host execution mode' }, 403);
68
+ }
69
+ // Only admin can create script tasks
70
+ const execType = execution_type || 'agent';
71
+ if (execType === 'script' && authUser.role !== 'admin') {
72
+ return c.json({ error: '只有管理员可以创建脚本类型任务' }, 403);
73
+ }
74
+ // Determine execution_mode
75
+ let taskExecutionMode;
76
+ if (authUser.role === 'admin') {
77
+ taskExecutionMode = validation.data.execution_mode || 'host';
78
+ }
79
+ else {
80
+ if (validation.data.execution_mode === 'host') {
81
+ return c.json({ error: '只有管理员可以创建宿主机任务' }, 403);
82
+ }
83
+ taskExecutionMode = 'container';
84
+ }
85
+ const taskId = crypto.randomUUID();
86
+ const now = new Date().toISOString();
87
+ let nextRun;
88
+ if (schedule_type === 'cron') {
89
+ try {
90
+ const cronNext = CronExpressionParser.parse(schedule_value, {
91
+ tz: TIMEZONE,
92
+ })
93
+ .next()
94
+ .toISOString();
95
+ if (!cronNext) {
96
+ return c.json({ error: 'Cron expression produced no next run time' }, 400);
97
+ }
98
+ nextRun = cronNext;
99
+ }
100
+ catch {
101
+ return c.json({ error: 'Invalid cron expression' }, 400);
102
+ }
103
+ }
104
+ else if (schedule_type === 'interval') {
105
+ nextRun = new Date(Date.now() + Number(schedule_value)).toISOString();
106
+ }
107
+ else {
108
+ // once — use the target time from schedule_value
109
+ nextRun = new Date(schedule_value).toISOString();
110
+ }
111
+ createTask({
112
+ id: taskId,
113
+ group_folder: groupFolder,
114
+ chat_jid: chatJid,
115
+ prompt: prompt || '',
116
+ schedule_type,
117
+ schedule_value,
118
+ context_mode: validation.data.context_mode || 'group',
119
+ execution_type: execType,
120
+ execution_mode: taskExecutionMode,
121
+ script_command: script_command ?? null,
122
+ next_run: nextRun,
123
+ status: 'active',
124
+ created_at: now,
125
+ created_by: authUser.id,
126
+ notify_channels: notify_channels ?? null,
127
+ });
128
+ return c.json({ success: true, taskId });
129
+ });
130
+ tasksRoutes.patch('/:id', authMiddleware, async (c) => {
131
+ const id = c.req.param('id');
132
+ const existing = getTaskById(id);
133
+ if (!existing)
134
+ return c.json({ error: 'Task not found' }, 404);
135
+ const authUser = c.get('user');
136
+ const group = getRegisteredGroup(existing.chat_jid);
137
+ if (!group) {
138
+ if (authUser.role !== 'admin')
139
+ return c.json({ error: 'Task not found' }, 404);
140
+ }
141
+ else {
142
+ if (!canAccessGroup({ id: authUser.id, role: authUser.role }, group)) {
143
+ return c.json({ error: 'Task not found' }, 404);
144
+ }
145
+ if (isHostExecutionGroup(group) && !hasHostExecutionPermission(authUser)) {
146
+ return c.json({ error: 'Insufficient permissions for host execution mode' }, 403);
147
+ }
148
+ }
149
+ const body = await c.req.json().catch(() => ({}));
150
+ const validation = TaskPatchSchema.safeParse(body);
151
+ if (!validation.success) {
152
+ return c.json({ error: 'Invalid request body', details: validation.error.format() }, 400);
153
+ }
154
+ // Only admin can create/modify script tasks
155
+ const isScriptTask = validation.data.execution_type === 'script' ||
156
+ (existing.execution_type === 'script' &&
157
+ validation.data.script_command !== undefined);
158
+ if (isScriptTask && authUser.role !== 'admin') {
159
+ return c.json({ error: '只有管理员可以创建或修改脚本类型任务' }, 403);
160
+ }
161
+ // Only admin can set execution_mode to 'host'
162
+ if (validation.data.execution_mode === 'host' && authUser.role !== 'admin') {
163
+ return c.json({ error: '只有管理员可以设置宿主机执行模式' }, 403);
164
+ }
165
+ // Auto-recalculate next_run when schedule changes (avoid pulling cron-parser into frontend)
166
+ const patchData = { ...validation.data };
167
+ if (patchData.schedule_type !== undefined ||
168
+ patchData.schedule_value !== undefined) {
169
+ const schedType = patchData.schedule_type ?? existing.schedule_type;
170
+ const schedValue = patchData.schedule_value ?? existing.schedule_value;
171
+ try {
172
+ if (schedType === 'cron') {
173
+ patchData.next_run =
174
+ CronExpressionParser.parse(schedValue, { tz: TIMEZONE })
175
+ .next()
176
+ .toISOString() || new Date().toISOString();
177
+ }
178
+ else if (schedType === 'interval') {
179
+ const ms = parseInt(schedValue, 10);
180
+ if (!Number.isFinite(ms) || ms <= 0) {
181
+ return c.json({ error: 'Invalid interval value' }, 400);
182
+ }
183
+ patchData.next_run = new Date(Date.now() + ms).toISOString();
184
+ }
185
+ else if (schedType === 'once') {
186
+ const ts = Date.parse(schedValue);
187
+ if (isNaN(ts)) {
188
+ return c.json({ error: 'Invalid once schedule value' }, 400);
189
+ }
190
+ patchData.next_run = new Date(ts).toISOString();
191
+ }
192
+ }
193
+ catch {
194
+ return c.json({ error: 'Invalid schedule value for the given schedule type' }, 400);
195
+ }
196
+ }
197
+ updateTask(id, patchData);
198
+ return c.json({ success: true });
199
+ });
200
+ tasksRoutes.delete('/:id', authMiddleware, (c) => {
201
+ const id = c.req.param('id');
202
+ const existing = getTaskById(id);
203
+ if (!existing)
204
+ return c.json({ error: 'Task not found' }, 404);
205
+ const authUser = c.get('user');
206
+ const group = getRegisteredGroup(existing.chat_jid);
207
+ if (!group) {
208
+ if (authUser.role !== 'admin')
209
+ return c.json({ error: 'Task not found' }, 404);
210
+ }
211
+ else {
212
+ if (!canAccessGroup({ id: authUser.id, role: authUser.role }, group)) {
213
+ return c.json({ error: 'Task not found' }, 404);
214
+ }
215
+ if (isHostExecutionGroup(group) && !hasHostExecutionPermission(authUser)) {
216
+ return c.json({ error: 'Insufficient permissions for host execution mode' }, 403);
217
+ }
218
+ }
219
+ // Only admin can delete script tasks
220
+ if (existing.execution_type === 'script' && authUser.role !== 'admin') {
221
+ return c.json({ error: '只有管理员可以删除脚本类型任务' }, 403);
222
+ }
223
+ // Prevent deleting a running task
224
+ if (getRunningTaskIds().includes(id)) {
225
+ return c.json({ error: '任务正在运行中,请先等待完成或停止任务' }, 409);
226
+ }
227
+ // Clean up dedicated workspace if exists
228
+ if (existing.workspace_jid && existing.workspace_folder) {
229
+ const wsGroup = getRegisteredGroup(existing.workspace_jid);
230
+ if (wsGroup) {
231
+ deleteGroupData(existing.workspace_jid, existing.workspace_folder);
232
+ }
233
+ // Remove all flow artifacts (groups/, sessions/, ipc/, env/, memory/)
234
+ removeFlowArtifacts(existing.workspace_folder);
235
+ const deps = getWebDeps();
236
+ if (deps) {
237
+ delete deps.getRegisteredGroups()[existing.workspace_jid];
238
+ delete deps.getSessions()[existing.workspace_folder];
239
+ }
240
+ logger.info({
241
+ taskId: id,
242
+ workspaceJid: existing.workspace_jid,
243
+ workspaceFolder: existing.workspace_folder,
244
+ }, 'Task workspace deleted');
245
+ }
246
+ deleteTask(id);
247
+ return c.json({ success: true });
248
+ });
249
+ tasksRoutes.post('/:id/run', authMiddleware, (c) => {
250
+ const id = c.req.param('id');
251
+ const existing = getTaskById(id);
252
+ if (!existing)
253
+ return c.json({ error: 'Task not found' }, 404);
254
+ const authUser = c.get('user');
255
+ const group = getRegisteredGroup(existing.chat_jid);
256
+ if (!group) {
257
+ if (authUser.role !== 'admin')
258
+ return c.json({ error: 'Task not found' }, 404);
259
+ }
260
+ else {
261
+ if (!canAccessGroup({ id: authUser.id, role: authUser.role }, group)) {
262
+ return c.json({ error: 'Task not found' }, 404);
263
+ }
264
+ if (isHostExecutionGroup(group) && !hasHostExecutionPermission(authUser)) {
265
+ return c.json({ error: 'Insufficient permissions for host execution mode' }, 403);
266
+ }
267
+ }
268
+ // Only admin can run script tasks
269
+ if (existing.execution_type === 'script' && authUser.role !== 'admin') {
270
+ return c.json({ error: '只有管理员可以运行脚本类型任务' }, 403);
271
+ }
272
+ const deps = getWebDeps();
273
+ if (!deps?.triggerTaskRun)
274
+ return c.json({ error: 'Scheduler not available' }, 503);
275
+ const result = deps.triggerTaskRun(id);
276
+ if (!result.success)
277
+ return c.json({ error: result.error }, 409);
278
+ return c.json({ success: true });
279
+ });
280
+ tasksRoutes.get('/:id/logs', authMiddleware, (c) => {
281
+ const id = c.req.param('id');
282
+ const existing = getTaskById(id);
283
+ if (!existing)
284
+ return c.json({ error: 'Task not found' }, 404);
285
+ const authUser = c.get('user');
286
+ const group = getRegisteredGroup(existing.chat_jid);
287
+ if (!group) {
288
+ if (authUser.role !== 'admin')
289
+ return c.json({ error: 'Task not found' }, 404);
290
+ }
291
+ else {
292
+ if (!canAccessGroup({ id: authUser.id, role: authUser.role }, group)) {
293
+ return c.json({ error: 'Task not found' }, 404);
294
+ }
295
+ if (isHostExecutionGroup(group) && !hasHostExecutionPermission(authUser)) {
296
+ return c.json({ error: 'Insufficient permissions for host execution mode' }, 403);
297
+ }
298
+ }
299
+ const limitRaw = parseInt(c.req.query('limit') || '20', 10);
300
+ const limit = Math.min(Number.isFinite(limitRaw) ? Math.max(1, limitRaw) : 20, 200);
301
+ const logs = getTaskRunLogs(id, limit);
302
+ return c.json({ logs });
303
+ });
304
+ /** Build the AI parse prompt for a task description */
305
+ function buildParsePrompt(description) {
306
+ const now = new Date();
307
+ return `你是一个任务调度解析器。用户会用自然语言描述他们想要创建的定时任务,你需要解析出结构化的任务参数。
308
+
309
+ 当前时间: ${now.toISOString()}
310
+ 当前时区: ${TIMEZONE}
311
+
312
+ 用户描述: "${description}"
313
+
314
+ 请返回一个 JSON 对象(不要包含任何其他文字),包含以下字段:
315
+ - "prompt": string — 任务要执行的 prompt(精炼用户的意图,作为 Agent 的指令)
316
+ - "schedule_type": "cron" | "interval" | "once" — 调度类型
317
+ - "schedule_value": string — 调度值:
318
+ - cron 类型: cron 表达式(推荐 5 段:分 时 日 月 周,也支持 6 段含秒)
319
+ - interval 类型: 毫秒数字符串(如 "3600000" 表示 1 小时)
320
+ - once 类型: ISO 8601 日期时间字符串
321
+ - "context_mode": "group" | "isolated" — 上下文模式(大多数情况推荐 "group")
322
+ - "summary": string — 用一句话解释你的理解(中文)
323
+
324
+ 注意:
325
+ - cron 表达式中的时间为北京时间(UTC+8)
326
+ - 推荐使用 5 段格式:分 时 日 月 星期
327
+ - 支持特殊字符:*/n(步长)、a-b(范围)、a,b,c(列表)、L(最后)、W(工作日)、#(第N个)
328
+ - 支持预定义表达式:@daily, @hourly, @weekly, @monthly, @yearly
329
+ - "每天早上 9 点" → cron "0 9 * * *"
330
+ - "每小时" → interval "3600000"
331
+ - "每 30 分钟" → interval "1800000"
332
+ - "明天下午 3 点" → once,计算出具体的 ISO 时间
333
+ - "每周一早上 10 点" → cron "0 10 * * 1"
334
+ - "每月最后一天" → cron "0 0 L * *"
335
+ - "每 5 分钟" → cron "*/5 * * * *"
336
+
337
+ 只返回 JSON,不要返回其他任何内容。`;
338
+ }
339
+ /** Parse AI response text into structured task params */
340
+ function parseAiResult(result, description) {
341
+ try {
342
+ let jsonStr = result;
343
+ const fenced = result.match(/```(?:json)?\s*([\s\S]*?)```/);
344
+ if (fenced)
345
+ jsonStr = fenced[1].trim();
346
+ const parsed = JSON.parse(jsonStr);
347
+ return {
348
+ prompt: parsed.prompt || description,
349
+ schedule_type: parsed.schedule_type || 'cron',
350
+ schedule_value: parsed.schedule_value || '',
351
+ summary: parsed.summary || '',
352
+ };
353
+ }
354
+ catch {
355
+ return null;
356
+ }
357
+ }
358
+ /**
359
+ * AI create: immediately create task in 'parsing' status, resolve schedule in background.
360
+ */
361
+ tasksRoutes.post('/ai', authMiddleware, async (c) => {
362
+ const authUser = c.get('user');
363
+ const body = await c.req.json().catch(() => ({}));
364
+ const description = typeof body.description === 'string' ? body.description.trim() : '';
365
+ if (!description) {
366
+ return c.json({ error: '请输入任务描述' }, 400);
367
+ }
368
+ const notifyChannels = body.notify_channels ?? null;
369
+ // Resolve home group
370
+ const homeGroup = getUserHomeGroup(authUser.id);
371
+ if (!homeGroup)
372
+ return c.json({ error: 'Home group not found' }, 400);
373
+ const taskId = crypto.randomUUID();
374
+ const now = new Date().toISOString();
375
+ // Determine execution_mode
376
+ const taskExecutionMode = authUser.role === 'admin' ? 'host' : 'container';
377
+ // Create task immediately with 'parsing' status and description as prompt
378
+ createTask({
379
+ id: taskId,
380
+ group_folder: homeGroup.folder,
381
+ chat_jid: homeGroup.jid,
382
+ prompt: description,
383
+ schedule_type: 'cron',
384
+ schedule_value: '0 0 * * *', // placeholder, will be updated after parsing
385
+ context_mode: 'group',
386
+ execution_type: 'agent',
387
+ execution_mode: taskExecutionMode,
388
+ script_command: null,
389
+ next_run: null,
390
+ status: 'parsing',
391
+ created_at: now,
392
+ created_by: authUser.id,
393
+ notify_channels: notifyChannels,
394
+ });
395
+ logger.info({ taskId, description: description.slice(0, 80) }, 'AI task created, parsing in background');
396
+ // Background: parse with SDK and update task
397
+ void (async () => {
398
+ try {
399
+ const parsePrompt = buildParsePrompt(description);
400
+ const model = process.env.RECALL_MODEL || undefined;
401
+ const result = await sdkQuery(parsePrompt, { model, timeout: 60_000 });
402
+ if (!result) {
403
+ const cur = getTaskById(taskId);
404
+ if (!cur || cur.status !== 'parsing')
405
+ return;
406
+ updateTask(taskId, {
407
+ status: 'paused',
408
+ prompt: description,
409
+ });
410
+ logger.warn({ taskId }, 'AI parse returned null, task paused');
411
+ return;
412
+ }
413
+ const parsed = parseAiResult(result, description);
414
+ if (!parsed || !parsed.schedule_value) {
415
+ const cur = getTaskById(taskId);
416
+ if (!cur || cur.status !== 'parsing')
417
+ return;
418
+ updateTask(taskId, {
419
+ status: 'paused',
420
+ prompt: description,
421
+ });
422
+ logger.warn({ taskId }, 'AI parse result invalid, task paused');
423
+ return;
424
+ }
425
+ // Compute next_run from parsed schedule
426
+ let nextRun = null;
427
+ try {
428
+ if (parsed.schedule_type === 'cron') {
429
+ nextRun = CronExpressionParser.parse(parsed.schedule_value, {
430
+ tz: TIMEZONE,
431
+ })
432
+ .next()
433
+ .toISOString();
434
+ }
435
+ else if (parsed.schedule_type === 'interval') {
436
+ nextRun = new Date(Date.now() + Number(parsed.schedule_value)).toISOString();
437
+ }
438
+ else {
439
+ nextRun = new Date(parsed.schedule_value).toISOString();
440
+ }
441
+ }
442
+ catch {
443
+ // Invalid schedule, keep paused
444
+ const cur = getTaskById(taskId);
445
+ if (!cur || cur.status !== 'parsing')
446
+ return;
447
+ updateTask(taskId, {
448
+ status: 'paused',
449
+ prompt: parsed.prompt,
450
+ });
451
+ logger.warn({ taskId, scheduleValue: parsed.schedule_value }, 'AI parsed schedule invalid, task paused');
452
+ return;
453
+ }
454
+ const cur = getTaskById(taskId);
455
+ if (!cur || cur.status !== 'parsing')
456
+ return;
457
+ updateTask(taskId, {
458
+ prompt: parsed.prompt,
459
+ schedule_type: parsed.schedule_type,
460
+ schedule_value: parsed.schedule_value,
461
+ next_run: nextRun,
462
+ status: 'active',
463
+ });
464
+ logger.info({
465
+ taskId,
466
+ scheduleType: parsed.schedule_type,
467
+ scheduleValue: parsed.schedule_value,
468
+ }, 'AI task parse complete, activated');
469
+ }
470
+ catch (err) {
471
+ logger.error({ taskId, err }, 'AI task background parse failed');
472
+ const cur = getTaskById(taskId);
473
+ if (cur && cur.status === 'parsing') {
474
+ updateTask(taskId, { status: 'paused' });
475
+ }
476
+ }
477
+ })().catch((err) => logger.error({ taskId, err }, 'Unhandled AI task parse error'));
478
+ return c.json({ success: true, taskId });
479
+ });
480
+ /**
481
+ * Parse natural language task description (synchronous, kept for backward compat).
482
+ */
483
+ tasksRoutes.post('/parse', authMiddleware, async (c) => {
484
+ const body = await c.req.json().catch(() => ({}));
485
+ const description = typeof body.description === 'string' ? body.description.trim() : '';
486
+ if (!description) {
487
+ return c.json({ error: '请输入任务描述' }, 400);
488
+ }
489
+ try {
490
+ const model = process.env.RECALL_MODEL || undefined;
491
+ const result = await sdkQuery(buildParsePrompt(description), {
492
+ model,
493
+ timeout: 30_000,
494
+ });
495
+ if (!result) {
496
+ return c.json({ error: 'AI 解析失败,请重试或切换到手动模式' }, 502);
497
+ }
498
+ const parsed = parseAiResult(result, description);
499
+ if (!parsed) {
500
+ return c.json({ error: 'AI 返回格式异常,请重试或切换到手动模式' }, 502);
501
+ }
502
+ return c.json({ success: true, parsed });
503
+ }
504
+ catch (err) {
505
+ logger.warn({ err }, 'task-parse: failed to parse AI response');
506
+ return c.json({ error: 'AI 返回格式异常,请重试或切换到手动模式' }, 502);
507
+ }
508
+ });
509
+ export default tasksRoutes;
@@ -0,0 +1,64 @@
1
+ import { Hono } from 'hono';
2
+ import { authMiddleware } from '../middleware/auth.js';
3
+ import { getUsageDailyStats, getUsageDailySummary, getUsageModels, getUsageUsers, } from '../db.js';
4
+ const usage = new Hono();
5
+ usage.use('*', authMiddleware);
6
+ /**
7
+ * Resolve userId for queries:
8
+ * - Admin can filter by any userId or see all (undefined = all)
9
+ * - Member always sees only their own data
10
+ */
11
+ function resolveUserId(user, requestedUserId) {
12
+ if (user.role === 'admin') {
13
+ return requestedUserId || undefined; // undefined = all users
14
+ }
15
+ return user.id; // member always sees only own data
16
+ }
17
+ /**
18
+ * GET /api/usage/stats?days=7&userId=&model=
19
+ * Returns aggregated token usage statistics from usage_daily_summary.
20
+ * Fixes: token KPI (uses modelUsage data) + timezone (local date grouping).
21
+ */
22
+ usage.get('/stats', (c) => {
23
+ const user = c.get('user');
24
+ const daysParam = c.req.query('days');
25
+ const days = daysParam
26
+ ? Math.min(Math.max(parseInt(daysParam, 10) || 7, 1), 365)
27
+ : 7;
28
+ const userId = resolveUserId(user, c.req.query('userId') || undefined);
29
+ const model = c.req.query('model') || undefined;
30
+ const summary = getUsageDailySummary(days, userId, model);
31
+ const breakdown = getUsageDailyStats(days, userId, model);
32
+ // Compute actual data range for frontend display
33
+ const dates = breakdown.map((r) => r.date);
34
+ const uniqueDates = [...new Set(dates)].sort();
35
+ const dataRange = uniqueDates.length > 0
36
+ ? {
37
+ from: uniqueDates[0],
38
+ to: uniqueDates[uniqueDates.length - 1],
39
+ activeDays: uniqueDates.length,
40
+ }
41
+ : null;
42
+ return c.json({ summary, breakdown, days, dataRange });
43
+ });
44
+ /**
45
+ * GET /api/usage/models
46
+ * Returns list of all models that have usage data.
47
+ */
48
+ usage.get('/models', (c) => {
49
+ const models = getUsageModels();
50
+ return c.json({ models });
51
+ });
52
+ /**
53
+ * GET /api/usage/users
54
+ * Returns list of users that have usage data. Admin only.
55
+ */
56
+ usage.get('/users', (c) => {
57
+ const user = c.get('user');
58
+ if (user.role !== 'admin') {
59
+ return c.json({ users: [{ id: user.id, username: user.username }] });
60
+ }
61
+ const users = getUsageUsers();
62
+ return c.json({ users });
63
+ });
64
+ export { usage };