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,671 @@
|
|
|
1
|
+
import { Hono } from 'hono';
|
|
2
|
+
import { authMiddleware } from '../middleware/auth.js';
|
|
3
|
+
import { isHostExecutionGroup, hasHostExecutionPermission, canAccessGroup, } from '../web-context.js';
|
|
4
|
+
import { getJidsByFolder, getRegisteredGroup } from '../db.js';
|
|
5
|
+
import { resolveEffectiveHostWorkspaceCwd } from '../host-workspace-cwd.js';
|
|
6
|
+
import { logger } from '../logger.js';
|
|
7
|
+
import { listFiles, validateAndResolvePath, deleteFile, createDirectory, isSystemPath, MAX_FILE_SIZE, getGroupStorageUsage, invalidateGroupStorageUsage, } from '../file-manager.js';
|
|
8
|
+
import { checkStorageLimit, isBillingEnabled } from '../billing.js';
|
|
9
|
+
import { execFile } from 'node:child_process';
|
|
10
|
+
import fs from 'node:fs';
|
|
11
|
+
import path from 'node:path';
|
|
12
|
+
import { Readable } from 'node:stream';
|
|
13
|
+
import { promisify } from 'node:util';
|
|
14
|
+
const execFileAsync = promisify(execFile);
|
|
15
|
+
// MIME 类型映射(预览和编辑端点共用)
|
|
16
|
+
const MIME_MAP = {
|
|
17
|
+
// 图片
|
|
18
|
+
png: 'image/png',
|
|
19
|
+
jpg: 'image/jpeg',
|
|
20
|
+
jpeg: 'image/jpeg',
|
|
21
|
+
gif: 'image/gif',
|
|
22
|
+
svg: 'image/svg+xml',
|
|
23
|
+
webp: 'image/webp',
|
|
24
|
+
// 文本和代码
|
|
25
|
+
txt: 'text/plain',
|
|
26
|
+
md: 'text/markdown',
|
|
27
|
+
json: 'application/json',
|
|
28
|
+
js: 'text/javascript',
|
|
29
|
+
ts: 'text/typescript',
|
|
30
|
+
jsx: 'text/javascript',
|
|
31
|
+
tsx: 'text/typescript',
|
|
32
|
+
css: 'text/css',
|
|
33
|
+
html: 'text/html',
|
|
34
|
+
xml: 'application/xml',
|
|
35
|
+
py: 'text/x-python',
|
|
36
|
+
go: 'text/x-go',
|
|
37
|
+
rs: 'text/x-rust',
|
|
38
|
+
java: 'text/x-java',
|
|
39
|
+
c: 'text/x-c',
|
|
40
|
+
cpp: 'text/x-c++',
|
|
41
|
+
h: 'text/x-c',
|
|
42
|
+
sh: 'text/x-sh',
|
|
43
|
+
yaml: 'text/yaml',
|
|
44
|
+
yml: 'text/yaml',
|
|
45
|
+
toml: 'text/x-toml',
|
|
46
|
+
ini: 'text/plain',
|
|
47
|
+
conf: 'text/plain',
|
|
48
|
+
log: 'text/plain',
|
|
49
|
+
csv: 'text/csv',
|
|
50
|
+
// PDF
|
|
51
|
+
pdf: 'application/pdf',
|
|
52
|
+
// 压缩文件
|
|
53
|
+
zip: 'application/zip',
|
|
54
|
+
tar: 'application/x-tar',
|
|
55
|
+
gz: 'application/gzip',
|
|
56
|
+
'7z': 'application/x-7z-compressed',
|
|
57
|
+
};
|
|
58
|
+
// 文本文件扩展名(用于编辑端点判断)
|
|
59
|
+
const TEXT_EXTENSIONS = new Set([
|
|
60
|
+
'txt',
|
|
61
|
+
'md',
|
|
62
|
+
'json',
|
|
63
|
+
'js',
|
|
64
|
+
'ts',
|
|
65
|
+
'jsx',
|
|
66
|
+
'tsx',
|
|
67
|
+
'css',
|
|
68
|
+
'html',
|
|
69
|
+
'xml',
|
|
70
|
+
'py',
|
|
71
|
+
'go',
|
|
72
|
+
'rs',
|
|
73
|
+
'java',
|
|
74
|
+
'c',
|
|
75
|
+
'cpp',
|
|
76
|
+
'h',
|
|
77
|
+
'sh',
|
|
78
|
+
'yaml',
|
|
79
|
+
'yml',
|
|
80
|
+
'toml',
|
|
81
|
+
'ini',
|
|
82
|
+
'conf',
|
|
83
|
+
'log',
|
|
84
|
+
'csv',
|
|
85
|
+
'svg',
|
|
86
|
+
]);
|
|
87
|
+
// 允许 inline 预览的安全 MIME 类型(排除 HTML 和 SVG 以防止 XSS)
|
|
88
|
+
const SAFE_PREVIEW_MIME_TYPES = new Set([
|
|
89
|
+
'image/png',
|
|
90
|
+
'image/jpeg',
|
|
91
|
+
'image/gif',
|
|
92
|
+
'image/webp',
|
|
93
|
+
'text/plain',
|
|
94
|
+
'text/markdown',
|
|
95
|
+
'text/css',
|
|
96
|
+
'text/csv',
|
|
97
|
+
'text/yaml',
|
|
98
|
+
'text/x-python',
|
|
99
|
+
'text/x-go',
|
|
100
|
+
'text/x-rust',
|
|
101
|
+
'text/x-java',
|
|
102
|
+
'text/x-c',
|
|
103
|
+
'text/x-c++',
|
|
104
|
+
'text/x-sh',
|
|
105
|
+
'text/x-toml',
|
|
106
|
+
'text/javascript',
|
|
107
|
+
'text/typescript',
|
|
108
|
+
'application/json',
|
|
109
|
+
'application/xml',
|
|
110
|
+
'application/pdf',
|
|
111
|
+
]);
|
|
112
|
+
/**
|
|
113
|
+
* 获取文件操作的根目录覆盖。
|
|
114
|
+
* 宿主机模式下以有效工作目录为根。
|
|
115
|
+
*/
|
|
116
|
+
export function resolveFileRootOverride(group, homeGroup) {
|
|
117
|
+
if (group.executionMode !== 'host')
|
|
118
|
+
return undefined;
|
|
119
|
+
return resolveEffectiveHostWorkspaceCwd(group, homeGroup ?? undefined);
|
|
120
|
+
}
|
|
121
|
+
function findHomeSiblingGroup(group) {
|
|
122
|
+
if (group.is_home)
|
|
123
|
+
return undefined;
|
|
124
|
+
for (const jid of getJidsByFolder(group.folder)) {
|
|
125
|
+
const sibling = getRegisteredGroup(jid);
|
|
126
|
+
if (sibling?.is_home) {
|
|
127
|
+
return sibling;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return undefined;
|
|
131
|
+
}
|
|
132
|
+
function getRequiredFileRootOverride(group) {
|
|
133
|
+
const rootOverride = resolveFileRootOverride(group, findHomeSiblingGroup(group));
|
|
134
|
+
if (group.executionMode === 'host' && !rootOverride) {
|
|
135
|
+
throw new Error('Host workspace is missing custom_cwd');
|
|
136
|
+
}
|
|
137
|
+
return rootOverride;
|
|
138
|
+
}
|
|
139
|
+
function buildAttachmentContentDisposition(fileName) {
|
|
140
|
+
const sanitized = fileName.replace(/["\\\r\n]/g, '_');
|
|
141
|
+
const asciiFallback = sanitized.replace(/[^\x20-\x7E]/g, '_') || 'download';
|
|
142
|
+
const encoded = encodeURIComponent(fileName);
|
|
143
|
+
return `attachment; filename="${asciiFallback}"; filename*=UTF-8''${encoded}`;
|
|
144
|
+
}
|
|
145
|
+
function parseSingleRange(rangeHeader, fileSize) {
|
|
146
|
+
if (fileSize <= 0)
|
|
147
|
+
return null;
|
|
148
|
+
const match = /^bytes=(\d*)-(\d*)$/.exec(rangeHeader.trim());
|
|
149
|
+
if (!match)
|
|
150
|
+
return null;
|
|
151
|
+
const [, rawStart, rawEnd] = match;
|
|
152
|
+
if (!rawStart && !rawEnd)
|
|
153
|
+
return null;
|
|
154
|
+
// Suffix bytes range (e.g. bytes=-500)
|
|
155
|
+
if (!rawStart) {
|
|
156
|
+
const suffixLength = Number(rawEnd);
|
|
157
|
+
if (!Number.isInteger(suffixLength) || suffixLength <= 0)
|
|
158
|
+
return null;
|
|
159
|
+
if (suffixLength >= fileSize)
|
|
160
|
+
return { start: 0, end: fileSize - 1 };
|
|
161
|
+
return { start: fileSize - suffixLength, end: fileSize - 1 };
|
|
162
|
+
}
|
|
163
|
+
const start = Number(rawStart);
|
|
164
|
+
if (!Number.isInteger(start) || start < 0 || start >= fileSize)
|
|
165
|
+
return null;
|
|
166
|
+
const parsedEnd = rawEnd ? Number(rawEnd) : fileSize - 1;
|
|
167
|
+
if (!Number.isInteger(parsedEnd) || parsedEnd < start)
|
|
168
|
+
return null;
|
|
169
|
+
return { start, end: Math.min(parsedEnd, fileSize - 1) };
|
|
170
|
+
}
|
|
171
|
+
async function openDirectoryInFileManager(targetDir) {
|
|
172
|
+
const attempts = (() => {
|
|
173
|
+
if (process.platform === 'darwin') {
|
|
174
|
+
return [{ cmd: 'open', args: [targetDir] }];
|
|
175
|
+
}
|
|
176
|
+
if (process.platform === 'win32') {
|
|
177
|
+
return [{ cmd: 'explorer', args: [targetDir] }];
|
|
178
|
+
}
|
|
179
|
+
// Linux 桌面环境兼容:优先 xdg-open,失败后回退到常见 opener
|
|
180
|
+
return [
|
|
181
|
+
{ cmd: 'xdg-open', args: [targetDir] },
|
|
182
|
+
{ cmd: 'gio', args: ['open', targetDir] },
|
|
183
|
+
{ cmd: 'kde-open5', args: [targetDir] },
|
|
184
|
+
{ cmd: 'kde-open', args: [targetDir] },
|
|
185
|
+
];
|
|
186
|
+
})();
|
|
187
|
+
const failureCodes = [];
|
|
188
|
+
for (const attempt of attempts) {
|
|
189
|
+
try {
|
|
190
|
+
await execFileAsync(attempt.cmd, attempt.args, { timeout: 10_000 });
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
catch (error) {
|
|
194
|
+
const code = error.code;
|
|
195
|
+
// 命令不存在:继续尝试下一个 opener
|
|
196
|
+
if (code === 'ENOENT') {
|
|
197
|
+
failureCodes.push(`${attempt.cmd}:ENOENT`);
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
failureCodes.push(`${attempt.cmd}:${code || 'ERROR'}`);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
const err = new Error('No compatible desktop opener available');
|
|
204
|
+
err.code = 'NO_FILE_OPENER';
|
|
205
|
+
err.detail = failureCodes;
|
|
206
|
+
throw err;
|
|
207
|
+
}
|
|
208
|
+
const fileRoutes = new Hono();
|
|
209
|
+
// GET /api/groups/:jid/files?path= - 列出文件
|
|
210
|
+
fileRoutes.get('/:jid/files', authMiddleware, (c) => {
|
|
211
|
+
const jid = c.req.param('jid');
|
|
212
|
+
const subPath = c.req.query('path') || '';
|
|
213
|
+
const group = getRegisteredGroup(jid);
|
|
214
|
+
if (!group) {
|
|
215
|
+
return c.json({ error: 'Group not found' }, 404);
|
|
216
|
+
}
|
|
217
|
+
const authUser = c.get('user');
|
|
218
|
+
if (!canAccessGroup({ id: authUser.id, role: authUser.role }, group)) {
|
|
219
|
+
return c.json({ error: 'Group not found' }, 404);
|
|
220
|
+
}
|
|
221
|
+
if (isHostExecutionGroup(group) && !hasHostExecutionPermission(authUser)) {
|
|
222
|
+
return c.json({ error: 'Insufficient permissions for host execution mode' }, 403);
|
|
223
|
+
}
|
|
224
|
+
try {
|
|
225
|
+
const result = listFiles(group.folder, subPath, getRequiredFileRootOverride(group));
|
|
226
|
+
return c.json(result);
|
|
227
|
+
}
|
|
228
|
+
catch (error) {
|
|
229
|
+
logger.error({ err: error }, `Failed to list files for ${jid}`);
|
|
230
|
+
return c.json({ error: 'Failed to list files' }, 500);
|
|
231
|
+
}
|
|
232
|
+
});
|
|
233
|
+
// POST /api/groups/:jid/files - 上传文件
|
|
234
|
+
fileRoutes.post('/:jid/files', authMiddleware, async (c) => {
|
|
235
|
+
const jid = c.req.param('jid');
|
|
236
|
+
const group = getRegisteredGroup(jid);
|
|
237
|
+
if (!group) {
|
|
238
|
+
return c.json({ error: 'Group not found' }, 404);
|
|
239
|
+
}
|
|
240
|
+
const authUser = c.get('user');
|
|
241
|
+
if (!canAccessGroup({ id: authUser.id, role: authUser.role }, group)) {
|
|
242
|
+
return c.json({ error: 'Group not found' }, 404);
|
|
243
|
+
}
|
|
244
|
+
if (isHostExecutionGroup(group) && !hasHostExecutionPermission(authUser)) {
|
|
245
|
+
return c.json({ error: 'Insufficient permissions for host execution mode' }, 403);
|
|
246
|
+
}
|
|
247
|
+
try {
|
|
248
|
+
const rootOverride = getRequiredFileRootOverride(group);
|
|
249
|
+
const body = await c.req.parseBody({ all: true });
|
|
250
|
+
const targetPath = (typeof body.path === 'string' ? body.path : '') || '';
|
|
251
|
+
const files = body.files;
|
|
252
|
+
if (!files) {
|
|
253
|
+
return c.json({ error: 'No files provided' }, 400);
|
|
254
|
+
}
|
|
255
|
+
// 支持单文件和多文件上传
|
|
256
|
+
const fileList = Array.isArray(files) ? files : [files];
|
|
257
|
+
const uploadedFiles = [];
|
|
258
|
+
// Billing: check storage limit before uploading
|
|
259
|
+
if (isBillingEnabled() && group.created_by) {
|
|
260
|
+
const totalUploadSize = fileList.reduce((sum, f) => sum + (f instanceof File ? f.size : 0), 0);
|
|
261
|
+
const currentUsage = getGroupStorageUsage(group.folder, rootOverride);
|
|
262
|
+
const storageCheck = checkStorageLimit(group.created_by, authUser.role, currentUsage, totalUploadSize);
|
|
263
|
+
if (!storageCheck.allowed) {
|
|
264
|
+
return c.json({ error: storageCheck.reason }, 403);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
for (const file of fileList) {
|
|
268
|
+
if (!(file instanceof File))
|
|
269
|
+
continue;
|
|
270
|
+
// 检查文件大小
|
|
271
|
+
if (file.size > MAX_FILE_SIZE) {
|
|
272
|
+
return c.json({ error: `File ${file.name} exceeds maximum size of 50MB` }, 400);
|
|
273
|
+
}
|
|
274
|
+
// 验证文件名,防止路径遍历攻击
|
|
275
|
+
if (file.name.includes('..') || file.name.startsWith('/')) {
|
|
276
|
+
return c.json({ error: `Invalid file name: ${file.name}` }, 400);
|
|
277
|
+
}
|
|
278
|
+
// 禁止写入系统路径
|
|
279
|
+
const relativeFilePath = path.join(targetPath, file.name);
|
|
280
|
+
if (isSystemPath(targetPath) || isSystemPath(relativeFilePath)) {
|
|
281
|
+
return c.json({ error: 'Cannot upload to system path' }, 403);
|
|
282
|
+
}
|
|
283
|
+
// 验证目标路径 + 文件名的完整路径(防止 file.name 含 ../../ 绕过)
|
|
284
|
+
const fullRelativePath = path.join(targetPath, file.name);
|
|
285
|
+
const targetFilePath = validateAndResolvePath(group.folder, fullRelativePath, rootOverride);
|
|
286
|
+
const targetDir = path.dirname(targetFilePath);
|
|
287
|
+
// 确保目标目录存在
|
|
288
|
+
if (!fs.existsSync(targetDir)) {
|
|
289
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
290
|
+
}
|
|
291
|
+
// 写入文件
|
|
292
|
+
const buffer = await file.arrayBuffer();
|
|
293
|
+
fs.writeFileSync(targetFilePath, Buffer.from(buffer));
|
|
294
|
+
uploadedFiles.push(file.name);
|
|
295
|
+
}
|
|
296
|
+
invalidateGroupStorageUsage(group.folder, rootOverride);
|
|
297
|
+
return c.json({ success: true, files: uploadedFiles });
|
|
298
|
+
}
|
|
299
|
+
catch (error) {
|
|
300
|
+
logger.error({ err: error }, `Failed to upload files for ${jid}`);
|
|
301
|
+
return c.json({ error: 'Failed to upload files' }, 500);
|
|
302
|
+
}
|
|
303
|
+
});
|
|
304
|
+
// POST /api/groups/:jid/files/open-directory - 在本地文件管理器中打开目录
|
|
305
|
+
fileRoutes.post('/:jid/files/open-directory', authMiddleware, async (c) => {
|
|
306
|
+
const jid = c.req.param('jid');
|
|
307
|
+
const group = getRegisteredGroup(jid);
|
|
308
|
+
if (!group) {
|
|
309
|
+
return c.json({ error: 'Group not found' }, 404);
|
|
310
|
+
}
|
|
311
|
+
const authUser = c.get('user');
|
|
312
|
+
if (!canAccessGroup({ id: authUser.id, role: authUser.role }, group)) {
|
|
313
|
+
return c.json({ error: 'Group not found' }, 404);
|
|
314
|
+
}
|
|
315
|
+
// 打开本地目录属于宿主机操作,限制为有宿主机权限的用户
|
|
316
|
+
if (!hasHostExecutionPermission(authUser)) {
|
|
317
|
+
return c.json({ error: 'Insufficient permissions to open local directory' }, 403);
|
|
318
|
+
}
|
|
319
|
+
try {
|
|
320
|
+
const body = await c.req.json().catch(() => ({}));
|
|
321
|
+
const targetPath = typeof body.path === 'string' ? body.path : '';
|
|
322
|
+
const absolutePath = validateAndResolvePath(group.folder, targetPath, getRequiredFileRootOverride(group));
|
|
323
|
+
if (!fs.existsSync(absolutePath)) {
|
|
324
|
+
return c.json({ error: 'Directory not found' }, 404);
|
|
325
|
+
}
|
|
326
|
+
const stats = fs.statSync(absolutePath);
|
|
327
|
+
const targetDir = stats.isDirectory()
|
|
328
|
+
? absolutePath
|
|
329
|
+
: path.dirname(absolutePath);
|
|
330
|
+
await openDirectoryInFileManager(targetDir);
|
|
331
|
+
return c.json({ success: true });
|
|
332
|
+
}
|
|
333
|
+
catch (error) {
|
|
334
|
+
logger.error({ err: error }, `Failed to open local directory for ${jid}`);
|
|
335
|
+
const code = error.code;
|
|
336
|
+
if (code === 'NO_FILE_OPENER') {
|
|
337
|
+
return c.json({ error: 'No desktop opener available on server' }, 503);
|
|
338
|
+
}
|
|
339
|
+
const msg = error.message;
|
|
340
|
+
const safeMessages = [
|
|
341
|
+
'Path traversal detected',
|
|
342
|
+
'Symlink traversal detected',
|
|
343
|
+
];
|
|
344
|
+
const publicMsg = safeMessages.includes(msg)
|
|
345
|
+
? msg
|
|
346
|
+
: 'Failed to open local directory';
|
|
347
|
+
const status = safeMessages.includes(msg) ? 400 : 500;
|
|
348
|
+
return c.json({ error: publicMsg }, status);
|
|
349
|
+
}
|
|
350
|
+
});
|
|
351
|
+
// GET /api/groups/:jid/files/download/:path - 下载文件
|
|
352
|
+
fileRoutes.get('/:jid/files/download/:path', authMiddleware, (c) => {
|
|
353
|
+
const jid = c.req.param('jid');
|
|
354
|
+
const encodedPath = c.req.param('path');
|
|
355
|
+
const group = getRegisteredGroup(jid);
|
|
356
|
+
if (!group) {
|
|
357
|
+
return c.json({ error: 'Group not found' }, 404);
|
|
358
|
+
}
|
|
359
|
+
const authUser = c.get('user');
|
|
360
|
+
if (!canAccessGroup({ id: authUser.id, role: authUser.role }, group)) {
|
|
361
|
+
return c.json({ error: 'Group not found' }, 404);
|
|
362
|
+
}
|
|
363
|
+
if (isHostExecutionGroup(group) && !hasHostExecutionPermission(authUser)) {
|
|
364
|
+
return c.json({ error: 'Insufficient permissions for host execution mode' }, 403);
|
|
365
|
+
}
|
|
366
|
+
try {
|
|
367
|
+
// 解码 base64url 路径
|
|
368
|
+
const relativePath = Buffer.from(encodedPath, 'base64url').toString('utf-8');
|
|
369
|
+
const absolutePath = validateAndResolvePath(group.folder, relativePath, getRequiredFileRootOverride(group));
|
|
370
|
+
if (!fs.existsSync(absolutePath)) {
|
|
371
|
+
return c.json({ error: 'File not found' }, 404);
|
|
372
|
+
}
|
|
373
|
+
const stats = fs.statSync(absolutePath);
|
|
374
|
+
if (stats.isDirectory()) {
|
|
375
|
+
return c.json({ error: 'Cannot download directory' }, 400);
|
|
376
|
+
}
|
|
377
|
+
const fileName = path.basename(absolutePath);
|
|
378
|
+
const fileSize = stats.size;
|
|
379
|
+
const commonHeaders = {
|
|
380
|
+
'Content-Disposition': buildAttachmentContentDisposition(fileName),
|
|
381
|
+
'Content-Type': 'application/octet-stream',
|
|
382
|
+
'X-Content-Type-Options': 'nosniff',
|
|
383
|
+
'Content-Security-Policy': "default-src 'none'; sandbox",
|
|
384
|
+
'Accept-Ranges': 'bytes',
|
|
385
|
+
};
|
|
386
|
+
const rangeHeader = c.req.header('range');
|
|
387
|
+
if (rangeHeader) {
|
|
388
|
+
const normalizedRange = rangeHeader.trim();
|
|
389
|
+
const isBytesRange = normalizedRange.toLowerCase().startsWith('bytes=');
|
|
390
|
+
const isMultiRange = isBytesRange && normalizedRange.includes(',');
|
|
391
|
+
// 多区间请求当前未实现 multipart/byteranges,回退为完整下载响应
|
|
392
|
+
if (isBytesRange && !isMultiRange) {
|
|
393
|
+
const parsedRange = parseSingleRange(normalizedRange, fileSize);
|
|
394
|
+
if (!parsedRange) {
|
|
395
|
+
return new Response(null, {
|
|
396
|
+
status: 416,
|
|
397
|
+
headers: {
|
|
398
|
+
...commonHeaders,
|
|
399
|
+
'Content-Range': `bytes */${fileSize}`,
|
|
400
|
+
},
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
const { start, end } = parsedRange;
|
|
404
|
+
const stream = Readable.toWeb(fs.createReadStream(absolutePath, { start, end }));
|
|
405
|
+
return new Response(stream, {
|
|
406
|
+
status: 206,
|
|
407
|
+
headers: {
|
|
408
|
+
...commonHeaders,
|
|
409
|
+
'Content-Length': String(end - start + 1),
|
|
410
|
+
'Content-Range': `bytes ${start}-${end}/${fileSize}`,
|
|
411
|
+
},
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
const stream = Readable.toWeb(fs.createReadStream(absolutePath));
|
|
416
|
+
return new Response(stream, {
|
|
417
|
+
status: 200,
|
|
418
|
+
headers: {
|
|
419
|
+
...commonHeaders,
|
|
420
|
+
'Content-Length': String(fileSize),
|
|
421
|
+
},
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
catch (error) {
|
|
425
|
+
logger.error({ err: error }, `Failed to download file for ${jid}`);
|
|
426
|
+
return c.json({ error: 'Failed to download file' }, 500);
|
|
427
|
+
}
|
|
428
|
+
});
|
|
429
|
+
// GET /api/groups/:jid/files/preview/:path - 预览文件
|
|
430
|
+
fileRoutes.get('/:jid/files/preview/:path', authMiddleware, (c) => {
|
|
431
|
+
const jid = c.req.param('jid');
|
|
432
|
+
const encodedPath = c.req.param('path');
|
|
433
|
+
const group = getRegisteredGroup(jid);
|
|
434
|
+
if (!group) {
|
|
435
|
+
return c.json({ error: 'Group not found' }, 404);
|
|
436
|
+
}
|
|
437
|
+
const authUser = c.get('user');
|
|
438
|
+
if (!canAccessGroup({ id: authUser.id, role: authUser.role }, group)) {
|
|
439
|
+
return c.json({ error: 'Group not found' }, 404);
|
|
440
|
+
}
|
|
441
|
+
if (isHostExecutionGroup(group) && !hasHostExecutionPermission(authUser)) {
|
|
442
|
+
return c.json({ error: 'Insufficient permissions for host execution mode' }, 403);
|
|
443
|
+
}
|
|
444
|
+
try {
|
|
445
|
+
// 解码 base64url 路径
|
|
446
|
+
const relativePath = Buffer.from(encodedPath, 'base64url').toString('utf-8');
|
|
447
|
+
const absolutePath = validateAndResolvePath(group.folder, relativePath, getRequiredFileRootOverride(group));
|
|
448
|
+
if (!fs.existsSync(absolutePath)) {
|
|
449
|
+
return c.json({ error: 'File not found' }, 404);
|
|
450
|
+
}
|
|
451
|
+
const stats = fs.statSync(absolutePath);
|
|
452
|
+
if (stats.isDirectory()) {
|
|
453
|
+
return c.json({ error: 'Cannot preview directory' }, 400);
|
|
454
|
+
}
|
|
455
|
+
// 检测 MIME 类型(基于扩展名)
|
|
456
|
+
const ext = path.extname(absolutePath).slice(1).toLowerCase();
|
|
457
|
+
const mimeType = MIME_MAP[ext] || 'application/octet-stream';
|
|
458
|
+
// 读取文件并返回
|
|
459
|
+
const fileContent = fs.readFileSync(absolutePath);
|
|
460
|
+
const fileName = path.basename(absolutePath);
|
|
461
|
+
// 安全头:始终添加 CSP sandbox 和 nosniff
|
|
462
|
+
c.header('Content-Security-Policy', "default-src 'none'; sandbox");
|
|
463
|
+
c.header('X-Content-Type-Options', 'nosniff');
|
|
464
|
+
if (SAFE_PREVIEW_MIME_TYPES.has(mimeType)) {
|
|
465
|
+
// 安全类型:允许 inline 预览
|
|
466
|
+
c.header('Content-Type', mimeType);
|
|
467
|
+
c.header('Content-Disposition', 'inline');
|
|
468
|
+
}
|
|
469
|
+
else {
|
|
470
|
+
// 不安全类型(HTML、SVG 等):强制下载
|
|
471
|
+
c.header('Content-Type', 'application/octet-stream');
|
|
472
|
+
c.header('Content-Disposition', `attachment; filename="${encodeURIComponent(fileName)}"`);
|
|
473
|
+
}
|
|
474
|
+
return c.body(fileContent);
|
|
475
|
+
}
|
|
476
|
+
catch (error) {
|
|
477
|
+
logger.error({ err: error }, `Failed to preview file for ${jid}`);
|
|
478
|
+
return c.json({ error: 'Failed to preview file' }, 500);
|
|
479
|
+
}
|
|
480
|
+
});
|
|
481
|
+
// GET /api/groups/:jid/files/content/:path - 读取文本文件内容
|
|
482
|
+
fileRoutes.get('/:jid/files/content/:path', authMiddleware, (c) => {
|
|
483
|
+
const jid = c.req.param('jid');
|
|
484
|
+
const encodedPath = c.req.param('path');
|
|
485
|
+
const group = getRegisteredGroup(jid);
|
|
486
|
+
if (!group) {
|
|
487
|
+
return c.json({ error: 'Group not found' }, 404);
|
|
488
|
+
}
|
|
489
|
+
const authUser = c.get('user');
|
|
490
|
+
if (!canAccessGroup({ id: authUser.id, role: authUser.role }, group)) {
|
|
491
|
+
return c.json({ error: 'Group not found' }, 404);
|
|
492
|
+
}
|
|
493
|
+
if (isHostExecutionGroup(group) && !hasHostExecutionPermission(authUser)) {
|
|
494
|
+
return c.json({ error: 'Insufficient permissions for host execution mode' }, 403);
|
|
495
|
+
}
|
|
496
|
+
try {
|
|
497
|
+
const rootOverride = getRequiredFileRootOverride(group);
|
|
498
|
+
const relativePath = Buffer.from(encodedPath, 'base64url').toString('utf-8');
|
|
499
|
+
const absolutePath = validateAndResolvePath(group.folder, relativePath, rootOverride);
|
|
500
|
+
if (!fs.existsSync(absolutePath)) {
|
|
501
|
+
return c.json({ error: 'File not found' }, 404);
|
|
502
|
+
}
|
|
503
|
+
const stats = fs.statSync(absolutePath);
|
|
504
|
+
if (stats.isDirectory()) {
|
|
505
|
+
return c.json({ error: 'Cannot read directory content' }, 400);
|
|
506
|
+
}
|
|
507
|
+
// 仅允许文本文件
|
|
508
|
+
const ext = path.extname(absolutePath).slice(1).toLowerCase();
|
|
509
|
+
if (!TEXT_EXTENSIONS.has(ext)) {
|
|
510
|
+
return c.json({ error: 'File type not supported for content reading' }, 400);
|
|
511
|
+
}
|
|
512
|
+
// 限制文件大小(10MB)
|
|
513
|
+
if (stats.size > 10 * 1024 * 1024) {
|
|
514
|
+
return c.json({ error: 'File too large to read (max 10MB)' }, 400);
|
|
515
|
+
}
|
|
516
|
+
const content = fs.readFileSync(absolutePath, 'utf-8');
|
|
517
|
+
return c.json({ content, size: stats.size });
|
|
518
|
+
}
|
|
519
|
+
catch (error) {
|
|
520
|
+
logger.error({ err: error }, `Failed to read file content for ${jid}`);
|
|
521
|
+
return c.json({ error: 'Failed to read file content' }, 500);
|
|
522
|
+
}
|
|
523
|
+
});
|
|
524
|
+
// PUT /api/groups/:jid/files/content/:path - 保存文本文件内容
|
|
525
|
+
fileRoutes.put('/:jid/files/content/:path', authMiddleware, async (c) => {
|
|
526
|
+
const jid = c.req.param('jid');
|
|
527
|
+
const encodedPath = c.req.param('path');
|
|
528
|
+
const group = getRegisteredGroup(jid);
|
|
529
|
+
if (!group) {
|
|
530
|
+
return c.json({ error: 'Group not found' }, 404);
|
|
531
|
+
}
|
|
532
|
+
const authUser = c.get('user');
|
|
533
|
+
if (!canAccessGroup({ id: authUser.id, role: authUser.role }, group)) {
|
|
534
|
+
return c.json({ error: 'Group not found' }, 404);
|
|
535
|
+
}
|
|
536
|
+
if (isHostExecutionGroup(group) && !hasHostExecutionPermission(authUser)) {
|
|
537
|
+
return c.json({ error: 'Insufficient permissions for host execution mode' }, 403);
|
|
538
|
+
}
|
|
539
|
+
try {
|
|
540
|
+
const rootOverride = getRequiredFileRootOverride(group);
|
|
541
|
+
const relativePath = Buffer.from(encodedPath, 'base64url').toString('utf-8');
|
|
542
|
+
// 禁止写入系统路径
|
|
543
|
+
if (isSystemPath(relativePath)) {
|
|
544
|
+
return c.json({ error: 'Cannot edit system file' }, 403);
|
|
545
|
+
}
|
|
546
|
+
const absolutePath = validateAndResolvePath(group.folder, relativePath, rootOverride);
|
|
547
|
+
if (!fs.existsSync(absolutePath)) {
|
|
548
|
+
return c.json({ error: 'File not found' }, 404);
|
|
549
|
+
}
|
|
550
|
+
const stats = fs.statSync(absolutePath);
|
|
551
|
+
if (stats.isDirectory()) {
|
|
552
|
+
return c.json({ error: 'Cannot edit directory content' }, 400);
|
|
553
|
+
}
|
|
554
|
+
// 仅允许文本文件
|
|
555
|
+
const ext = path.extname(absolutePath).slice(1).toLowerCase();
|
|
556
|
+
if (!TEXT_EXTENSIONS.has(ext)) {
|
|
557
|
+
return c.json({ error: 'File type not supported for editing' }, 400);
|
|
558
|
+
}
|
|
559
|
+
const body = await c.req.json().catch(() => ({}));
|
|
560
|
+
if (typeof body.content !== 'string') {
|
|
561
|
+
return c.json({ error: 'Content field is required' }, 400);
|
|
562
|
+
}
|
|
563
|
+
// 限制内容大小(10MB)
|
|
564
|
+
if (Buffer.byteLength(body.content, 'utf-8') > 10 * 1024 * 1024) {
|
|
565
|
+
return c.json({ error: 'Content too large (max 10MB)' }, 400);
|
|
566
|
+
}
|
|
567
|
+
if (isBillingEnabled() && group.created_by) {
|
|
568
|
+
const nextSize = Buffer.byteLength(body.content, 'utf-8');
|
|
569
|
+
const additionalBytes = Math.max(0, nextSize - stats.size);
|
|
570
|
+
if (additionalBytes > 0) {
|
|
571
|
+
const currentUsage = getGroupStorageUsage(group.folder, rootOverride);
|
|
572
|
+
const storageCheck = checkStorageLimit(group.created_by, authUser.role, currentUsage, additionalBytes);
|
|
573
|
+
if (!storageCheck.allowed) {
|
|
574
|
+
return c.json({ error: storageCheck.reason }, 403);
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
// 原子写入
|
|
579
|
+
const tmp = `${absolutePath}.tmp`;
|
|
580
|
+
fs.writeFileSync(tmp, body.content, 'utf-8');
|
|
581
|
+
fs.renameSync(tmp, absolutePath);
|
|
582
|
+
invalidateGroupStorageUsage(group.folder, rootOverride);
|
|
583
|
+
return c.json({ success: true });
|
|
584
|
+
}
|
|
585
|
+
catch (error) {
|
|
586
|
+
logger.error({ err: error }, `Failed to save file content for ${jid}`);
|
|
587
|
+
return c.json({ error: 'Failed to save file content' }, 500);
|
|
588
|
+
}
|
|
589
|
+
});
|
|
590
|
+
// DELETE /api/groups/:jid/files/:path - 删除文件
|
|
591
|
+
fileRoutes.delete('/:jid/files/:path', authMiddleware, (c) => {
|
|
592
|
+
const jid = c.req.param('jid');
|
|
593
|
+
const encodedPath = c.req.param('path');
|
|
594
|
+
const group = getRegisteredGroup(jid);
|
|
595
|
+
if (!group) {
|
|
596
|
+
return c.json({ error: 'Group not found' }, 404);
|
|
597
|
+
}
|
|
598
|
+
const authUser = c.get('user');
|
|
599
|
+
if (!canAccessGroup({ id: authUser.id, role: authUser.role }, group)) {
|
|
600
|
+
return c.json({ error: 'Group not found' }, 404);
|
|
601
|
+
}
|
|
602
|
+
if (isHostExecutionGroup(group) && !hasHostExecutionPermission(authUser)) {
|
|
603
|
+
return c.json({ error: 'Insufficient permissions for host execution mode' }, 403);
|
|
604
|
+
}
|
|
605
|
+
try {
|
|
606
|
+
const rootOverride = getRequiredFileRootOverride(group);
|
|
607
|
+
// 解码 base64url 路径
|
|
608
|
+
const relativePath = Buffer.from(encodedPath, 'base64url').toString('utf-8');
|
|
609
|
+
deleteFile(group.folder, relativePath, rootOverride);
|
|
610
|
+
invalidateGroupStorageUsage(group.folder, rootOverride);
|
|
611
|
+
return c.json({ success: true });
|
|
612
|
+
}
|
|
613
|
+
catch (error) {
|
|
614
|
+
logger.error({ err: error }, `Failed to delete file for ${jid}`);
|
|
615
|
+
const msg = error.message;
|
|
616
|
+
// Only expose known safe error messages, not internal paths
|
|
617
|
+
const safeMessages = [
|
|
618
|
+
'Cannot delete system path',
|
|
619
|
+
'Cannot delete root directory',
|
|
620
|
+
'File or directory not found',
|
|
621
|
+
'Path traversal detected',
|
|
622
|
+
'Symlink traversal detected',
|
|
623
|
+
];
|
|
624
|
+
const publicMsg = safeMessages.includes(msg)
|
|
625
|
+
? msg
|
|
626
|
+
: 'Failed to delete file';
|
|
627
|
+
return c.json({ error: publicMsg }, 400);
|
|
628
|
+
}
|
|
629
|
+
});
|
|
630
|
+
// POST /api/groups/:jid/directories - 创建目录
|
|
631
|
+
fileRoutes.post('/:jid/directories', authMiddleware, async (c) => {
|
|
632
|
+
const jid = c.req.param('jid');
|
|
633
|
+
const group = getRegisteredGroup(jid);
|
|
634
|
+
if (!group) {
|
|
635
|
+
return c.json({ error: 'Group not found' }, 404);
|
|
636
|
+
}
|
|
637
|
+
const authUser = c.get('user');
|
|
638
|
+
if (!canAccessGroup({ id: authUser.id, role: authUser.role }, group)) {
|
|
639
|
+
return c.json({ error: 'Group not found' }, 404);
|
|
640
|
+
}
|
|
641
|
+
if (isHostExecutionGroup(group) && !hasHostExecutionPermission(authUser)) {
|
|
642
|
+
return c.json({ error: 'Insufficient permissions for host execution mode' }, 403);
|
|
643
|
+
}
|
|
644
|
+
try {
|
|
645
|
+
const body = await c.req.json();
|
|
646
|
+
const { path: parentPath, name } = body;
|
|
647
|
+
if (!name || typeof name !== 'string') {
|
|
648
|
+
return c.json({ error: 'Directory name is required' }, 400);
|
|
649
|
+
}
|
|
650
|
+
createDirectory(group.folder, parentPath || '', name, getRequiredFileRootOverride(group));
|
|
651
|
+
return c.json({ success: true });
|
|
652
|
+
}
|
|
653
|
+
catch (error) {
|
|
654
|
+
logger.error({ err: error }, `Failed to create directory for ${jid}`);
|
|
655
|
+
const msg = error.message;
|
|
656
|
+
const safeMessages = [
|
|
657
|
+
'Cannot create system path',
|
|
658
|
+
'Cannot create root directory',
|
|
659
|
+
'Directory already exists',
|
|
660
|
+
'Path traversal detected',
|
|
661
|
+
'Symlink traversal detected',
|
|
662
|
+
'Directory name is required',
|
|
663
|
+
'Invalid directory name',
|
|
664
|
+
];
|
|
665
|
+
const publicMsg = safeMessages.includes(msg)
|
|
666
|
+
? msg
|
|
667
|
+
: 'Failed to create directory';
|
|
668
|
+
return c.json({ error: publicMsg }, 400);
|
|
669
|
+
}
|
|
670
|
+
});
|
|
671
|
+
export default fileRoutes;
|