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,35 @@
|
|
|
1
|
+
import { resolveEffectiveHostWorkspaceCwd } from './host-workspace-cwd.js';
|
|
2
|
+
export function normalizeAgentType(raw) {
|
|
3
|
+
if (raw === 'codex')
|
|
4
|
+
return 'codex';
|
|
5
|
+
return 'claude';
|
|
6
|
+
}
|
|
7
|
+
export function enforceAgentExecutionMode(agentType, executionMode) {
|
|
8
|
+
if (agentType === 'codex' && executionMode !== 'host') {
|
|
9
|
+
return 'Codex only supports host execution mode';
|
|
10
|
+
}
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
export function validateGroupRuntimeUpdate(options) {
|
|
14
|
+
if (options.isHome &&
|
|
15
|
+
options.nextExecutionMode !== options.currentExecutionMode) {
|
|
16
|
+
return 'Cannot change execution mode of home containers';
|
|
17
|
+
}
|
|
18
|
+
return enforceAgentExecutionMode(options.nextAgentType, options.nextExecutionMode);
|
|
19
|
+
}
|
|
20
|
+
export function hasRuntimeBoundaryChange(options) {
|
|
21
|
+
return (options.currentAgentType !== options.nextAgentType ||
|
|
22
|
+
options.currentExecutionMode !== options.nextExecutionMode);
|
|
23
|
+
}
|
|
24
|
+
export function buildEffectiveGroupFromHomeSibling(group, homeGroup) {
|
|
25
|
+
return {
|
|
26
|
+
...group,
|
|
27
|
+
agentType: homeGroup.agentType ?? group.agentType,
|
|
28
|
+
executionMode: homeGroup.executionMode ?? group.executionMode,
|
|
29
|
+
model: homeGroup.model ?? group.model,
|
|
30
|
+
reasoningEffort: homeGroup.reasoningEffort ?? group.reasoningEffort,
|
|
31
|
+
customCwd: resolveEffectiveHostWorkspaceCwd(group, homeGroup),
|
|
32
|
+
created_by: group.created_by || homeGroup.created_by,
|
|
33
|
+
is_home: true,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { LAUNCH_CWD } from './app-root.js';
|
|
4
|
+
import { findAllowedRoot, loadMountAllowlist } from './mount-security.js';
|
|
5
|
+
export function validateHostWorkspaceCwd(cwd, options = {}) {
|
|
6
|
+
const fieldLabel = options.fieldLabel ?? 'custom_cwd';
|
|
7
|
+
if (!path.isAbsolute(cwd)) {
|
|
8
|
+
return { error: `${fieldLabel} must be an absolute path` };
|
|
9
|
+
}
|
|
10
|
+
let stat;
|
|
11
|
+
try {
|
|
12
|
+
stat = fs.statSync(cwd);
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return { error: `${fieldLabel} directory does not exist` };
|
|
16
|
+
}
|
|
17
|
+
if (!stat.isDirectory()) {
|
|
18
|
+
return { error: `${fieldLabel} must be an existing directory` };
|
|
19
|
+
}
|
|
20
|
+
const normalizedCwd = fs.realpathSync(cwd);
|
|
21
|
+
const allowlist = options.allowlist !== undefined ? options.allowlist : loadMountAllowlist();
|
|
22
|
+
if (allowlist?.allowedRoots?.length) {
|
|
23
|
+
const allowedRoot = findAllowedRoot(normalizedCwd, allowlist.allowedRoots);
|
|
24
|
+
if (!allowedRoot) {
|
|
25
|
+
const allowedPaths = allowlist.allowedRoots
|
|
26
|
+
.map((root) => root.path)
|
|
27
|
+
.join(', ');
|
|
28
|
+
return {
|
|
29
|
+
error: `${fieldLabel} must be under an allowed root. Allowed roots: ${allowedPaths}. Check config/mount-allowlist.json`,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return { cwd: normalizedCwd };
|
|
34
|
+
}
|
|
35
|
+
export function materializeHostWorkspaceDefaultCwd(group, launchCwdOrOptions = {}, maybeOptions = {}) {
|
|
36
|
+
const options = typeof launchCwdOrOptions === 'string'
|
|
37
|
+
? {
|
|
38
|
+
...maybeOptions,
|
|
39
|
+
launchCwd: launchCwdOrOptions,
|
|
40
|
+
}
|
|
41
|
+
: launchCwdOrOptions;
|
|
42
|
+
if ((group.executionMode ?? 'container') !== 'host') {
|
|
43
|
+
return { group, materialized: false };
|
|
44
|
+
}
|
|
45
|
+
if (group.customCwd) {
|
|
46
|
+
const validation = validateHostWorkspaceCwd(group.customCwd, {
|
|
47
|
+
allowlist: options.allowlist,
|
|
48
|
+
fieldLabel: options.fieldLabel ?? 'custom_cwd',
|
|
49
|
+
});
|
|
50
|
+
if ('error' in validation) {
|
|
51
|
+
return validation;
|
|
52
|
+
}
|
|
53
|
+
return {
|
|
54
|
+
group: {
|
|
55
|
+
...group,
|
|
56
|
+
customCwd: validation.cwd,
|
|
57
|
+
},
|
|
58
|
+
materialized: false,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
const launchCwd = options.launchCwd ?? LAUNCH_CWD;
|
|
62
|
+
const validation = validateHostWorkspaceCwd(launchCwd, {
|
|
63
|
+
allowlist: options.allowlist,
|
|
64
|
+
fieldLabel: options.fieldLabel ?? 'launch cwd',
|
|
65
|
+
});
|
|
66
|
+
if ('error' in validation) {
|
|
67
|
+
return validation;
|
|
68
|
+
}
|
|
69
|
+
return {
|
|
70
|
+
group: {
|
|
71
|
+
...group,
|
|
72
|
+
customCwd: validation.cwd,
|
|
73
|
+
},
|
|
74
|
+
materialized: true,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
export function resolveEffectiveHostWorkspaceCwd(group, homeGroup) {
|
|
78
|
+
const effectiveExecutionMode = group.is_home
|
|
79
|
+
? group.executionMode
|
|
80
|
+
: (homeGroup?.executionMode ?? group.executionMode);
|
|
81
|
+
if ((effectiveExecutionMode ?? 'container') !== 'host') {
|
|
82
|
+
return undefined;
|
|
83
|
+
}
|
|
84
|
+
return homeGroup?.customCwd || group.customCwd;
|
|
85
|
+
}
|
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unified IM Channel Interface
|
|
3
|
+
*
|
|
4
|
+
* Defines a standard interface for all IM integrations (Feishu, Telegram, etc.)
|
|
5
|
+
* and provides adapter factories that wrap existing connection implementations.
|
|
6
|
+
*/
|
|
7
|
+
import { createFeishuConnection, } from './feishu.js';
|
|
8
|
+
import { createTelegramConnection, } from './telegram.js';
|
|
9
|
+
import { createQQConnection, } from './qq.js';
|
|
10
|
+
import { createWeChatConnection, } from './wechat.js';
|
|
11
|
+
import { createDingTalkConnection, } from './dingtalk.js';
|
|
12
|
+
import { logger } from './logger.js';
|
|
13
|
+
import { StreamingCardController, } from './feishu-streaming-card.js';
|
|
14
|
+
import { CHANNEL_PREFIXES } from './channel-prefixes.js';
|
|
15
|
+
// ─── Channel Registry ───────────────────────────────────────────
|
|
16
|
+
/** Backward-compatible registry derived from the shared CHANNEL_PREFIXES. */
|
|
17
|
+
export const CHANNEL_REGISTRY = Object.fromEntries(Object.entries(CHANNEL_PREFIXES).map(([type, prefix]) => [
|
|
18
|
+
type,
|
|
19
|
+
{ prefix },
|
|
20
|
+
]));
|
|
21
|
+
/**
|
|
22
|
+
* Determine the channel type from a JID string.
|
|
23
|
+
* Returns the matching channelType key or null if no prefix matches.
|
|
24
|
+
*/
|
|
25
|
+
export function getChannelType(jid) {
|
|
26
|
+
for (const [type, prefix] of Object.entries(CHANNEL_PREFIXES)) {
|
|
27
|
+
if (jid.startsWith(prefix))
|
|
28
|
+
return type;
|
|
29
|
+
}
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Strip the channel prefix from a JID, returning the raw chat ID.
|
|
34
|
+
*/
|
|
35
|
+
export function extractChatId(jid) {
|
|
36
|
+
for (const prefix of Object.values(CHANNEL_PREFIXES)) {
|
|
37
|
+
if (jid.startsWith(prefix))
|
|
38
|
+
return jid.slice(prefix.length);
|
|
39
|
+
}
|
|
40
|
+
return jid;
|
|
41
|
+
}
|
|
42
|
+
// ─── Feishu Adapter ─────────────────────────────────────────────
|
|
43
|
+
export function createFeishuChannel(config) {
|
|
44
|
+
let inner = null;
|
|
45
|
+
const channel = {
|
|
46
|
+
channelType: 'feishu',
|
|
47
|
+
async connect(opts) {
|
|
48
|
+
inner = createFeishuConnection(config);
|
|
49
|
+
const connected = await inner.connect({
|
|
50
|
+
onReady: opts.onReady,
|
|
51
|
+
onNewChat: opts.onNewChat,
|
|
52
|
+
ignoreMessagesBefore: opts.ignoreMessagesBefore,
|
|
53
|
+
onCommand: opts.onCommand,
|
|
54
|
+
resolveGroupFolder: opts.resolveGroupFolder,
|
|
55
|
+
resolveEffectiveChatJid: opts.resolveEffectiveChatJid,
|
|
56
|
+
onAgentMessage: opts.onAgentMessage,
|
|
57
|
+
onBotAddedToGroup: opts.onBotAddedToGroup,
|
|
58
|
+
onBotRemovedFromGroup: opts.onBotRemovedFromGroup,
|
|
59
|
+
shouldProcessGroupMessage: opts.shouldProcessGroupMessage,
|
|
60
|
+
onCardInterrupt: opts.onCardInterrupt,
|
|
61
|
+
});
|
|
62
|
+
if (!connected) {
|
|
63
|
+
inner = null;
|
|
64
|
+
}
|
|
65
|
+
return connected;
|
|
66
|
+
},
|
|
67
|
+
async disconnect() {
|
|
68
|
+
if (inner) {
|
|
69
|
+
await inner.stop();
|
|
70
|
+
inner = null;
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
async sendMessage(chatId, text, localImagePaths) {
|
|
74
|
+
if (!inner) {
|
|
75
|
+
logger.warn({ chatId }, 'Feishu channel not connected, skip sending message');
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
await inner.sendMessage(chatId, text, localImagePaths);
|
|
79
|
+
},
|
|
80
|
+
async sendImage(chatId, imageBuffer, mimeType, caption, fileName) {
|
|
81
|
+
if (!inner) {
|
|
82
|
+
logger.warn({ chatId }, 'Feishu channel not connected, skip sending image');
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
await inner.sendImage(chatId, imageBuffer, mimeType, caption, fileName);
|
|
86
|
+
},
|
|
87
|
+
async setTyping(chatId, isTyping) {
|
|
88
|
+
if (!inner)
|
|
89
|
+
return;
|
|
90
|
+
await inner.sendReaction(chatId, isTyping);
|
|
91
|
+
},
|
|
92
|
+
clearAckReaction(chatId) {
|
|
93
|
+
if (!inner)
|
|
94
|
+
return;
|
|
95
|
+
inner.clearAckReaction(chatId);
|
|
96
|
+
},
|
|
97
|
+
isConnected() {
|
|
98
|
+
return inner?.isConnected() ?? false;
|
|
99
|
+
},
|
|
100
|
+
async syncGroups() {
|
|
101
|
+
if (!inner)
|
|
102
|
+
return;
|
|
103
|
+
await inner.syncGroups();
|
|
104
|
+
},
|
|
105
|
+
async sendFile(chatId, filePath, fileName) {
|
|
106
|
+
if (!inner) {
|
|
107
|
+
logger.warn({ chatId }, 'Feishu channel not connected, skip sending file');
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
await inner.sendFile(chatId, filePath, fileName);
|
|
111
|
+
},
|
|
112
|
+
async getChatInfo(chatId) {
|
|
113
|
+
if (!inner)
|
|
114
|
+
return null;
|
|
115
|
+
return inner.getChatInfo(chatId);
|
|
116
|
+
},
|
|
117
|
+
createStreamingSession(chatId, onCardCreated) {
|
|
118
|
+
if (!inner)
|
|
119
|
+
return undefined;
|
|
120
|
+
const larkClient = inner.getLarkClient();
|
|
121
|
+
if (!larkClient)
|
|
122
|
+
return undefined;
|
|
123
|
+
const opts = {
|
|
124
|
+
client: larkClient,
|
|
125
|
+
chatId,
|
|
126
|
+
replyToMsgId: inner.getLastMessageId(chatId),
|
|
127
|
+
onCardCreated,
|
|
128
|
+
};
|
|
129
|
+
return new StreamingCardController(opts);
|
|
130
|
+
},
|
|
131
|
+
};
|
|
132
|
+
return channel;
|
|
133
|
+
}
|
|
134
|
+
// ─── Telegram Adapter ───────────────────────────────────────────
|
|
135
|
+
export function createTelegramChannel(config) {
|
|
136
|
+
let inner = null;
|
|
137
|
+
// Telegram typing indicator expires after ~5s; resend every 4s while active.
|
|
138
|
+
let typingTimer = null;
|
|
139
|
+
function clearTypingTimer() {
|
|
140
|
+
if (typingTimer) {
|
|
141
|
+
clearInterval(typingTimer);
|
|
142
|
+
typingTimer = null;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
const channel = {
|
|
146
|
+
channelType: 'telegram',
|
|
147
|
+
async connect(opts) {
|
|
148
|
+
inner = createTelegramConnection(config);
|
|
149
|
+
try {
|
|
150
|
+
await inner.connect({
|
|
151
|
+
onReady: opts.onReady,
|
|
152
|
+
onNewChat: opts.onNewChat,
|
|
153
|
+
isChatAuthorized: opts.isChatAuthorized ?? (() => true),
|
|
154
|
+
onPairAttempt: opts.onPairAttempt,
|
|
155
|
+
onCommand: opts.onCommand,
|
|
156
|
+
ignoreMessagesBefore: opts.ignoreMessagesBefore,
|
|
157
|
+
resolveGroupFolder: opts.resolveGroupFolder,
|
|
158
|
+
resolveEffectiveChatJid: opts.resolveEffectiveChatJid,
|
|
159
|
+
onAgentMessage: opts.onAgentMessage,
|
|
160
|
+
onBotAddedToGroup: opts.onBotAddedToGroup,
|
|
161
|
+
onBotRemovedFromGroup: opts.onBotRemovedFromGroup,
|
|
162
|
+
});
|
|
163
|
+
return inner.isConnected();
|
|
164
|
+
}
|
|
165
|
+
catch (err) {
|
|
166
|
+
logger.error({ err }, 'Telegram channel connect failed');
|
|
167
|
+
inner = null;
|
|
168
|
+
return false;
|
|
169
|
+
}
|
|
170
|
+
},
|
|
171
|
+
async disconnect() {
|
|
172
|
+
clearTypingTimer();
|
|
173
|
+
if (inner) {
|
|
174
|
+
await inner.disconnect();
|
|
175
|
+
inner = null;
|
|
176
|
+
}
|
|
177
|
+
},
|
|
178
|
+
async sendMessage(chatId, text, localImagePaths) {
|
|
179
|
+
if (!inner) {
|
|
180
|
+
logger.warn({ chatId }, 'Telegram channel not connected, skip sending message');
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
await inner.sendMessage(chatId, text, localImagePaths);
|
|
184
|
+
},
|
|
185
|
+
async sendImage(chatId, imageBuffer, mimeType, caption, fileName) {
|
|
186
|
+
if (!inner) {
|
|
187
|
+
logger.warn({ chatId }, 'Telegram channel not connected, skip sending image');
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
await inner.sendImage(chatId, imageBuffer, mimeType, caption, fileName);
|
|
191
|
+
},
|
|
192
|
+
async sendFile(chatId, filePath, fileName) {
|
|
193
|
+
if (!inner) {
|
|
194
|
+
logger.warn({ chatId }, 'Telegram channel not connected, skip sending file');
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
await inner.sendFile(chatId, filePath, fileName);
|
|
198
|
+
},
|
|
199
|
+
async setTyping(chatId, isTyping) {
|
|
200
|
+
// Always clear existing timer first
|
|
201
|
+
clearTypingTimer();
|
|
202
|
+
if (!isTyping || !inner)
|
|
203
|
+
return;
|
|
204
|
+
const sendAction = async () => {
|
|
205
|
+
if (!inner)
|
|
206
|
+
return;
|
|
207
|
+
await inner.sendChatAction(chatId, 'typing');
|
|
208
|
+
};
|
|
209
|
+
// Send immediately, then repeat every 4s to keep indicator alive
|
|
210
|
+
void sendAction();
|
|
211
|
+
typingTimer = setInterval(() => {
|
|
212
|
+
void sendAction();
|
|
213
|
+
}, 4000);
|
|
214
|
+
},
|
|
215
|
+
isConnected() {
|
|
216
|
+
return inner?.isConnected() ?? false;
|
|
217
|
+
},
|
|
218
|
+
};
|
|
219
|
+
return channel;
|
|
220
|
+
}
|
|
221
|
+
// ─── QQ Adapter ─────────────────────────────────────────────────
|
|
222
|
+
export function createQQChannel(config) {
|
|
223
|
+
let inner = null;
|
|
224
|
+
const channel = {
|
|
225
|
+
channelType: 'qq',
|
|
226
|
+
async connect(opts) {
|
|
227
|
+
inner = createQQConnection(config);
|
|
228
|
+
try {
|
|
229
|
+
await inner.connect({
|
|
230
|
+
onReady: opts.onReady,
|
|
231
|
+
onNewChat: opts.onNewChat,
|
|
232
|
+
isChatAuthorized: opts.isChatAuthorized ?? (() => true),
|
|
233
|
+
onPairAttempt: opts.onPairAttempt,
|
|
234
|
+
onCommand: opts.onCommand,
|
|
235
|
+
ignoreMessagesBefore: opts.ignoreMessagesBefore,
|
|
236
|
+
resolveGroupFolder: opts.resolveGroupFolder,
|
|
237
|
+
resolveEffectiveChatJid: opts.resolveEffectiveChatJid,
|
|
238
|
+
onAgentMessage: opts.onAgentMessage,
|
|
239
|
+
});
|
|
240
|
+
return inner.isConnected();
|
|
241
|
+
}
|
|
242
|
+
catch (err) {
|
|
243
|
+
logger.error({ err }, 'QQ channel connect failed');
|
|
244
|
+
inner = null;
|
|
245
|
+
return false;
|
|
246
|
+
}
|
|
247
|
+
},
|
|
248
|
+
async disconnect() {
|
|
249
|
+
if (inner) {
|
|
250
|
+
await inner.disconnect();
|
|
251
|
+
inner = null;
|
|
252
|
+
}
|
|
253
|
+
},
|
|
254
|
+
async sendMessage(chatId, text) {
|
|
255
|
+
if (!inner) {
|
|
256
|
+
logger.warn({ chatId }, 'QQ channel not connected, skip sending message');
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
await inner.sendMessage(chatId, text);
|
|
260
|
+
},
|
|
261
|
+
async setTyping(_chatId, _isTyping) {
|
|
262
|
+
// QQ Bot API v2 does not support typing indicators
|
|
263
|
+
},
|
|
264
|
+
isConnected() {
|
|
265
|
+
return inner?.isConnected() ?? false;
|
|
266
|
+
},
|
|
267
|
+
};
|
|
268
|
+
return channel;
|
|
269
|
+
}
|
|
270
|
+
// ─── WeChat Adapter ─────────────────────────────────────────────
|
|
271
|
+
export function createWeChatChannel(config) {
|
|
272
|
+
let inner = null;
|
|
273
|
+
const channel = {
|
|
274
|
+
channelType: 'wechat',
|
|
275
|
+
async connect(opts) {
|
|
276
|
+
inner = createWeChatConnection(config);
|
|
277
|
+
try {
|
|
278
|
+
await inner.connect({
|
|
279
|
+
onReady: opts.onReady,
|
|
280
|
+
onNewChat: opts.onNewChat,
|
|
281
|
+
onCommand: opts.onCommand,
|
|
282
|
+
ignoreMessagesBefore: opts.ignoreMessagesBefore,
|
|
283
|
+
resolveGroupFolder: opts.resolveGroupFolder,
|
|
284
|
+
resolveEffectiveChatJid: opts.resolveEffectiveChatJid,
|
|
285
|
+
onAgentMessage: opts.onAgentMessage,
|
|
286
|
+
});
|
|
287
|
+
return inner.isConnected();
|
|
288
|
+
}
|
|
289
|
+
catch (err) {
|
|
290
|
+
logger.error({ err }, 'WeChat channel connect failed');
|
|
291
|
+
inner = null;
|
|
292
|
+
return false;
|
|
293
|
+
}
|
|
294
|
+
},
|
|
295
|
+
async disconnect() {
|
|
296
|
+
if (inner) {
|
|
297
|
+
await inner.disconnect();
|
|
298
|
+
inner = null;
|
|
299
|
+
}
|
|
300
|
+
},
|
|
301
|
+
async sendMessage(chatId, text) {
|
|
302
|
+
if (!inner) {
|
|
303
|
+
logger.warn({ chatId }, 'WeChat channel not connected, skip sending message');
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
await inner.sendMessage(chatId, text);
|
|
307
|
+
},
|
|
308
|
+
async setTyping(chatId, isTyping) {
|
|
309
|
+
if (!inner)
|
|
310
|
+
return;
|
|
311
|
+
await inner.sendTyping(chatId, isTyping);
|
|
312
|
+
},
|
|
313
|
+
isConnected() {
|
|
314
|
+
return inner?.isConnected() ?? false;
|
|
315
|
+
},
|
|
316
|
+
};
|
|
317
|
+
return channel;
|
|
318
|
+
}
|
|
319
|
+
// ─── DingTalk Adapter ────────────────────────────────────────────
|
|
320
|
+
export function createDingTalkChannel(config) {
|
|
321
|
+
let inner = null;
|
|
322
|
+
const channel = {
|
|
323
|
+
channelType: 'dingtalk',
|
|
324
|
+
async connect(opts) {
|
|
325
|
+
inner = createDingTalkConnection(config);
|
|
326
|
+
try {
|
|
327
|
+
await inner.connect({
|
|
328
|
+
onReady: opts.onReady,
|
|
329
|
+
onNewChat: opts.onNewChat,
|
|
330
|
+
isChatAuthorized: opts.isChatAuthorized ?? (() => true),
|
|
331
|
+
onPairAttempt: opts.onPairAttempt,
|
|
332
|
+
onCommand: opts.onCommand,
|
|
333
|
+
ignoreMessagesBefore: opts.ignoreMessagesBefore,
|
|
334
|
+
resolveGroupFolder: opts.resolveGroupFolder,
|
|
335
|
+
resolveEffectiveChatJid: opts.resolveEffectiveChatJid,
|
|
336
|
+
onAgentMessage: opts.onAgentMessage,
|
|
337
|
+
onBotAddedToGroup: opts.onBotAddedToGroup,
|
|
338
|
+
onBotRemovedFromGroup: opts.onBotRemovedFromGroup,
|
|
339
|
+
shouldProcessGroupMessage: opts.shouldProcessGroupMessage,
|
|
340
|
+
});
|
|
341
|
+
return inner.isConnected();
|
|
342
|
+
}
|
|
343
|
+
catch (err) {
|
|
344
|
+
logger.error({ err }, 'DingTalk channel connect failed');
|
|
345
|
+
inner = null;
|
|
346
|
+
return false;
|
|
347
|
+
}
|
|
348
|
+
},
|
|
349
|
+
async disconnect() {
|
|
350
|
+
if (inner) {
|
|
351
|
+
await inner.disconnect();
|
|
352
|
+
inner = null;
|
|
353
|
+
}
|
|
354
|
+
},
|
|
355
|
+
async sendMessage(chatId, text) {
|
|
356
|
+
if (!inner) {
|
|
357
|
+
logger.warn({ chatId }, 'DingTalk channel not connected, skip sending message');
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
await inner.sendMessage(chatId, text);
|
|
361
|
+
},
|
|
362
|
+
async setTyping(_chatId, _isTyping) {
|
|
363
|
+
// DingTalk Stream SDK does not support typing indicators
|
|
364
|
+
},
|
|
365
|
+
async sendImage(chatId, imageBuffer, mimeType, caption, fileName) {
|
|
366
|
+
if (!inner) {
|
|
367
|
+
logger.warn({ chatId }, 'DingTalk channel not connected, skip sending image');
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
await inner.sendImage(chatId, imageBuffer, mimeType, caption, fileName);
|
|
371
|
+
},
|
|
372
|
+
async sendFile(chatId, filePath, fileName) {
|
|
373
|
+
if (!inner) {
|
|
374
|
+
logger.warn({ chatId }, 'DingTalk channel not connected, skip sending file');
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
await inner.sendFile(chatId, filePath, fileName);
|
|
378
|
+
},
|
|
379
|
+
isConnected() {
|
|
380
|
+
return inner?.isConnected() ?? false;
|
|
381
|
+
},
|
|
382
|
+
};
|
|
383
|
+
return channel;
|
|
384
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure utility functions for IM slash commands.
|
|
3
|
+
* Extracted from index.ts to enable unit testing without DB/state dependencies.
|
|
4
|
+
*/
|
|
5
|
+
// ─── Context Formatting ─────────────────────────────────────────
|
|
6
|
+
/**
|
|
7
|
+
* Format recent messages into a compact context summary.
|
|
8
|
+
* Messages should be in chronological order (oldest first).
|
|
9
|
+
*
|
|
10
|
+
* @param messages Array of messages (oldest first)
|
|
11
|
+
* @param maxLen Per-message truncation length
|
|
12
|
+
* @returns Formatted text block, or empty string if no displayable messages
|
|
13
|
+
*/
|
|
14
|
+
export function formatContextMessages(messages, maxLen = 80) {
|
|
15
|
+
if (messages.length === 0)
|
|
16
|
+
return '';
|
|
17
|
+
const lines = [];
|
|
18
|
+
for (const msg of messages) {
|
|
19
|
+
if (msg.sender === '__system__')
|
|
20
|
+
continue;
|
|
21
|
+
const who = msg.is_from_me ? '🤖' : `👤${msg.sender_name || ''}`;
|
|
22
|
+
let text = msg.content || '';
|
|
23
|
+
if (text.length > maxLen)
|
|
24
|
+
text = text.slice(0, maxLen) + '…';
|
|
25
|
+
text = text.replace(/\n/g, ' ');
|
|
26
|
+
lines.push(` ${who}: ${text}`);
|
|
27
|
+
}
|
|
28
|
+
return lines.length > 0 ? '\n\n📋 最近消息:\n' + lines.join('\n') : '';
|
|
29
|
+
}
|
|
30
|
+
// ─── List Formatting ────────────────────────────────────────────
|
|
31
|
+
/**
|
|
32
|
+
* Format workspace list with current-position markers.
|
|
33
|
+
*/
|
|
34
|
+
export function formatWorkspaceList(workspaces, currentFolder, currentAgentId, currentOnMain = true) {
|
|
35
|
+
if (workspaces.length === 0)
|
|
36
|
+
return '没有可用的工作区';
|
|
37
|
+
const lines = ['📂 工作区列表:'];
|
|
38
|
+
for (const ws of workspaces) {
|
|
39
|
+
const isCurrent = ws.folder === currentFolder;
|
|
40
|
+
const marker = isCurrent ? ' ▶' : '';
|
|
41
|
+
lines.push(`${marker} ${ws.name} (${ws.folder})`);
|
|
42
|
+
const mainMarker = isCurrent && currentOnMain ? ' ← 当前' : '';
|
|
43
|
+
lines.push(` · 主对话${mainMarker}`);
|
|
44
|
+
for (const agent of ws.agents) {
|
|
45
|
+
const agentMarker = isCurrent && currentAgentId === agent.id ? ' ← 当前' : '';
|
|
46
|
+
const statusIcon = agent.status === 'running' ? '🔄' : '';
|
|
47
|
+
const shortId = agent.id.slice(0, 4);
|
|
48
|
+
lines.push(` · ${agent.name} [${shortId}] ${statusIcon}${agentMarker}`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
lines.push('');
|
|
52
|
+
lines.push('💡 /sw <消息> 并行任务 · /recall 总结 · /clear 重置');
|
|
53
|
+
return lines.join('\n');
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Resolve location info from a registered group.
|
|
57
|
+
* Pure function — all state access goes through callbacks.
|
|
58
|
+
*/
|
|
59
|
+
export function resolveLocationInfo(group, getRegisteredGroup, getAgent, findGroupNameByFolder) {
|
|
60
|
+
let locationLine;
|
|
61
|
+
let folder;
|
|
62
|
+
if (group.target_agent_id) {
|
|
63
|
+
const agent = getAgent(group.target_agent_id);
|
|
64
|
+
const parent = agent ? getRegisteredGroup(agent.chat_jid) : undefined;
|
|
65
|
+
const workspaceName = parent?.name || parent?.folder || group.folder;
|
|
66
|
+
locationLine = `${workspaceName} / ${agent?.name || group.target_agent_id}`;
|
|
67
|
+
folder = parent?.folder || group.folder;
|
|
68
|
+
}
|
|
69
|
+
else if (group.target_main_jid) {
|
|
70
|
+
const target = getRegisteredGroup(group.target_main_jid);
|
|
71
|
+
locationLine = `${target?.name || group.target_main_jid} / 主对话`;
|
|
72
|
+
folder = target?.folder || group.folder;
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
const folderName = findGroupNameByFolder(group.folder);
|
|
76
|
+
locationLine = `${folderName} / 主对话`;
|
|
77
|
+
folder = group.folder;
|
|
78
|
+
}
|
|
79
|
+
const replyPolicy = group.target_main_jid || group.target_agent_id
|
|
80
|
+
? group.reply_policy || 'source_only'
|
|
81
|
+
: null;
|
|
82
|
+
return { locationLine, folder, replyPolicy };
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Resolve the real chat target for IM slash commands.
|
|
86
|
+
*
|
|
87
|
+
* Non-main workspaces use random web JIDs (`web:<uuid>`), so commands must not
|
|
88
|
+
* reconstruct targets from `folder`. They need the actual bound workspace JID.
|
|
89
|
+
*/
|
|
90
|
+
export function resolveBoundChatTarget(sourceChatJid, group, getRegisteredGroup, getAgent, findGroupNameByFolder) {
|
|
91
|
+
if (group.target_agent_id) {
|
|
92
|
+
const agent = getAgent(group.target_agent_id);
|
|
93
|
+
const parent = agent ? getRegisteredGroup(agent.chat_jid) : undefined;
|
|
94
|
+
const workspaceName = parent?.name || findGroupNameByFolder(parent?.folder || group.folder);
|
|
95
|
+
const baseChatJid = agent?.chat_jid || sourceChatJid;
|
|
96
|
+
return {
|
|
97
|
+
baseChatJid,
|
|
98
|
+
targetChatJid: `${baseChatJid}#agent:${group.target_agent_id}`,
|
|
99
|
+
folder: parent?.folder || group.folder,
|
|
100
|
+
agentId: group.target_agent_id,
|
|
101
|
+
locationLine: `${workspaceName} / ${agent?.name || group.target_agent_id}`,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
if (group.target_main_jid) {
|
|
105
|
+
const target = getRegisteredGroup(group.target_main_jid);
|
|
106
|
+
return {
|
|
107
|
+
baseChatJid: group.target_main_jid,
|
|
108
|
+
targetChatJid: group.target_main_jid,
|
|
109
|
+
folder: target?.folder || group.folder,
|
|
110
|
+
agentId: null,
|
|
111
|
+
locationLine: `${target?.name || group.target_main_jid} / 主对话`,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
const workspaceName = findGroupNameByFolder(group.folder);
|
|
115
|
+
return {
|
|
116
|
+
baseChatJid: sourceChatJid,
|
|
117
|
+
targetChatJid: sourceChatJid,
|
|
118
|
+
folder: group.folder,
|
|
119
|
+
agentId: null,
|
|
120
|
+
locationLine: `${workspaceName} / 主对话`,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Format system status output for /status command.
|
|
125
|
+
*/
|
|
126
|
+
export function formatSystemStatus(location, queueStatus, isActive, queuePosition) {
|
|
127
|
+
const statusText = isActive
|
|
128
|
+
? '运行中'
|
|
129
|
+
: queuePosition !== null
|
|
130
|
+
? `排队中 (#${queuePosition})`
|
|
131
|
+
: '空闲';
|
|
132
|
+
const lines = [
|
|
133
|
+
'📊 系统状态',
|
|
134
|
+
'━━━━━━━━━━',
|
|
135
|
+
`📍 位置: ${location.locationLine}`,
|
|
136
|
+
`⚡ 状态: ${statusText}`,
|
|
137
|
+
`📦 负载: ${queueStatus.activeContainerCount}/${queueStatus.maxContainers} 容器, ${queueStatus.activeHostProcessCount}/${queueStatus.maxHostProcesses} 进程`,
|
|
138
|
+
'',
|
|
139
|
+
'💡 /sw <消息> 并行任务 · /where 绑定 · /list 全部',
|
|
140
|
+
];
|
|
141
|
+
return lines.join('\n');
|
|
142
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { GROUPS_DIR } from './config.js';
|
|
4
|
+
export const MAX_FILE_SIZE = 50 * 1024 * 1024; // 50 MB
|
|
5
|
+
export class FileTooLargeError extends Error {
|
|
6
|
+
constructor(filename, size) {
|
|
7
|
+
super(`File "${filename}" is too large: ${size} bytes (max ${MAX_FILE_SIZE} bytes)`);
|
|
8
|
+
this.name = 'FileTooLargeError';
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* 将 Buffer 写入 downloads/{channel}/{YYYY-MM-DD}/ 目录,
|
|
13
|
+
* 返回工作区相对路径(如 downloads/feishu/2026-03-01/report.pdf)。
|
|
14
|
+
* @throws FileTooLargeError 当 buffer.length > MAX_FILE_SIZE
|
|
15
|
+
*/
|
|
16
|
+
export async function saveDownloadedFile(groupFolder, channel, originalFilename, buffer) {
|
|
17
|
+
if (buffer.length > MAX_FILE_SIZE) {
|
|
18
|
+
throw new FileTooLargeError(originalFilename, buffer.length);
|
|
19
|
+
}
|
|
20
|
+
const dateStr = new Date().toISOString().slice(0, 10); // YYYY-MM-DD
|
|
21
|
+
const dir = path.join(GROUPS_DIR, groupFolder, 'downloads', channel, dateStr);
|
|
22
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
23
|
+
// 安全处理:剥离目录部分,空时 fallback 为时间戳名
|
|
24
|
+
let safeName = path.basename(originalFilename).trim();
|
|
25
|
+
if (!safeName) {
|
|
26
|
+
safeName = `file_${Date.now()}`;
|
|
27
|
+
}
|
|
28
|
+
// 冲突处理:目标文件已存在时,在扩展名之前追加 _HHmmss 后缀
|
|
29
|
+
let absPath = path.join(dir, safeName);
|
|
30
|
+
if (fs.existsSync(absPath)) {
|
|
31
|
+
const now = new Date();
|
|
32
|
+
const hh = String(now.getHours()).padStart(2, '0');
|
|
33
|
+
const mm = String(now.getMinutes()).padStart(2, '0');
|
|
34
|
+
const ss = String(now.getSeconds()).padStart(2, '0');
|
|
35
|
+
const suffix = `_${hh}${mm}${ss}`;
|
|
36
|
+
const ext = path.extname(safeName);
|
|
37
|
+
const base = path.basename(safeName, ext);
|
|
38
|
+
safeName = `${base}${suffix}${ext}`;
|
|
39
|
+
absPath = path.join(dir, safeName);
|
|
40
|
+
}
|
|
41
|
+
fs.writeFileSync(absPath, buffer, { mode: 0o644 });
|
|
42
|
+
// 返回相对于群组工作区根目录的路径
|
|
43
|
+
const groupRoot = path.join(GROUPS_DIR, groupFolder);
|
|
44
|
+
return path.relative(groupRoot, absPath).replace(/\\/g, '/');
|
|
45
|
+
}
|