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.
- package/LICENSE +21 -0
- package/README.md +245 -0
- package/config/default-groups.json +1 -0
- package/config/global-agents-md.template.md +37 -0
- package/config/mount-allowlist.json +11 -0
- package/container/Dockerfile +160 -0
- package/container/agent-runner/dist/.tsbuildinfo +1 -0
- package/container/agent-runner/dist/agent-definitions.js +22 -0
- package/container/agent-runner/dist/channel-prefixes.js +16 -0
- package/container/agent-runner/dist/codex-config.js +29 -0
- package/container/agent-runner/dist/image-detector.js +96 -0
- package/container/agent-runner/dist/index.js +2587 -0
- package/container/agent-runner/dist/mcp-tools.js +1076 -0
- package/container/agent-runner/dist/stream-event.types.js +5 -0
- package/container/agent-runner/dist/stream-processor.js +867 -0
- package/container/agent-runner/dist/types.js +6 -0
- package/container/agent-runner/dist/utils.js +115 -0
- package/container/agent-runner/package.json +36 -0
- package/container/agent-runner/prompts/security-rules.md +31 -0
- package/container/agent-runner/src/agent-definitions.ts +27 -0
- package/container/agent-runner/src/channel-prefixes.ts +16 -0
- package/container/agent-runner/src/codex-config.ts +40 -0
- package/container/agent-runner/src/image-detector.ts +116 -0
- package/container/agent-runner/src/index.ts +3107 -0
- package/container/agent-runner/src/mcp-tools.ts +1295 -0
- package/container/agent-runner/src/stream-event.types.ts +10 -0
- package/container/agent-runner/src/stream-processor.ts +932 -0
- package/container/agent-runner/src/types.ts +75 -0
- package/container/agent-runner/src/utils.ts +114 -0
- package/container/agent-runner/tsconfig.json +17 -0
- package/container/build.sh +28 -0
- package/container/entrypoint.sh +64 -0
- package/container/skills/agent-browser/SKILL.md +159 -0
- package/container/skills/install-skill/SKILL.md +64 -0
- package/container/skills/post-test-cleanup/SKILL.md +121 -0
- package/dist/.tsbuildinfo +1 -0
- package/dist/agent-output-parser.js +459 -0
- package/dist/app-root.js +52 -0
- package/dist/assistant-meta-footer.js +1 -0
- package/dist/auth.js +91 -0
- package/dist/billing.js +694 -0
- package/dist/channel-prefixes.js +16 -0
- package/dist/cli.js +86 -0
- package/dist/commands.js +79 -0
- package/dist/config.js +120 -0
- package/dist/container-runner.js +981 -0
- package/dist/daily-summary.js +210 -0
- package/dist/db.js +3683 -0
- package/dist/dingtalk.js +1347 -0
- package/dist/feishu-markdown-style.js +97 -0
- package/dist/feishu-streaming-card.js +1875 -0
- package/dist/feishu.js +1628 -0
- package/dist/file-manager.js +270 -0
- package/dist/group-queue.js +1070 -0
- package/dist/group-runtime.js +35 -0
- package/dist/host-workspace-cwd.js +85 -0
- package/dist/im-channel.js +384 -0
- package/dist/im-command-utils.js +142 -0
- package/dist/im-downloader.js +45 -0
- package/dist/im-manager.js +527 -0
- package/dist/im-utils.js +53 -0
- package/dist/image-detector.js +96 -0
- package/dist/index.js +5828 -0
- package/dist/logger.js +22 -0
- package/dist/mcp-utils.js +66 -0
- package/dist/message-attachments.js +69 -0
- package/dist/message-notifier.js +36 -0
- package/dist/middleware/auth.js +85 -0
- package/dist/mount-security.js +315 -0
- package/dist/permissions.js +67 -0
- package/dist/project-memory.js +6 -0
- package/dist/provider-pool.js +189 -0
- package/dist/qq.js +826 -0
- package/dist/reset-admin.js +42 -0
- package/dist/routes/admin.js +543 -0
- package/dist/routes/agent-definitions.js +241 -0
- package/dist/routes/agents.js +533 -0
- package/dist/routes/auth.js +675 -0
- package/dist/routes/billing.js +490 -0
- package/dist/routes/browse.js +210 -0
- package/dist/routes/bug-report.js +387 -0
- package/dist/routes/config.js +1868 -0
- package/dist/routes/files.js +671 -0
- package/dist/routes/groups.js +1367 -0
- package/dist/routes/mcp-servers.js +320 -0
- package/dist/routes/memory.js +523 -0
- package/dist/routes/monitor.js +307 -0
- package/dist/routes/skills.js +777 -0
- package/dist/routes/tasks.js +509 -0
- package/dist/routes/usage.js +64 -0
- package/dist/routes/workspace-config.js +458 -0
- package/dist/runtime-build.js +112 -0
- package/dist/runtime-command-handler.js +189 -0
- package/dist/runtime-command-registry.js +1 -0
- package/dist/runtime-config.js +1777 -0
- package/dist/runtime-identity.js +52 -0
- package/dist/schemas.js +590 -0
- package/dist/script-runner.js +64 -0
- package/dist/sdk-query.js +82 -0
- package/dist/skill-utils.js +145 -0
- package/dist/sqlite-compat.js +19 -0
- package/dist/stream-event.types.js +5 -0
- package/dist/streaming-runtime-meta.js +29 -0
- package/dist/task-scheduler.js +695 -0
- package/dist/task-utils.js +13 -0
- package/dist/telegram-pairing.js +59 -0
- package/dist/telegram.js +897 -0
- package/dist/terminal-manager.js +307 -0
- package/dist/tool-step-display.js +1 -0
- package/dist/types.js +1 -0
- package/dist/utils.js +85 -0
- package/dist/web-context.js +161 -0
- package/dist/web.js +1377 -0
- package/dist/wechat-crypto.js +182 -0
- package/dist/wechat.js +589 -0
- package/dist/workspace-runtime-reset.js +35 -0
- package/package.json +107 -0
- package/shared/assistant-meta-footer.ts +127 -0
- package/shared/channel-prefixes.ts +16 -0
- package/shared/dist/assistant-meta-footer.d.ts +29 -0
- package/shared/dist/assistant-meta-footer.js +85 -0
- package/shared/dist/channel-prefixes.d.ts +4 -0
- package/shared/dist/channel-prefixes.js +16 -0
- package/shared/dist/image-detector.d.ts +20 -0
- package/shared/dist/image-detector.js +96 -0
- package/shared/dist/runtime-command-registry.d.ts +38 -0
- package/shared/dist/runtime-command-registry.js +185 -0
- package/shared/dist/stream-event.d.ts +65 -0
- package/shared/dist/stream-event.js +8 -0
- package/shared/dist/tool-step-display.d.ts +4 -0
- package/shared/dist/tool-step-display.js +11 -0
- package/shared/image-detector.ts +116 -0
- package/shared/runtime-command-registry.ts +252 -0
- package/shared/stream-event.ts +67 -0
- package/shared/tool-step-display.ts +21 -0
- package/shared/tsconfig.json +24 -0
- package/web/dist/assets/BillingPage-B1wBR_o-.js +52 -0
- package/web/dist/assets/ChatPage-6GBZ9nXN.css +32 -0
- package/web/dist/assets/ChatPage-BOJcXtaj.js +161 -0
- package/web/dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
- package/web/dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
- package/web/dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
- package/web/dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
- package/web/dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
- package/web/dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
- package/web/dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
- package/web/dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
- package/web/dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
- package/web/dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
- package/web/dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
- package/web/dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
- package/web/dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
- package/web/dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
- package/web/dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
- package/web/dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
- package/web/dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
- package/web/dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
- package/web/dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
- package/web/dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
- package/web/dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
- package/web/dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
- package/web/dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
- package/web/dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
- package/web/dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
- package/web/dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
- package/web/dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
- package/web/dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
- package/web/dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
- package/web/dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
- package/web/dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
- package/web/dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
- package/web/dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
- package/web/dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
- package/web/dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
- package/web/dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
- package/web/dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
- package/web/dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
- package/web/dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
- package/web/dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
- package/web/dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
- package/web/dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
- package/web/dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
- package/web/dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
- package/web/dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
- package/web/dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
- package/web/dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
- package/web/dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
- package/web/dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
- package/web/dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
- package/web/dist/assets/SettingsPage-DoY7FoZ_.js +153 -0
- package/web/dist/assets/ShareImageDialog-C1ga8b7l.js +22 -0
- package/web/dist/assets/TasksPage-CRivnNsx.js +14 -0
- package/web/dist/assets/_basePickBy-Bf-bSoS9.js +1 -0
- package/web/dist/assets/_baseUniq-zAOaCuKw.js +1 -0
- package/web/dist/assets/arc-Dm9mVQ9U.js +1 -0
- package/web/dist/assets/architectureDiagram-2XIMDMQ5-BLmzX1wr.js +36 -0
- package/web/dist/assets/band-CquvqAHh.js +1 -0
- package/web/dist/assets/blockDiagram-WCTKOSBZ-B9pcqm3j.js +132 -0
- package/web/dist/assets/c4Diagram-IC4MRINW-Cytx1q3b.js +10 -0
- package/web/dist/assets/channel-BOVj73LR.js +1 -0
- package/web/dist/assets/channel-meta-CQD0Pei-.js +41 -0
- package/web/dist/assets/chunk-4BX2VUAB-0ToDr6RE.js +1 -0
- package/web/dist/assets/chunk-55IACEB6-DQDjnXfS.js +1 -0
- package/web/dist/assets/chunk-FMBD7UC4-Di8ABm6c.js +15 -0
- package/web/dist/assets/chunk-JSJVCQXG-BZQN6rnX.js +1 -0
- package/web/dist/assets/chunk-KX2RTZJC-zBbcpaN_.js +1 -0
- package/web/dist/assets/chunk-NQ4KR5QH-BCrLoU88.js +220 -0
- package/web/dist/assets/chunk-QZHKN3VN-Bqk8juan.js +1 -0
- package/web/dist/assets/chunk-WL4C6EOR-D2YX-MHY.js +189 -0
- package/web/dist/assets/classDiagram-VBA2DB6C-DUUoMyaK.js +1 -0
- package/web/dist/assets/classDiagram-v2-RAHNMMFH-DUUoMyaK.js +1 -0
- package/web/dist/assets/clone-BmaCesfa.js +1 -0
- package/web/dist/assets/cose-bilkent-S5V4N54A-CTsv6qQA.js +1 -0
- package/web/dist/assets/cytoscape.esm-BQaXIfA_.js +331 -0
- package/web/dist/assets/dagre-KLK3FWXG-Ci4Jh9nu.js +4 -0
- package/web/dist/assets/defaultLocale-DX6XiGOO.js +1 -0
- package/web/dist/assets/diagram-E7M64L7V-BFRnfTI2.js +24 -0
- package/web/dist/assets/diagram-IFDJBPK2-B7Zhnp0b.js +43 -0
- package/web/dist/assets/diagram-P4PSJMXO-BVyP7nwq.js +24 -0
- package/web/dist/assets/erDiagram-INFDFZHY-NorKdTOF.js +70 -0
- package/web/dist/assets/error-CGD5mp5f.js +1 -0
- package/web/dist/assets/flowDiagram-PKNHOUZH-Ch97nABF.js +162 -0
- package/web/dist/assets/ganttDiagram-A5KZAMGK-BQ2pLWsy.js +292 -0
- package/web/dist/assets/gitGraphDiagram-K3NZZRJ6-bcvnBsD2.js +65 -0
- package/web/dist/assets/graph-CeAEckur.js +1 -0
- package/web/dist/assets/index-CPnL1_qC.js +768 -0
- package/web/dist/assets/index-DVevCbcO.css +10 -0
- package/web/dist/assets/infoDiagram-LFFYTUFH-CcsrFdj-.js +2 -0
- package/web/dist/assets/init-Dmth1JHB.js +1 -0
- package/web/dist/assets/ishikawaDiagram-PHBUUO56-1upyMfHN.js +70 -0
- package/web/dist/assets/journeyDiagram-4ABVD52K-CKUi-V0c.js +139 -0
- package/web/dist/assets/kanban-definition-K7BYSVSG-DOnQwXfL.js +89 -0
- package/web/dist/assets/layout-BmMMqTnJ.js +1 -0
- package/web/dist/assets/linear-DiaJloY5.js +1 -0
- package/web/dist/assets/mermaid.core-BWLV1B2v.js +254 -0
- package/web/dist/assets/mindmap-definition-YRQLILUH-BeAKHVWP.js +68 -0
- package/web/dist/assets/ordinal-DILIJJjt.js +1 -0
- package/web/dist/assets/pieDiagram-SKSYHLDU-DfiMSfWo.js +30 -0
- package/web/dist/assets/quadrantDiagram-337W2JSQ-wZxZOJxd.js +7 -0
- package/web/dist/assets/requirementDiagram-Z7DCOOCP-BK4HHm17.js +73 -0
- package/web/dist/assets/sankeyDiagram-WA2Y5GQK-BX6t2avX.js +10 -0
- package/web/dist/assets/sequenceDiagram-2WXFIKYE-BPQlkbAa.js +145 -0
- package/web/dist/assets/sheet-rI0FfB1g.js +6 -0
- package/web/dist/assets/sliders-horizontal-CuijWFNK.js +6 -0
- package/web/dist/assets/sparkles-BsMYXJoT.js +11 -0
- package/web/dist/assets/square-0CqMX1Q3.js +11 -0
- package/web/dist/assets/stateDiagram-RAJIS63D-DxkV0Vwd.js +1 -0
- package/web/dist/assets/stateDiagram-v2-FVOUBMTO-qLYoiOPe.js +1 -0
- package/web/dist/assets/step-D51IIHGA.js +1 -0
- package/web/dist/assets/tasks-D8JjBTwx.js +1 -0
- package/web/dist/assets/time-O8zIGux3.js +1 -0
- package/web/dist/assets/timeline-definition-YZTLITO2-kNp1DyFc.js +61 -0
- package/web/dist/assets/treemap-KZPCXAKY-CkrClVhk.js +162 -0
- package/web/dist/assets/utils-KGAn0XTg.js +11 -0
- package/web/dist/assets/vennDiagram-LZ73GAT5-CgdzEZz4.js +34 -0
- package/web/dist/assets/xychartDiagram-JWTSCODW-DfYGPfNB.js +7 -0
- package/web/dist/assets/zap-_hKJYy7J.js +6 -0
- package/web/dist/favicon.svg +332 -0
- package/web/dist/fonts/AlibabaPuHuiTi-3-55-Regular.woff2 +0 -0
- package/web/dist/fonts/AlibabaPuHuiTi-3-65-Medium.woff2 +0 -0
- package/web/dist/fonts/AlibabaPuHuiTi-3-75-SemiBold.woff2 +0 -0
- package/web/dist/fonts/DMSans-latin-ext.woff2 +0 -0
- package/web/dist/fonts/DMSans-latin.woff2 +0 -0
- package/web/dist/icons/README.md +20 -0
- package/web/dist/icons/apple-touch-icon-180.png +0 -0
- package/web/dist/icons/icon-128.png +0 -0
- package/web/dist/icons/icon-144.png +0 -0
- package/web/dist/icons/icon-152.png +0 -0
- package/web/dist/icons/icon-192.png +0 -0
- package/web/dist/icons/icon-192.svg +332 -0
- package/web/dist/icons/icon-384.png +0 -0
- package/web/dist/icons/icon-48.png +0 -0
- package/web/dist/icons/icon-512-maskable.png +0 -0
- package/web/dist/icons/icon-512.png +0 -0
- package/web/dist/icons/icon-512.svg +332 -0
- package/web/dist/icons/icon-72.png +0 -0
- package/web/dist/icons/icon-96.png +0 -0
- package/web/dist/icons/loading-logo.svg +332 -0
- package/web/dist/icons/logo-1024.png +0 -0
- package/web/dist/icons/logo-icon.svg +332 -0
- package/web/dist/icons/logo-text.svg +332 -0
- package/web/dist/index.html +30 -0
- package/web/dist/manifest.webmanifest +1 -0
- package/web/dist/registerSW.js +1 -0
- package/web/dist/sw.js +1 -0
- 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
|
+
}
|