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,1367 @@
|
|
|
1
|
+
import { Hono } from 'hono';
|
|
2
|
+
import { authMiddleware } from '../middleware/auth.js';
|
|
3
|
+
import { GroupCreateSchema, GroupPatchSchema, GroupMemberAddSchema, ContainerEnvSchema, } from '../schemas.js';
|
|
4
|
+
import { checkGroupLimit } from '../billing.js';
|
|
5
|
+
import { DATA_DIR, GROUPS_DIR, isDockerAvailable } from '../config.js';
|
|
6
|
+
import { LAUNCH_CWD } from '../app-root.js';
|
|
7
|
+
import { enforceAgentExecutionMode, hasRuntimeBoundaryChange, normalizeAgentType, validateGroupRuntimeUpdate, } from '../group-runtime.js';
|
|
8
|
+
import { materializeHostWorkspaceDefaultCwd, validateHostWorkspaceCwd, } from '../host-workspace-cwd.js';
|
|
9
|
+
import { getModelPresets, normalizeModelPreset, normalizeReasoningEffortPreset, supportsReasoningEffort, } from '../runtime-command-registry.js';
|
|
10
|
+
import { getRuntimeBuildStatus, isRuntimeBuildStale, } from '../runtime-build.js';
|
|
11
|
+
import { isHostExecutionGroup, hasHostExecutionPermission, canAccessGroup, canModifyGroup, canDeleteGroup, canManageGroupMembers, MAX_GROUP_NAME_LEN, getWebDeps, } from '../web-context.js';
|
|
12
|
+
import { getRegisteredGroup, setRegisteredGroup, deleteRegisteredGroup, getAllRegisteredGroups, getAllChats, getJidsByFolder, updateChatName, deleteSession, deleteChatHistory, deleteGroupData, ensureChatExists, storeMessageDirect, getMessagesPage, getMessagesAfter, getMessagesPageMulti, getMessagesAfterMulti, addGroupMember, removeGroupMember, getGroupMembers, getGroupMemberRole, getUserById, getAgent, listUsers, listAgentsByJid, getGroupsByTargetAgent, getGroupsByTargetMainJid, getMessage, deleteMessage, getUserPinnedGroups, pinGroup, unpinGroup, } from '../db.js';
|
|
13
|
+
import { logger } from '../logger.js';
|
|
14
|
+
import { getContainerEnvConfig, saveContainerEnvConfig, toPublicContainerEnvConfig, } from '../runtime-config.js';
|
|
15
|
+
import { loadMountAllowlist, findAllowedRoot, matchesBlockedPattern, } from '../mount-security.js';
|
|
16
|
+
import crypto from 'node:crypto';
|
|
17
|
+
import { execFile } from 'node:child_process';
|
|
18
|
+
import { promisify } from 'node:util';
|
|
19
|
+
import fs from 'node:fs';
|
|
20
|
+
import fsp from 'node:fs/promises';
|
|
21
|
+
import path from 'node:path';
|
|
22
|
+
import net from 'node:net';
|
|
23
|
+
import { broadcastNewMessage, invalidateAllowedUserCache } from '../web.js';
|
|
24
|
+
import { getStreamingSession } from '../feishu-streaming-card.js';
|
|
25
|
+
const execFileAsync = promisify(execFile);
|
|
26
|
+
function normalizeOptionalRuntimeModel(agentType, rawValue) {
|
|
27
|
+
if (rawValue == null)
|
|
28
|
+
return null;
|
|
29
|
+
const trimmed = rawValue.trim();
|
|
30
|
+
if (!trimmed)
|
|
31
|
+
return null;
|
|
32
|
+
return normalizeModelPreset(agentType, trimmed);
|
|
33
|
+
}
|
|
34
|
+
function normalizeOptionalReasoningEffort(agentType, rawValue) {
|
|
35
|
+
if (!supportsReasoningEffort(agentType) || rawValue == null)
|
|
36
|
+
return null;
|
|
37
|
+
const trimmed = rawValue.trim();
|
|
38
|
+
if (!trimmed)
|
|
39
|
+
return null;
|
|
40
|
+
return normalizeReasoningEffortPreset(trimmed);
|
|
41
|
+
}
|
|
42
|
+
function readHistoryCursorQuery(c, prefix) {
|
|
43
|
+
const timestamp = c.req.query(prefix);
|
|
44
|
+
if (!timestamp)
|
|
45
|
+
return undefined;
|
|
46
|
+
const id = c.req.query(`${prefix}_id`);
|
|
47
|
+
const chatJid = c.req.query(`${prefix}_chat_jid`);
|
|
48
|
+
if (!id)
|
|
49
|
+
return timestamp;
|
|
50
|
+
return {
|
|
51
|
+
timestamp,
|
|
52
|
+
id,
|
|
53
|
+
...(chatJid ? { chat_jid: chatJid } : {}),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* 检查 hostname 是否为内网地址(SSRF 防护)。
|
|
58
|
+
* 拒绝 127.x, 10.x, 172.16-31.x, 192.168.x, 169.254.x, ::1, fd00::, fe80:: 等。
|
|
59
|
+
*/
|
|
60
|
+
function isPrivateHostname(hostname) {
|
|
61
|
+
// localhost 变体
|
|
62
|
+
if (hostname === 'localhost' || hostname.endsWith('.localhost'))
|
|
63
|
+
return true;
|
|
64
|
+
// IPv6: 移除方括号
|
|
65
|
+
const cleaned = hostname.replace(/^\[|\]$/g, '');
|
|
66
|
+
if (net.isIPv6(cleaned)) {
|
|
67
|
+
const lower = cleaned.toLowerCase();
|
|
68
|
+
if (lower === '::1' || lower === '::')
|
|
69
|
+
return true;
|
|
70
|
+
// fd00::/8 (unique local) 和 fe80::/10 (link-local)
|
|
71
|
+
if (lower.startsWith('fd') || lower.startsWith('fe80'))
|
|
72
|
+
return true;
|
|
73
|
+
// ::ffff:127.0.0.1 等 IPv4-mapped IPv6
|
|
74
|
+
if (lower.startsWith('::ffff:')) {
|
|
75
|
+
const ipv4Part = lower.slice(7);
|
|
76
|
+
return isPrivateIPv4(ipv4Part);
|
|
77
|
+
}
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
if (net.isIPv4(cleaned)) {
|
|
81
|
+
return isPrivateIPv4(cleaned);
|
|
82
|
+
}
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
function isPrivateIPv4(ip) {
|
|
86
|
+
const parts = ip.split('.').map(Number);
|
|
87
|
+
if (parts.length !== 4 || parts.some((p) => isNaN(p)))
|
|
88
|
+
return false;
|
|
89
|
+
const [a, b] = parts;
|
|
90
|
+
// 127.0.0.0/8
|
|
91
|
+
if (a === 127)
|
|
92
|
+
return true;
|
|
93
|
+
// 10.0.0.0/8
|
|
94
|
+
if (a === 10)
|
|
95
|
+
return true;
|
|
96
|
+
// 172.16.0.0/12
|
|
97
|
+
if (a === 172 && b >= 16 && b <= 31)
|
|
98
|
+
return true;
|
|
99
|
+
// 192.168.0.0/16
|
|
100
|
+
if (a === 192 && b === 168)
|
|
101
|
+
return true;
|
|
102
|
+
// 169.254.0.0/16 (link-local)
|
|
103
|
+
if (a === 169 && b === 254)
|
|
104
|
+
return true;
|
|
105
|
+
// 0.0.0.0
|
|
106
|
+
if (a === 0)
|
|
107
|
+
return true;
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
const groupRoutes = new Hono();
|
|
111
|
+
// --- Helper functions ---
|
|
112
|
+
function normalizeGroupName(name) {
|
|
113
|
+
if (typeof name !== 'string')
|
|
114
|
+
return '';
|
|
115
|
+
return name.trim().slice(0, MAX_GROUP_NAME_LEN);
|
|
116
|
+
}
|
|
117
|
+
function buildGroupsPayload(user) {
|
|
118
|
+
const groups = getAllRegisteredGroups();
|
|
119
|
+
const chats = new Map(getAllChats().map((chat) => [chat.jid, chat]));
|
|
120
|
+
const isAdmin = hasHostExecutionPermission(user);
|
|
121
|
+
const isSharedAdminHomeGroup = (jid, group) => isAdmin && !!group.is_home && jid === 'web:main' && group.folder === 'main';
|
|
122
|
+
const homeFolders = new Set(Object.entries(groups)
|
|
123
|
+
.filter(([jid, group]) => jid.startsWith('web:') && !!group.is_home)
|
|
124
|
+
.map(([_, group]) => group.folder));
|
|
125
|
+
const result = {};
|
|
126
|
+
// 先过滤出要显示的群组 jid
|
|
127
|
+
const visibleEntries = [];
|
|
128
|
+
for (const [jid, group] of Object.entries(groups)) {
|
|
129
|
+
const isHome = !!group.is_home;
|
|
130
|
+
const isWeb = jid.startsWith('web:');
|
|
131
|
+
const isHost = isHostExecutionGroup(group);
|
|
132
|
+
const isSharedAdminHome = isSharedAdminHomeGroup(jid, group);
|
|
133
|
+
// Hide IM channels that belong to a home folder.
|
|
134
|
+
// These are merged into the home conversation in UI and message APIs.
|
|
135
|
+
if (!isWeb && !isHome && homeFolders.has(group.folder))
|
|
136
|
+
continue;
|
|
137
|
+
// Hide other users' home groups from the chat sidebar.
|
|
138
|
+
// Each user only sees their own home container.
|
|
139
|
+
if (isHome && group.created_by !== user.id && !isSharedAdminHome)
|
|
140
|
+
continue;
|
|
141
|
+
// Host execution groups require admin unless it's the user's own home group
|
|
142
|
+
if (isHost &&
|
|
143
|
+
!isAdmin &&
|
|
144
|
+
!(isHome && (group.created_by === user.id || isSharedAdminHome)))
|
|
145
|
+
continue;
|
|
146
|
+
// User isolation: all users only see their own groups + shared groups
|
|
147
|
+
if (!canAccessGroup({ id: user.id, role: user.role }, { ...group, jid }))
|
|
148
|
+
continue;
|
|
149
|
+
visibleEntries.push([jid, group]);
|
|
150
|
+
}
|
|
151
|
+
// 批量获取每个 jid 的最新消息(替代 N+1 逐个查询)
|
|
152
|
+
const visibleJids = visibleEntries.map(([jid]) => jid);
|
|
153
|
+
const latestByJid = new Map();
|
|
154
|
+
if (visibleJids.length > 0) {
|
|
155
|
+
// 用 multi 查询获取足够多的消息来覆盖所有 jid
|
|
156
|
+
const allLatest = getMessagesPageMulti(visibleJids, undefined, visibleJids.length * 3);
|
|
157
|
+
for (const msg of allLatest) {
|
|
158
|
+
if (!latestByJid.has(msg.chat_jid)) {
|
|
159
|
+
latestByJid.set(msg.chat_jid, {
|
|
160
|
+
content: msg.content,
|
|
161
|
+
timestamp: msg.timestamp,
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
// Fetch user's pinned groups
|
|
167
|
+
const pins = getUserPinnedGroups(user.id);
|
|
168
|
+
// Cache member info per folder (avoid repeated queries)
|
|
169
|
+
const memberCache = new Map();
|
|
170
|
+
function getMemberInfo(folder) {
|
|
171
|
+
let cached = memberCache.get(folder);
|
|
172
|
+
if (!cached) {
|
|
173
|
+
const members = getGroupMembers(folder);
|
|
174
|
+
const role = members.find((m) => m.user_id === user.id)?.role ?? null;
|
|
175
|
+
cached = { count: members.length, role };
|
|
176
|
+
memberCache.set(folder, cached);
|
|
177
|
+
}
|
|
178
|
+
return cached;
|
|
179
|
+
}
|
|
180
|
+
for (const [jid, group] of visibleEntries) {
|
|
181
|
+
const isHome = !!group.is_home;
|
|
182
|
+
const isWeb = jid.startsWith('web:');
|
|
183
|
+
const isSharedAdminHome = isSharedAdminHomeGroup(jid, group);
|
|
184
|
+
const latest = latestByJid.get(jid);
|
|
185
|
+
const memberInfo = !isHome ? getMemberInfo(group.folder) : null;
|
|
186
|
+
const isShared = memberInfo ? memberInfo.count > 1 : false;
|
|
187
|
+
result[jid] = {
|
|
188
|
+
name: group.name,
|
|
189
|
+
folder: group.folder,
|
|
190
|
+
added_at: group.added_at,
|
|
191
|
+
agent_type: group.agentType || 'claude',
|
|
192
|
+
model: group.model ?? null,
|
|
193
|
+
reasoning_effort: group.reasoningEffort ?? null,
|
|
194
|
+
kind: isHome ? 'home' : isWeb ? 'web' : 'feishu',
|
|
195
|
+
editable: isWeb,
|
|
196
|
+
deletable: isWeb && !isHome,
|
|
197
|
+
lastMessage: latest?.content,
|
|
198
|
+
lastMessageTime: latest?.timestamp ||
|
|
199
|
+
chats.get(jid)?.last_message_time ||
|
|
200
|
+
group.added_at,
|
|
201
|
+
execution_mode: group.executionMode || 'container',
|
|
202
|
+
custom_cwd: isAdmin ? group.customCwd : undefined,
|
|
203
|
+
is_home: isHome || undefined,
|
|
204
|
+
is_my_home: (isHome && (group.created_by === user.id || isSharedAdminHome)) ||
|
|
205
|
+
undefined,
|
|
206
|
+
is_shared: isShared || undefined,
|
|
207
|
+
member_role: memberInfo?.role ?? undefined,
|
|
208
|
+
member_count: isShared ? memberInfo?.count : undefined,
|
|
209
|
+
pinned_at: pins[jid] || undefined,
|
|
210
|
+
activation_mode: group.activation_mode ?? 'auto',
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
return result;
|
|
214
|
+
}
|
|
215
|
+
import { removeFlowArtifacts } from '../file-manager.js';
|
|
216
|
+
import { clearSessionJsonlFiles, resetWorkspaceRuntimeState, } from '../workspace-runtime-reset.js';
|
|
217
|
+
export { removeFlowArtifacts };
|
|
218
|
+
function resetWorkspaceForGroup(folder) {
|
|
219
|
+
// 1. 清除工作目录(Agent 文件、AGENTS.md、logs/ 等),然后重建空目录
|
|
220
|
+
const groupDir = path.join(GROUPS_DIR, folder);
|
|
221
|
+
fs.rmSync(groupDir, { recursive: true, force: true });
|
|
222
|
+
fs.mkdirSync(groupDir, { recursive: true });
|
|
223
|
+
// 2. 清除整个 Claude 会话目录(下次启动时 container-runner 会重建)
|
|
224
|
+
fs.rmSync(path.join(DATA_DIR, 'sessions', folder), {
|
|
225
|
+
recursive: true,
|
|
226
|
+
force: true,
|
|
227
|
+
});
|
|
228
|
+
// 3. 清除 IPC 残留并重建目录结构
|
|
229
|
+
const ipcDir = path.join(DATA_DIR, 'ipc', folder);
|
|
230
|
+
fs.rmSync(ipcDir, { recursive: true, force: true });
|
|
231
|
+
fs.mkdirSync(path.join(ipcDir, 'input'), { recursive: true });
|
|
232
|
+
fs.mkdirSync(path.join(ipcDir, 'messages'), { recursive: true });
|
|
233
|
+
fs.mkdirSync(path.join(ipcDir, 'tasks'), { recursive: true });
|
|
234
|
+
// 4. 清除日期记忆目录(~/.cli-claw/memory/{folder}/)
|
|
235
|
+
fs.rmSync(path.join(DATA_DIR, 'memory', folder), {
|
|
236
|
+
recursive: true,
|
|
237
|
+
force: true,
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
function toPublicContainerEnvForUser(config, user) {
|
|
241
|
+
const base = toPublicContainerEnvConfig(config);
|
|
242
|
+
if (user.role === 'admin' ||
|
|
243
|
+
(user.permissions && user.permissions.includes('manage_group_env'))) {
|
|
244
|
+
return base;
|
|
245
|
+
}
|
|
246
|
+
return {
|
|
247
|
+
...base,
|
|
248
|
+
customEnv: {},
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
// --- Routes ---
|
|
252
|
+
// GET /api/groups - 获取群组列表
|
|
253
|
+
groupRoutes.get('/', authMiddleware, (c) => {
|
|
254
|
+
const user = c.get('user');
|
|
255
|
+
const groups = buildGroupsPayload(user);
|
|
256
|
+
return c.json({ groups });
|
|
257
|
+
});
|
|
258
|
+
// POST /api/groups - 创建新群组
|
|
259
|
+
groupRoutes.post('/', authMiddleware, async (c) => {
|
|
260
|
+
const deps = getWebDeps();
|
|
261
|
+
if (!deps)
|
|
262
|
+
return c.json({ error: 'Server not initialized' }, 500);
|
|
263
|
+
const body = await c.req.json().catch(() => ({}));
|
|
264
|
+
const validation = GroupCreateSchema.safeParse(body);
|
|
265
|
+
if (!validation.success) {
|
|
266
|
+
return c.json({ error: 'Invalid request body' }, 400);
|
|
267
|
+
}
|
|
268
|
+
const name = normalizeGroupName(validation.data.name);
|
|
269
|
+
if (!name) {
|
|
270
|
+
return c.json({ error: 'Group name is required' }, 400);
|
|
271
|
+
}
|
|
272
|
+
const agentType = normalizeAgentType(validation.data.agent_type);
|
|
273
|
+
const executionMode = validation.data.execution_mode ||
|
|
274
|
+
(agentType === 'codex'
|
|
275
|
+
? 'host'
|
|
276
|
+
: (await isDockerAvailable())
|
|
277
|
+
? 'container'
|
|
278
|
+
: 'host');
|
|
279
|
+
const model = normalizeOptionalRuntimeModel(agentType, validation.data.model);
|
|
280
|
+
if (validation.data.model && !model) {
|
|
281
|
+
return c.json({
|
|
282
|
+
error: `Unsupported ${agentType} model preset`,
|
|
283
|
+
presets: getModelPresets(agentType),
|
|
284
|
+
}, 400);
|
|
285
|
+
}
|
|
286
|
+
const reasoningEffort = normalizeOptionalReasoningEffort(agentType, validation.data.reasoning_effort);
|
|
287
|
+
if (validation.data.reasoning_effort && !reasoningEffort) {
|
|
288
|
+
return c.json({
|
|
289
|
+
error: supportsReasoningEffort(agentType)
|
|
290
|
+
? 'Unsupported reasoning_effort preset'
|
|
291
|
+
: `${agentType} does not support reasoning_effort`,
|
|
292
|
+
}, 400);
|
|
293
|
+
}
|
|
294
|
+
const customCwd = validation.data.custom_cwd; // Schema already trims and converts empty to undefined
|
|
295
|
+
const initSourcePath = validation.data.init_source_path;
|
|
296
|
+
const initGitUrl = validation.data.init_git_url;
|
|
297
|
+
const authUser = c.get('user');
|
|
298
|
+
let normalizedCustomCwd;
|
|
299
|
+
const runtimeError = enforceAgentExecutionMode(agentType, executionMode);
|
|
300
|
+
if (runtimeError) {
|
|
301
|
+
return c.json({ error: runtimeError }, 400);
|
|
302
|
+
}
|
|
303
|
+
// Billing: check group limit
|
|
304
|
+
const groupLimit = checkGroupLimit(authUser.id, authUser.role);
|
|
305
|
+
if (!groupLimit.allowed) {
|
|
306
|
+
return c.json({ error: groupLimit.reason }, 403);
|
|
307
|
+
}
|
|
308
|
+
// 互斥校验:init_source_path 和 init_git_url 不能同时指定
|
|
309
|
+
if (initSourcePath && initGitUrl) {
|
|
310
|
+
return c.json({ error: 'init_source_path and init_git_url are mutually exclusive' }, 400);
|
|
311
|
+
}
|
|
312
|
+
// init_source_path / init_git_url 仅 container 模式可用
|
|
313
|
+
if (executionMode === 'host' && (initSourcePath || initGitUrl)) {
|
|
314
|
+
return c.json({
|
|
315
|
+
error: 'init_source_path and init_git_url are only valid for container mode',
|
|
316
|
+
}, 400);
|
|
317
|
+
}
|
|
318
|
+
if (executionMode === 'host') {
|
|
319
|
+
if (!hasHostExecutionPermission(authUser)) {
|
|
320
|
+
return c.json({ error: 'Insufficient permissions for host execution mode' }, 403);
|
|
321
|
+
}
|
|
322
|
+
if (customCwd) {
|
|
323
|
+
const validation = validateHostWorkspaceCwd(customCwd, {
|
|
324
|
+
fieldLabel: 'custom_cwd',
|
|
325
|
+
});
|
|
326
|
+
if ('error' in validation) {
|
|
327
|
+
return c.json({ error: validation.error }, validation.error.includes('under an allowed root') ? 403 : 400);
|
|
328
|
+
}
|
|
329
|
+
normalizedCustomCwd = validation.cwd;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
else if (customCwd) {
|
|
333
|
+
return c.json({ error: 'custom_cwd is only valid for host mode' }, 400);
|
|
334
|
+
}
|
|
335
|
+
// 验证 init_source_path
|
|
336
|
+
if (initSourcePath) {
|
|
337
|
+
if (!hasHostExecutionPermission(authUser)) {
|
|
338
|
+
return c.json({ error: 'Insufficient permissions: init_source_path requires admin' }, 403);
|
|
339
|
+
}
|
|
340
|
+
if (!path.isAbsolute(initSourcePath)) {
|
|
341
|
+
return c.json({ error: 'init_source_path must be an absolute path' }, 400);
|
|
342
|
+
}
|
|
343
|
+
let realPath;
|
|
344
|
+
try {
|
|
345
|
+
const stat = fs.statSync(initSourcePath);
|
|
346
|
+
if (!stat.isDirectory()) {
|
|
347
|
+
return c.json({ error: 'init_source_path must be an existing directory' }, 400);
|
|
348
|
+
}
|
|
349
|
+
realPath = fs.realpathSync(initSourcePath);
|
|
350
|
+
}
|
|
351
|
+
catch {
|
|
352
|
+
return c.json({ error: 'init_source_path directory does not exist' }, 400);
|
|
353
|
+
}
|
|
354
|
+
// 白名单校验
|
|
355
|
+
const allowlist = loadMountAllowlist();
|
|
356
|
+
if (allowlist &&
|
|
357
|
+
allowlist.allowedRoots &&
|
|
358
|
+
allowlist.allowedRoots.length > 0) {
|
|
359
|
+
const allowedRoot = findAllowedRoot(realPath, allowlist.allowedRoots);
|
|
360
|
+
if (!allowedRoot) {
|
|
361
|
+
const allowedPaths = allowlist.allowedRoots
|
|
362
|
+
.map((r) => r.path)
|
|
363
|
+
.join(', ');
|
|
364
|
+
return c.json({
|
|
365
|
+
error: `init_source_path must be under an allowed root. Allowed roots: ${allowedPaths}. Check config/mount-allowlist.json`,
|
|
366
|
+
}, 403);
|
|
367
|
+
}
|
|
368
|
+
// 敏感路径过滤
|
|
369
|
+
const blockedMatch = matchesBlockedPattern(realPath, allowlist.blockedPatterns);
|
|
370
|
+
if (blockedMatch) {
|
|
371
|
+
return c.json({
|
|
372
|
+
error: `init_source_path matches blocked pattern "${blockedMatch}"`,
|
|
373
|
+
}, 403);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
// 验证 init_git_url(SSRF 防护 + admin 权限)
|
|
378
|
+
if (initGitUrl) {
|
|
379
|
+
if (!hasHostExecutionPermission(authUser)) {
|
|
380
|
+
return c.json({ error: 'Insufficient permissions: init_git_url requires admin' }, 403);
|
|
381
|
+
}
|
|
382
|
+
if (initGitUrl.length > 2000) {
|
|
383
|
+
return c.json({ error: 'init_git_url is too long (max 2000 characters)' }, 400);
|
|
384
|
+
}
|
|
385
|
+
let gitUrl;
|
|
386
|
+
try {
|
|
387
|
+
gitUrl = new URL(initGitUrl);
|
|
388
|
+
}
|
|
389
|
+
catch {
|
|
390
|
+
return c.json({ error: 'init_git_url is not a valid URL' }, 400);
|
|
391
|
+
}
|
|
392
|
+
// 仅允许 https 协议(HTTP 明文传输存在中间人攻击风险)
|
|
393
|
+
if (gitUrl.protocol !== 'https:') {
|
|
394
|
+
return c.json({ error: 'init_git_url must use https protocol' }, 400);
|
|
395
|
+
}
|
|
396
|
+
// 阻止内网地址
|
|
397
|
+
if (isPrivateHostname(gitUrl.hostname)) {
|
|
398
|
+
return c.json({ error: 'init_git_url must not point to a private/internal address' }, 400);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
const jid = `web:${crypto.randomUUID()}`;
|
|
402
|
+
const folder = `flow-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 6)}`;
|
|
403
|
+
const now = new Date().toISOString();
|
|
404
|
+
const group = {
|
|
405
|
+
name,
|
|
406
|
+
folder,
|
|
407
|
+
added_at: now,
|
|
408
|
+
agentType,
|
|
409
|
+
executionMode: executionMode,
|
|
410
|
+
model,
|
|
411
|
+
reasoningEffort,
|
|
412
|
+
customCwd: executionMode === 'host' ? normalizedCustomCwd : undefined,
|
|
413
|
+
initSourcePath: executionMode !== 'host' ? initSourcePath : undefined,
|
|
414
|
+
initGitUrl: executionMode !== 'host' ? initGitUrl : undefined,
|
|
415
|
+
created_by: authUser.id,
|
|
416
|
+
};
|
|
417
|
+
const materializedGroup = materializeHostWorkspaceDefaultCwd(group, {
|
|
418
|
+
launchCwd: LAUNCH_CWD,
|
|
419
|
+
fieldLabel: 'CLI launch cwd',
|
|
420
|
+
});
|
|
421
|
+
if ('error' in materializedGroup) {
|
|
422
|
+
return c.json({ error: materializedGroup.error }, 500);
|
|
423
|
+
}
|
|
424
|
+
setRegisteredGroup(jid, materializedGroup.group);
|
|
425
|
+
updateChatName(jid, name);
|
|
426
|
+
deps.getRegisteredGroups()[jid] = materializedGroup.group;
|
|
427
|
+
// Register creator as owner in group_members
|
|
428
|
+
addGroupMember(folder, authUser.id, 'owner', authUser.id);
|
|
429
|
+
// 工作区初始化
|
|
430
|
+
const groupDir = path.join(GROUPS_DIR, folder);
|
|
431
|
+
try {
|
|
432
|
+
if (initSourcePath) {
|
|
433
|
+
await fsp.mkdir(groupDir, { recursive: true });
|
|
434
|
+
await fsp.cp(initSourcePath, groupDir, { recursive: true });
|
|
435
|
+
logger.info({ folder, source: initSourcePath }, 'Workspace initialized from local directory');
|
|
436
|
+
}
|
|
437
|
+
if (initGitUrl) {
|
|
438
|
+
await execFileAsync('git', ['clone', '--depth', '1', initGitUrl, groupDir], {
|
|
439
|
+
timeout: 120_000,
|
|
440
|
+
});
|
|
441
|
+
logger.info({ folder, url: initGitUrl }, 'Workspace initialized from git clone');
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
catch (err) {
|
|
445
|
+
// 初始化失败时清理
|
|
446
|
+
logger.error({ folder, err }, 'Workspace initialization failed, cleaning up');
|
|
447
|
+
fs.rmSync(groupDir, { recursive: true, force: true });
|
|
448
|
+
deleteRegisteredGroup(jid);
|
|
449
|
+
deleteChatHistory(jid);
|
|
450
|
+
delete deps.getRegisteredGroups()[jid];
|
|
451
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
452
|
+
return c.json({ error: `Workspace initialization failed: ${errMsg}` }, 500);
|
|
453
|
+
}
|
|
454
|
+
// 容器模式工作区创建后立即启动容器预热,避免用户打开终端时还需等待
|
|
455
|
+
if (executionMode === 'container') {
|
|
456
|
+
deps.ensureTerminalContainerStarted(jid);
|
|
457
|
+
}
|
|
458
|
+
return c.json({
|
|
459
|
+
success: true,
|
|
460
|
+
jid,
|
|
461
|
+
group: {
|
|
462
|
+
name: group.name,
|
|
463
|
+
folder: group.folder,
|
|
464
|
+
added_at: group.added_at,
|
|
465
|
+
agent_type: group.agentType || 'claude',
|
|
466
|
+
execution_mode: group.executionMode || 'container',
|
|
467
|
+
model: group.model ?? null,
|
|
468
|
+
reasoning_effort: group.reasoningEffort ?? null,
|
|
469
|
+
custom_cwd: hasHostExecutionPermission(authUser)
|
|
470
|
+
? materializedGroup.group.customCwd
|
|
471
|
+
: undefined,
|
|
472
|
+
kind: 'web',
|
|
473
|
+
editable: true,
|
|
474
|
+
deletable: true,
|
|
475
|
+
lastMessage: undefined,
|
|
476
|
+
lastMessageTime: now,
|
|
477
|
+
member_role: 'owner',
|
|
478
|
+
member_count: 1,
|
|
479
|
+
is_shared: false,
|
|
480
|
+
},
|
|
481
|
+
});
|
|
482
|
+
});
|
|
483
|
+
// PATCH /api/groups/:jid - 重命名群组
|
|
484
|
+
groupRoutes.patch('/:jid', authMiddleware, async (c) => {
|
|
485
|
+
const deps = getWebDeps();
|
|
486
|
+
if (!deps)
|
|
487
|
+
return c.json({ error: 'Server not initialized' }, 500);
|
|
488
|
+
const jid = c.req.param('jid');
|
|
489
|
+
const existing = getRegisteredGroup(jid);
|
|
490
|
+
if (!existing)
|
|
491
|
+
return c.json({ error: 'Group not found' }, 404);
|
|
492
|
+
const authUser = c.get('user');
|
|
493
|
+
const body = await c.req.json().catch(() => ({}));
|
|
494
|
+
const validation = GroupPatchSchema.safeParse(body);
|
|
495
|
+
if (!validation.success) {
|
|
496
|
+
return c.json({ error: 'Invalid request body' }, 400);
|
|
497
|
+
}
|
|
498
|
+
const { name: rawName, is_pinned, activation_mode, agent_type, execution_mode, model, reasoning_effort, } = validation.data;
|
|
499
|
+
const name = rawName ? normalizeGroupName(rawName) : undefined;
|
|
500
|
+
// 至少需要提供一个字段
|
|
501
|
+
if (!name &&
|
|
502
|
+
is_pinned === undefined &&
|
|
503
|
+
activation_mode === undefined &&
|
|
504
|
+
agent_type === undefined &&
|
|
505
|
+
execution_mode === undefined &&
|
|
506
|
+
model === undefined &&
|
|
507
|
+
reasoning_effort === undefined) {
|
|
508
|
+
return c.json({ error: 'No fields to update' }, 400);
|
|
509
|
+
}
|
|
510
|
+
// member 用户不允许使用 host 模式(安全限制)
|
|
511
|
+
if (execution_mode === 'host' && !hasHostExecutionPermission(authUser)) {
|
|
512
|
+
return c.json({ error: 'Insufficient permissions for host execution mode' }, 403);
|
|
513
|
+
}
|
|
514
|
+
// Pin/unpin only requires canAccessGroup (it's a per-user preference)
|
|
515
|
+
const isPinOnly = is_pinned !== undefined &&
|
|
516
|
+
!name &&
|
|
517
|
+
activation_mode === undefined &&
|
|
518
|
+
agent_type === undefined &&
|
|
519
|
+
execution_mode === undefined &&
|
|
520
|
+
model === undefined &&
|
|
521
|
+
reasoning_effort === undefined;
|
|
522
|
+
if (isPinOnly) {
|
|
523
|
+
if (!canAccessGroup({ id: authUser.id, role: authUser.role }, { ...existing, jid })) {
|
|
524
|
+
return c.json({ error: 'Group not found' }, 404);
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
else {
|
|
528
|
+
// Name/skills changes require canModifyGroup (owner only)
|
|
529
|
+
if (!canModifyGroup({ id: authUser.id, role: authUser.role }, { ...existing, jid })) {
|
|
530
|
+
return c.json({ error: 'Group not found' }, 404);
|
|
531
|
+
}
|
|
532
|
+
if (!jid.startsWith('web:') && authUser.role !== 'admin') {
|
|
533
|
+
return c.json({ error: 'This group cannot be edited' }, 403);
|
|
534
|
+
}
|
|
535
|
+
if (isHostExecutionGroup(existing) &&
|
|
536
|
+
!hasHostExecutionPermission(authUser)) {
|
|
537
|
+
return c.json({ error: 'Insufficient permissions for host execution mode' }, 403);
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
// Handle pin/unpin (per-user, separate table)
|
|
541
|
+
let pinned_at;
|
|
542
|
+
if (is_pinned === true) {
|
|
543
|
+
pinned_at = pinGroup(authUser.id, jid);
|
|
544
|
+
}
|
|
545
|
+
else if (is_pinned === false) {
|
|
546
|
+
unpinGroup(authUser.id, jid);
|
|
547
|
+
}
|
|
548
|
+
// Update registered group if any editable field changed
|
|
549
|
+
if (name ||
|
|
550
|
+
activation_mode !== undefined ||
|
|
551
|
+
agent_type !== undefined ||
|
|
552
|
+
execution_mode !== undefined ||
|
|
553
|
+
model !== undefined ||
|
|
554
|
+
reasoning_effort !== undefined) {
|
|
555
|
+
const nextAgentType = agent_type !== undefined
|
|
556
|
+
? normalizeAgentType(agent_type)
|
|
557
|
+
: existing.agentType || 'claude';
|
|
558
|
+
const nextExecutionMode = execution_mode !== undefined
|
|
559
|
+
? execution_mode
|
|
560
|
+
: existing.executionMode || 'container';
|
|
561
|
+
const nextModel = model !== undefined
|
|
562
|
+
? normalizeOptionalRuntimeModel(nextAgentType, model)
|
|
563
|
+
: (existing.model ?? null);
|
|
564
|
+
if (model !== undefined && model !== null && !nextModel) {
|
|
565
|
+
return c.json({
|
|
566
|
+
error: `Unsupported ${nextAgentType} model preset`,
|
|
567
|
+
presets: getModelPresets(nextAgentType),
|
|
568
|
+
}, 400);
|
|
569
|
+
}
|
|
570
|
+
const nextReasoningEffort = reasoning_effort !== undefined
|
|
571
|
+
? normalizeOptionalReasoningEffort(nextAgentType, reasoning_effort)
|
|
572
|
+
: (existing.reasoningEffort ?? null);
|
|
573
|
+
if (reasoning_effort !== undefined &&
|
|
574
|
+
reasoning_effort !== null &&
|
|
575
|
+
!nextReasoningEffort) {
|
|
576
|
+
return c.json({
|
|
577
|
+
error: supportsReasoningEffort(nextAgentType)
|
|
578
|
+
? 'Unsupported reasoning_effort preset'
|
|
579
|
+
: `${nextAgentType} does not support reasoning_effort`,
|
|
580
|
+
}, 400);
|
|
581
|
+
}
|
|
582
|
+
const runtimeBoundaryChanged = hasRuntimeBoundaryChange({
|
|
583
|
+
currentAgentType: existing.agentType || 'claude',
|
|
584
|
+
currentExecutionMode: existing.executionMode || 'container',
|
|
585
|
+
nextAgentType,
|
|
586
|
+
nextExecutionMode,
|
|
587
|
+
});
|
|
588
|
+
const runtimeSettingsChanged = runtimeBoundaryChanged ||
|
|
589
|
+
(existing.model ?? null) !== nextModel ||
|
|
590
|
+
(existing.reasoningEffort ?? null) !== nextReasoningEffort;
|
|
591
|
+
const runtimeError = validateGroupRuntimeUpdate({
|
|
592
|
+
isHome: !!existing.is_home,
|
|
593
|
+
currentExecutionMode: existing.executionMode || 'container',
|
|
594
|
+
nextAgentType,
|
|
595
|
+
nextExecutionMode,
|
|
596
|
+
});
|
|
597
|
+
if (runtimeError) {
|
|
598
|
+
return c.json({ error: runtimeError }, runtimeError === 'Cannot change execution mode of home containers'
|
|
599
|
+
? 403
|
|
600
|
+
: 400);
|
|
601
|
+
}
|
|
602
|
+
if (runtimeBoundaryChanged && isRuntimeBuildStale()) {
|
|
603
|
+
const buildStatus = getRuntimeBuildStatus();
|
|
604
|
+
logger.warn({
|
|
605
|
+
jid,
|
|
606
|
+
folder: existing.folder,
|
|
607
|
+
previousAgentType: existing.agentType || 'claude',
|
|
608
|
+
nextAgentType,
|
|
609
|
+
previousExecutionMode: existing.executionMode || 'container',
|
|
610
|
+
nextExecutionMode,
|
|
611
|
+
buildStatus,
|
|
612
|
+
}, 'Rejected workspace runtime change because backend process is stale');
|
|
613
|
+
return c.json({
|
|
614
|
+
error: 'Runtime change requires a backend restart because the current process is older than the on-disk build',
|
|
615
|
+
stale_build: true,
|
|
616
|
+
}, 409);
|
|
617
|
+
}
|
|
618
|
+
const updated = {
|
|
619
|
+
name: name || existing.name,
|
|
620
|
+
folder: existing.folder,
|
|
621
|
+
added_at: existing.added_at,
|
|
622
|
+
containerConfig: existing.containerConfig,
|
|
623
|
+
agentType: nextAgentType,
|
|
624
|
+
executionMode: execution_mode !== undefined
|
|
625
|
+
? execution_mode
|
|
626
|
+
: existing.executionMode,
|
|
627
|
+
model: nextModel,
|
|
628
|
+
reasoningEffort: nextReasoningEffort,
|
|
629
|
+
customCwd: existing.customCwd,
|
|
630
|
+
initSourcePath: existing.initSourcePath,
|
|
631
|
+
initGitUrl: existing.initGitUrl,
|
|
632
|
+
created_by: existing.created_by,
|
|
633
|
+
is_home: existing.is_home,
|
|
634
|
+
target_agent_id: existing.target_agent_id,
|
|
635
|
+
target_main_jid: existing.target_main_jid,
|
|
636
|
+
reply_policy: existing.reply_policy,
|
|
637
|
+
require_mention: existing.require_mention,
|
|
638
|
+
activation_mode: activation_mode !== undefined
|
|
639
|
+
? activation_mode
|
|
640
|
+
: existing.activation_mode,
|
|
641
|
+
};
|
|
642
|
+
const materializedGroup = materializeHostWorkspaceDefaultCwd(updated, {
|
|
643
|
+
launchCwd: LAUNCH_CWD,
|
|
644
|
+
fieldLabel: 'CLI launch cwd',
|
|
645
|
+
});
|
|
646
|
+
if ('error' in materializedGroup) {
|
|
647
|
+
return c.json({ error: materializedGroup.error }, 500);
|
|
648
|
+
}
|
|
649
|
+
const persistedGroup = materializedGroup.group;
|
|
650
|
+
setRegisteredGroup(jid, persistedGroup);
|
|
651
|
+
if (name)
|
|
652
|
+
updateChatName(jid, name);
|
|
653
|
+
deps.getRegisteredGroups()[jid] = persistedGroup;
|
|
654
|
+
if (runtimeSettingsChanged) {
|
|
655
|
+
try {
|
|
656
|
+
await resetWorkspaceRuntimeState(deps, jid, persistedGroup);
|
|
657
|
+
}
|
|
658
|
+
catch (err) {
|
|
659
|
+
logger.error({
|
|
660
|
+
jid,
|
|
661
|
+
folder: persistedGroup.folder,
|
|
662
|
+
previousAgentType: existing.agentType || 'claude',
|
|
663
|
+
nextAgentType,
|
|
664
|
+
previousExecutionMode: existing.executionMode || 'container',
|
|
665
|
+
nextExecutionMode,
|
|
666
|
+
previousModel: existing.model ?? null,
|
|
667
|
+
nextModel,
|
|
668
|
+
previousReasoningEffort: existing.reasoningEffort ?? null,
|
|
669
|
+
nextReasoningEffort,
|
|
670
|
+
err,
|
|
671
|
+
}, 'Workspace runtime changed but failed to reset active runners');
|
|
672
|
+
return c.json({
|
|
673
|
+
error: 'Workspace runtime updated, but failed to reset active sessions',
|
|
674
|
+
}, 500);
|
|
675
|
+
}
|
|
676
|
+
logger.info({
|
|
677
|
+
jid,
|
|
678
|
+
folder: persistedGroup.folder,
|
|
679
|
+
previousAgentType: existing.agentType || 'claude',
|
|
680
|
+
nextAgentType,
|
|
681
|
+
previousExecutionMode: existing.executionMode || 'container',
|
|
682
|
+
nextExecutionMode,
|
|
683
|
+
previousModel: existing.model ?? null,
|
|
684
|
+
nextModel,
|
|
685
|
+
previousReasoningEffort: existing.reasoningEffort ?? null,
|
|
686
|
+
nextReasoningEffort,
|
|
687
|
+
}, 'Workspace runtime changed, reset active runners and sessions');
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
return c.json({ success: true, pinned_at });
|
|
691
|
+
});
|
|
692
|
+
// DELETE /api/groups/:jid - 删除群组
|
|
693
|
+
groupRoutes.delete('/:jid', authMiddleware, async (c) => {
|
|
694
|
+
const deps = getWebDeps();
|
|
695
|
+
if (!deps)
|
|
696
|
+
return c.json({ error: 'Server not initialized' }, 500);
|
|
697
|
+
const jid = c.req.param('jid');
|
|
698
|
+
const existing = getRegisteredGroup(jid);
|
|
699
|
+
if (!existing)
|
|
700
|
+
return c.json({ error: 'Group not found' }, 404);
|
|
701
|
+
const authUser = c.get('user');
|
|
702
|
+
if (!canDeleteGroup({ id: authUser.id, role: authUser.role }, existing)) {
|
|
703
|
+
return c.json({ error: 'Group not found' }, 404);
|
|
704
|
+
}
|
|
705
|
+
if (!jid.startsWith('web:')) {
|
|
706
|
+
return c.json({ error: 'This group cannot be deleted' }, 403);
|
|
707
|
+
}
|
|
708
|
+
if (isHostExecutionGroup(existing) && !hasHostExecutionPermission(authUser)) {
|
|
709
|
+
return c.json({ error: 'Insufficient permissions for host execution mode' }, 403);
|
|
710
|
+
}
|
|
711
|
+
// Block deletion if any IM binding exists (agent or main conversation)
|
|
712
|
+
const agents = listAgentsByJid(jid);
|
|
713
|
+
const boundAgents = [];
|
|
714
|
+
for (const a of agents) {
|
|
715
|
+
if (a.kind === 'conversation') {
|
|
716
|
+
const linked = getGroupsByTargetAgent(a.id);
|
|
717
|
+
if (linked.length > 0) {
|
|
718
|
+
boundAgents.push({
|
|
719
|
+
agentId: a.id,
|
|
720
|
+
agentName: a.name,
|
|
721
|
+
imGroups: linked.map((l) => ({ jid: l.jid, name: l.group.name })),
|
|
722
|
+
});
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
// Search by actual JID; also check legacy folder-based format for backward compat
|
|
727
|
+
const mainBoundByJid = getGroupsByTargetMainJid(jid);
|
|
728
|
+
const legacyMainJid = `web:${existing.folder}`;
|
|
729
|
+
const mainBoundByFolder = legacyMainJid !== jid ? getGroupsByTargetMainJid(legacyMainJid) : [];
|
|
730
|
+
const mainBoundJids = new Set(mainBoundByJid.map((l) => l.jid));
|
|
731
|
+
const mainBound = [
|
|
732
|
+
...mainBoundByJid,
|
|
733
|
+
...mainBoundByFolder.filter((l) => !mainBoundJids.has(l.jid)),
|
|
734
|
+
];
|
|
735
|
+
if (boundAgents.length > 0 || mainBound.length > 0) {
|
|
736
|
+
const mainImGroups = mainBound.map((l) => ({
|
|
737
|
+
jid: l.jid,
|
|
738
|
+
name: l.group.name,
|
|
739
|
+
}));
|
|
740
|
+
return c.json({
|
|
741
|
+
error: '该工作区绑定了 IM 群组,请先解绑后再删除。',
|
|
742
|
+
bound_agents: boundAgents,
|
|
743
|
+
bound_main_im_groups: mainImGroups,
|
|
744
|
+
}, 409);
|
|
745
|
+
}
|
|
746
|
+
// Wait for container to fully stop before cleaning up its files
|
|
747
|
+
try {
|
|
748
|
+
await deps.queue.stopGroup(jid);
|
|
749
|
+
}
|
|
750
|
+
catch (err) {
|
|
751
|
+
logger.error({ jid, err }, 'Failed to stop container before deleting group');
|
|
752
|
+
return c.json({ error: 'Failed to stop container, group not deleted' }, 500);
|
|
753
|
+
}
|
|
754
|
+
deleteGroupData(jid, existing.folder);
|
|
755
|
+
removeFlowArtifacts(existing.folder);
|
|
756
|
+
delete deps.getRegisteredGroups()[jid];
|
|
757
|
+
delete deps.getSessions()[existing.folder];
|
|
758
|
+
deps.setLastAgentTimestamp(jid, { timestamp: '', id: '' });
|
|
759
|
+
return c.json({ success: true });
|
|
760
|
+
});
|
|
761
|
+
// POST /api/groups/:jid/stop - 停止当前运行的容器/进程
|
|
762
|
+
groupRoutes.post('/:jid/stop', authMiddleware, async (c) => {
|
|
763
|
+
const deps = getWebDeps();
|
|
764
|
+
if (!deps)
|
|
765
|
+
return c.json({ error: 'Server not initialized' }, 500);
|
|
766
|
+
const jid = c.req.param('jid');
|
|
767
|
+
const group = getRegisteredGroup(jid);
|
|
768
|
+
if (!group)
|
|
769
|
+
return c.json({ error: 'Group not found' }, 404);
|
|
770
|
+
const authUser = c.get('user');
|
|
771
|
+
if (!canAccessGroup({ id: authUser.id, role: authUser.role }, group)) {
|
|
772
|
+
return c.json({ error: 'Group not found' }, 404);
|
|
773
|
+
}
|
|
774
|
+
try {
|
|
775
|
+
await deps.queue.stopGroup(jid);
|
|
776
|
+
return c.json({ success: true });
|
|
777
|
+
}
|
|
778
|
+
catch (err) {
|
|
779
|
+
logger.error({ jid, err }, 'Failed to stop group');
|
|
780
|
+
return c.json({ error: 'Failed to stop container' }, 500);
|
|
781
|
+
}
|
|
782
|
+
});
|
|
783
|
+
// POST /api/groups/:jid/interrupt - 中断当前查询(不杀容器)
|
|
784
|
+
groupRoutes.post('/:jid/interrupt', authMiddleware, async (c) => {
|
|
785
|
+
const deps = getWebDeps();
|
|
786
|
+
if (!deps)
|
|
787
|
+
return c.json({ error: 'Server not initialized' }, 500);
|
|
788
|
+
const rawJid = c.req.param('jid');
|
|
789
|
+
const jid = decodeURIComponent(rawJid);
|
|
790
|
+
// Support virtual JIDs for conversation agents: {jid}#agent:{agentId}
|
|
791
|
+
const agentSep = jid.indexOf('#agent:');
|
|
792
|
+
const baseJid = agentSep >= 0 ? jid.slice(0, agentSep) : jid;
|
|
793
|
+
const group = getRegisteredGroup(baseJid);
|
|
794
|
+
if (!group)
|
|
795
|
+
return c.json({ error: 'Group not found' }, 404);
|
|
796
|
+
const authUser = c.get('user');
|
|
797
|
+
if (!canAccessGroup({ id: authUser.id, role: authUser.role }, group)) {
|
|
798
|
+
return c.json({ error: 'Group not found' }, 404);
|
|
799
|
+
}
|
|
800
|
+
const interrupted = deps.queue.interruptQuery(jid);
|
|
801
|
+
if (interrupted) {
|
|
802
|
+
// ── 立即 abort 飞书流式卡片 ──
|
|
803
|
+
const session = getStreamingSession(jid);
|
|
804
|
+
if (session?.isActive()) {
|
|
805
|
+
session.abort('已中断').catch(() => { });
|
|
806
|
+
}
|
|
807
|
+
// Persist interrupt as a system marker so refresh/state-restore can
|
|
808
|
+
// deterministically clear waiting even when no assistant reply exists.
|
|
809
|
+
const messageId = crypto.randomUUID();
|
|
810
|
+
const timestamp = new Date().toISOString();
|
|
811
|
+
try {
|
|
812
|
+
ensureChatExists(jid);
|
|
813
|
+
storeMessageDirect(messageId, jid, '__system__', 'system', 'query_interrupted', timestamp, true);
|
|
814
|
+
broadcastNewMessage(jid, {
|
|
815
|
+
id: messageId,
|
|
816
|
+
chat_jid: jid,
|
|
817
|
+
sender: '__system__',
|
|
818
|
+
sender_name: 'system',
|
|
819
|
+
content: 'query_interrupted',
|
|
820
|
+
timestamp,
|
|
821
|
+
is_from_me: true,
|
|
822
|
+
});
|
|
823
|
+
}
|
|
824
|
+
catch (err) {
|
|
825
|
+
logger.warn({ jid, err }, 'Interrupt succeeded but failed to append system marker');
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
return c.json({ success: true, interrupted });
|
|
829
|
+
});
|
|
830
|
+
// POST /api/groups/:jid/reset-session - 重置会话上下文
|
|
831
|
+
// Optional body: { agentId?: string } — when provided, only reset that agent's session
|
|
832
|
+
groupRoutes.post('/:jid/reset-session', authMiddleware, async (c) => {
|
|
833
|
+
const deps = getWebDeps();
|
|
834
|
+
if (!deps)
|
|
835
|
+
return c.json({ error: 'Server not initialized' }, 500);
|
|
836
|
+
const jid = c.req.param('jid');
|
|
837
|
+
const group = getRegisteredGroup(jid);
|
|
838
|
+
if (!group)
|
|
839
|
+
return c.json({ error: 'Group not found' }, 404);
|
|
840
|
+
const authUser = c.get('user');
|
|
841
|
+
if (!canModifyGroup({ id: authUser.id, role: authUser.role }, { ...group, jid })) {
|
|
842
|
+
return c.json({ error: 'Group not found' }, 404);
|
|
843
|
+
}
|
|
844
|
+
if (isHostExecutionGroup(group) && !hasHostExecutionPermission(authUser)) {
|
|
845
|
+
return c.json({ error: 'Insufficient permissions for host execution mode' }, 403);
|
|
846
|
+
}
|
|
847
|
+
// Read optional agentId from request body
|
|
848
|
+
let agentId;
|
|
849
|
+
try {
|
|
850
|
+
const body = await c.req.json().catch(() => ({}));
|
|
851
|
+
if (body && typeof body.agentId === 'string' && body.agentId) {
|
|
852
|
+
agentId = body.agentId;
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
catch {
|
|
856
|
+
/* no body or invalid JSON — treat as main session reset */
|
|
857
|
+
}
|
|
858
|
+
// Validate agentId belongs to this group
|
|
859
|
+
if (agentId) {
|
|
860
|
+
const agent = getAgent(agentId);
|
|
861
|
+
if (!agent || agent.chat_jid !== jid) {
|
|
862
|
+
return c.json({ error: 'Agent not found' }, 404);
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
// 1. Stop running processes
|
|
866
|
+
try {
|
|
867
|
+
if (agentId) {
|
|
868
|
+
// Agent-specific: only stop the agent's virtual JID process
|
|
869
|
+
const virtualJid = `${jid}#agent:${agentId}`;
|
|
870
|
+
await deps.queue.stopGroup(virtualJid, { force: true });
|
|
871
|
+
}
|
|
872
|
+
else {
|
|
873
|
+
// Main session: stop ALL processes for this folder
|
|
874
|
+
const siblingJids = getJidsByFolder(group.folder);
|
|
875
|
+
await Promise.all(siblingJids.map((j) => deps.queue.stopGroup(j, { force: true })));
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
catch (err) {
|
|
879
|
+
logger.error({ jid, agentId, err }, 'Failed to stop containers before resetting session');
|
|
880
|
+
return c.json({ error: 'Failed to stop container, session not reset' }, 500);
|
|
881
|
+
}
|
|
882
|
+
// 2. Delete session JSONL files so Claude starts fresh.
|
|
883
|
+
try {
|
|
884
|
+
clearSessionJsonlFiles(group.folder, agentId);
|
|
885
|
+
}
|
|
886
|
+
catch (err) {
|
|
887
|
+
logger.error({ jid, folder: group.folder, agentId, err }, 'Failed to clear session files during reset');
|
|
888
|
+
return c.json({ error: 'Failed to clear session files, session not reset' }, 500);
|
|
889
|
+
}
|
|
890
|
+
// 3. Delete session from DB (and in-memory cache for main session).
|
|
891
|
+
try {
|
|
892
|
+
deleteSession(group.folder, agentId);
|
|
893
|
+
if (!agentId) {
|
|
894
|
+
delete deps.getSessions()[group.folder];
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
catch (err) {
|
|
898
|
+
logger.error({ jid, folder: group.folder, agentId, err }, 'Failed to clear session state during reset');
|
|
899
|
+
return c.json({ error: 'Failed to clear session state, session not reset' }, 500);
|
|
900
|
+
}
|
|
901
|
+
// 4. Insert system divider message into the correct JID (best-effort).
|
|
902
|
+
const targetJid = agentId ? `${jid}#agent:${agentId}` : jid;
|
|
903
|
+
const dividerMessageId = crypto.randomUUID();
|
|
904
|
+
const timestamp = new Date().toISOString();
|
|
905
|
+
try {
|
|
906
|
+
ensureChatExists(targetJid);
|
|
907
|
+
storeMessageDirect(dividerMessageId, targetJid, '__system__', 'system', 'context_reset', timestamp, true);
|
|
908
|
+
broadcastNewMessage(targetJid, {
|
|
909
|
+
id: dividerMessageId,
|
|
910
|
+
chat_jid: targetJid,
|
|
911
|
+
sender: '__system__',
|
|
912
|
+
sender_name: 'system',
|
|
913
|
+
content: 'context_reset',
|
|
914
|
+
timestamp,
|
|
915
|
+
is_from_me: true,
|
|
916
|
+
});
|
|
917
|
+
}
|
|
918
|
+
catch (err) {
|
|
919
|
+
logger.warn({ jid, agentId, err }, 'Session reset succeeded but failed to append divider message');
|
|
920
|
+
}
|
|
921
|
+
// 5. Advance lastAgentTimestamp so old messages before the reset are not
|
|
922
|
+
// re-sent to the next fresh agent session.
|
|
923
|
+
if (agentId) {
|
|
924
|
+
const virtualJid = `${jid}#agent:${agentId}`;
|
|
925
|
+
deps.setLastAgentTimestamp(virtualJid, { timestamp, id: dividerMessageId });
|
|
926
|
+
}
|
|
927
|
+
else {
|
|
928
|
+
// Main session: advance cursor for ALL sibling JIDs sharing this folder.
|
|
929
|
+
const siblingJids = getJidsByFolder(group.folder);
|
|
930
|
+
for (const siblingJid of siblingJids) {
|
|
931
|
+
deps.setLastAgentTimestamp(siblingJid, {
|
|
932
|
+
timestamp,
|
|
933
|
+
id: dividerMessageId,
|
|
934
|
+
});
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
logger.info({ jid, folder: group.folder, agentId }, 'Session reset: cleared session files and stopped containers');
|
|
938
|
+
return c.json({ success: true, dividerMessageId });
|
|
939
|
+
});
|
|
940
|
+
// POST /api/groups/:jid/clear-history - 清除聊天历史
|
|
941
|
+
groupRoutes.post('/:jid/clear-history', authMiddleware, async (c) => {
|
|
942
|
+
const deps = getWebDeps();
|
|
943
|
+
if (!deps)
|
|
944
|
+
return c.json({ error: 'Server not initialized' }, 500);
|
|
945
|
+
const jid = c.req.param('jid');
|
|
946
|
+
const group = getRegisteredGroup(jid);
|
|
947
|
+
if (!group)
|
|
948
|
+
return c.json({ error: 'Group not found' }, 404);
|
|
949
|
+
const authUser = c.get('user');
|
|
950
|
+
if (!canModifyGroup({ id: authUser.id, role: authUser.role }, { ...group, jid })) {
|
|
951
|
+
return c.json({ error: 'Group not found' }, 404);
|
|
952
|
+
}
|
|
953
|
+
if (isHostExecutionGroup(group) && !hasHostExecutionPermission(authUser)) {
|
|
954
|
+
return c.json({ error: 'Insufficient permissions for host execution mode' }, 403);
|
|
955
|
+
}
|
|
956
|
+
// Collect all JIDs sharing the same folder (e.g., web:main + feishu groups)
|
|
957
|
+
const siblingJids = getJidsByFolder(group.folder);
|
|
958
|
+
// 1. Stop ALL active processes for this folder first to avoid writes during cleanup.
|
|
959
|
+
try {
|
|
960
|
+
await Promise.all(siblingJids.map((j) => deps.queue.stopGroup(j, { force: true })));
|
|
961
|
+
}
|
|
962
|
+
catch (err) {
|
|
963
|
+
logger.error({ jid, siblingJids, err }, 'Failed to stop containers before clearing history');
|
|
964
|
+
return c.json({ error: 'Failed to stop container, history not cleared' }, 500);
|
|
965
|
+
}
|
|
966
|
+
// 2. Reset workspace: clear working directory, session files, and IPC artifacts.
|
|
967
|
+
try {
|
|
968
|
+
resetWorkspaceForGroup(group.folder);
|
|
969
|
+
}
|
|
970
|
+
catch (err) {
|
|
971
|
+
logger.error({ jid, folder: group.folder, err }, 'Failed to reset workspace while clearing history');
|
|
972
|
+
return c.json({ error: 'Failed to reset workspace, history not cleared' }, 500);
|
|
973
|
+
}
|
|
974
|
+
// 3. Clear session state and message history for ALL sibling JIDs.
|
|
975
|
+
try {
|
|
976
|
+
deleteSession(group.folder);
|
|
977
|
+
delete deps.getSessions()[group.folder];
|
|
978
|
+
for (const siblingJid of siblingJids) {
|
|
979
|
+
deleteChatHistory(siblingJid);
|
|
980
|
+
// Re-create the chats row so subsequent messages work properly
|
|
981
|
+
ensureChatExists(siblingJid);
|
|
982
|
+
deps.setLastAgentTimestamp(siblingJid, { timestamp: '', id: '' });
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
catch (err) {
|
|
986
|
+
logger.error({ jid, folder: group.folder, err }, 'Failed to clear history state');
|
|
987
|
+
return c.json({ error: 'Failed to clear history' }, 500);
|
|
988
|
+
}
|
|
989
|
+
logger.info({ jid, folder: group.folder, siblingJids }, 'Cleared workspace, context and chat history for group and all siblings');
|
|
990
|
+
return c.json({ success: true });
|
|
991
|
+
});
|
|
992
|
+
// GET /api/groups/:jid/messages - 获取消息历史
|
|
993
|
+
groupRoutes.get('/:jid/messages', authMiddleware, async (c) => {
|
|
994
|
+
const jid = c.req.param('jid');
|
|
995
|
+
const group = getRegisteredGroup(jid);
|
|
996
|
+
if (!group) {
|
|
997
|
+
return c.json({ error: 'Group not found' }, 404);
|
|
998
|
+
}
|
|
999
|
+
const authUser = c.get('user');
|
|
1000
|
+
if (!canAccessGroup({ id: authUser.id, role: authUser.role }, group)) {
|
|
1001
|
+
return c.json({ error: 'Group not found' }, 404);
|
|
1002
|
+
}
|
|
1003
|
+
if (isHostExecutionGroup(group) && !hasHostExecutionPermission(authUser)) {
|
|
1004
|
+
return c.json({ error: 'Insufficient permissions for host execution mode' }, 403);
|
|
1005
|
+
}
|
|
1006
|
+
const before = readHistoryCursorQuery(c, 'before');
|
|
1007
|
+
const after = readHistoryCursorQuery(c, 'after');
|
|
1008
|
+
const agentIdParam = c.req.query('agentId');
|
|
1009
|
+
const limitRaw = parseInt(c.req.query('limit') || '50', 10);
|
|
1010
|
+
const limit = Math.min(Number.isFinite(limitRaw) ? Math.max(1, limitRaw) : 50, 200);
|
|
1011
|
+
// Agent conversation: query messages from the virtual chat_jid
|
|
1012
|
+
if (agentIdParam) {
|
|
1013
|
+
const agent = getAgent(agentIdParam);
|
|
1014
|
+
if (!agent || agent.chat_jid !== jid) {
|
|
1015
|
+
return c.json({ error: 'Agent not found' }, 404);
|
|
1016
|
+
}
|
|
1017
|
+
const virtualJid = `${jid}#agent:${agentIdParam}`;
|
|
1018
|
+
if (after) {
|
|
1019
|
+
const messages = getMessagesAfter(virtualJid, after, limit);
|
|
1020
|
+
return c.json({ messages });
|
|
1021
|
+
}
|
|
1022
|
+
const rows = getMessagesPage(virtualJid, before, limit + 1);
|
|
1023
|
+
const hasMore = rows.length > limit;
|
|
1024
|
+
const messages = hasMore ? rows.slice(0, limit) : rows;
|
|
1025
|
+
return c.json({ messages, hasMore });
|
|
1026
|
+
}
|
|
1027
|
+
// is_home 群组合并查询:将同 folder 下所有 JID(web + feishu/telegram IM 通道)的消息合并展示
|
|
1028
|
+
// - admin: merge all siblings in the folder (shared admin home)
|
|
1029
|
+
// - member: merge only siblings with same owner to prevent cross-user leakage
|
|
1030
|
+
const queryJids = [jid];
|
|
1031
|
+
if (group.is_home) {
|
|
1032
|
+
const siblingJids = getJidsByFolder(group.folder);
|
|
1033
|
+
for (const siblingJid of siblingJids) {
|
|
1034
|
+
if (siblingJid === jid)
|
|
1035
|
+
continue;
|
|
1036
|
+
const siblingGroup = getRegisteredGroup(siblingJid);
|
|
1037
|
+
if (!siblingGroup)
|
|
1038
|
+
continue;
|
|
1039
|
+
// Merge siblings by ownership: same creator, or admin's own IM channels
|
|
1040
|
+
const ownerMatch = group.created_by && siblingGroup.created_by === group.created_by;
|
|
1041
|
+
const adminSelfMatch = authUser.role === 'admin' && siblingGroup.created_by === authUser.id;
|
|
1042
|
+
if (ownerMatch || adminSelfMatch) {
|
|
1043
|
+
queryJids.push(siblingJid);
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
if (queryJids.length === 1) {
|
|
1048
|
+
// 单 JID 走原路径
|
|
1049
|
+
if (after) {
|
|
1050
|
+
const messages = getMessagesAfter(jid, after, limit);
|
|
1051
|
+
return c.json({ messages });
|
|
1052
|
+
}
|
|
1053
|
+
const rows = getMessagesPage(jid, before, limit + 1);
|
|
1054
|
+
const hasMore = rows.length > limit;
|
|
1055
|
+
const messages = hasMore ? rows.slice(0, limit) : rows;
|
|
1056
|
+
return c.json({ messages, hasMore });
|
|
1057
|
+
}
|
|
1058
|
+
// 多 JID 合并查询
|
|
1059
|
+
if (after) {
|
|
1060
|
+
const messages = getMessagesAfterMulti(queryJids, after, limit);
|
|
1061
|
+
return c.json({ messages });
|
|
1062
|
+
}
|
|
1063
|
+
const rows = getMessagesPageMulti(queryJids, before, limit + 1);
|
|
1064
|
+
const hasMore = rows.length > limit;
|
|
1065
|
+
const messages = hasMore ? rows.slice(0, limit) : rows;
|
|
1066
|
+
return c.json({ messages, hasMore });
|
|
1067
|
+
});
|
|
1068
|
+
// DELETE /api/groups/:jid/messages/:messageId - 删除单条消息
|
|
1069
|
+
groupRoutes.delete('/:jid/messages/:messageId', authMiddleware, (c) => {
|
|
1070
|
+
const jid = c.req.param('jid');
|
|
1071
|
+
const messageId = c.req.param('messageId');
|
|
1072
|
+
const group = getRegisteredGroup(jid);
|
|
1073
|
+
if (!group) {
|
|
1074
|
+
return c.json({ error: 'Group not found' }, 404);
|
|
1075
|
+
}
|
|
1076
|
+
const authUser = c.get('user');
|
|
1077
|
+
if (!canAccessGroup({ id: authUser.id, role: authUser.role }, group)) {
|
|
1078
|
+
return c.json({ error: 'Group not found' }, 404);
|
|
1079
|
+
}
|
|
1080
|
+
// Ownership check: admin can delete any message, non-admin can only delete their own
|
|
1081
|
+
const msg = getMessage(jid, messageId);
|
|
1082
|
+
if (!msg) {
|
|
1083
|
+
return c.json({ error: 'Message not found' }, 404);
|
|
1084
|
+
}
|
|
1085
|
+
if (authUser.role !== 'admin') {
|
|
1086
|
+
// AI messages (is_from_me=1) cannot be deleted by non-admin
|
|
1087
|
+
// User messages can only be deleted by the sender
|
|
1088
|
+
if (msg.is_from_me === 1 || (msg.sender && msg.sender !== authUser.id)) {
|
|
1089
|
+
return c.json({ error: 'Permission denied' }, 403);
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
const deleted = deleteMessage(jid, messageId);
|
|
1093
|
+
if (!deleted) {
|
|
1094
|
+
return c.json({ error: 'Message not found' }, 404);
|
|
1095
|
+
}
|
|
1096
|
+
return c.json({ success: true });
|
|
1097
|
+
});
|
|
1098
|
+
// GET /api/groups/:jid/env - 获取容器环境变量配置
|
|
1099
|
+
groupRoutes.get('/:jid/env', authMiddleware, (c) => {
|
|
1100
|
+
const jid = c.req.param('jid');
|
|
1101
|
+
const group = getRegisteredGroup(jid);
|
|
1102
|
+
if (!group)
|
|
1103
|
+
return c.json({ error: 'Group not found' }, 404);
|
|
1104
|
+
if ((group.agentType || 'claude') === 'codex') {
|
|
1105
|
+
return c.json({
|
|
1106
|
+
error: 'This workspace uses Codex and does not support Claude env overrides',
|
|
1107
|
+
}, 400);
|
|
1108
|
+
}
|
|
1109
|
+
const user = c.get('user');
|
|
1110
|
+
if (!canAccessGroup({ id: user.id, role: user.role }, group)) {
|
|
1111
|
+
return c.json({ error: 'Group not found' }, 404);
|
|
1112
|
+
}
|
|
1113
|
+
if (isHostExecutionGroup(group) && !hasHostExecutionPermission(user)) {
|
|
1114
|
+
return c.json({ error: 'Insufficient permissions for host execution mode' }, 403);
|
|
1115
|
+
}
|
|
1116
|
+
// Check permissions
|
|
1117
|
+
if (user.role !== 'admin' &&
|
|
1118
|
+
(!user.permissions || !user.permissions.includes('manage_group_env'))) {
|
|
1119
|
+
return c.json({ error: 'Insufficient permissions' }, 403);
|
|
1120
|
+
}
|
|
1121
|
+
const config = getContainerEnvConfig(group.folder);
|
|
1122
|
+
return c.json(toPublicContainerEnvForUser(config, user));
|
|
1123
|
+
});
|
|
1124
|
+
// PUT /api/groups/:jid/env - 更新容器环境变量配置
|
|
1125
|
+
groupRoutes.put('/:jid/env', authMiddleware, async (c) => {
|
|
1126
|
+
const jid = c.req.param('jid');
|
|
1127
|
+
const group = getRegisteredGroup(jid);
|
|
1128
|
+
if (!group)
|
|
1129
|
+
return c.json({ error: 'Group not found' }, 404);
|
|
1130
|
+
if ((group.agentType || 'claude') === 'codex') {
|
|
1131
|
+
return c.json({
|
|
1132
|
+
error: 'This workspace uses Codex and does not support Claude env overrides',
|
|
1133
|
+
}, 400);
|
|
1134
|
+
}
|
|
1135
|
+
const envUser = c.get('user');
|
|
1136
|
+
if (!canAccessGroup({ id: envUser.id, role: envUser.role }, group)) {
|
|
1137
|
+
return c.json({ error: 'Group not found' }, 404);
|
|
1138
|
+
}
|
|
1139
|
+
if (isHostExecutionGroup(group) && !hasHostExecutionPermission(envUser)) {
|
|
1140
|
+
return c.json({ error: 'Insufficient permissions for host execution mode' }, 403);
|
|
1141
|
+
}
|
|
1142
|
+
// Check permissions
|
|
1143
|
+
if (envUser.role !== 'admin' &&
|
|
1144
|
+
(!envUser.permissions || !envUser.permissions.includes('manage_group_env'))) {
|
|
1145
|
+
return c.json({ error: 'Insufficient permissions' }, 403);
|
|
1146
|
+
}
|
|
1147
|
+
const body = await c.req.json().catch(() => ({}));
|
|
1148
|
+
const validation = ContainerEnvSchema.safeParse(body);
|
|
1149
|
+
if (!validation.success) {
|
|
1150
|
+
return c.json({ error: 'Invalid request body' }, 400);
|
|
1151
|
+
}
|
|
1152
|
+
const data = validation.data;
|
|
1153
|
+
// Validate customEnv keys/values to prevent env injection
|
|
1154
|
+
if (data.customEnv) {
|
|
1155
|
+
const envKeyRe = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
1156
|
+
for (const [key, value] of Object.entries(data.customEnv)) {
|
|
1157
|
+
if (!envKeyRe.test(key)) {
|
|
1158
|
+
return c.json({
|
|
1159
|
+
error: `Invalid env key: "${key}". Keys must match [A-Za-z_][A-Za-z0-9_]*`,
|
|
1160
|
+
}, 400);
|
|
1161
|
+
}
|
|
1162
|
+
if (/[\r\n\0]/.test(value)) {
|
|
1163
|
+
return c.json({
|
|
1164
|
+
error: `Env value for "${key}" contains invalid control characters`,
|
|
1165
|
+
}, 400);
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
const current = getContainerEnvConfig(group.folder);
|
|
1170
|
+
// Build updated config: only update fields that are explicitly provided
|
|
1171
|
+
const updated = { ...current };
|
|
1172
|
+
if (data.anthropicBaseUrl !== undefined)
|
|
1173
|
+
updated.anthropicBaseUrl = data.anthropicBaseUrl;
|
|
1174
|
+
if (data.anthropicAuthToken !== undefined)
|
|
1175
|
+
updated.anthropicAuthToken = data.anthropicAuthToken;
|
|
1176
|
+
if (data.anthropicApiKey !== undefined)
|
|
1177
|
+
updated.anthropicApiKey = data.anthropicApiKey;
|
|
1178
|
+
if (data.claudeCodeOauthToken !== undefined)
|
|
1179
|
+
updated.claudeCodeOauthToken = data.claudeCodeOauthToken;
|
|
1180
|
+
if (data.anthropicModel !== undefined)
|
|
1181
|
+
updated.anthropicModel = data.anthropicModel;
|
|
1182
|
+
if (data.customEnv !== undefined)
|
|
1183
|
+
updated.customEnv = data.customEnv;
|
|
1184
|
+
try {
|
|
1185
|
+
saveContainerEnvConfig(group.folder, updated);
|
|
1186
|
+
// Restart container so it picks up the new env immediately
|
|
1187
|
+
const deps = getWebDeps();
|
|
1188
|
+
if (deps) {
|
|
1189
|
+
await deps.queue.restartGroup(jid);
|
|
1190
|
+
logger.info({ jid, folder: group.folder }, 'Restarted container after env config update');
|
|
1191
|
+
}
|
|
1192
|
+
return c.json(toPublicContainerEnvConfig(updated));
|
|
1193
|
+
}
|
|
1194
|
+
catch (err) {
|
|
1195
|
+
logger.error({ err }, 'Failed to save container env config');
|
|
1196
|
+
return c.json({ error: 'Failed to save config' }, 500);
|
|
1197
|
+
}
|
|
1198
|
+
});
|
|
1199
|
+
// --- Member Management Routes ---
|
|
1200
|
+
// GET /api/groups/:jid/members - 列出成员
|
|
1201
|
+
groupRoutes.get('/:jid/members', authMiddleware, (c) => {
|
|
1202
|
+
const jid = c.req.param('jid');
|
|
1203
|
+
const group = getRegisteredGroup(jid);
|
|
1204
|
+
if (!group)
|
|
1205
|
+
return c.json({ error: 'Group not found' }, 404);
|
|
1206
|
+
const authUser = c.get('user');
|
|
1207
|
+
if (!canAccessGroup({ id: authUser.id, role: authUser.role }, group)) {
|
|
1208
|
+
return c.json({ error: 'Group not found' }, 404);
|
|
1209
|
+
}
|
|
1210
|
+
const members = getGroupMembers(group.folder);
|
|
1211
|
+
return c.json({ members });
|
|
1212
|
+
});
|
|
1213
|
+
// GET /api/groups/:jid/members/search?q=... - 搜索可添加的用户(owner/admin 权限)
|
|
1214
|
+
groupRoutes.get('/:jid/members/search', authMiddleware, (c) => {
|
|
1215
|
+
const jid = c.req.param('jid');
|
|
1216
|
+
const group = getRegisteredGroup(jid);
|
|
1217
|
+
if (!group)
|
|
1218
|
+
return c.json({ error: 'Group not found' }, 404);
|
|
1219
|
+
const authUser = c.get('user');
|
|
1220
|
+
if (!canManageGroupMembers({ id: authUser.id, role: authUser.role }, { ...group, jid })) {
|
|
1221
|
+
return c.json({ error: 'Forbidden' }, 403);
|
|
1222
|
+
}
|
|
1223
|
+
const q = c.req.query('q') || '';
|
|
1224
|
+
if (!q.trim())
|
|
1225
|
+
return c.json({ users: [] });
|
|
1226
|
+
const result = listUsers({ query: q.trim(), status: 'active', pageSize: 10 });
|
|
1227
|
+
const existingIds = new Set(getGroupMembers(group.folder).map((m) => m.user_id));
|
|
1228
|
+
const users = result.users
|
|
1229
|
+
.filter((u) => !existingIds.has(u.id))
|
|
1230
|
+
.map((u) => ({
|
|
1231
|
+
id: u.id,
|
|
1232
|
+
username: u.username,
|
|
1233
|
+
display_name: u.display_name,
|
|
1234
|
+
}));
|
|
1235
|
+
return c.json({ users });
|
|
1236
|
+
});
|
|
1237
|
+
// POST /api/groups/:jid/members - 添加成员
|
|
1238
|
+
groupRoutes.post('/:jid/members', authMiddleware, async (c) => {
|
|
1239
|
+
const jid = c.req.param('jid');
|
|
1240
|
+
const group = getRegisteredGroup(jid);
|
|
1241
|
+
if (!group)
|
|
1242
|
+
return c.json({ error: 'Group not found' }, 404);
|
|
1243
|
+
const authUser = c.get('user');
|
|
1244
|
+
if (!canManageGroupMembers({ id: authUser.id, role: authUser.role }, group)) {
|
|
1245
|
+
return c.json({ error: 'Insufficient permissions' }, 403);
|
|
1246
|
+
}
|
|
1247
|
+
if (group.is_home) {
|
|
1248
|
+
return c.json({ error: 'Cannot add members to home groups' }, 400);
|
|
1249
|
+
}
|
|
1250
|
+
const body = await c.req.json().catch(() => ({}));
|
|
1251
|
+
const validation = GroupMemberAddSchema.safeParse(body);
|
|
1252
|
+
if (!validation.success) {
|
|
1253
|
+
return c.json({ error: 'Invalid request body' }, 400);
|
|
1254
|
+
}
|
|
1255
|
+
const { user_id: targetUserId } = validation.data;
|
|
1256
|
+
// Check target user exists and is active
|
|
1257
|
+
const targetUser = getUserById(targetUserId);
|
|
1258
|
+
if (!targetUser || targetUser.status !== 'active') {
|
|
1259
|
+
return c.json({ error: 'User not found or inactive' }, 404);
|
|
1260
|
+
}
|
|
1261
|
+
// Check if already a member
|
|
1262
|
+
const existingRole = getGroupMemberRole(group.folder, targetUserId);
|
|
1263
|
+
if (existingRole !== null) {
|
|
1264
|
+
return c.json({ error: 'User is already a member' }, 409);
|
|
1265
|
+
}
|
|
1266
|
+
addGroupMember(group.folder, targetUserId, 'member', authUser.id);
|
|
1267
|
+
invalidateAllowedUserCache(jid);
|
|
1268
|
+
logger.info({ jid, folder: group.folder, targetUserId, addedBy: authUser.id }, 'Group member added');
|
|
1269
|
+
const members = getGroupMembers(group.folder);
|
|
1270
|
+
return c.json({ success: true, members });
|
|
1271
|
+
});
|
|
1272
|
+
// DELETE /api/groups/:jid/members/:userId - 移除成员
|
|
1273
|
+
groupRoutes.delete('/:jid/members/:userId', authMiddleware, (c) => {
|
|
1274
|
+
const jid = c.req.param('jid');
|
|
1275
|
+
const targetUserId = c.req.param('userId');
|
|
1276
|
+
const group = getRegisteredGroup(jid);
|
|
1277
|
+
if (!group)
|
|
1278
|
+
return c.json({ error: 'Group not found' }, 404);
|
|
1279
|
+
const authUser = c.get('user');
|
|
1280
|
+
// Self-removal: any member can leave
|
|
1281
|
+
const isSelfRemoval = targetUserId === authUser.id;
|
|
1282
|
+
if (!isSelfRemoval) {
|
|
1283
|
+
if (!canManageGroupMembers({ id: authUser.id, role: authUser.role }, group)) {
|
|
1284
|
+
return c.json({ error: 'Insufficient permissions' }, 403);
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
1287
|
+
// Check target is actually a member
|
|
1288
|
+
const targetRole = getGroupMemberRole(group.folder, targetUserId);
|
|
1289
|
+
if (targetRole === null) {
|
|
1290
|
+
return c.json({ error: 'User is not a member' }, 404);
|
|
1291
|
+
}
|
|
1292
|
+
// Owner cannot be removed
|
|
1293
|
+
if (targetRole === 'owner') {
|
|
1294
|
+
return c.json({ error: 'Cannot remove the owner' }, 400);
|
|
1295
|
+
}
|
|
1296
|
+
removeGroupMember(group.folder, targetUserId);
|
|
1297
|
+
invalidateAllowedUserCache(jid);
|
|
1298
|
+
logger.info({
|
|
1299
|
+
jid,
|
|
1300
|
+
folder: group.folder,
|
|
1301
|
+
targetUserId,
|
|
1302
|
+
removedBy: authUser.id,
|
|
1303
|
+
isSelfRemoval,
|
|
1304
|
+
}, 'Group member removed');
|
|
1305
|
+
const members = getGroupMembers(group.folder);
|
|
1306
|
+
return c.json({ success: true, members });
|
|
1307
|
+
});
|
|
1308
|
+
// --- MCP Configuration Routes ---
|
|
1309
|
+
// GET /api/groups/:jid/mcp - 获取工作区 MCP 配置
|
|
1310
|
+
groupRoutes.get('/:jid/mcp', authMiddleware, (c) => {
|
|
1311
|
+
const jid = c.req.param('jid');
|
|
1312
|
+
const group = getRegisteredGroup(jid);
|
|
1313
|
+
if (!group)
|
|
1314
|
+
return c.json({ error: 'Group not found' }, 404);
|
|
1315
|
+
const authUser = c.get('user');
|
|
1316
|
+
if (!canAccessGroup({ id: authUser.id, role: authUser.role }, group)) {
|
|
1317
|
+
return c.json({ error: 'Group not found' }, 404);
|
|
1318
|
+
}
|
|
1319
|
+
return c.json({
|
|
1320
|
+
mcp_mode: group.mcp_mode ?? 'inherit',
|
|
1321
|
+
selected_mcps: group.selected_mcps ?? null,
|
|
1322
|
+
});
|
|
1323
|
+
});
|
|
1324
|
+
// PUT /api/groups/:jid/mcp - 更新工作区 MCP 配置
|
|
1325
|
+
groupRoutes.put('/:jid/mcp', authMiddleware, async (c) => {
|
|
1326
|
+
const jid = c.req.param('jid');
|
|
1327
|
+
const group = getRegisteredGroup(jid);
|
|
1328
|
+
if (!group)
|
|
1329
|
+
return c.json({ error: 'Group not found' }, 404);
|
|
1330
|
+
const authUser = c.get('user');
|
|
1331
|
+
if (!canAccessGroup({ id: authUser.id, role: authUser.role }, group)) {
|
|
1332
|
+
return c.json({ error: 'Group not found' }, 404);
|
|
1333
|
+
}
|
|
1334
|
+
const body = await c.req.json().catch(() => ({}));
|
|
1335
|
+
const mcp_mode = body.mcp_mode;
|
|
1336
|
+
const selected_mcps = body.selected_mcps;
|
|
1337
|
+
// Validate mcp_mode
|
|
1338
|
+
if (mcp_mode !== undefined &&
|
|
1339
|
+
mcp_mode !== 'inherit' &&
|
|
1340
|
+
mcp_mode !== 'custom') {
|
|
1341
|
+
return c.json({ error: 'Invalid mcp_mode' }, 400);
|
|
1342
|
+
}
|
|
1343
|
+
// Validate selected_mcps
|
|
1344
|
+
if (selected_mcps !== undefined && selected_mcps !== null) {
|
|
1345
|
+
if (!Array.isArray(selected_mcps)) {
|
|
1346
|
+
return c.json({ error: 'selected_mcps must be an array' }, 400);
|
|
1347
|
+
}
|
|
1348
|
+
for (const mcp of selected_mcps) {
|
|
1349
|
+
if (typeof mcp !== 'string') {
|
|
1350
|
+
return c.json({ error: 'selected_mcps must contain strings' }, 400);
|
|
1351
|
+
}
|
|
1352
|
+
}
|
|
1353
|
+
}
|
|
1354
|
+
// Update the group
|
|
1355
|
+
const updatedGroup = {
|
|
1356
|
+
...group,
|
|
1357
|
+
mcp_mode: mcp_mode ?? group.mcp_mode ?? 'inherit',
|
|
1358
|
+
selected_mcps: selected_mcps !== undefined ? selected_mcps : group.selected_mcps,
|
|
1359
|
+
};
|
|
1360
|
+
setRegisteredGroup(jid, updatedGroup);
|
|
1361
|
+
return c.json({
|
|
1362
|
+
success: true,
|
|
1363
|
+
mcp_mode: updatedGroup.mcp_mode,
|
|
1364
|
+
selected_mcps: updatedGroup.selected_mcps,
|
|
1365
|
+
});
|
|
1366
|
+
});
|
|
1367
|
+
export default groupRoutes;
|