@yancyyu/openhermit 1.6.42 → 1.6.43
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/README.md +98 -89
- package/bin/hermit.mjs +96 -0
- package/dist-renderer/assets/{ProjectEditorOverlay-DlFQ6mai.js → ProjectEditorOverlay-C98qSs7-.js} +1 -1
- package/dist-renderer/assets/{TeamGraphOverlay-D2TPMPGR.js → TeamGraphOverlay-CsBbZwcL.js} +1 -1
- package/dist-renderer/assets/{_basePickBy-Cmd0RHLQ.js → _basePickBy-ZOyLWjMK.js} +1 -1
- package/dist-renderer/assets/{_baseUniq-BI_iy8ea.js → _baseUniq-DBb726rt.js} +1 -1
- package/dist-renderer/assets/{arc-NzW2mjTP.js → arc-CdiTaR_R.js} +1 -1
- package/dist-renderer/assets/{architectureDiagram-VXUJARFQ-Bzq85AYv.js → architectureDiagram-VXUJARFQ-Cz3sc5TH.js} +1 -1
- package/dist-renderer/assets/{blockDiagram-VD42YOAC-D1PvYS-b.js → blockDiagram-VD42YOAC-DE4c-KJ3.js} +1 -1
- package/dist-renderer/assets/{c4Diagram-YG6GDRKO-D49RKzPC.js → c4Diagram-YG6GDRKO-CmTMDTrV.js} +1 -1
- package/dist-renderer/assets/channel-KTpqi9eT.js +1 -0
- package/dist-renderer/assets/{chunk-4BX2VUAB-fmI_MQmQ.js → chunk-4BX2VUAB-rhHy3tFl.js} +1 -1
- package/dist-renderer/assets/{chunk-55IACEB6-Xsv9RCXZ.js → chunk-55IACEB6-fLZBzuo_.js} +1 -1
- package/dist-renderer/assets/{chunk-B4BG7PRW-BE1KO8Um.js → chunk-B4BG7PRW-DOzxQhim.js} +1 -1
- package/dist-renderer/assets/{chunk-DI55MBZ5-tqJ7Mv7f.js → chunk-DI55MBZ5-COQCcXC5.js} +1 -1
- package/dist-renderer/assets/{chunk-FMBD7UC4-DMD45MVJ.js → chunk-FMBD7UC4-IKU9U_Y4.js} +1 -1
- package/dist-renderer/assets/{chunk-QN33PNHL-DOhGrz-q.js → chunk-QN33PNHL-D6WV154X.js} +1 -1
- package/dist-renderer/assets/{chunk-QZHKN3VN-D8yDgJdD.js → chunk-QZHKN3VN-D90_2DQp.js} +1 -1
- package/dist-renderer/assets/{chunk-TZMSLE5B-BcsEDu7A.js → chunk-TZMSLE5B-BQEil57G.js} +1 -1
- package/dist-renderer/assets/classDiagram-2ON5EDUG-lpzulY5X.js +1 -0
- package/dist-renderer/assets/classDiagram-v2-WZHVMYZB-lpzulY5X.js +1 -0
- package/dist-renderer/assets/clone-CriGymY9.js +1 -0
- package/dist-renderer/assets/{cose-bilkent-S5V4N54A-DlSqGHMX.js → cose-bilkent-S5V4N54A-6WiK6U2P.js} +1 -1
- package/dist-renderer/assets/{dagre-6UL2VRFP-BTT9tSAx.js → dagre-6UL2VRFP-DF4MMuTn.js} +1 -1
- package/dist-renderer/assets/{diagram-PSM6KHXK-Du-U-mK2.js → diagram-PSM6KHXK-CcF1eZ7E.js} +1 -1
- package/dist-renderer/assets/{diagram-QEK2KX5R-jFdHeKas.js → diagram-QEK2KX5R-DYlOVPQB.js} +1 -1
- package/dist-renderer/assets/{diagram-S2PKOQOG-DKLNK2bu.js → diagram-S2PKOQOG-BHXWsZOP.js} +1 -1
- package/dist-renderer/assets/{erDiagram-Q2GNP2WA-CZxHgIIo.js → erDiagram-Q2GNP2WA-GjmuBx8d.js} +1 -1
- package/dist-renderer/assets/{flowDiagram-NV44I4VS-v4XStCD0.js → flowDiagram-NV44I4VS-BuS7YVHk.js} +1 -1
- package/dist-renderer/assets/{ganttDiagram-JELNMOA3-DJjD_BEL.js → ganttDiagram-JELNMOA3-3Teu5tAa.js} +1 -1
- package/dist-renderer/assets/{gitGraphDiagram-V2S2FVAM-BNy-jr03.js → gitGraphDiagram-V2S2FVAM-BiLdCYu5.js} +1 -1
- package/dist-renderer/assets/{graph-DDTrn6je.js → graph-CDP_R8ct.js} +1 -1
- package/dist-renderer/assets/{index-BBp78BAu.js → index-BSZdT-g-.js} +1 -1
- package/dist-renderer/assets/{index-eotrJaYy.js → index-BhWvMqsz.js} +1 -1
- package/dist-renderer/assets/{index-D8_B-cfs.js → index-C2_AupSj.js} +1 -1
- package/dist-renderer/assets/{index-BQrwHZ-k.js → index-C5ujiwAR.js} +580 -588
- package/dist-renderer/assets/index-CIS2CTK9.css +1 -0
- package/dist-renderer/assets/{index-CRKQSG9S.js → index-CVNjLwkq.js} +1 -1
- package/dist-renderer/assets/{index-DR6Wz52b.js → index-CwG3se0q.js} +1 -1
- package/dist-renderer/assets/{infoDiagram-HS3SLOUP-DqnOsuza.js → infoDiagram-HS3SLOUP-DLHUFo72.js} +1 -1
- package/dist-renderer/assets/{journeyDiagram-XKPGCS4Q-DTobaO1d.js → journeyDiagram-XKPGCS4Q-BE07RpJD.js} +1 -1
- package/dist-renderer/assets/{kanban-definition-3W4ZIXB7-HbwVOvWc.js → kanban-definition-3W4ZIXB7-DDHZy4NB.js} +1 -1
- package/dist-renderer/assets/{layout--VYmTcw2.js → layout-5nA5wUxO.js} +1 -1
- package/dist-renderer/assets/{linear-BsJh89Mr.js → linear-BtF1i2qN.js} +1 -1
- package/dist-renderer/assets/{mindmap-definition-VGOIOE7T-BZqUZePd.js → mindmap-definition-VGOIOE7T-Z1Ui9Sqy.js} +1 -1
- package/dist-renderer/assets/{pieDiagram-ADFJNKIX-B1q_nH6P.js → pieDiagram-ADFJNKIX-LCjxckWv.js} +1 -1
- package/dist-renderer/assets/{quadrantDiagram-AYHSOK5B-UD8QhSEu.js → quadrantDiagram-AYHSOK5B-BOwKjSco.js} +1 -1
- package/dist-renderer/assets/{requirementDiagram-UZGBJVZJ-BA_i7Nw8.js → requirementDiagram-UZGBJVZJ-pChP8Znd.js} +1 -1
- package/dist-renderer/assets/{sankeyDiagram-TZEHDZUN-CMTnX-2d.js → sankeyDiagram-TZEHDZUN-DifZ2qpo.js} +1 -1
- package/dist-renderer/assets/{sequenceDiagram-WL72ISMW-BQXDB615.js → sequenceDiagram-WL72ISMW-CJg-WYyY.js} +1 -1
- package/dist-renderer/assets/{splashScene-D0YB9uxm.js → splashScene-94xWCzLA.js} +1 -1
- package/dist-renderer/assets/{stateDiagram-FKZM4ZOC-BAsPXy6X.js → stateDiagram-FKZM4ZOC-DWHOoFdv.js} +1 -1
- package/dist-renderer/assets/stateDiagram-v2-4FDKWEC3-CGYZOoMb.js +1 -0
- package/dist-renderer/assets/{timeline-definition-IT6M3QCI-BdasmVkC.js → timeline-definition-IT6M3QCI-CPgokIo8.js} +1 -1
- package/dist-renderer/assets/{treemap-GDKQZRPO-BkKQqIui.js → treemap-GDKQZRPO-DAVqSR9L.js} +1 -1
- package/dist-renderer/assets/{xychartDiagram-PRI3JC2R-EAlPHOdx.js → xychartDiagram-PRI3JC2R-CCOcGbrD.js} +1 -1
- package/dist-renderer/chat-community-qr.jpg +0 -0
- package/dist-renderer/fonts/Agave-Bold.ttf +0 -0
- package/dist-renderer/fonts/Agave-Regular.ttf +0 -0
- package/dist-renderer/icon.png +0 -0
- package/dist-renderer/icon.rar +0 -0
- package/dist-renderer/index.html +3 -3
- package/package.json +21 -26
- package/src/features/worker-society/core/application/WorkerSocietyService.test.ts +802 -0
- package/src/features/worker-society/core/application/WorkerSocietyService.ts +428 -0
- package/src/features/worker-society/core/application/fakes.ts +101 -0
- package/src/features/worker-society/core/application/ports.ts +70 -0
- package/src/features/worker-society/core/domain/models/society.ts +141 -0
- package/src/features/worker-society/core/domain/policies/societyPolicies.test.ts +739 -0
- package/src/features/worker-society/core/domain/policies/societyPolicies.ts +496 -0
- package/src/features/worker-society/main/adapters/input/societyMcp.test.ts +317 -0
- package/src/features/worker-society/main/adapters/input/societyMcp.ts +257 -0
- package/src/features/worker-society/main/adapters/input/societyRoutes.test.ts +695 -0
- package/src/features/worker-society/main/adapters/input/societyRoutes.ts +194 -0
- package/src/features/worker-society/main/composition/societyComposition.test.ts +74 -0
- package/src/features/worker-society/main/composition/societyComposition.ts +70 -0
- package/src/features/worker-society/main/composition/workerSocietyPlugin.test.ts +69 -0
- package/src/features/worker-society/main/composition/workerSocietyPlugin.ts +67 -0
- package/src/features/worker-society/main/infrastructure/crossTeamMessageGateway.test.ts +132 -0
- package/src/features/worker-society/main/infrastructure/crossTeamMessageGateway.ts +84 -0
- package/src/features/worker-society/main/infrastructure/fsStores.test.ts +216 -0
- package/src/features/worker-society/main/infrastructure/fsStores.ts +113 -0
- package/src/features/worker-society/main/infrastructure/mergingProfileStore.test.ts +195 -0
- package/src/features/worker-society/main/infrastructure/mergingProfileStore.ts +96 -0
- package/src/features/worker-society/renderer/SocietyGraph.tsx +166 -0
- package/src/features/worker-society/renderer/SocietyNodeLabels.tsx +139 -0
- package/src/features/worker-society/renderer/SocietyNodeOverlay.tsx +339 -0
- package/src/features/worker-society/renderer/SocietyView.tsx +437 -0
- package/src/features/worker-society/renderer/index.ts +11 -0
- package/src/features/worker-society/renderer/societyApi.test.ts +259 -0
- package/src/features/worker-society/renderer/societyApi.ts +144 -0
- package/src/features/worker-society/renderer/societyGraphAdapter.test.ts +321 -0
- package/src/features/worker-society/renderer/societyGraphAdapter.ts +240 -0
- package/src/features/worker-society/renderer/societyOverlayActions.test.ts +57 -0
- package/src/features/worker-society/renderer/societyOverlayActions.ts +49 -0
- package/src/features/worker-society/renderer/societyStore.test.ts +218 -0
- package/src/features/worker-society/renderer/societyStore.ts +146 -0
- package/src/features/worker-society/renderer/societyViewUtils.test.ts +81 -0
- package/src/features/worker-society/renderer/societyViewUtils.ts +68 -0
- package/src/main/ipc/extensions.ts +27 -0
- package/src/main/server.ts +1709 -534
- package/src/main/services/ccConnect/CcConnectBridge.ts +26 -11
- package/src/main/services/ccConnect/CcConnectClient.ts +9 -2
- package/src/main/services/ccConnect/workDirReconcile.test.ts +57 -0
- package/src/main/services/ccConnect/workDirReconcile.ts +36 -0
- package/src/main/services/direct-cli/DirectCliSessionManager.test.ts +397 -0
- package/src/main/services/direct-cli/DirectCliSessionManager.ts +508 -0
- package/src/main/services/direct-cli/DirectCliSessionStore.test.ts +79 -0
- package/src/main/services/direct-cli/DirectCliSessionStore.ts +97 -0
- package/src/main/services/direct-cli/__tests__/directCliMessageId.test.ts +40 -0
- package/src/main/services/direct-cli/directCliMessageId.ts +21 -0
- package/src/main/services/direct-cli/index.ts +17 -0
- package/src/main/services/extensions/capability-packs/CapabilityPackLoaderService.ts +637 -0
- package/src/main/services/extensions/catalog/PluginCatalogService.ts +2 -2
- package/src/main/services/loop-assets/LoopAssetsScannerService.ts +657 -0
- package/src/main/services/runtime/providerAwareCliEnv.ts +33 -5
- package/src/main/services/session-intelligence/LocalSessionScanner.ts +156 -71
- package/src/main/services/session-intelligence/SessionUsageParser.ts +103 -8
- package/src/main/services/session-intelligence/UsageTelemetryService.ts +11 -0
- package/src/main/services/session-intelligence/__tests__/teamSessionListMapper.test.ts +104 -0
- package/src/main/services/session-intelligence/teamSessionListMapper.ts +78 -0
- package/src/main/services/system-manager/AdminLoopInitializer.ts +95 -0
- package/src/main/services/system-manager/BuiltinWorkflowSeeder.ts +679 -74
- package/src/main/services/system-manager/SystemManagerConfigService.ts +19 -1
- package/src/main/services/system-manager/WorkflowPromptService.ts +58 -5
- package/src/main/services/system-manager/__tests__/AdminLoopInitializer.test.ts +129 -0
- package/src/main/services/system-manager/__tests__/SystemManagerConfigService.test.ts +60 -0
- package/src/main/services/teams-mvp/CollaborationBoardService.ts +2 -0
- package/src/main/services/teams-mvp/OpsRunbookContext.ts +60 -0
- package/src/main/services/teams-mvp/TaskDispatchService.test.ts +305 -0
- package/src/main/services/teams-mvp/TaskDispatchService.ts +250 -131
- package/src/main/services/teams-mvp/TeamProvisioningService.ts +12 -2
- package/src/main/services/teams-mvp/TeamWorkspaceService.test.ts +207 -0
- package/src/main/services/teams-mvp/TeamWorkspaceService.ts +104 -51
- package/src/main/services/teams-mvp/index.ts +6 -0
- package/src/main/utils/externalPlatformSessionRouting.ts +92 -0
- package/src/main/utils/toolApprovalRules.ts +151 -0
- package/src/renderer/App.tsx +24 -89
- package/src/renderer/api/httpClient.ts +115 -37
- package/src/renderer/api/providers.ts +5 -16
- package/src/renderer/components/chat/CommunityChatView.tsx +81 -0
- package/src/renderer/components/dashboard/DashboardView.tsx +130 -84
- package/src/renderer/components/extensions/ExtensionStoreView.tsx +39 -5
- package/src/renderer/components/extensions/ExtensionsSubTabTrigger.tsx +2 -1
- package/src/renderer/components/extensions/capability-packs/CapabilityPacksPanel.tsx +170 -0
- package/src/renderer/components/layout/PaneContent.tsx +10 -2
- package/src/renderer/components/layout/SortableTab.tsx +4 -0
- package/src/renderer/components/layout/TabBarActions.tsx +13 -16
- package/src/renderer/components/runtime/ProviderRuntimeSettingsDialog.tsx +4 -135
- package/src/renderer/components/schedules/SchedulesView.tsx +22 -14
- package/src/renderer/components/schedules/calendar/CalendarEventBlock.tsx +7 -6
- package/src/renderer/components/settings/SettingsTabs.tsx +24 -21
- package/src/renderer/components/settings/SettingsView.tsx +22 -13
- package/src/renderer/components/settings/components/SettingRow.tsx +13 -5
- package/src/renderer/components/settings/components/SettingsSectionCard.tsx +53 -0
- package/src/renderer/components/settings/components/SettingsSectionHeader.tsx +10 -6
- package/src/renderer/components/settings/components/SettingsSelect.tsx +12 -9
- package/src/renderer/components/settings/components/SettingsToggle.tsx +6 -5
- package/src/renderer/components/settings/components/index.ts +1 -0
- package/src/renderer/components/settings/sections/AdvancedSection.tsx +78 -59
- package/src/renderer/components/settings/sections/CliStatusSection.tsx +32 -44
- package/src/renderer/components/settings/sections/ConfigEditorDialog.tsx +1 -1
- package/src/renderer/components/settings/sections/GeneralSection.tsx +216 -186
- package/src/renderer/components/settings/sections/PlatformsSection.tsx +25 -17
- package/src/renderer/components/settings/sections/TaskBusSection.tsx +63 -22
- package/src/renderer/components/sidebar/SidebarSessions.tsx +120 -80
- package/src/renderer/components/sidebar/SidebarTaskItem.tsx +1 -1
- package/src/renderer/components/splash/splashScene.ts +6 -2
- package/src/renderer/components/system-manager/SystemManagerView.tsx +169 -255
- package/src/renderer/components/tasks/TasksView.tsx +63 -37
- package/src/renderer/components/team/CcSessionsSection.tsx +124 -89
- package/src/renderer/components/team/HarnessBrandLogos.tsx +318 -0
- package/src/renderer/components/team/HarnessSelect.tsx +25 -26
- package/src/renderer/components/team/TeamDetailView.tsx +137 -153
- package/src/renderer/components/team/TeamEmptyState.tsx +9 -37
- package/src/renderer/components/team/TeamListView.tsx +143 -30
- package/src/renderer/components/team/__tests__/CcSessionsSection.hasLocalFile.test.tsx +128 -0
- package/src/renderer/components/team/activity/ActivityItem.tsx +21 -9
- package/src/renderer/components/team/activity/ActivityTimeline.tsx +2 -2
- package/src/renderer/components/team/dialogs/AdvancedCliSection.tsx +1 -1
- package/src/renderer/components/team/dialogs/CreateTaskDialog.tsx +13 -10
- package/src/renderer/components/team/dialogs/CreateTeamDialog.tsx +156 -83
- package/src/renderer/components/team/dialogs/EditTeamDialog.tsx +9 -157
- package/src/renderer/components/team/dialogs/LaunchTeamDialog.tsx +19 -15
- package/src/renderer/components/team/dialogs/PlatformBindingDialog.tsx +48 -10
- package/src/renderer/components/team/dialogs/PlatformManualForm.tsx +11 -12
- package/src/renderer/components/team/dialogs/PlatformSetupQR.tsx +39 -37
- package/src/renderer/components/team/dialogs/RuntimeConfigDialog.tsx +434 -64
- package/src/renderer/components/team/dialogs/SendMessageDialog.tsx +12 -10
- package/src/renderer/components/team/dialogs/TaskDetailDialog.tsx +2 -2
- package/src/renderer/components/team/dialogs/__tests__/CreateTeamDialog.bindProject.test.tsx +399 -0
- package/src/renderer/components/team/dialogs/__tests__/CreateTeamDialog.chineseRepro.test.tsx +253 -0
- package/src/renderer/components/team/dialogs/platformAllowUtils.ts +91 -0
- package/src/renderer/components/team/dialogs/teammateRuntimeCompatibility.tsx +1 -1
- package/src/renderer/components/team/kanban/KanbanTaskCard.test.tsx +41 -0
- package/src/renderer/components/team/kanban/KanbanTaskCard.tsx +41 -86
- package/src/renderer/components/team/loop-console/LoopCommandComposer.tsx +310 -0
- package/src/renderer/components/team/loop-console/LoopConsolePanel.tsx +372 -0
- package/src/renderer/components/team/loop-console/loopSendIntent.test.ts +85 -0
- package/src/renderer/components/team/loop-console/loopSendIntent.ts +221 -0
- package/src/renderer/components/team/loop-console/useLeadSessionToolActivity.ts +74 -0
- package/src/renderer/components/team/loop-console/useLoopCommandSuggestions.ts +165 -0
- package/src/renderer/components/team/loop-console/useLoopConsoleController.ts +266 -0
- package/src/renderer/components/team/members/LeadModelRow.test.tsx +1 -1
- package/src/renderer/components/team/members/LeadModelRow.tsx +5 -3
- package/src/renderer/components/team/members/MemberDetailDialog.tsx +11 -0
- package/src/renderer/components/team/members/MemberDetailStats.tsx +13 -3
- package/src/renderer/components/team/members/MemberDraftRow.test.tsx +1 -1
- package/src/renderer/components/team/members/MemberDraftRow.tsx +1 -1
- package/src/renderer/components/team/members/MemberMessagesTab.tsx +2 -2
- package/src/renderer/components/team/members/MemberStatsTab.tsx +1 -1
- package/src/renderer/components/team/messages/MessageComposer.tsx +150 -44
- package/src/renderer/components/team/messages/MessagesFilterPopover.tsx +2 -2
- package/src/renderer/components/team/messages/MessagesPanel.tsx +34 -28
- package/src/renderer/components/team/schedule/CcCronScheduleDialog.tsx +6 -6
- package/src/renderer/components/team/taskLogs/ExactTaskLogCard.tsx +2 -2
- package/src/renderer/components/team/taskLogs/TaskLogStreamSection.tsx +1 -1
- package/src/renderer/components/terminal/TerminalPanel.tsx +2 -3
- package/src/renderer/components/ui/MentionableTextarea.tsx +5 -1
- package/src/renderer/constants/teamColors.ts +5 -5
- package/src/renderer/hooks/useExtensionsTabState.ts +1 -1
- package/src/renderer/hooks/useMentionDetection.ts +5 -1
- package/src/renderer/hooks/useProjectWorkflowCommands.ts +57 -0
- package/src/renderer/hooks/useTeamSuggestions.ts +2 -0
- package/src/renderer/index.css +19 -2
- package/src/renderer/main.tsx +7 -1
- package/src/renderer/store/index.ts +18 -1
- package/src/renderer/store/slices/extensionsSlice.ts +83 -0
- package/src/renderer/store/slices/tabSlice.ts +61 -0
- package/src/renderer/store/slices/teamSlice.ts +138 -9
- package/src/renderer/types/mention.ts +8 -0
- package/src/renderer/types/tabs.ts +3 -1
- package/src/renderer/utils/__tests__/bindProjectSlug.test.ts +69 -0
- package/src/renderer/utils/__tests__/groupTransformer.test.ts +148 -0
- package/src/renderer/utils/__tests__/initialRoute.test.ts +101 -0
- package/src/renderer/utils/__tests__/leadToolActivity.test.ts +124 -0
- package/src/renderer/utils/__tests__/mergeTeamMessages.test.ts +81 -0
- package/src/renderer/utils/__tests__/teamMessageFiltering.test.ts +213 -0
- package/src/renderer/utils/__tests__/teamMessageKey.test.ts +75 -0
- package/src/renderer/utils/__tests__/workflowCommandExecution.test.ts +173 -0
- package/src/renderer/utils/__tests__/workflowCommandSuggestions.test.ts +59 -0
- package/src/renderer/utils/bindProjectSlug.ts +57 -0
- package/src/renderer/utils/capabilityCommandExecution.ts +113 -0
- package/src/renderer/utils/initialRoute.ts +89 -0
- package/src/renderer/utils/leadToolActivity.ts +117 -0
- package/src/renderer/utils/loopShortcutSuggestions.ts +106 -0
- package/src/renderer/utils/mentionSuggestions.ts +1 -1
- package/src/renderer/utils/slashCommandRegistry.ts +231 -0
- package/src/renderer/utils/teamMentionDirective.ts +31 -0
- package/src/renderer/utils/workflowCommandExecution.ts +96 -0
- package/src/renderer/utils/workflowCommandSuggestions.ts +49 -0
- package/src/shared/types/api.ts +79 -4
- package/src/shared/types/ccConnect.ts +1 -0
- package/src/shared/types/extensions/api.ts +19 -0
- package/src/shared/types/extensions/capabilityPack.ts +118 -0
- package/src/shared/types/extensions/index.ts +29 -1
- package/src/shared/types/index.ts +6 -0
- package/src/shared/types/loopAssets.ts +54 -0
- package/src/shared/types/providers.ts +0 -16
- package/src/shared/types/systemManager.ts +26 -1
- package/src/shared/types/team.ts +41 -5
- package/src/shared/types/terminal.ts +2 -36
- package/src/shared/types/worker.test.ts +28 -0
- package/src/shared/types/worker.ts +3 -0
- package/src/shared/utils/__tests__/effortLevels.test.ts +88 -0
- package/src/shared/utils/__tests__/providerBackend.test.ts +88 -0
- package/src/shared/utils/__tests__/providerLaunchArgs.test.ts +220 -0
- package/src/shared/utils/claudeStreamJson.test.ts +187 -0
- package/src/shared/utils/claudeStreamJson.ts +153 -0
- package/src/shared/utils/providerLaunchArgs.ts +217 -0
- package/src/shared/utils/slashCommands.ts +10 -0
- package/src/types/node-pty.d.ts +8 -0
- package/dist-renderer/assets/channel-Ch7JrfUu.js +0 -1
- package/dist-renderer/assets/classDiagram-2ON5EDUG-z9I4AnFy.js +0 -1
- package/dist-renderer/assets/classDiagram-v2-WZHVMYZB-z9I4AnFy.js +0 -1
- package/dist-renderer/assets/clone-Dfi1Jx6l.js +0 -1
- package/dist-renderer/assets/index-iyjkpSus.css +0 -32
- package/dist-renderer/assets/stateDiagram-v2-4FDKWEC3-DTUIBfce.js +0 -1
- package/src/main/services/system-manager/SystemManagerPtyService.ts +0 -233
- package/src/renderer/components/common/TerminalPane.tsx +0 -213
- package/src/renderer/components/team/dialogs/useTeamEditForm.ts +0 -292
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import { afterAll, afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
|
|
6
|
+
import { TeamWorkspaceService } from './TeamWorkspaceService';
|
|
7
|
+
import type { GroupMessage } from './TeamWorkspaceService';
|
|
8
|
+
|
|
9
|
+
// CLAUDE.md 把 "server-side appendMessage ID propagation" 列为零覆盖的关键回归路径:
|
|
10
|
+
// 去重管线按 id 去重,若 appendMessage 重新生成 id,同一条逻辑消息会得到不同 id →
|
|
11
|
+
// 重复消息回归。这里直测真实文件 IO 实现(TaskDispatchService.test.ts 用的是 FakeWorkspace,
|
|
12
|
+
// 完全绕过真实落盘)。
|
|
13
|
+
const PREV_HERMIT_HOME = process.env.HERMIT_HOME;
|
|
14
|
+
let tmpHome = '';
|
|
15
|
+
|
|
16
|
+
function svc() {
|
|
17
|
+
return new TeamWorkspaceService();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
afterAll(() => {
|
|
21
|
+
if (PREV_HERMIT_HOME === undefined) delete process.env.HERMIT_HOME;
|
|
22
|
+
else process.env.HERMIT_HOME = PREV_HERMIT_HOME;
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
beforeEach(() => {
|
|
26
|
+
tmpHome = path.join(
|
|
27
|
+
os.tmpdir(),
|
|
28
|
+
`hermit-tws-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`
|
|
29
|
+
);
|
|
30
|
+
process.env.HERMIT_HOME = tmpHome;
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
afterEach(async () => {
|
|
34
|
+
await fs.promises.rm(tmpHome, { recursive: true, force: true });
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
describe('TeamWorkspaceService.appendMessage — message pipeline', () => {
|
|
38
|
+
it('preserves a caller-supplied id (dedup guarantee: never regenerates)', async () => {
|
|
39
|
+
const out = await svc().appendMessage('alpha', { id: 'fixed-1', from: 'user', content: 'hi' });
|
|
40
|
+
expect(out.id).toBe('fixed-1');
|
|
41
|
+
// 第二条用相同显式 id 仍各自落盘(去重发生在渲染层,service 不负责丢弃)。
|
|
42
|
+
await svc().appendMessage('alpha', { id: 'fixed-1', from: 'user', content: 'dup' });
|
|
43
|
+
const msgs = await svc().readMessages('alpha');
|
|
44
|
+
expect(msgs.map((m) => m.id)).toEqual(['fixed-1', 'fixed-1']);
|
|
45
|
+
expect(msgs.map((m) => m.content)).toEqual(['hi', 'dup']);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('generates an m_-prefixed id when none is supplied', async () => {
|
|
49
|
+
const out = await svc().appendMessage('alpha', { from: 'agent-7', content: 'x' });
|
|
50
|
+
expect(out.id).toMatch(/^m_/);
|
|
51
|
+
expect(out.id.length).toBeGreaterThan(4);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('stamps a server-side ISO ts and defaults `to` to "team"', async () => {
|
|
55
|
+
const out = await svc().appendMessage('alpha', { from: 'user', content: 'x' });
|
|
56
|
+
expect(out.ts).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/);
|
|
57
|
+
expect(out.to).toBe('team');
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('derives role from `from` and honors an explicit role override', async () => {
|
|
61
|
+
const s = svc();
|
|
62
|
+
const user = await s.appendMessage('alpha', { from: 'user', content: 'u' });
|
|
63
|
+
const agent = await s.appendMessage('alpha', { from: 'agent-7', content: 'a' });
|
|
64
|
+
const system = await s.appendMessage('alpha', {
|
|
65
|
+
from: 'agent-7',
|
|
66
|
+
role: 'system',
|
|
67
|
+
content: 's',
|
|
68
|
+
});
|
|
69
|
+
expect(user.role).toBe('user');
|
|
70
|
+
expect(agent.role).toBe('agent');
|
|
71
|
+
expect(system.role).toBe('system');
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('passes meta through (null default + object preserved)', async () => {
|
|
75
|
+
const s = svc();
|
|
76
|
+
const noMeta = await s.appendMessage('alpha', { from: 'user', content: 'a' });
|
|
77
|
+
const withMeta = await s.appendMessage('alpha', {
|
|
78
|
+
from: 'user',
|
|
79
|
+
content: 'b',
|
|
80
|
+
meta: { taskId: 't_1', mentions: ['@d'] },
|
|
81
|
+
});
|
|
82
|
+
expect(noMeta.meta).toBeNull();
|
|
83
|
+
expect(withMeta.meta).toEqual({ taskId: 't_1', mentions: ['@d'] });
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('persists each message as its own newline-delimited JSONL line', async () => {
|
|
87
|
+
const s = svc();
|
|
88
|
+
await s.appendMessage('alpha', { id: 'a', from: 'user', content: 'first' });
|
|
89
|
+
await s.appendMessage('alpha', { id: 'b', from: 'user', content: 'second' });
|
|
90
|
+
const file = path.join(tmpHome, 'teams', 'alpha', 'messages', 'group.jsonl');
|
|
91
|
+
const raw = await fs.promises.readFile(file, 'utf8');
|
|
92
|
+
const lines = raw.split('\n').filter(Boolean);
|
|
93
|
+
expect(lines).toHaveLength(2);
|
|
94
|
+
expect(JSON.parse(lines[0]).id).toBe('a');
|
|
95
|
+
expect(JSON.parse(lines[1]).id).toBe('b');
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('readMessages round-trips appended messages sorted by ts', async () => {
|
|
99
|
+
const s = svc();
|
|
100
|
+
await s.appendMessage('alpha', { id: 'm1', from: 'user', content: 'one' });
|
|
101
|
+
await s.appendMessage('alpha', { id: 'm2', from: 'user', content: 'two' });
|
|
102
|
+
await s.appendMessage('alpha', { id: 'm3', from: 'user', content: 'three' });
|
|
103
|
+
const msgs = await s.readMessages('alpha');
|
|
104
|
+
expect(msgs.map((m) => m.id)).toEqual(['m1', 'm2', 'm3']);
|
|
105
|
+
for (let i = 1; i < msgs.length; i++) {
|
|
106
|
+
expect(msgs[i].ts.localeCompare(msgs[i - 1].ts)).toBeGreaterThanOrEqual(0);
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('readMessages returns [] for an external platform session slug', async () => {
|
|
111
|
+
await expect(svc().readMessages('feishu:some-session')).resolves.toEqual([]);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('appendMessage rejects an external platform session slug', async () => {
|
|
115
|
+
await expect(
|
|
116
|
+
svc().appendMessage('feishu:some-session', { from: 'user', content: 'x' })
|
|
117
|
+
).rejects.toThrow(/外部平台/);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('survives a corrupted JSONL line on read (skips unparseable lines)', async () => {
|
|
121
|
+
const s = svc();
|
|
122
|
+
await s.appendMessage('alpha', { id: 'good', from: 'user', content: 'ok' });
|
|
123
|
+
const file = path.join(tmpHome, 'teams', 'alpha', 'messages', 'group.jsonl');
|
|
124
|
+
await fs.promises.appendFile(file, '{ not valid json\n');
|
|
125
|
+
const msgs = await s.readMessages('alpha');
|
|
126
|
+
expect(msgs.map((m: GroupMessage) => m.id)).toEqual(['good']);
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
describe('TeamWorkspaceService task board', () => {
|
|
131
|
+
it('createTask requires a title', async () => {
|
|
132
|
+
await expect(svc().createTask('alpha', { title: '' } as never)).rejects.toThrow('title');
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('createTask assigns incrementing order within a column and round-trips', async () => {
|
|
136
|
+
const s = svc();
|
|
137
|
+
const t1 = await s.createTask('alpha', { title: 'A' });
|
|
138
|
+
const t2 = await s.createTask('alpha', { title: 'B' });
|
|
139
|
+
const t3 = await s.createTask('alpha', { title: 'C', status: 'done' });
|
|
140
|
+
expect(t1.id).toMatch(/^t_/);
|
|
141
|
+
expect(t1.order).toBe(0);
|
|
142
|
+
expect(t2.order).toBe(1); // same 'todo' column increments
|
|
143
|
+
expect(t3.order).toBe(0); // different column ('done') starts fresh
|
|
144
|
+
expect(t3.status).toBe('done');
|
|
145
|
+
const tasks = await s.readTasks('alpha');
|
|
146
|
+
expect(tasks.map((t) => t.title).sort()).toEqual(['A', 'B', 'C']);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('patchTask updates fields, bumps updatedAt, and pins id/teamSlug', async () => {
|
|
150
|
+
const s = svc();
|
|
151
|
+
const t = await s.createTask('alpha', { title: 'A' });
|
|
152
|
+
const patched = await s.patchTask('alpha', t.id, { status: 'doing', assignee: 'w1' });
|
|
153
|
+
expect(patched.status).toBe('doing');
|
|
154
|
+
expect(patched.assignee).toBe('w1');
|
|
155
|
+
expect(patched.id).toBe(t.id);
|
|
156
|
+
expect(patched.teamSlug).toBe('alpha');
|
|
157
|
+
expect(patched.updatedAt).not.toBe(t.updatedAt);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('patchTask throws for an unknown id', async () => {
|
|
161
|
+
await expect(svc().patchTask('alpha', 'nope', { status: 'done' })).rejects.toThrow('not found');
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it('deleteTask returns true when removed, false when absent, and persists', async () => {
|
|
165
|
+
const s = svc();
|
|
166
|
+
const t = await s.createTask('alpha', { title: 'A' });
|
|
167
|
+
expect(await s.deleteTask('alpha', 'nope')).toBe(false);
|
|
168
|
+
expect(await s.deleteTask('alpha', t.id)).toBe(true);
|
|
169
|
+
expect(await s.readTasks('alpha')).toEqual([]);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
// F-4 (gstack-QA TEAM-010-007): a cross-team dispatch writes a received task
|
|
173
|
+
// onto the TARGET team's board via createOrReuseReceivedTask → createTask. The
|
|
174
|
+
// target team's kanban (GET /api/teams/:name/tasks → readTasks) must reflect it
|
|
175
|
+
// — read and write share resolveStorageSlug + teamRoot, so this is the
|
|
176
|
+
// read-after-write guarantee that the reported "API returns []" contradicted.
|
|
177
|
+
// (The cited route /api/teams/:name/board does not exist; the real route is
|
|
178
|
+
// /tasks. This test pins target-side visibility with the dispatchMeta intact.)
|
|
179
|
+
it('a dispatched received task is visible on the target team board (dispatchMeta round-trip)', async () => {
|
|
180
|
+
const s = svc();
|
|
181
|
+
const dispatchMeta = {
|
|
182
|
+
dispatchId: 'loop-cross-team-test-1',
|
|
183
|
+
originTeam: 'team-jcve',
|
|
184
|
+
targetTeam: 'team-4',
|
|
185
|
+
status: 'received' as const,
|
|
186
|
+
dispatchedAt: '2026-06-14T08:26:32.118Z',
|
|
187
|
+
receivedAt: '2026-06-14T08:26:32.118Z',
|
|
188
|
+
};
|
|
189
|
+
const created = await s.createTask('team-4', {
|
|
190
|
+
title: '[TEAM-010-005] cross-team dispatch arrival',
|
|
191
|
+
description: '@team-4 ...',
|
|
192
|
+
status: 'todo',
|
|
193
|
+
dispatchMeta,
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
// Re-read from disk the same way the board endpoint does.
|
|
197
|
+
const tasks = await s.readTasks('team-4');
|
|
198
|
+
expect(tasks).toHaveLength(1);
|
|
199
|
+
expect(tasks[0].id).toBe(created.id);
|
|
200
|
+
expect(tasks[0].status).toBe('todo');
|
|
201
|
+
expect(tasks[0].dispatchMeta).toEqual(dispatchMeta);
|
|
202
|
+
|
|
203
|
+
// A second, unrelated read of a different team must NOT bleed the task over
|
|
204
|
+
// (guards against a slug-misroute false visible/invisible).
|
|
205
|
+
expect(await s.readTasks('team-jcve')).toEqual([]);
|
|
206
|
+
});
|
|
207
|
+
});
|
|
@@ -12,11 +12,11 @@
|
|
|
12
12
|
* └─ tasks/board.json # 任务看板
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
+
import { resolveExternalPlatformSessionTeamSlug } from '@main/utils/externalPlatformSessionRouting';
|
|
16
|
+
import { createLogger } from '@shared/utils/logger';
|
|
15
17
|
import * as fs from 'fs';
|
|
16
|
-
import * as path from 'path';
|
|
17
18
|
import * as os from 'os';
|
|
18
|
-
|
|
19
|
-
import { createLogger } from '@shared/utils/logger';
|
|
19
|
+
import * as path from 'path';
|
|
20
20
|
|
|
21
21
|
const logger = createLogger('TeamWorkspace');
|
|
22
22
|
|
|
@@ -33,7 +33,7 @@ export interface TeamManifest {
|
|
|
33
33
|
bindProject: string;
|
|
34
34
|
/** agent 类型,用于 MCP 配置注入等 harness 特定逻辑 */
|
|
35
35
|
harness: string;
|
|
36
|
-
/** agent 工作目录(cc-connect project work_dir) */
|
|
36
|
+
/** agent runtime 工作目录(cc-connect project work_dir) */
|
|
37
37
|
workDir: string;
|
|
38
38
|
color?: string;
|
|
39
39
|
description?: string;
|
|
@@ -91,6 +91,7 @@ export interface GroupMessage {
|
|
|
91
91
|
}
|
|
92
92
|
|
|
93
93
|
export interface AppendGroupMessageInput {
|
|
94
|
+
id?: string;
|
|
94
95
|
from: string;
|
|
95
96
|
to?: string;
|
|
96
97
|
role?: GroupMessage['role'];
|
|
@@ -136,6 +137,18 @@ export function toSlug(input: string, fallback = 'team'): string {
|
|
|
136
137
|
return ascii || fallback;
|
|
137
138
|
}
|
|
138
139
|
|
|
140
|
+
export function isValidBindProject(value: string): boolean {
|
|
141
|
+
return /^[a-z0-9][a-z0-9_-]*$/.test(value);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function assertValidBindProject(value: string): void {
|
|
145
|
+
if (!isValidBindProject(value)) {
|
|
146
|
+
throw new Error(
|
|
147
|
+
'bindProject must contain only lowercase ASCII letters, digits, hyphens, and underscores, and start with a letter or digit'
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
139
152
|
export function teamsRoot(): string {
|
|
140
153
|
return path.join(hermitHome(), 'teams');
|
|
141
154
|
}
|
|
@@ -144,6 +157,10 @@ export function teamRoot(teamSlug: string): string {
|
|
|
144
157
|
return path.join(teamsRoot(), teamSlug);
|
|
145
158
|
}
|
|
146
159
|
|
|
160
|
+
function isExternalPlatformSlug(teamSlug: string): boolean {
|
|
161
|
+
return /^(feishu|lark|weixin|telegram|discord|slack):/.test(teamSlug);
|
|
162
|
+
}
|
|
163
|
+
|
|
147
164
|
export function groupSessionKey(teamSlug: string): string {
|
|
148
165
|
return `hermit:${teamSlug}:session`;
|
|
149
166
|
}
|
|
@@ -179,16 +196,46 @@ async function writeJson(p: string, data: unknown): Promise<void> {
|
|
|
179
196
|
// ---------------------------------------------------------------------------
|
|
180
197
|
|
|
181
198
|
export class TeamWorkspaceService {
|
|
199
|
+
private async readTeamManifestByStorageSlug(storageSlug: string): Promise<TeamManifest> {
|
|
200
|
+
const root = teamRoot(storageSlug);
|
|
201
|
+
const manifest = await readJson<TeamManifest | null>(path.join(root, 'team.json'), null);
|
|
202
|
+
if (!manifest) {
|
|
203
|
+
if (!(await pathExists(root))) {
|
|
204
|
+
throw new Error(`团队 "${storageSlug}" 不存在 (${root})`);
|
|
205
|
+
}
|
|
206
|
+
const stat = await fs.promises.stat(root).catch(() => null);
|
|
207
|
+
return {
|
|
208
|
+
schemaVersion: 2,
|
|
209
|
+
slug: storageSlug,
|
|
210
|
+
displayName: storageSlug,
|
|
211
|
+
bindProject: storageSlug,
|
|
212
|
+
harness: 'claudecode',
|
|
213
|
+
workDir: '',
|
|
214
|
+
collaboration: true,
|
|
215
|
+
rootPath: root,
|
|
216
|
+
createdAt: (stat?.birthtime ?? stat?.mtime ?? new Date()).toISOString(),
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
return manifest;
|
|
220
|
+
}
|
|
221
|
+
|
|
182
222
|
private async resolveStorageSlug(teamSlug: string): Promise<string> {
|
|
183
223
|
if (await pathExists(path.join(teamRoot(teamSlug), 'team.json'))) {
|
|
184
224
|
return teamSlug;
|
|
185
225
|
}
|
|
186
|
-
const
|
|
187
|
-
|
|
226
|
+
const teams = await this.listTeams();
|
|
227
|
+
const directMatch = teams.find((manifest) => manifest.bindProject === teamSlug);
|
|
228
|
+
if (directMatch) return directMatch.slug;
|
|
229
|
+
if (isExternalPlatformSlug(teamSlug)) {
|
|
230
|
+
const platformMatch = resolveExternalPlatformSessionTeamSlug(teamSlug, teams);
|
|
231
|
+
if (platformMatch) return platformMatch;
|
|
232
|
+
}
|
|
233
|
+
return teamSlug;
|
|
188
234
|
}
|
|
189
235
|
|
|
190
|
-
private async createUniqueStorageSlug(
|
|
191
|
-
|
|
236
|
+
private async createUniqueStorageSlug(bindProject: string): Promise<string> {
|
|
237
|
+
assertValidBindProject(bindProject);
|
|
238
|
+
const baseSlug = bindProject;
|
|
192
239
|
let slug = baseSlug;
|
|
193
240
|
let suffix = 2;
|
|
194
241
|
while (await pathExists(path.join(teamRoot(slug), 'team.json'))) {
|
|
@@ -201,11 +248,14 @@ export class TeamWorkspaceService {
|
|
|
201
248
|
async createTeam(
|
|
202
249
|
input: CreateTeamInput
|
|
203
250
|
): Promise<{ slug: string; root: string; manifest: TeamManifest }> {
|
|
204
|
-
|
|
205
|
-
|
|
251
|
+
const displayName = input.displayName.trim();
|
|
252
|
+
const bindProject = input.bindProject.trim();
|
|
253
|
+
if (!displayName) throw new Error('displayName is required');
|
|
254
|
+
if (!bindProject) throw new Error('bindProject is required');
|
|
206
255
|
if (!input.workDir) throw new Error('workDir is required');
|
|
256
|
+
assertValidBindProject(bindProject);
|
|
207
257
|
|
|
208
|
-
const slug = await this.createUniqueStorageSlug(
|
|
258
|
+
const slug = await this.createUniqueStorageSlug(bindProject);
|
|
209
259
|
const root = teamRoot(slug);
|
|
210
260
|
|
|
211
261
|
await fs.promises.mkdir(root, { recursive: true });
|
|
@@ -215,8 +265,8 @@ export class TeamWorkspaceService {
|
|
|
215
265
|
const manifest: TeamManifest = {
|
|
216
266
|
schemaVersion: 2,
|
|
217
267
|
slug,
|
|
218
|
-
displayName
|
|
219
|
-
bindProject
|
|
268
|
+
displayName,
|
|
269
|
+
bindProject,
|
|
220
270
|
harness: input.harness,
|
|
221
271
|
workDir: input.workDir,
|
|
222
272
|
color: input.color,
|
|
@@ -230,43 +280,17 @@ export class TeamWorkspaceService {
|
|
|
230
280
|
};
|
|
231
281
|
|
|
232
282
|
await writeJson(path.join(root, 'team.json'), manifest);
|
|
233
|
-
logger.info(`created team ${slug} → cc-project:${
|
|
283
|
+
logger.info(`created team ${slug} → cc-project:${bindProject}`);
|
|
234
284
|
return { slug, root, manifest };
|
|
235
285
|
}
|
|
236
286
|
|
|
237
287
|
async readTeamManifest(teamSlug: string): Promise<TeamManifest> {
|
|
238
|
-
const
|
|
239
|
-
|
|
240
|
-
if (!manifest) {
|
|
241
|
-
if (!(await pathExists(root))) {
|
|
242
|
-
throw new Error(`团队 "${teamSlug}" 不存在 (${root})`);
|
|
243
|
-
}
|
|
244
|
-
const stat = await fs.promises.stat(root).catch(() => null);
|
|
245
|
-
return {
|
|
246
|
-
schemaVersion: 2,
|
|
247
|
-
slug: teamSlug,
|
|
248
|
-
displayName: teamSlug,
|
|
249
|
-
bindProject: teamSlug,
|
|
250
|
-
harness: 'claudecode',
|
|
251
|
-
workDir: '',
|
|
252
|
-
collaboration: true,
|
|
253
|
-
rootPath: root,
|
|
254
|
-
createdAt: (stat?.birthtime ?? stat?.mtime ?? new Date()).toISOString(),
|
|
255
|
-
};
|
|
256
|
-
}
|
|
257
|
-
return manifest;
|
|
288
|
+
const storageSlug = await this.resolveStorageSlug(teamSlug);
|
|
289
|
+
return this.readTeamManifestByStorageSlug(storageSlug);
|
|
258
290
|
}
|
|
259
291
|
|
|
260
292
|
async readTeamManifestByProject(projectName: string): Promise<TeamManifest> {
|
|
261
|
-
|
|
262
|
-
return await this.readTeamManifest(projectName);
|
|
263
|
-
} catch {
|
|
264
|
-
const match = (await this.listTeams()).find(
|
|
265
|
-
(manifest) => manifest.bindProject === projectName
|
|
266
|
-
);
|
|
267
|
-
if (match) return match;
|
|
268
|
-
throw new Error(`团队 "${projectName}" 不存在 (${teamsRoot()})`);
|
|
269
|
-
}
|
|
293
|
+
return this.readTeamManifest(projectName);
|
|
270
294
|
}
|
|
271
295
|
|
|
272
296
|
async listTeams(): Promise<TeamManifest[]> {
|
|
@@ -278,7 +302,7 @@ export class TeamWorkspaceService {
|
|
|
278
302
|
if (!e.isDirectory()) continue;
|
|
279
303
|
if (e.name.startsWith('.')) continue;
|
|
280
304
|
try {
|
|
281
|
-
out.push(await this.
|
|
305
|
+
out.push(await this.readTeamManifestByStorageSlug(e.name));
|
|
282
306
|
} catch {
|
|
283
307
|
// skip broken dirs
|
|
284
308
|
}
|
|
@@ -326,19 +350,23 @@ export class TeamWorkspaceService {
|
|
|
326
350
|
if (opts.deleteFiles) {
|
|
327
351
|
await fs.promises.rm(root, { recursive: true, force: true });
|
|
328
352
|
} else {
|
|
329
|
-
const archive = path.join(teamsRoot(), `.archived-${
|
|
353
|
+
const archive = path.join(teamsRoot(), `.archived-${manifest.slug}-${Date.now()}`);
|
|
330
354
|
await fs.promises.rename(root, archive);
|
|
331
355
|
}
|
|
332
|
-
logger.info(`deleted team ${
|
|
356
|
+
logger.info(`deleted team ${manifest.slug} (deleteFiles=${opts.deleteFiles ?? false})`);
|
|
333
357
|
}
|
|
334
358
|
|
|
335
359
|
// ---- 消息记录 ----
|
|
336
360
|
|
|
337
361
|
async appendMessage(teamSlug: string, msg: AppendGroupMessageInput): Promise<GroupMessage> {
|
|
338
|
-
const
|
|
362
|
+
const storageSlug = await this.resolveStorageSlug(teamSlug);
|
|
363
|
+
if (storageSlug === teamSlug && isExternalPlatformSlug(teamSlug)) {
|
|
364
|
+
throw new Error(`外部平台 session_key 不能作为 Hermit team slug 写入消息: ${teamSlug}`);
|
|
365
|
+
}
|
|
366
|
+
const file = path.join(teamRoot(storageSlug), 'messages', 'group.jsonl');
|
|
339
367
|
await fs.promises.mkdir(path.dirname(file), { recursive: true });
|
|
340
368
|
const entry: GroupMessage = {
|
|
341
|
-
id: `m_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`,
|
|
369
|
+
id: msg.id || `m_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`,
|
|
342
370
|
ts: new Date().toISOString(),
|
|
343
371
|
from: msg.from,
|
|
344
372
|
to: msg.to || 'team',
|
|
@@ -350,9 +378,7 @@ export class TeamWorkspaceService {
|
|
|
350
378
|
return entry;
|
|
351
379
|
}
|
|
352
380
|
|
|
353
|
-
async
|
|
354
|
-
const limit = opts.limit ?? 200;
|
|
355
|
-
const file = path.join(teamRoot(teamSlug), 'messages', 'group.jsonl');
|
|
381
|
+
private async readGroupMessageFile(file: string): Promise<GroupMessage[]> {
|
|
356
382
|
let raw: string;
|
|
357
383
|
try {
|
|
358
384
|
raw = await fs.promises.readFile(file, 'utf8');
|
|
@@ -369,6 +395,33 @@ export class TeamWorkspaceService {
|
|
|
369
395
|
/* skip */
|
|
370
396
|
}
|
|
371
397
|
}
|
|
398
|
+
return all;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
private async findLegacyExternalMessageFiles(storageSlug: string): Promise<string[]> {
|
|
402
|
+
const dir = teamsRoot();
|
|
403
|
+
if (!(await pathExists(dir))) return [];
|
|
404
|
+
const manifests = await this.listTeams();
|
|
405
|
+
const entries = await fs.promises.readdir(dir, { withFileTypes: true });
|
|
406
|
+
return entries
|
|
407
|
+
.filter((entry) => entry.isDirectory() && isExternalPlatformSlug(entry.name))
|
|
408
|
+
.filter(
|
|
409
|
+
(entry) => resolveExternalPlatformSessionTeamSlug(entry.name, manifests) === storageSlug
|
|
410
|
+
)
|
|
411
|
+
.map((entry) => path.join(dir, entry.name, 'messages', 'group.jsonl'));
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
async readMessages(teamSlug: string, opts: { limit?: number } = {}): Promise<GroupMessage[]> {
|
|
415
|
+
const limit = opts.limit ?? 200;
|
|
416
|
+
const storageSlug = await this.resolveStorageSlug(teamSlug);
|
|
417
|
+
if (storageSlug === teamSlug && isExternalPlatformSlug(teamSlug)) return [];
|
|
418
|
+
|
|
419
|
+
const files = [
|
|
420
|
+
path.join(teamRoot(storageSlug), 'messages', 'group.jsonl'),
|
|
421
|
+
...(await this.findLegacyExternalMessageFiles(storageSlug)),
|
|
422
|
+
];
|
|
423
|
+
const all = (await Promise.all(files.map((file) => this.readGroupMessageFile(file)))).flat();
|
|
424
|
+
all.sort((a, b) => a.ts.localeCompare(b.ts));
|
|
372
425
|
return all.length <= limit ? all : all.slice(all.length - limit);
|
|
373
426
|
}
|
|
374
427
|
|
|
@@ -25,5 +25,11 @@ export type {
|
|
|
25
25
|
} from './TeamWorkspaceService';
|
|
26
26
|
|
|
27
27
|
export { TeamProvisioningService } from './TeamProvisioningService';
|
|
28
|
+
export {
|
|
29
|
+
HERMIT_OPS_GUIDE_URL,
|
|
30
|
+
buildHermitOpsRunbookContext,
|
|
31
|
+
buildMemberWorkflowWithOpsContext,
|
|
32
|
+
removeHermitOpsRunbookContext,
|
|
33
|
+
} from './OpsRunbookContext';
|
|
28
34
|
|
|
29
35
|
export { CollaborationBoardService } from './CollaborationBoardService';
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import type { TeamManifest } from '@main/services/teams-mvp/TeamWorkspaceService';
|
|
2
|
+
|
|
3
|
+
const EXTERNAL_PLATFORM_SESSION_RE = /^(feishu|lark|weixin|telegram|discord|slack):/;
|
|
4
|
+
const FEISHU_LARK_KEYS = new Set(['feishu', 'lark']);
|
|
5
|
+
|
|
6
|
+
export interface ExternalPlatformSessionKey {
|
|
7
|
+
platform: string;
|
|
8
|
+
chatId?: string;
|
|
9
|
+
userId?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function isExternalPlatformSessionKey(sessionKey: string): boolean {
|
|
13
|
+
return EXTERNAL_PLATFORM_SESSION_RE.test(sessionKey);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function parseExternalPlatformSessionKey(
|
|
17
|
+
sessionKey: string
|
|
18
|
+
): ExternalPlatformSessionKey | null {
|
|
19
|
+
if (!isExternalPlatformSessionKey(sessionKey)) return null;
|
|
20
|
+
const [platform, chatId, userId] = sessionKey.split(':');
|
|
21
|
+
return {
|
|
22
|
+
platform,
|
|
23
|
+
chatId: chatId?.trim() || undefined,
|
|
24
|
+
userId: userId?.trim() || undefined,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function getPlatformAllowValue(
|
|
29
|
+
record: Record<string, string> | undefined,
|
|
30
|
+
platform: string
|
|
31
|
+
): string {
|
|
32
|
+
if (!record) return '';
|
|
33
|
+
if (FEISHU_LARK_KEYS.has(platform)) {
|
|
34
|
+
return record[platform] ?? record[platform === 'feishu' ? 'lark' : 'feishu'] ?? '';
|
|
35
|
+
}
|
|
36
|
+
return record[platform] ?? '';
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function matchesAllowList(allowList: string, value: string | undefined): boolean {
|
|
40
|
+
const trimmed = allowList.trim();
|
|
41
|
+
if (!trimmed) return false;
|
|
42
|
+
if (trimmed === '*') return true;
|
|
43
|
+
if (!value) return false;
|
|
44
|
+
return trimmed
|
|
45
|
+
.split(/[\s,]+/)
|
|
46
|
+
.map((entry) => entry.trim())
|
|
47
|
+
.filter(Boolean)
|
|
48
|
+
.includes(value);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function scoreManifestForSession(
|
|
52
|
+
manifest: TeamManifest,
|
|
53
|
+
parsed: ExternalPlatformSessionKey
|
|
54
|
+
): number {
|
|
55
|
+
const allowChat = getPlatformAllowValue(manifest.platformAllowChat, parsed.platform);
|
|
56
|
+
const allowFrom = getPlatformAllowValue(manifest.platformAllowFrom, parsed.platform);
|
|
57
|
+
let score = 0;
|
|
58
|
+
|
|
59
|
+
if (allowChat) {
|
|
60
|
+
if (!matchesAllowList(allowChat, parsed.chatId)) return 0;
|
|
61
|
+
score += allowChat.trim() === '*' ? 1 : 4;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (allowFrom) {
|
|
65
|
+
if (!matchesAllowList(allowFrom, parsed.userId)) return 0;
|
|
66
|
+
score += allowFrom.trim() === '*' ? 1 : 3;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return score;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Resolves an external platform session key (for example `feishu:{chat}:{user}`)
|
|
74
|
+
* to a Hermit-managed team slug using local allow-list metadata. Returns null
|
|
75
|
+
* when no team matches or when multiple teams are equally plausible.
|
|
76
|
+
*/
|
|
77
|
+
export function resolveExternalPlatformSessionTeamSlug(
|
|
78
|
+
sessionKey: string,
|
|
79
|
+
manifests: readonly TeamManifest[]
|
|
80
|
+
): string | null {
|
|
81
|
+
const parsed = parseExternalPlatformSessionKey(sessionKey);
|
|
82
|
+
if (!parsed) return null;
|
|
83
|
+
|
|
84
|
+
const ranked = manifests
|
|
85
|
+
.map((manifest) => ({ manifest, score: scoreManifestForSession(manifest, parsed) }))
|
|
86
|
+
.filter((entry) => entry.score > 0)
|
|
87
|
+
.sort((a, b) => b.score - a.score);
|
|
88
|
+
|
|
89
|
+
if (ranked.length === 0) return null;
|
|
90
|
+
if (ranked.length > 1 && ranked[0].score === ranked[1].score) return null;
|
|
91
|
+
return ranked[0].manifest.slug || ranked[0].manifest.bindProject;
|
|
92
|
+
}
|