@yancyyu/openhermit 1.6.38 → 1.6.40
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/dist-renderer/assets/ProjectEditorOverlay-CemDOX-3.js +58 -0
- package/dist-renderer/assets/{TeamGraphOverlay-ZEDfZyHb.js → TeamGraphOverlay-hPY770Db.js} +1 -1
- package/dist-renderer/assets/{_basePickBy-CIhniz70.js → _basePickBy-BHHrJT1i.js} +1 -1
- package/dist-renderer/assets/{_baseUniq-cKAW4Q8I.js → _baseUniq-CWErBtke.js} +1 -1
- package/dist-renderer/assets/{arc-YmNsoDXW.js → arc-C_o2_Uv8.js} +1 -1
- package/dist-renderer/assets/{architectureDiagram-VXUJARFQ-DHEls2sX.js → architectureDiagram-VXUJARFQ-DUW0LI3t.js} +1 -1
- package/dist-renderer/assets/{blockDiagram-VD42YOAC-Bpwf1Sbg.js → blockDiagram-VD42YOAC-CWbCE9hQ.js} +1 -1
- package/dist-renderer/assets/{c4Diagram-YG6GDRKO-B0IaQ4w5.js → c4Diagram-YG6GDRKO-BjLadrfV.js} +1 -1
- package/dist-renderer/assets/channel-DyP9YlCF.js +1 -0
- package/dist-renderer/assets/{chunk-4BX2VUAB-DLk-hcFc.js → chunk-4BX2VUAB-CPnvjZl9.js} +1 -1
- package/dist-renderer/assets/{chunk-55IACEB6-1XRmX_Zm.js → chunk-55IACEB6-OlL47yXQ.js} +1 -1
- package/dist-renderer/assets/{chunk-B4BG7PRW-1waH1DAD.js → chunk-B4BG7PRW-DTasjbm8.js} +1 -1
- package/dist-renderer/assets/{chunk-DI55MBZ5-BqpZBtrN.js → chunk-DI55MBZ5-C5_Xaqkk.js} +1 -1
- package/dist-renderer/assets/{chunk-FMBD7UC4-Bly7vVym.js → chunk-FMBD7UC4-NdoM4DMR.js} +1 -1
- package/dist-renderer/assets/{chunk-QN33PNHL-Ci2QWBAs.js → chunk-QN33PNHL-C8Fybejy.js} +1 -1
- package/dist-renderer/assets/{chunk-QZHKN3VN-YCqFW7d-.js → chunk-QZHKN3VN-E98TYFXJ.js} +1 -1
- package/dist-renderer/assets/{chunk-TZMSLE5B-B0xGXInl.js → chunk-TZMSLE5B-h4lFgkIq.js} +1 -1
- package/dist-renderer/assets/classDiagram-2ON5EDUG-BqffFTae.js +1 -0
- package/dist-renderer/assets/classDiagram-v2-WZHVMYZB-BqffFTae.js +1 -0
- package/dist-renderer/assets/clone-MPcKWs2O.js +1 -0
- package/dist-renderer/assets/{cose-bilkent-S5V4N54A-DxcFNQKT.js → cose-bilkent-S5V4N54A-DtQ7fkrs.js} +1 -1
- package/dist-renderer/assets/{dagre-6UL2VRFP-DPo_RfZY.js → dagre-6UL2VRFP-CN-nL_z4.js} +1 -1
- package/dist-renderer/assets/{diagram-PSM6KHXK-U3hQsFe4.js → diagram-PSM6KHXK-DVJtqmm-.js} +1 -1
- package/dist-renderer/assets/{diagram-QEK2KX5R-OrwrAy0V.js → diagram-QEK2KX5R-DlxHxyXh.js} +1 -1
- package/dist-renderer/assets/{diagram-S2PKOQOG-CXATPWVw.js → diagram-S2PKOQOG-7dpzO6x6.js} +1 -1
- package/dist-renderer/assets/{erDiagram-Q2GNP2WA-B0e8AfMF.js → erDiagram-Q2GNP2WA-GP1TqsHi.js} +1 -1
- package/dist-renderer/assets/{flowDiagram-NV44I4VS-CXfzA4jJ.js → flowDiagram-NV44I4VS-C7ZLETuH.js} +1 -1
- package/dist-renderer/assets/{ganttDiagram-JELNMOA3-CMr08qVl.js → ganttDiagram-JELNMOA3-CvPB68dH.js} +1 -1
- package/dist-renderer/assets/{gitGraphDiagram-V2S2FVAM-vYFHpPmy.js → gitGraphDiagram-V2S2FVAM-B5yOm3w7.js} +1 -1
- package/dist-renderer/assets/{graph-DOe5j8dH.js → graph-smeyY1YZ.js} +1 -1
- package/dist-renderer/assets/{index-BySQS7AB.js → index-BJx8XvG1.js} +1 -1
- package/dist-renderer/assets/{index-C_okzZXP.js → index-CQaXUAua.js} +1 -1
- package/dist-renderer/assets/{index-VJ-MM9xa.js → index-CajRpxO2.js} +1 -1
- package/dist-renderer/assets/{index-V7dAKPqd.js → index-ChG4rE-E.js} +587 -705
- package/dist-renderer/assets/index-DUd0uw9C.css +32 -0
- package/dist-renderer/assets/{index-CzWxVCRL.js → index-IhmXZWqf.js} +1 -1
- package/dist-renderer/assets/{index-B2Dy7M2G.js → index-x_JkoDRH.js} +1 -1
- package/dist-renderer/assets/{infoDiagram-HS3SLOUP-D_WubR0B.js → infoDiagram-HS3SLOUP-D-hWRQGY.js} +1 -1
- package/dist-renderer/assets/{journeyDiagram-XKPGCS4Q-w9ca-1TI.js → journeyDiagram-XKPGCS4Q-Bb6W8rUG.js} +1 -1
- package/dist-renderer/assets/{kanban-definition-3W4ZIXB7-Jg9p6_pN.js → kanban-definition-3W4ZIXB7-CnHdUX0q.js} +1 -1
- package/dist-renderer/assets/{layout-B-z3y17c.js → layout-pqss_zkI.js} +1 -1
- package/dist-renderer/assets/{linear-D-RTX5UW.js → linear-B1mFITNh.js} +1 -1
- package/dist-renderer/assets/{mindmap-definition-VGOIOE7T-CDQmHOYP.js → mindmap-definition-VGOIOE7T-DTD9q7-D.js} +1 -1
- package/dist-renderer/assets/{pieDiagram-ADFJNKIX-D_odsQL7.js → pieDiagram-ADFJNKIX-Df3mhrn7.js} +1 -1
- package/dist-renderer/assets/{quadrantDiagram-AYHSOK5B-BRsmYWSA.js → quadrantDiagram-AYHSOK5B-B1FZ09vH.js} +1 -1
- package/dist-renderer/assets/{requirementDiagram-UZGBJVZJ-ChNE_BOV.js → requirementDiagram-UZGBJVZJ-aEO78thZ.js} +1 -1
- package/dist-renderer/assets/{sankeyDiagram-TZEHDZUN-C8FtpwKc.js → sankeyDiagram-TZEHDZUN-6Ui--jp-.js} +1 -1
- package/dist-renderer/assets/{sequenceDiagram-WL72ISMW-DmLCzNcc.js → sequenceDiagram-WL72ISMW-DF4Q1cAM.js} +1 -1
- package/dist-renderer/assets/splashScene-D0YB9uxm.js +17 -0
- package/dist-renderer/assets/{stateDiagram-FKZM4ZOC-WJBm4bhu.js → stateDiagram-FKZM4ZOC-BqA2BI8C.js} +1 -1
- package/dist-renderer/assets/stateDiagram-v2-4FDKWEC3-Cs2ZtUD2.js +1 -0
- package/dist-renderer/assets/{timeline-definition-IT6M3QCI-BXs_hOJs.js → timeline-definition-IT6M3QCI-DoOkw_A8.js} +1 -1
- package/dist-renderer/assets/{treemap-GDKQZRPO-o04MA0G9.js → treemap-GDKQZRPO-DUe26QdD.js} +1 -1
- package/dist-renderer/assets/{xychartDiagram-PRI3JC2R-Czj69XRd.js → xychartDiagram-PRI3JC2R-BKCnj5Xn.js} +1 -1
- package/dist-renderer/index.html +20 -53
- package/package.json +25 -18
- package/src/main/ipc/extensions.ts +2 -1
- package/src/main/server.ts +873 -221
- package/src/main/services/extensions/ExtensionFacadeService.ts +2 -5
- package/src/main/services/extensions/catalog/PluginCatalogService.ts +4 -2
- package/src/main/services/session-intelligence/ConversationTelemetryService.ts +1101 -0
- package/src/main/services/session-intelligence/LocalSessionScanner.ts +512 -0
- package/src/main/services/session-intelligence/SessionUsageParser.ts +4 -4
- package/src/main/services/system-manager/SystemManagerConfigService.ts +122 -0
- package/src/main/services/system-manager/SystemManagerPtyService.ts +233 -0
- package/src/main/services/system-manager/WorkflowPromptService.ts +75 -0
- package/src/main/services/teams-mvp/TaskDispatchService.ts +5 -6
- package/src/main/services/teams-mvp/TeamProvisioningService.ts +39 -2
- package/src/main/services/teams-mvp/TeamWorkspaceService.ts +22 -4
- package/src/main/utils/teamProjectResolution.ts +15 -0
- package/src/renderer/App.tsx +8 -4
- package/src/renderer/api/httpClient.ts +68 -18
- package/src/renderer/api/providers.ts +23 -2
- package/src/renderer/assets/participant-avatars/01.svg +3 -0
- package/src/renderer/assets/participant-avatars/02.svg +3 -0
- package/src/renderer/assets/participant-avatars/03.svg +3 -0
- package/src/renderer/assets/participant-avatars/04.svg +3 -0
- package/src/renderer/assets/participant-avatars/05.svg +3 -0
- package/src/renderer/assets/participant-avatars/06.svg +3 -0
- package/src/renderer/assets/participant-avatars/07.svg +3 -0
- package/src/renderer/assets/participant-avatars/08.svg +3 -0
- package/src/renderer/assets/participant-avatars/09.svg +3 -0
- package/src/renderer/assets/participant-avatars/10.svg +3 -0
- package/src/renderer/assets/participant-avatars/11.svg +3 -0
- package/src/renderer/assets/participant-avatars/12.svg +3 -0
- package/src/renderer/assets/participant-avatars/13.svg +3 -0
- package/src/renderer/components/chat/ChatHistoryItem.tsx +1 -1
- package/src/renderer/components/chat/items/SubagentItem.tsx +2 -2
- package/src/renderer/components/chat/viewers/MermaidDiagram.tsx +2 -2
- package/src/renderer/components/common/ErrorBoundary.tsx +1 -1
- package/src/renderer/components/common/TerminalPane.tsx +213 -0
- package/src/renderer/components/dashboard/CliStatusBanner.tsx +7 -7
- package/src/renderer/components/dashboard/DashboardView.tsx +9 -36
- package/src/renderer/components/extensions/ExtensionStoreView.tsx +7 -126
- package/src/renderer/components/extensions/ExtensionsSubTabTrigger.tsx +1 -1
- package/src/renderer/components/extensions/common/ExtensionToast.tsx +3 -3
- package/src/renderer/components/extensions/common/SourceBadge.tsx +1 -1
- package/src/renderer/components/extensions/mcp/McpLibraryEnableDialog.tsx +305 -0
- package/src/renderer/components/extensions/mcp/McpLibraryEntryDialog.tsx +418 -0
- package/src/renderer/components/extensions/mcp/McpLibraryPanel.tsx +404 -0
- package/src/renderer/components/extensions/plugins/CategoryChips.tsx +1 -1
- package/src/renderer/components/extensions/plugins/PluginCard.tsx +6 -6
- package/src/renderer/components/extensions/plugins/PluginDetailDialog.tsx +2 -2
- package/src/renderer/components/extensions/plugins/PluginsPanel.tsx +34 -21
- package/src/renderer/components/extensions/skills/SkillEditorDialog.tsx +1 -1
- package/src/renderer/components/extensions/skills/SkillsLibraryPanel.tsx +335 -0
- package/src/renderer/components/layout/PaneContent.tsx +8 -1
- package/src/renderer/components/layout/PaneResizeHandle.tsx +2 -2
- package/src/renderer/components/layout/Sidebar.tsx +13 -56
- package/src/renderer/components/layout/SortableTab.tsx +22 -33
- package/src/renderer/components/layout/TabBar.tsx +1 -1
- package/src/renderer/components/layout/TabContextMenu.tsx +1 -1
- package/src/renderer/components/report/sections/CostSection.tsx +2 -2
- package/src/renderer/components/report/sections/InsightsSection.tsx +1 -1
- package/src/renderer/components/runtime/ProviderRuntimeBackendSelector.tsx +2 -2
- package/src/renderer/components/runtime/ProviderRuntimeSettingsDialog.tsx +768 -157
- package/src/renderer/components/schedules/SchedulesView.tsx +51 -462
- package/src/renderer/components/schedules/calendar/CalendarDayView.tsx +173 -0
- package/src/renderer/components/schedules/calendar/CalendarEventBlock.tsx +113 -0
- package/src/renderer/components/schedules/calendar/CalendarHeader.tsx +148 -0
- package/src/renderer/components/schedules/calendar/CalendarMonthView.tsx +142 -0
- package/src/renderer/components/schedules/calendar/CalendarWeekView.tsx +219 -0
- package/src/renderer/components/schedules/calendar/ScheduleCalendarBoard.tsx +41 -0
- package/src/renderer/components/schedules/calendar/TeamGanttView.tsx +405 -0
- package/src/renderer/components/schedules/calendar/computeOccurrences.ts +234 -0
- package/src/renderer/components/schedules/calendar/index.ts +2 -0
- package/src/renderer/components/schedules/calendar/types.ts +44 -0
- package/src/renderer/components/search/CommandPalette.tsx +4 -4
- package/src/renderer/components/settings/SettingsTabs.tsx +50 -55
- package/src/renderer/components/settings/SettingsView.tsx +30 -35
- package/src/renderer/components/settings/components/SettingsSectionHeader.tsx +5 -1
- package/src/renderer/components/settings/components/SettingsSelect.tsx +5 -3
- package/src/renderer/components/settings/components/SettingsToggle.tsx +2 -2
- package/src/renderer/components/settings/sections/AdvancedSection.tsx +11 -42
- package/src/renderer/components/settings/sections/CliStatusSection.tsx +72 -113
- package/src/renderer/components/settings/sections/ConfigEditorDialog.tsx +1 -1
- package/src/renderer/components/settings/sections/GeneralSection.tsx +11 -3
- package/src/renderer/components/settings/sections/HarnessSection.tsx +18 -14
- package/src/renderer/components/settings/sections/PlatformsSection.tsx +3 -3
- package/src/renderer/components/settings/sections/TaskBusSection.tsx +33 -40
- package/src/renderer/components/settings/sections/index.ts +0 -1
- package/src/renderer/components/sidebar/SessionFiltersPopover.tsx +1 -1
- package/src/renderer/components/sidebar/SessionItem.tsx +3 -3
- package/src/renderer/components/sidebar/SidebarSessions.tsx +184 -6
- package/src/renderer/components/sidebar/SidebarTaskItem.tsx +4 -4
- package/src/renderer/components/sidebar/WorkspaceBrowser.tsx +40 -5
- package/src/renderer/components/splash/splashScene.ts +121 -929
- package/src/renderer/components/system-manager/FolderBrowser.tsx +163 -0
- package/src/renderer/components/system-manager/SystemManagerView.tsx +351 -0
- package/src/renderer/components/tasks/TasksView.tsx +112 -134
- package/src/renderer/components/team/CcSessionsSection.tsx +431 -89
- package/src/renderer/components/team/ClaudeLogsFilterPopover.tsx +1 -1
- package/src/renderer/components/team/ClaudeLogsPanel.tsx +1 -1
- package/src/renderer/components/team/CollapsibleTeamSection.tsx +17 -32
- package/src/renderer/components/team/ProcessesSection.tsx +2 -2
- package/src/renderer/components/team/TaskTooltip.tsx +2 -2
- package/src/renderer/components/team/TeamDetailView.tsx +319 -123
- package/src/renderer/components/team/TeamListFilterPopover.tsx +1 -1
- package/src/renderer/components/team/TeamListView.tsx +109 -124
- package/src/renderer/components/team/TeamSessionsSection.tsx +6 -6
- package/src/renderer/components/team/UnreadCommentsBadge.tsx +1 -1
- package/src/renderer/components/team/activity/ActivityItem.tsx +9 -9
- package/src/renderer/components/team/activity/ActivityTimeline.tsx +5 -5
- package/src/renderer/components/team/activity/LeadThoughtsGroup.tsx +3 -3
- package/src/renderer/components/team/activity/ReplyQuoteBlock.tsx +4 -4
- package/src/renderer/components/team/dialogs/CreateTaskDialog.tsx +4 -4
- package/src/renderer/components/team/dialogs/CreateTeamDialog.tsx +84 -306
- package/src/renderer/components/team/dialogs/EditTeamDialog.tsx +259 -342
- package/src/renderer/components/team/dialogs/GlobalTaskDetailDialog.tsx +1 -1
- package/src/renderer/components/team/dialogs/LaunchTeamDialog.tsx +18 -16
- package/src/renderer/components/team/dialogs/PlatformBindingDialog.tsx +221 -0
- package/src/renderer/components/team/dialogs/PlatformManualForm.tsx +8 -1
- package/src/renderer/components/team/dialogs/PlatformSetupQR.tsx +5 -5
- package/src/renderer/components/team/dialogs/RuntimeConfigDialog.tsx +361 -0
- package/src/renderer/components/team/dialogs/SendMessageDialog.tsx +6 -6
- package/src/renderer/components/team/dialogs/SkipPermissionsCheckbox.tsx +6 -6
- package/src/renderer/components/team/dialogs/StatusHistoryTimeline.tsx +1 -1
- package/src/renderer/components/team/dialogs/TaskAttachments.tsx +1 -1
- package/src/renderer/components/team/dialogs/TaskCommentInput.tsx +6 -6
- package/src/renderer/components/team/dialogs/TaskCommentsSection.tsx +4 -4
- package/src/renderer/components/team/dialogs/TaskDetailDialog.tsx +3 -3
- package/src/renderer/components/team/dialogs/platformMeta.ts +122 -11
- package/src/renderer/components/team/dialogs/useTeamEditForm.ts +17 -5
- package/src/renderer/components/team/editor/EditorFileTree.tsx +4 -4
- package/src/renderer/components/team/editor/EditorSearchPanel.tsx +1 -1
- package/src/renderer/components/team/editor/MarkdownSplitView.tsx +1 -1
- package/src/renderer/components/team/editor/NewFileDialog.tsx +1 -1
- package/src/renderer/components/team/editor/ProjectEditorOverlay.tsx +1 -1
- package/src/renderer/components/team/editor/SearchInFilesPanel.tsx +1 -1
- package/src/renderer/components/team/kanban/KanbanBoard.tsx +9 -9
- package/src/renderer/components/team/kanban/KanbanFilterPopover.tsx +4 -4
- package/src/renderer/components/team/kanban/KanbanSearchInput.tsx +1 -1
- package/src/renderer/components/team/kanban/KanbanSortPopover.tsx +5 -5
- package/src/renderer/components/team/kanban/KanbanTaskCard.tsx +4 -4
- package/src/renderer/components/team/members/MemberCard.tsx +14 -47
- package/src/renderer/components/team/members/MemberDetailDialog.tsx +3 -95
- package/src/renderer/components/team/members/MemberDetailStats.tsx +50 -65
- package/src/renderer/components/team/members/MemberDraftRow.tsx +1 -1
- package/src/renderer/components/team/members/MemberStatsTab.tsx +2 -2
- package/src/renderer/components/team/members/MemberWorkspaceTab.tsx +1 -1
- package/src/renderer/components/team/messages/MessageComposer.tsx +10 -112
- package/src/renderer/components/team/messages/MessagesFilterPopover.tsx +1 -1
- package/src/renderer/components/team/messages/MessagesPanel.tsx +136 -119
- package/src/renderer/components/team/review/ChangeReviewDialog.tsx +1 -1
- package/src/renderer/components/team/schedule/ScheduleStatusBadge.tsx +3 -3
- package/src/renderer/components/team/sidebar/TeamSidebarRail.tsx +4 -4
- package/src/renderer/components/team/tasks/TaskRow.tsx +1 -1
- package/src/renderer/components/team/tools/AddMcpInline.tsx +27 -17
- package/src/renderer/components/team/tools/McpChip.tsx +6 -3
- package/src/renderer/components/team/tools/SkillChip.tsx +3 -3
- package/src/renderer/components/team/tools/ToolsSection.tsx +418 -70
- package/src/renderer/components/ui/MemberSelect.tsx +2 -2
- package/src/renderer/components/ui/MentionSuggestionList.tsx +2 -2
- package/src/renderer/components/ui/MentionableTextarea.tsx +3 -3
- package/src/renderer/hooks/useExtensionsTabState.ts +3 -114
- package/src/renderer/index.css +56 -39
- package/src/renderer/index.html +17 -50
- package/src/renderer/store/index.ts +2 -1
- package/src/renderer/store/slices/scheduleSlice.ts +1 -1
- package/src/renderer/store/slices/teamSlice.ts +45 -168
- package/src/renderer/utils/claudeCodeOnlyProviders.ts +3 -10
- package/src/renderer/utils/memberHelpers.ts +5 -17
- package/src/renderer/utils/openCodeRuntimeDeliveryDiagnostics.ts +4 -2
- package/src/renderer/utils/providerSlashCommands.ts +0 -5
- package/src/renderer/utils/scheduleFormatters.ts +3 -1
- package/src/renderer/utils/teamMessageFiltering.ts +14 -1
- package/src/renderer/utils/teamModelAvailability.ts +18 -2
- package/src/shared/types/api.ts +121 -2
- package/src/shared/types/ccConnect.ts +2 -0
- package/src/shared/types/index.ts +3 -0
- package/src/shared/types/systemManager.ts +49 -0
- package/src/shared/types/team.ts +29 -0
- package/src/shared/types/terminal.ts +4 -2
- package/src/shared/utils/extensionNormalizers.ts +15 -8
- package/src/shared/utils/providerExtensionCapabilities.ts +2 -2
- package/dist-renderer/assets/ProjectEditorOverlay-lJZi-9Hp.js +0 -52
- package/dist-renderer/assets/channel-yIlSKy0e.js +0 -1
- package/dist-renderer/assets/classDiagram-2ON5EDUG-24fHez0s.js +0 -1
- package/dist-renderer/assets/classDiagram-v2-WZHVMYZB-24fHez0s.js +0 -1
- package/dist-renderer/assets/clone-BTNuUva-.js +0 -1
- package/dist-renderer/assets/index-Bi6nrZ4z.css +0 -1
- package/dist-renderer/assets/splashScene-C8lWNnm4.js +0 -1
- package/dist-renderer/assets/stateDiagram-v2-4FDKWEC3-_m6iPPUR.js +0 -1
package/src/main/server.ts
CHANGED
|
@@ -41,6 +41,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
41
41
|
|
|
42
42
|
import cors from '@fastify/cors';
|
|
43
43
|
import staticPlugin from '@fastify/static';
|
|
44
|
+
import { Cron } from 'croner';
|
|
44
45
|
import Fastify from 'fastify';
|
|
45
46
|
|
|
46
47
|
import {
|
|
@@ -48,13 +49,22 @@ import {
|
|
|
48
49
|
CROSS_TEAM_SOURCE,
|
|
49
50
|
formatCrossTeamText,
|
|
50
51
|
} from '@shared/constants/crossTeam';
|
|
51
|
-
import type { CcAgentType } from '../shared/types/ccConnect';
|
|
52
|
+
import type { CcAgentType, CcProjectPlatform } from '../shared/types/ccConnect';
|
|
52
53
|
import { CcConnectBridge } from './services/ccConnect/CcConnectBridge';
|
|
53
54
|
import { CcConnectClient } from './services/ccConnect/CcConnectClient';
|
|
54
55
|
import { TeamProvisioningService } from './services/teams-mvp';
|
|
55
56
|
import { TaskDispatchService } from './services/teams-mvp/TaskDispatchService';
|
|
57
|
+
import { resolveCcProjectName } from './utils/teamProjectResolution';
|
|
56
58
|
import { CollaborationBoardService } from './services/teams-mvp/CollaborationBoardService';
|
|
57
|
-
import
|
|
59
|
+
import { SystemManagerConfigService } from './services/system-manager/SystemManagerConfigService';
|
|
60
|
+
import { SystemManagerPtyService } from './services/system-manager/SystemManagerPtyService';
|
|
61
|
+
import { WorkflowPromptService } from './services/system-manager/WorkflowPromptService';
|
|
62
|
+
import {
|
|
63
|
+
SYSTEM_MANAGER_BIND_PROJECT,
|
|
64
|
+
SYSTEM_MANAGER_DISPLAY_NAME,
|
|
65
|
+
SYSTEM_MANAGER_TEAM_NAME,
|
|
66
|
+
} from '@shared/types/team';
|
|
67
|
+
import type { SystemManagerSummary, TaskBusConfig, TeamLaunchRequest } from '@shared/types/team';
|
|
58
68
|
import type { TeamManifest } from './services/teams-mvp/TeamWorkspaceService';
|
|
59
69
|
import { UpdateService } from './services/UpdateService';
|
|
60
70
|
import {
|
|
@@ -63,6 +73,11 @@ import {
|
|
|
63
73
|
triggerScan,
|
|
64
74
|
getTelemetryStatus,
|
|
65
75
|
} from './services/session-intelligence/UsageTelemetryService';
|
|
76
|
+
import {
|
|
77
|
+
ConversationTelemetryService,
|
|
78
|
+
shouldIncludeContent,
|
|
79
|
+
} from './services/session-intelligence/ConversationTelemetryService';
|
|
80
|
+
import { LocalSessionScanner } from './services/session-intelligence/LocalSessionScanner';
|
|
66
81
|
|
|
67
82
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
68
83
|
const pkg = JSON.parse(readFileSync(path.join(__dirname, '../../package.json'), 'utf-8'));
|
|
@@ -72,6 +87,34 @@ const HOST = process.env.HOST ?? '127.0.0.1';
|
|
|
72
87
|
const PORT = Number.parseInt(process.env.PORT ?? '5680', 10);
|
|
73
88
|
const STATIC_DIR = process.env.STATIC_DIR ?? path.resolve(REPO_ROOT, 'dist-renderer');
|
|
74
89
|
const HARNESS_BRIDGE_CONNECT_TIMEOUT_MS = 10_000;
|
|
90
|
+
const CC_AGENT_TYPES: readonly CcAgentType[] = [
|
|
91
|
+
'claudecode',
|
|
92
|
+
'codex',
|
|
93
|
+
'cursor',
|
|
94
|
+
'gemini',
|
|
95
|
+
'iflow',
|
|
96
|
+
'kimi',
|
|
97
|
+
'devin',
|
|
98
|
+
'opencode',
|
|
99
|
+
'qoder',
|
|
100
|
+
'pi',
|
|
101
|
+
'acp',
|
|
102
|
+
'tmux',
|
|
103
|
+
];
|
|
104
|
+
const SYSTEM_MANAGER_DESCRIPTION =
|
|
105
|
+
'项目级 Claude Code 控制台,负责插件、MCP、Env、数字员工和统计数据的托管管理。';
|
|
106
|
+
|
|
107
|
+
function toCcAgentType(value: string | undefined): CcAgentType {
|
|
108
|
+
return CC_AGENT_TYPES.includes(value as CcAgentType) ? (value as CcAgentType) : 'claudecode';
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function isReservedSystemTeamName(teamName: string): boolean {
|
|
112
|
+
return (
|
|
113
|
+
teamName === 'default' ||
|
|
114
|
+
teamName === SYSTEM_MANAGER_BIND_PROJECT ||
|
|
115
|
+
teamName === SYSTEM_MANAGER_TEAM_NAME
|
|
116
|
+
);
|
|
117
|
+
}
|
|
75
118
|
|
|
76
119
|
// ===========================================================================
|
|
77
120
|
// Hermit runtime config — ~/.hermit/config.json
|
|
@@ -144,8 +187,14 @@ function loadConfig(): HermitConfig {
|
|
|
144
187
|
const raw = JSON.parse(readFileSync(HERMIT_CONFIG_FILE, 'utf-8')) as Partial<HermitConfig>;
|
|
145
188
|
merged = { ...defaults, ...raw };
|
|
146
189
|
}
|
|
147
|
-
} catch {
|
|
148
|
-
|
|
190
|
+
} catch (err) {
|
|
191
|
+
const msg = err instanceof SyntaxError
|
|
192
|
+
? `${HERMIT_CONFIG_FILE} 格式错误: ${err.message}。将使用默认配置并覆盖修复。`
|
|
193
|
+
: `读取 ${HERMIT_CONFIG_FILE} 失败: ${err instanceof Error ? err.message : String(err)}`;
|
|
194
|
+
console.warn(`[Hermit] ${msg}`);
|
|
195
|
+
// Auto-heal: rewrite the config file with valid defaults + any readable env overrides
|
|
196
|
+
mkdirSync(HERMIT_HOME, { recursive: true });
|
|
197
|
+
writeFileSync(HERMIT_CONFIG_FILE, JSON.stringify(defaults, null, 2), 'utf-8');
|
|
149
198
|
}
|
|
150
199
|
if (!merged.ccBridgeToken.trim()) {
|
|
151
200
|
merged = { ...merged, ccBridgeToken: tomlBridgeToken || merged.ccToken };
|
|
@@ -175,7 +224,15 @@ function readHermitConfigRaw(): { path: string; content: string } {
|
|
|
175
224
|
}
|
|
176
225
|
|
|
177
226
|
function writeHermitConfigRaw(content: string): HermitConfig {
|
|
178
|
-
|
|
227
|
+
let parsed: unknown;
|
|
228
|
+
try {
|
|
229
|
+
parsed = JSON.parse(content);
|
|
230
|
+
} catch (err) {
|
|
231
|
+
if (err instanceof SyntaxError) {
|
|
232
|
+
throw new Error(`配置文件 JSON 格式错误: ${err.message}。请检查是否有尾逗号、单引号或注释等非法 JSON 语法。`);
|
|
233
|
+
}
|
|
234
|
+
throw err;
|
|
235
|
+
}
|
|
179
236
|
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
180
237
|
throw new Error('Hermit 配置必须是 JSON 对象');
|
|
181
238
|
}
|
|
@@ -198,6 +255,138 @@ const bridge = new CcConnectBridge({
|
|
|
198
255
|
bridgeToken: runtimeConfig.ccBridgeToken || runtimeConfig.ccToken,
|
|
199
256
|
});
|
|
200
257
|
const svc = new TeamProvisioningService(cc, bridge);
|
|
258
|
+
const systemManagerConfig = new SystemManagerConfigService(REPO_ROOT);
|
|
259
|
+
const systemManagerPty = new SystemManagerPtyService();
|
|
260
|
+
const workflowPromptService = new WorkflowPromptService();
|
|
261
|
+
|
|
262
|
+
systemManagerPty.on('data', (event) => broadcastSse('terminal:data', event));
|
|
263
|
+
systemManagerPty.on('exit', (event) => broadcastSse('terminal:exit', event));
|
|
264
|
+
|
|
265
|
+
async function getSystemManagerWorkDir(): Promise<string> {
|
|
266
|
+
try {
|
|
267
|
+
return (await systemManagerConfig.getConfig()).selectedWorkDir;
|
|
268
|
+
} catch {
|
|
269
|
+
return REPO_ROOT;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
async function syncSystemManagerManifestWorkDir(workDir: string): Promise<void> {
|
|
274
|
+
try {
|
|
275
|
+
const manifest = await svc.readTeamManifest(SYSTEM_MANAGER_TEAM_NAME);
|
|
276
|
+
if (manifest.workDir !== workDir) {
|
|
277
|
+
await svc.updateTeam(SYSTEM_MANAGER_TEAM_NAME, { workDir });
|
|
278
|
+
}
|
|
279
|
+
} catch {
|
|
280
|
+
// The console team may not exist yet; ensureSystemManager() will create it later.
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
let systemManagerEnsurePromise: Promise<SystemManagerSummary> | null = null;
|
|
285
|
+
|
|
286
|
+
async function ensureSystemManagerUncached(): Promise<SystemManagerSummary> {
|
|
287
|
+
const workDir = await getSystemManagerWorkDir();
|
|
288
|
+
let ccConnectProjectStatus: SystemManagerSummary['ccConnectProjectStatus'] = 'bound';
|
|
289
|
+
try {
|
|
290
|
+
await cc.getProject(SYSTEM_MANAGER_BIND_PROJECT);
|
|
291
|
+
} catch {
|
|
292
|
+
ccConnectProjectStatus = 'missing';
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
let manifest: TeamManifest;
|
|
296
|
+
try {
|
|
297
|
+
manifest = await svc.readTeamManifest(SYSTEM_MANAGER_TEAM_NAME);
|
|
298
|
+
} catch {
|
|
299
|
+
const created = await svc.createTeam({
|
|
300
|
+
displayName: SYSTEM_MANAGER_TEAM_NAME,
|
|
301
|
+
bindProject: SYSTEM_MANAGER_BIND_PROJECT,
|
|
302
|
+
harness: 'claudecode',
|
|
303
|
+
workDir,
|
|
304
|
+
color: 'slate',
|
|
305
|
+
description: SYSTEM_MANAGER_DESCRIPTION,
|
|
306
|
+
collaboration: false,
|
|
307
|
+
createCcProject: false,
|
|
308
|
+
injectInstructions: false,
|
|
309
|
+
});
|
|
310
|
+
manifest = created.manifest;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
if (
|
|
314
|
+
manifest.displayName !== SYSTEM_MANAGER_DISPLAY_NAME ||
|
|
315
|
+
manifest.bindProject !== SYSTEM_MANAGER_BIND_PROJECT ||
|
|
316
|
+
manifest.description !== SYSTEM_MANAGER_DESCRIPTION ||
|
|
317
|
+
manifest.color !== 'slate' ||
|
|
318
|
+
manifest.collaboration !== false ||
|
|
319
|
+
manifest.workDir !== workDir
|
|
320
|
+
) {
|
|
321
|
+
manifest = await svc.updateTeam(manifest.slug, {
|
|
322
|
+
displayName: SYSTEM_MANAGER_DISPLAY_NAME,
|
|
323
|
+
bindProject: SYSTEM_MANAGER_BIND_PROJECT,
|
|
324
|
+
color: 'slate',
|
|
325
|
+
description: SYSTEM_MANAGER_DESCRIPTION,
|
|
326
|
+
collaboration: false,
|
|
327
|
+
workDir,
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
return {
|
|
332
|
+
teamName: SYSTEM_MANAGER_TEAM_NAME,
|
|
333
|
+
displayName: SYSTEM_MANAGER_DISPLAY_NAME,
|
|
334
|
+
bindProject: SYSTEM_MANAGER_BIND_PROJECT,
|
|
335
|
+
workDir: manifest.workDir || workDir,
|
|
336
|
+
projectPath: manifest.workDir || workDir,
|
|
337
|
+
description: manifest.description || SYSTEM_MANAGER_DESCRIPTION,
|
|
338
|
+
localStatus: 'ready',
|
|
339
|
+
ccConnectProjectStatus,
|
|
340
|
+
feishuStatus: 'unbound',
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
async function ensureSystemManager(): Promise<SystemManagerSummary> {
|
|
345
|
+
systemManagerEnsurePromise ??= ensureSystemManagerUncached().finally(() => {
|
|
346
|
+
systemManagerEnsurePromise = null;
|
|
347
|
+
});
|
|
348
|
+
return systemManagerEnsurePromise;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const conversationTelemetry = new ConversationTelemetryService({
|
|
352
|
+
cc,
|
|
353
|
+
listTeams: () => svc.listTeams(),
|
|
354
|
+
readTeamManifest: (teamName) => svc.readTeamManifest(teamName),
|
|
355
|
+
});
|
|
356
|
+
const localSessionScanner = new LocalSessionScanner();
|
|
357
|
+
|
|
358
|
+
async function computeTeamStats(
|
|
359
|
+
workDir: string
|
|
360
|
+
): Promise<{ sessions: number; messages: number; tokens: number; durationMs: number } | undefined> {
|
|
361
|
+
if (!workDir) return undefined;
|
|
362
|
+
try {
|
|
363
|
+
const sessions = await localSessionScanner.scanSummaries(workDir, '');
|
|
364
|
+
if (sessions.length === 0) return undefined;
|
|
365
|
+
let tokens = 0;
|
|
366
|
+
let messages = 0;
|
|
367
|
+
let earliest: string | null = null;
|
|
368
|
+
let latest: string | null = null;
|
|
369
|
+
for (const s of sessions) {
|
|
370
|
+
tokens += s.inputTokens + s.outputTokens;
|
|
371
|
+
messages += s.messageCount;
|
|
372
|
+
if (s.startTime && (!earliest || s.startTime < earliest)) earliest = s.startTime;
|
|
373
|
+
if (s.endTime && (!latest || s.endTime > latest)) latest = s.endTime;
|
|
374
|
+
}
|
|
375
|
+
let durationMs = 0;
|
|
376
|
+
if (earliest && latest) {
|
|
377
|
+
durationMs = Date.parse(latest) - Date.parse(earliest);
|
|
378
|
+
if (durationMs < 0) durationMs = 0;
|
|
379
|
+
}
|
|
380
|
+
return { sessions: sessions.length, messages, tokens, durationMs };
|
|
381
|
+
} catch {
|
|
382
|
+
return undefined;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
async function resolveRouteCcProjectName(teamName: string): Promise<string> {
|
|
387
|
+
return resolveCcProjectName(teamName, (name) => svc.readTeamManifest(name));
|
|
388
|
+
}
|
|
389
|
+
|
|
201
390
|
const collabBoard = new CollaborationBoardService();
|
|
202
391
|
const taskDispatch = new TaskDispatchService(svc['workspace'], collabBoard);
|
|
203
392
|
|
|
@@ -387,9 +576,37 @@ const app = Fastify({
|
|
|
387
576
|
// Plugins
|
|
388
577
|
// ===========================================================================
|
|
389
578
|
|
|
579
|
+
const configuredCorsOrigins = process.env.CORS_ORIGIN?.split(',')
|
|
580
|
+
.map((origin) => origin.trim())
|
|
581
|
+
.filter(Boolean);
|
|
582
|
+
const defaultWebPort = process.env.WEB_PORT?.trim() || '5174';
|
|
583
|
+
const allowedCorsOrigins = configuredCorsOrigins?.length
|
|
584
|
+
? configuredCorsOrigins
|
|
585
|
+
: [
|
|
586
|
+
`http://127.0.0.1:${PORT}`,
|
|
587
|
+
`http://localhost:${PORT}`,
|
|
588
|
+
`http://127.0.0.1:${defaultWebPort}`,
|
|
589
|
+
`http://localhost:${defaultWebPort}`,
|
|
590
|
+
];
|
|
591
|
+
const allowedOriginSet = new Set(allowedCorsOrigins);
|
|
592
|
+
|
|
593
|
+
function isTrustedBrowserOrigin(origin: string | undefined): boolean {
|
|
594
|
+
if (!origin) return true;
|
|
595
|
+
return allowedOriginSet.has(origin);
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
function assertTrustedBrowserOrigin(request: import('fastify').FastifyRequest): void {
|
|
599
|
+
const origin = Array.isArray(request.headers.origin)
|
|
600
|
+
? request.headers.origin[0]
|
|
601
|
+
: request.headers.origin;
|
|
602
|
+
if (!isTrustedBrowserOrigin(origin)) {
|
|
603
|
+
throw new Error(`Forbidden origin: ${origin}`);
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
|
|
390
607
|
await app.register(cors, {
|
|
391
|
-
origin:
|
|
392
|
-
methods: ['GET', 'POST', 'PATCH', 'DELETE', 'OPTIONS'],
|
|
608
|
+
origin: allowedCorsOrigins,
|
|
609
|
+
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
|
|
393
610
|
});
|
|
394
611
|
|
|
395
612
|
// ===========================================================================
|
|
@@ -431,11 +648,28 @@ async function proxyToCcConnect(
|
|
|
431
648
|
}
|
|
432
649
|
|
|
433
650
|
const body = Buffer.from(await upstream.arrayBuffer());
|
|
651
|
+
|
|
652
|
+
// Detect non-JSON responses (HTML 404 pages, etc.) and return a clear error
|
|
653
|
+
// instead of forwarding garbage that will crash the frontend's JSON.parse.
|
|
654
|
+
const contentType = upstream.headers.get('content-type') ?? '';
|
|
655
|
+
if (!contentType.includes('json') && upstream.status >= 400) {
|
|
656
|
+
const snippet = body.toString('utf-8').slice(0, 100).trim();
|
|
657
|
+
request.log.warn(
|
|
658
|
+
{ target, status: upstream.status, contentType, snippet },
|
|
659
|
+
'cc-connect returned non-JSON error response'
|
|
660
|
+
);
|
|
661
|
+
return reply.code(upstream.status).send({
|
|
662
|
+
ok: false,
|
|
663
|
+
error: `cc-connect 端点 ${subPath} 返回了非 JSON 响应 (HTTP ${upstream.status})。` +
|
|
664
|
+
'请检查 cc-connect 是否正在运行且支持该端点。',
|
|
665
|
+
});
|
|
666
|
+
}
|
|
667
|
+
|
|
434
668
|
return reply
|
|
435
669
|
.code(upstream.status)
|
|
436
670
|
.header(
|
|
437
671
|
'Content-Type',
|
|
438
|
-
|
|
672
|
+
contentType || 'application/json; charset=utf-8'
|
|
439
673
|
)
|
|
440
674
|
.send(body);
|
|
441
675
|
}
|
|
@@ -786,72 +1020,241 @@ app.post('/api/cc-reload', async () => {
|
|
|
786
1020
|
// Teams — cc-connect projects 即团队,本地 ~/.hermit/teams/ 仅存 tasks + 额外元数据
|
|
787
1021
|
// ===========================================================================
|
|
788
1022
|
|
|
789
|
-
//
|
|
1023
|
+
// POST /api/system-manager/ensure → 确保项目级控制台存在
|
|
1024
|
+
app.post('/api/system-manager/ensure', async (_request, reply) => {
|
|
1025
|
+
try {
|
|
1026
|
+
return await ensureSystemManager();
|
|
1027
|
+
} catch (err) {
|
|
1028
|
+
return reply.code(500).send({ error: err instanceof Error ? err.message : String(err) });
|
|
1029
|
+
}
|
|
1030
|
+
});
|
|
1031
|
+
|
|
1032
|
+
app.get('/api/system-manager/status', async (_request, reply) => {
|
|
1033
|
+
try {
|
|
1034
|
+
return await systemManagerConfig.getStatus();
|
|
1035
|
+
} catch (err) {
|
|
1036
|
+
return reply.code(500).send({ error: err instanceof Error ? err.message : String(err) });
|
|
1037
|
+
}
|
|
1038
|
+
});
|
|
1039
|
+
|
|
1040
|
+
app.get('/api/system-manager/config', async (_request, reply) => {
|
|
1041
|
+
try {
|
|
1042
|
+
return await systemManagerConfig.getConfig();
|
|
1043
|
+
} catch (err) {
|
|
1044
|
+
return reply.code(500).send({ error: err instanceof Error ? err.message : String(err) });
|
|
1045
|
+
}
|
|
1046
|
+
});
|
|
1047
|
+
|
|
1048
|
+
app.put<{ Body: { selectedWorkDir?: string; workflowFolder?: string | null } }>(
|
|
1049
|
+
'/api/system-manager/config',
|
|
1050
|
+
async (request, reply) => {
|
|
1051
|
+
try {
|
|
1052
|
+
const config = await systemManagerConfig.updateConfig(request.body ?? {});
|
|
1053
|
+
await syncSystemManagerManifestWorkDir(config.selectedWorkDir);
|
|
1054
|
+
return config;
|
|
1055
|
+
} catch (err) {
|
|
1056
|
+
return reply.code(400).send({ error: err instanceof Error ? err.message : String(err) });
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
);
|
|
1060
|
+
|
|
1061
|
+
app.post<{ Body: { folder?: string } }>(
|
|
1062
|
+
'/api/system-manager/workflows/list',
|
|
1063
|
+
async (request, reply) => {
|
|
1064
|
+
try {
|
|
1065
|
+
assertTrustedBrowserOrigin(request);
|
|
1066
|
+
const config = await systemManagerConfig.getConfig();
|
|
1067
|
+
const folder =
|
|
1068
|
+
typeof request.body?.folder === 'string' ? request.body.folder : config.workflowFolder;
|
|
1069
|
+
if (!folder) return { folder: '', prompts: [], warnings: [] };
|
|
1070
|
+
const result = await workflowPromptService.list(folder);
|
|
1071
|
+
await systemManagerConfig.updateConfig({ workflowFolder: result.folder });
|
|
1072
|
+
return result;
|
|
1073
|
+
} catch {
|
|
1074
|
+
return { folder: '', prompts: [], warnings: [] };
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
);
|
|
1078
|
+
|
|
1079
|
+
app.post<{ Body: { id?: string } }>(
|
|
1080
|
+
'/api/system-manager/workflows/read',
|
|
1081
|
+
async (request, reply) => {
|
|
1082
|
+
try {
|
|
1083
|
+
assertTrustedBrowserOrigin(request);
|
|
1084
|
+
const config = await systemManagerConfig.getConfig();
|
|
1085
|
+
if (!config.workflowFolder)
|
|
1086
|
+
return reply.code(400).send({ error: 'workflowFolder is not configured' });
|
|
1087
|
+
const id = typeof request.body?.id === 'string' ? request.body.id : '';
|
|
1088
|
+
return await workflowPromptService.read(config.workflowFolder, id);
|
|
1089
|
+
} catch (err) {
|
|
1090
|
+
return reply.code(400).send({ error: err instanceof Error ? err.message : String(err) });
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
);
|
|
1094
|
+
|
|
1095
|
+
app.post<{ Body: { command?: string; args?: string[]; cwd?: string; cols?: number; rows?: number } }>(
|
|
1096
|
+
'/api/terminal/spawn',
|
|
1097
|
+
async (request, reply) => {
|
|
1098
|
+
try {
|
|
1099
|
+
assertTrustedBrowserOrigin(request);
|
|
1100
|
+
const requestedCwd = typeof request.body?.cwd === 'string' ? request.body.cwd.trim() : '';
|
|
1101
|
+
const command = typeof request.body?.command === 'string' ? request.body.command : 'claude';
|
|
1102
|
+
const args = Array.isArray(request.body?.args) ? request.body.args : [];
|
|
1103
|
+
|
|
1104
|
+
// Use requested cwd if provided; otherwise fall back to system manager config
|
|
1105
|
+
let cwd: string;
|
|
1106
|
+
if (requestedCwd) {
|
|
1107
|
+
cwd = requestedCwd;
|
|
1108
|
+
// Update system manager config to track the work dir
|
|
1109
|
+
await systemManagerConfig.updateConfig({ selectedWorkDir: cwd });
|
|
1110
|
+
await syncSystemManagerManifestWorkDir(cwd);
|
|
1111
|
+
} else {
|
|
1112
|
+
const config = await systemManagerConfig.getConfig();
|
|
1113
|
+
cwd = config.selectedWorkDir;
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
const ptyId = await systemManagerPty.spawn({
|
|
1117
|
+
command,
|
|
1118
|
+
args,
|
|
1119
|
+
cwd,
|
|
1120
|
+
cols: request.body?.cols,
|
|
1121
|
+
rows: request.body?.rows,
|
|
1122
|
+
});
|
|
1123
|
+
return { ptyId };
|
|
1124
|
+
} catch (err) {
|
|
1125
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1126
|
+
return reply
|
|
1127
|
+
.code(message.startsWith('Forbidden origin:') ? 403 : 500)
|
|
1128
|
+
.send({ error: message });
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
);
|
|
1132
|
+
|
|
1133
|
+
app.post<{ Params: { ptyId: string }; Body: { data?: string } }>(
|
|
1134
|
+
'/api/terminal/:ptyId/write',
|
|
1135
|
+
async (request, reply) => {
|
|
1136
|
+
try {
|
|
1137
|
+
assertTrustedBrowserOrigin(request);
|
|
1138
|
+
systemManagerPty.write(request.params.ptyId, request.body?.data ?? '');
|
|
1139
|
+
return { ok: true };
|
|
1140
|
+
} catch (err) {
|
|
1141
|
+
return reply.code(404).send({ error: err instanceof Error ? err.message : String(err) });
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
);
|
|
1145
|
+
|
|
1146
|
+
app.post<{ Params: { ptyId: string }; Body: { cols?: number; rows?: number } }>(
|
|
1147
|
+
'/api/terminal/:ptyId/resize',
|
|
1148
|
+
async (request, reply) => {
|
|
1149
|
+
try {
|
|
1150
|
+
assertTrustedBrowserOrigin(request);
|
|
1151
|
+
systemManagerPty.resize(
|
|
1152
|
+
request.params.ptyId,
|
|
1153
|
+
request.body?.cols ?? 120,
|
|
1154
|
+
request.body?.rows ?? 34
|
|
1155
|
+
);
|
|
1156
|
+
return { ok: true };
|
|
1157
|
+
} catch (err) {
|
|
1158
|
+
return reply.code(403).send({ error: err instanceof Error ? err.message : String(err) });
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
);
|
|
1162
|
+
|
|
1163
|
+
app.delete<{ Params: { ptyId: string } }>('/api/terminal/:ptyId', async (request, reply) => {
|
|
1164
|
+
try {
|
|
1165
|
+
assertTrustedBrowserOrigin(request);
|
|
1166
|
+
await systemManagerPty.kill(request.params.ptyId);
|
|
1167
|
+
return { ok: true };
|
|
1168
|
+
} catch (err) {
|
|
1169
|
+
return reply.code(403).send({ error: err instanceof Error ? err.message : String(err) });
|
|
1170
|
+
}
|
|
1171
|
+
});
|
|
1172
|
+
|
|
1173
|
+
// POST /api/terminal/open-external — open command in system Terminal.app
|
|
1174
|
+
app.post<{ Body: { command: string; args?: string[]; cwd?: string } }>(
|
|
1175
|
+
'/api/terminal/open-external',
|
|
1176
|
+
async (request, reply) => {
|
|
1177
|
+
try {
|
|
1178
|
+
const { command, args = [], cwd } = request.body ?? {};
|
|
1179
|
+
if (!command) return reply.code(400).send({ error: 'command is required' });
|
|
1180
|
+
const cmd = [command, ...args].join(' ');
|
|
1181
|
+
const script = cwd
|
|
1182
|
+
? `tell application "Terminal"\ndo script "cd ${cwd.replace(/"/g, '\\"')} && ${cmd.replace(/"/g, '\\"')}"\nactivate\nend tell`
|
|
1183
|
+
: `tell application "Terminal"\ndo script "${cmd.replace(/"/g, '\\"')}"\nactivate\nend tell`;
|
|
1184
|
+
const { execFile } = await import('node:child_process');
|
|
1185
|
+
execFile('osascript', ['-e', script]);
|
|
1186
|
+
return { ok: true };
|
|
1187
|
+
} catch (err) {
|
|
1188
|
+
return reply.code(500).send({ error: err instanceof Error ? err.message : String(err) });
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
);
|
|
1192
|
+
|
|
1193
|
+
// GET /api/teams → Hermit 本地团队优先,裸 cc-connect project 作为历史兼容显示;过滤飞书/系统项目
|
|
790
1194
|
app.get('/api/teams', async () => {
|
|
791
1195
|
try {
|
|
792
|
-
const projects = await
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
let displayName = p.name;
|
|
800
|
-
let color = 'blue';
|
|
801
|
-
let description = `${p.agent_type} · ${p.platforms?.join(', ') ?? ''}`;
|
|
802
|
-
let workDir = '';
|
|
803
|
-
let pendingDelete = false;
|
|
804
|
-
let restartRequired = false;
|
|
805
|
-
try {
|
|
806
|
-
const meta = await svc.readTeamManifest(p.name);
|
|
807
|
-
if (meta.displayName) displayName = meta.displayName;
|
|
808
|
-
if (meta.color) color = meta.color;
|
|
809
|
-
if (meta.description) description = meta.description;
|
|
810
|
-
pendingDelete = meta.pendingDelete === true;
|
|
811
|
-
restartRequired = meta.restartRequired === true;
|
|
812
|
-
if (typeof meta.workDir === 'string') {
|
|
813
|
-
workDir = meta.workDir.trim();
|
|
814
|
-
}
|
|
815
|
-
} catch {
|
|
816
|
-
/* no local manifest, use defaults */
|
|
817
|
-
}
|
|
1196
|
+
const [projects, localTeams] = await Promise.all([
|
|
1197
|
+
cc.listProjects().catch(() => []),
|
|
1198
|
+
svc.listTeams().catch(() => []),
|
|
1199
|
+
]);
|
|
1200
|
+
const projectByName = new Map(projects.map((project) => [project.name, project]));
|
|
1201
|
+
const shouldHideProject = (name: string): boolean =>
|
|
1202
|
+
isReservedSystemTeamName(name) || name.startsWith('feishu:');
|
|
818
1203
|
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
1204
|
+
const summaries = await Promise.all(
|
|
1205
|
+
localTeams
|
|
1206
|
+
.filter((meta) => {
|
|
1207
|
+
const bindProject = meta.bindProject || meta.slug;
|
|
1208
|
+
return (
|
|
1209
|
+
meta.pendingDelete !== true &&
|
|
1210
|
+
!isReservedSystemTeamName(meta.slug) &&
|
|
1211
|
+
!shouldHideProject(bindProject) &&
|
|
1212
|
+
!meta.slug.startsWith('feishu:')
|
|
1213
|
+
);
|
|
1214
|
+
})
|
|
1215
|
+
.map(async (meta) => {
|
|
1216
|
+
const bindProject = meta.bindProject || meta.slug;
|
|
1217
|
+
const project = projectByName.get(bindProject);
|
|
1218
|
+
const isOnline = Array.isArray(project?.platforms) && project.platforms.length > 0;
|
|
1219
|
+
let workDir = (meta.workDir || '').trim();
|
|
1220
|
+
if (!workDir && project) {
|
|
1221
|
+
try {
|
|
1222
|
+
const detail = await cc.getProject(bindProject);
|
|
1223
|
+
if (typeof detail.work_dir === 'string' && detail.work_dir.trim()) {
|
|
1224
|
+
workDir = detail.work_dir.trim();
|
|
1225
|
+
}
|
|
1226
|
+
} catch {
|
|
1227
|
+
// ignore detail read failure, keep manifest/default path
|
|
825
1228
|
}
|
|
826
|
-
} catch {
|
|
827
|
-
// ignore detail read failure, keep empty path
|
|
828
1229
|
}
|
|
829
|
-
|
|
1230
|
+
const harness = toCcAgentType(project?.agent_type || meta.harness);
|
|
1231
|
+
const color = meta.color || 'blue';
|
|
1232
|
+
const displayName = meta.displayName || meta.slug;
|
|
830
1233
|
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
return summaries.filter(
|
|
853
|
-
(team) => team.pendingDelete !== true && team.teamName !== 'my-project'
|
|
1234
|
+
return {
|
|
1235
|
+
teamName: meta.slug,
|
|
1236
|
+
displayName,
|
|
1237
|
+
description: meta.description || '本地数字员工',
|
|
1238
|
+
color,
|
|
1239
|
+
memberCount: 1,
|
|
1240
|
+
members: [{ name: displayName, role: 'agent', agentId: harness, color }],
|
|
1241
|
+
taskCount: 0,
|
|
1242
|
+
lastActivity: null,
|
|
1243
|
+
isAlive: isOnline,
|
|
1244
|
+
harness,
|
|
1245
|
+
bindProject,
|
|
1246
|
+
workDir,
|
|
1247
|
+
projectPath: workDir || undefined,
|
|
1248
|
+
sessionsCount: project?.sessions_count ?? 0,
|
|
1249
|
+
heartbeatEnabled: project?.heartbeat_enabled ?? false,
|
|
1250
|
+
pendingDelete: meta.pendingDelete === true,
|
|
1251
|
+
restartRequired: meta.restartRequired === true,
|
|
1252
|
+
stats: await computeTeamStats(workDir),
|
|
1253
|
+
};
|
|
1254
|
+
})
|
|
854
1255
|
);
|
|
1256
|
+
|
|
1257
|
+
return summaries;
|
|
855
1258
|
} catch {
|
|
856
1259
|
return [];
|
|
857
1260
|
}
|
|
@@ -869,41 +1272,36 @@ app.post('/api/teams/create', async (request, reply) => {
|
|
|
869
1272
|
if (!name) return reply.code(400).send({ error: 'name required' });
|
|
870
1273
|
if (!workDir) return reply.code(400).send({ error: 'workDir required' });
|
|
871
1274
|
|
|
1275
|
+
// Check for duplicate displayName to prevent creating multiple teams with the same Chinese name
|
|
1276
|
+
const existingTeams = await svc.listTeams().catch(() => []);
|
|
1277
|
+
const normalizedName = displayName.toLowerCase();
|
|
1278
|
+
const duplicate = existingTeams.find(
|
|
1279
|
+
(t) => t.displayName?.toLowerCase() === normalizedName
|
|
1280
|
+
);
|
|
1281
|
+
if (duplicate) {
|
|
1282
|
+
return reply.code(409).send({
|
|
1283
|
+
error: `数字员工"${displayName}"已存在(ID: ${duplicate.slug})。请使用不同的名称。`,
|
|
1284
|
+
});
|
|
1285
|
+
}
|
|
1286
|
+
|
|
872
1287
|
// Normalize path: fullwidth tilde → regular tilde, expand ~ to home
|
|
873
1288
|
workDir = workDir.replace(/\uff5e/g, '~');
|
|
874
1289
|
if (workDir.startsWith('~')) {
|
|
875
1290
|
workDir = path.join(os.homedir(), workDir.slice(1));
|
|
876
1291
|
}
|
|
877
1292
|
|
|
878
|
-
//
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
description: typeof body.description === 'string' ? body.description : undefined,
|
|
889
|
-
platform: platformType,
|
|
890
|
-
createCcProject: false,
|
|
891
|
-
});
|
|
892
|
-
} catch (err) {
|
|
893
|
-
request.log.warn({ err, teamName: name }, 'failed to persist local team metadata');
|
|
894
|
-
}
|
|
895
|
-
|
|
896
|
-
// Bind provider refs if specified
|
|
897
|
-
const providerRefs = Array.isArray(body.providerRefs) ? (body.providerRefs as string[]) : [];
|
|
898
|
-
if (providerRefs.length > 0) {
|
|
899
|
-
try {
|
|
900
|
-
await cc.setProviderRefs(name, providerRefs);
|
|
901
|
-
} catch (err) {
|
|
902
|
-
request.log.warn({ err, teamName: name, providerRefs }, 'failed to set provider refs');
|
|
903
|
-
}
|
|
904
|
-
}
|
|
1293
|
+
// 本地创建只落 Hermit 团队目录;飞书/微信等外部平台在团队内按需绑定。
|
|
1294
|
+
await svc.createTeam({
|
|
1295
|
+
displayName,
|
|
1296
|
+
bindProject: name,
|
|
1297
|
+
harness,
|
|
1298
|
+
workDir,
|
|
1299
|
+
color: typeof body.color === 'string' ? body.color : undefined,
|
|
1300
|
+
description: typeof body.description === 'string' ? body.description : undefined,
|
|
1301
|
+
createCcProject: false,
|
|
1302
|
+
});
|
|
905
1303
|
|
|
906
|
-
return {
|
|
1304
|
+
return { runId: `local:${name}:${Date.now()}` };
|
|
907
1305
|
} catch (err) {
|
|
908
1306
|
return reply.code(500).send({ error: err instanceof Error ? err.message : String(err) });
|
|
909
1307
|
}
|
|
@@ -928,11 +1326,14 @@ app.get<{ Params: { name: string } }>('/api/teams/:name/data', async (request, r
|
|
|
928
1326
|
let managedSources = '*';
|
|
929
1327
|
let disabledCommands: string[] = [];
|
|
930
1328
|
let platformAllowFrom: Record<string, string> = {};
|
|
1329
|
+
let platformAllowChat: Record<string, string> = {};
|
|
1330
|
+
let bindProject = name;
|
|
931
1331
|
try {
|
|
932
1332
|
const meta = await svc.readTeamManifest(name);
|
|
933
1333
|
if (meta.displayName) displayName = meta.displayName;
|
|
934
1334
|
if (meta.color) color = meta.color;
|
|
935
1335
|
if (meta.description) description = meta.description;
|
|
1336
|
+
bindProject = meta.bindProject || name;
|
|
936
1337
|
collaboration = meta.collaboration ?? true;
|
|
937
1338
|
if (meta.workDir) workDir = meta.workDir;
|
|
938
1339
|
if (meta.harness) harness = meta.harness;
|
|
@@ -954,6 +1355,9 @@ app.get<{ Params: { name: string } }>('/api/teams/:name/data', async (request, r
|
|
|
954
1355
|
if (meta.platformAllowFrom) {
|
|
955
1356
|
platformAllowFrom = normalizePlatformAllowFrom(meta.platformAllowFrom);
|
|
956
1357
|
}
|
|
1358
|
+
if (meta.platformAllowChat) {
|
|
1359
|
+
platformAllowChat = normalizePlatformAllowFrom(meta.platformAllowChat);
|
|
1360
|
+
}
|
|
957
1361
|
} catch {
|
|
958
1362
|
/* no local manifest */
|
|
959
1363
|
}
|
|
@@ -963,7 +1367,8 @@ app.get<{ Params: { name: string } }>('/api/teams/:name/data', async (request, r
|
|
|
963
1367
|
const teamTasks = rawTasks.map(toTeamTask);
|
|
964
1368
|
|
|
965
1369
|
try {
|
|
966
|
-
|
|
1370
|
+
bindProject = await resolveRouteCcProjectName(name);
|
|
1371
|
+
const p = await cc.getProject(bindProject);
|
|
967
1372
|
const isOnline = Array.isArray(p.platforms) && p.platforms.some((pl) => pl.connected);
|
|
968
1373
|
const projectSettings = (p.settings ?? {}) as Record<string, unknown>;
|
|
969
1374
|
const resolvedLanguage =
|
|
@@ -998,12 +1403,21 @@ app.get<{ Params: { name: string } }>('/api/teams/:name/data', async (request, r
|
|
|
998
1403
|
}
|
|
999
1404
|
return platformAllowFrom;
|
|
1000
1405
|
})();
|
|
1406
|
+
const resolvedPlatformAllowChat = (() => {
|
|
1407
|
+
const normalized = normalizePlatformAllowFrom(
|
|
1408
|
+
(projectSettings as Record<string, unknown>).platform_allow_chat
|
|
1409
|
+
);
|
|
1410
|
+
if (Object.keys(normalized).length > 0) {
|
|
1411
|
+
return normalized;
|
|
1412
|
+
}
|
|
1413
|
+
return platformAllowChat;
|
|
1414
|
+
})();
|
|
1001
1415
|
const resolvedPermissionMode =
|
|
1002
1416
|
typeof p.agent_mode === 'string' && p.agent_mode.trim().length > 0
|
|
1003
1417
|
? p.agent_mode.trim()
|
|
1004
1418
|
: permissionMode;
|
|
1005
1419
|
const [providerRefs, globalProviders] = await Promise.all([
|
|
1006
|
-
cc.getProviderRefs(
|
|
1420
|
+
cc.getProviderRefs(bindProject).catch(() => []),
|
|
1007
1421
|
cc.listProviders().catch(() => []),
|
|
1008
1422
|
]);
|
|
1009
1423
|
|
|
@@ -1022,6 +1436,7 @@ app.get<{ Params: { name: string } }>('/api/teams/:name/data', async (request, r
|
|
|
1022
1436
|
managedSources: resolvedManagedSources,
|
|
1023
1437
|
disabledCommands: resolvedDisabledCommands,
|
|
1024
1438
|
platformAllowFrom: resolvedPlatformAllowFrom,
|
|
1439
|
+
platformAllowChat: resolvedPlatformAllowChat,
|
|
1025
1440
|
projectPath: p.work_dir ?? workDir,
|
|
1026
1441
|
members: [{ name: displayName, role: 'lead' }],
|
|
1027
1442
|
},
|
|
@@ -1040,11 +1455,12 @@ app.get<{ Params: { name: string } }>('/api/teams/:name/data', async (request, r
|
|
|
1040
1455
|
kanbanState: { teamName: name, reviewers: [], tasks: {} },
|
|
1041
1456
|
processes: [],
|
|
1042
1457
|
isAlive: isOnline,
|
|
1458
|
+
platforms: (p.platforms ?? []) as CcProjectPlatform[],
|
|
1043
1459
|
harness: p.agent_type,
|
|
1044
|
-
bindProject
|
|
1460
|
+
bindProject,
|
|
1045
1461
|
collaboration,
|
|
1046
1462
|
description,
|
|
1047
|
-
workDir: p.work_dir
|
|
1463
|
+
workDir: workDir || p.work_dir,
|
|
1048
1464
|
permissionMode: resolvedPermissionMode,
|
|
1049
1465
|
providerRefs,
|
|
1050
1466
|
globalProviders,
|
|
@@ -1057,6 +1473,7 @@ app.get<{ Params: { name: string } }>('/api/teams/:name/data', async (request, r
|
|
|
1057
1473
|
reply_footer: resolvedReplyFooter,
|
|
1058
1474
|
inject_sender: resolvedInjectSender,
|
|
1059
1475
|
platform_allow_from: resolvedPlatformAllowFrom,
|
|
1476
|
+
platform_allow_chat: resolvedPlatformAllowChat,
|
|
1060
1477
|
},
|
|
1061
1478
|
heartbeat: p.heartbeat,
|
|
1062
1479
|
activeSessions: p.active_session_keys ?? [],
|
|
@@ -1096,8 +1513,9 @@ app.get<{ Params: { name: string } }>('/api/teams/:name/data', async (request, r
|
|
|
1096
1513
|
kanbanState: { teamName: name, reviewers: [], tasks: {} },
|
|
1097
1514
|
processes: [],
|
|
1098
1515
|
isAlive: false,
|
|
1516
|
+
platforms: [] as CcProjectPlatform[],
|
|
1099
1517
|
harness,
|
|
1100
|
-
bindProject
|
|
1518
|
+
bindProject,
|
|
1101
1519
|
collaboration,
|
|
1102
1520
|
description,
|
|
1103
1521
|
workDir,
|
|
@@ -1137,8 +1555,8 @@ app.delete<{ Params: { name: string }; Querystring: { deleteFiles?: string } }>(
|
|
|
1137
1555
|
'/api/teams/:name',
|
|
1138
1556
|
async (request, reply) => {
|
|
1139
1557
|
const teamName = request.params.name;
|
|
1140
|
-
if (teamName
|
|
1141
|
-
return reply.code(403).send({ error: '
|
|
1558
|
+
if (isReservedSystemTeamName(teamName)) {
|
|
1559
|
+
return reply.code(403).send({ error: '控制台不可删除' });
|
|
1142
1560
|
}
|
|
1143
1561
|
try {
|
|
1144
1562
|
let restartRequired = false;
|
|
@@ -1347,7 +1765,8 @@ app.patch<{ Params: { name: string }; Body: { collaboration: boolean } }>(
|
|
|
1347
1765
|
|
|
1348
1766
|
app.get<{ Params: { name: string } }>('/api/teams/:name/heartbeat', async (request, reply) => {
|
|
1349
1767
|
try {
|
|
1350
|
-
const
|
|
1768
|
+
const bindProject = await resolveRouteCcProjectName(request.params.name);
|
|
1769
|
+
const data = await cc.getHeartbeat(bindProject);
|
|
1351
1770
|
return { ok: true, data };
|
|
1352
1771
|
} catch (err) {
|
|
1353
1772
|
return reply.code(404).send(reply500(err));
|
|
@@ -1358,7 +1777,8 @@ app.post<{ Params: { name: string } }>(
|
|
|
1358
1777
|
'/api/teams/:name/heartbeat/enable',
|
|
1359
1778
|
async (request, reply) => {
|
|
1360
1779
|
try {
|
|
1361
|
-
await
|
|
1780
|
+
const bindProject = await resolveRouteCcProjectName(request.params.name);
|
|
1781
|
+
await cc.resumeHeartbeat(bindProject);
|
|
1362
1782
|
return { ok: true };
|
|
1363
1783
|
} catch (err) {
|
|
1364
1784
|
return reply.code(500).send(reply500(err));
|
|
@@ -1370,7 +1790,8 @@ app.post<{ Params: { name: string } }>(
|
|
|
1370
1790
|
'/api/teams/:name/heartbeat/disable',
|
|
1371
1791
|
async (request, reply) => {
|
|
1372
1792
|
try {
|
|
1373
|
-
await
|
|
1793
|
+
const bindProject = await resolveRouteCcProjectName(request.params.name);
|
|
1794
|
+
await cc.pauseHeartbeat(bindProject);
|
|
1374
1795
|
return { ok: true };
|
|
1375
1796
|
} catch (err) {
|
|
1376
1797
|
return reply.code(500).send(reply500(err));
|
|
@@ -1382,7 +1803,8 @@ app.post<{ Params: { name: string } }>(
|
|
|
1382
1803
|
'/api/teams/:name/heartbeat/pause',
|
|
1383
1804
|
async (request, reply) => {
|
|
1384
1805
|
try {
|
|
1385
|
-
await
|
|
1806
|
+
const bindProject = await resolveRouteCcProjectName(request.params.name);
|
|
1807
|
+
await cc.pauseHeartbeat(bindProject);
|
|
1386
1808
|
return { ok: true };
|
|
1387
1809
|
} catch (err) {
|
|
1388
1810
|
return reply.code(500).send(reply500(err));
|
|
@@ -1394,7 +1816,8 @@ app.post<{ Params: { name: string } }>(
|
|
|
1394
1816
|
'/api/teams/:name/heartbeat/resume',
|
|
1395
1817
|
async (request, reply) => {
|
|
1396
1818
|
try {
|
|
1397
|
-
await
|
|
1819
|
+
const bindProject = await resolveRouteCcProjectName(request.params.name);
|
|
1820
|
+
await cc.resumeHeartbeat(bindProject);
|
|
1398
1821
|
return { ok: true };
|
|
1399
1822
|
} catch (err) {
|
|
1400
1823
|
return reply.code(500).send(reply500(err));
|
|
@@ -1407,8 +1830,9 @@ app.patch<{
|
|
|
1407
1830
|
Body: { interval_mins?: number; only_when_idle?: boolean; silent?: boolean };
|
|
1408
1831
|
}>('/api/teams/:name/heartbeat', async (request, reply) => {
|
|
1409
1832
|
try {
|
|
1410
|
-
await
|
|
1411
|
-
|
|
1833
|
+
const bindProject = await resolveRouteCcProjectName(request.params.name);
|
|
1834
|
+
await cc.updateProject(bindProject, request.body as Record<string, unknown>);
|
|
1835
|
+
const data = await cc.getHeartbeat(bindProject);
|
|
1412
1836
|
return { ok: true, data };
|
|
1413
1837
|
} catch (err) {
|
|
1414
1838
|
return reply.code(500).send(reply500(err));
|
|
@@ -1420,21 +1844,6 @@ app.patch<{
|
|
|
1420
1844
|
// GET /api/harnesses
|
|
1421
1845
|
// ===========================================================================
|
|
1422
1846
|
|
|
1423
|
-
const CC_AGENT_TYPES = [
|
|
1424
|
-
'claudecode',
|
|
1425
|
-
'codex',
|
|
1426
|
-
'cursor',
|
|
1427
|
-
'gemini',
|
|
1428
|
-
'iflow',
|
|
1429
|
-
'kimi',
|
|
1430
|
-
'devin',
|
|
1431
|
-
'opencode',
|
|
1432
|
-
'qoder',
|
|
1433
|
-
'pi',
|
|
1434
|
-
'acp',
|
|
1435
|
-
'tmux',
|
|
1436
|
-
] as const;
|
|
1437
|
-
|
|
1438
1847
|
app.get('/api/harnesses', async () => {
|
|
1439
1848
|
try {
|
|
1440
1849
|
const projects = await cc.listProjects();
|
|
@@ -1487,26 +1896,29 @@ app.post<{ Params: { name: string }; Body: Partial<TeamLaunchRequest> }>(
|
|
|
1487
1896
|
if (!workDir) {
|
|
1488
1897
|
return reply.code(400).send({ error: '团队缺少项目路径,无法启动 cc-connect project' });
|
|
1489
1898
|
}
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
}
|
|
1500
|
-
projectExists = true;
|
|
1501
|
-
} else {
|
|
1502
|
-
await cc.restart();
|
|
1899
|
+
try {
|
|
1900
|
+
await cc.createProject(
|
|
1901
|
+
bindProject,
|
|
1902
|
+
harness,
|
|
1903
|
+
workDir,
|
|
1904
|
+
platformType,
|
|
1905
|
+
platformOptions as Record<string, string>
|
|
1906
|
+
);
|
|
1907
|
+
projectExists = true;
|
|
1908
|
+
} catch { /* CC Connect project creation is best-effort */ }
|
|
1503
1909
|
}
|
|
1910
|
+
// Restart cc-connect to (re-)activate platform connections.
|
|
1911
|
+
// Covers: newly created project, existing project with disconnected platform,
|
|
1912
|
+
// Feishu/Lark IM that lost connection after cc-connect restart, etc.
|
|
1913
|
+
try {
|
|
1914
|
+
await cc.restart();
|
|
1915
|
+
} catch { /* restart is best-effort */ }
|
|
1504
1916
|
}
|
|
1505
1917
|
|
|
1506
1918
|
return {
|
|
1507
1919
|
runId: `cc-connect:${bindProject}:${Date.now()}`,
|
|
1508
1920
|
ok: true,
|
|
1509
|
-
data: { teamName: name, bindProject, projectExists, isOnline
|
|
1921
|
+
data: { teamName: name, bindProject, projectExists, isOnline },
|
|
1510
1922
|
};
|
|
1511
1923
|
} catch (err) {
|
|
1512
1924
|
return reply.code(404).send(reply500(err));
|
|
@@ -1516,8 +1928,11 @@ app.post<{ Params: { name: string }; Body: Partial<TeamLaunchRequest> }>(
|
|
|
1516
1928
|
|
|
1517
1929
|
app.post<{ Params: { name: string } }>('/api/teams/:name/stop', async (request) => {
|
|
1518
1930
|
const name = request.params.name;
|
|
1519
|
-
|
|
1520
|
-
|
|
1931
|
+
const bindProject = await resolveRouteCcProjectName(name);
|
|
1932
|
+
// Stop = delete project from cc-connect (best-effort, no restart)
|
|
1933
|
+
try {
|
|
1934
|
+
await cc.deleteProject(bindProject);
|
|
1935
|
+
} catch { /* project may not exist in cc-connect */ }
|
|
1521
1936
|
// Keep local team metadata intact by not deleting it
|
|
1522
1937
|
// The team will show as offline (isAlive: false) on next data fetch
|
|
1523
1938
|
return { ok: true };
|
|
@@ -2125,7 +2540,17 @@ function readAppConfig() {
|
|
|
2125
2540
|
return mergeConfigDefaults(DEFAULT_APP_CONFIG, raw);
|
|
2126
2541
|
}
|
|
2127
2542
|
} catch (err) {
|
|
2128
|
-
|
|
2543
|
+
const msg = err instanceof SyntaxError
|
|
2544
|
+
? `${HERMIT_APP_CONFIG_FILE} 格式错误: ${err.message}。将使用默认配置并覆盖修复。`
|
|
2545
|
+
: `读取 ${HERMIT_APP_CONFIG_FILE} 失败`;
|
|
2546
|
+
app.log.warn({ err }, msg);
|
|
2547
|
+
// Auto-heal: rewrite with valid defaults
|
|
2548
|
+
try {
|
|
2549
|
+
mkdirSync(HERMIT_HOME, { recursive: true });
|
|
2550
|
+
writeFileSync(HERMIT_APP_CONFIG_FILE, JSON.stringify(DEFAULT_APP_CONFIG, null, 2), 'utf-8');
|
|
2551
|
+
} catch {
|
|
2552
|
+
// Give up if write also fails
|
|
2553
|
+
}
|
|
2129
2554
|
}
|
|
2130
2555
|
return DEFAULT_APP_CONFIG;
|
|
2131
2556
|
}
|
|
@@ -2323,11 +2748,26 @@ function mapCronJobToSchedule(
|
|
|
2323
2748
|
createdAt: string;
|
|
2324
2749
|
updatedAt: string;
|
|
2325
2750
|
lastRunAt?: string;
|
|
2751
|
+
nextRunAt?: string;
|
|
2326
2752
|
launchConfig: { cwd: string; prompt: string };
|
|
2327
2753
|
} {
|
|
2328
2754
|
const lastRunAt = normalizeCronLastRun(cronJob.last_run);
|
|
2329
2755
|
const status: 'active' | 'paused' = cronJob.enabled ? 'active' : 'paused';
|
|
2330
2756
|
|
|
2757
|
+
// Compute next run time from cron expression
|
|
2758
|
+
let nextRunAt: string | undefined;
|
|
2759
|
+
if (cronJob.enabled && isNonEmptyString(cronJob.cron_expr)) {
|
|
2760
|
+
try {
|
|
2761
|
+
const job = new Cron(cronJob.cron_expr.trim(), { timezone: DEFAULT_SCHEDULE_TIMEZONE, paused: true });
|
|
2762
|
+
const next = job.nextRun();
|
|
2763
|
+
if (next) {
|
|
2764
|
+
nextRunAt = (next instanceof Date ? next : new Date(next)).toISOString();
|
|
2765
|
+
}
|
|
2766
|
+
} catch {
|
|
2767
|
+
// Invalid cron expression — leave nextRunAt undefined
|
|
2768
|
+
}
|
|
2769
|
+
}
|
|
2770
|
+
|
|
2331
2771
|
return {
|
|
2332
2772
|
id: cronJob.id,
|
|
2333
2773
|
teamName: cronJob.project,
|
|
@@ -2342,6 +2782,7 @@ function mapCronJobToSchedule(
|
|
|
2342
2782
|
createdAt: cronJob.created_at,
|
|
2343
2783
|
updatedAt: lastRunAt ?? cronJob.created_at,
|
|
2344
2784
|
lastRunAt,
|
|
2785
|
+
nextRunAt,
|
|
2345
2786
|
launchConfig: {
|
|
2346
2787
|
cwd,
|
|
2347
2788
|
prompt: cronJob.prompt,
|
|
@@ -2781,7 +3222,6 @@ app.post<{ Body: { dirPath?: string } }>('/api/workspace/list', async (request)
|
|
|
2781
3222
|
try {
|
|
2782
3223
|
const entries = readdirSync(target, { withFileTypes: true });
|
|
2783
3224
|
const files = entries
|
|
2784
|
-
.filter((e) => !e.name.startsWith('.'))
|
|
2785
3225
|
.slice(0, 500)
|
|
2786
3226
|
.map((e) => {
|
|
2787
3227
|
const fullPath = path.join(target, e.name);
|
|
@@ -3145,8 +3585,9 @@ app.get<{ Params: { name: string }; Querystring: { cursor?: string; limit?: stri
|
|
|
3145
3585
|
);
|
|
3146
3586
|
try {
|
|
3147
3587
|
// Keep a bounded history snapshot in memory for pagination safety.
|
|
3588
|
+
const bindProject = await resolveRouteCcProjectName(name);
|
|
3148
3589
|
const msgs = await svc.readMessages(name, { limit: 5000 });
|
|
3149
|
-
const sessions = await cc.listSessions(
|
|
3590
|
+
const sessions = await cc.listSessions(bindProject).catch(() => []);
|
|
3150
3591
|
const sessionByKey = new Map(sessions.map((session) => [session.session_key, session]));
|
|
3151
3592
|
const newestFirstMessages = [...msgs].reverse();
|
|
3152
3593
|
const pageSlice = newestFirstMessages.slice(offset, offset + limit);
|
|
@@ -3261,63 +3702,68 @@ app.get<{ Params: { name: string } }>('/api/teams/:name/lead-context', async ()
|
|
|
3261
3702
|
return { usage: null };
|
|
3262
3703
|
});
|
|
3263
3704
|
|
|
3264
|
-
// sessions —
|
|
3705
|
+
// sessions — scan local JSONL files, optionally enrich with cc-connect identity metadata
|
|
3265
3706
|
app.get<{ Params: { name: string } }>('/api/teams/:name/sessions', async (request) => {
|
|
3266
3707
|
try {
|
|
3267
|
-
const
|
|
3268
|
-
const
|
|
3269
|
-
const
|
|
3270
|
-
|
|
3271
|
-
|
|
3272
|
-
|
|
3273
|
-
|
|
3274
|
-
|
|
3275
|
-
|
|
3276
|
-
|
|
3277
|
-
|
|
3278
|
-
|
|
3279
|
-
|
|
3280
|
-
|
|
3281
|
-
|
|
3282
|
-
|
|
3708
|
+
const team = await svc.readTeamManifest(request.params.name);
|
|
3709
|
+
const workDir = team.workDir || team.bindProject || request.params.name;
|
|
3710
|
+
const localSessions = await localSessionScanner.scanSummaries(workDir, request.params.name);
|
|
3711
|
+
|
|
3712
|
+
// Attempt to merge cc-connect identity metadata (platform/chatName/userName/lastMessage)
|
|
3713
|
+
let ccById = new Map<string, { platform: string; userName: string | null; chatName: string | null; lastMessage: { role: string; content: string; timestamp: string } | null }>();
|
|
3714
|
+
try {
|
|
3715
|
+
const bindProject = await resolveRouteCcProjectName(request.params.name);
|
|
3716
|
+
const ccSessions = await cc.listSessions(bindProject);
|
|
3717
|
+
for (const s of ccSessions) {
|
|
3718
|
+
ccById.set(s.id, {
|
|
3719
|
+
platform: s.platform,
|
|
3720
|
+
userName: s.user_name ?? null,
|
|
3721
|
+
chatName: s.chat_name ?? null,
|
|
3722
|
+
lastMessage: s.last_message
|
|
3723
|
+
? { role: s.last_message.role, content: s.last_message.content, timestamp: s.last_message.timestamp }
|
|
3724
|
+
: null,
|
|
3725
|
+
});
|
|
3283
3726
|
}
|
|
3284
|
-
}
|
|
3727
|
+
} catch { /* cc-connect unavailable — local-only data */ }
|
|
3285
3728
|
|
|
3286
|
-
return
|
|
3287
|
-
|
|
3288
|
-
|
|
3289
|
-
|
|
3290
|
-
|
|
3291
|
-
|
|
3292
|
-
|
|
3293
|
-
|
|
3294
|
-
|
|
3295
|
-
|
|
3296
|
-
|
|
3297
|
-
|
|
3298
|
-
|
|
3299
|
-
|
|
3300
|
-
|
|
3301
|
-
|
|
3302
|
-
|
|
3303
|
-
|
|
3304
|
-
}
|
|
3305
|
-
: null,
|
|
3306
|
-
}));
|
|
3729
|
+
return localSessions.map((s) => {
|
|
3730
|
+
const ccMeta = ccById.get(s.id);
|
|
3731
|
+
return {
|
|
3732
|
+
id: s.id,
|
|
3733
|
+
title: s.title || s.id,
|
|
3734
|
+
projectId: request.params.name,
|
|
3735
|
+
sessionKey: s.id,
|
|
3736
|
+
platform: ccMeta?.platform ?? 'local',
|
|
3737
|
+
userName: ccMeta?.userName ?? null,
|
|
3738
|
+
chatName: ccMeta?.chatName ?? null,
|
|
3739
|
+
active: s.active,
|
|
3740
|
+
live: s.live,
|
|
3741
|
+
historyCount: s.messageCount,
|
|
3742
|
+
createdAt: s.createdAt,
|
|
3743
|
+
updatedAt: s.updatedAt,
|
|
3744
|
+
lastMessage: ccMeta?.lastMessage ?? null,
|
|
3745
|
+
};
|
|
3746
|
+
});
|
|
3307
3747
|
} catch {
|
|
3308
3748
|
return [];
|
|
3309
3749
|
}
|
|
3310
3750
|
});
|
|
3311
3751
|
|
|
3312
|
-
// GET session detail —
|
|
3313
|
-
app.get<{ Params: { name: string; sessionId: string }; Querystring: { history_limit?: string } }>(
|
|
3752
|
+
// GET session detail — read local JSONL file for session history with pagination
|
|
3753
|
+
app.get<{ Params: { name: string; sessionId: string }; Querystring: { history_limit?: string; offset?: string } }>(
|
|
3314
3754
|
'/api/teams/:name/sessions/:sessionId',
|
|
3315
|
-
async (request) => {
|
|
3316
|
-
const
|
|
3755
|
+
async (request, reply) => {
|
|
3756
|
+
const limit = request.query.history_limit
|
|
3317
3757
|
? parseInt(request.query.history_limit, 10)
|
|
3318
3758
|
: 500;
|
|
3319
|
-
const
|
|
3320
|
-
|
|
3759
|
+
const offset = request.query.offset
|
|
3760
|
+
? parseInt(request.query.offset, 10)
|
|
3761
|
+
: 0;
|
|
3762
|
+
const team = await svc.readTeamManifest(request.params.name);
|
|
3763
|
+
const workDir = team.workDir || team.bindProject || request.params.name;
|
|
3764
|
+
const detail = await localSessionScanner.readSessionDetail(workDir, request.params.sessionId, { offset, limit });
|
|
3765
|
+
if (!detail) return reply.code(404).send({ error: 'Session not found' });
|
|
3766
|
+
return detail;
|
|
3321
3767
|
}
|
|
3322
3768
|
);
|
|
3323
3769
|
|
|
@@ -3326,13 +3772,8 @@ app.delete<{ Params: { name: string; sessionId: string } }>(
|
|
|
3326
3772
|
'/api/teams/:name/sessions/:sessionId',
|
|
3327
3773
|
async (request, reply) => {
|
|
3328
3774
|
try {
|
|
3329
|
-
const
|
|
3330
|
-
await
|
|
3331
|
-
teamName: request.params.name,
|
|
3332
|
-
sessionKey: detail.session_key,
|
|
3333
|
-
text: '/stop',
|
|
3334
|
-
msgId: `hermit-stop-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
|
3335
|
-
});
|
|
3775
|
+
const bindProject = await resolveRouteCcProjectName(request.params.name);
|
|
3776
|
+
await cc.deleteSession(bindProject, request.params.sessionId);
|
|
3336
3777
|
return { ok: true };
|
|
3337
3778
|
} catch (err) {
|
|
3338
3779
|
return reply
|
|
@@ -3345,7 +3786,11 @@ app.delete<{ Params: { name: string; sessionId: string } }>(
|
|
|
3345
3786
|
// runtime/alive — 从 cc-connect 获取真实在线状态
|
|
3346
3787
|
app.get('/api/teams/runtime/alive', async () => {
|
|
3347
3788
|
try {
|
|
3348
|
-
const projects = await
|
|
3789
|
+
const [projects, localTeams] = await Promise.all([
|
|
3790
|
+
cc.listProjects(),
|
|
3791
|
+
svc.listTeams().catch(() => []),
|
|
3792
|
+
]);
|
|
3793
|
+
const localByProject = new Map(localTeams.map((team) => [team.bindProject, team]));
|
|
3349
3794
|
return await Promise.all(
|
|
3350
3795
|
projects.map(async (p) => {
|
|
3351
3796
|
let isAlive = false;
|
|
@@ -3355,7 +3800,7 @@ app.get('/api/teams/runtime/alive', async () => {
|
|
|
3355
3800
|
} catch {
|
|
3356
3801
|
/* degraded */
|
|
3357
3802
|
}
|
|
3358
|
-
return { teamName: p.name, isAlive, runId: p.name };
|
|
3803
|
+
return { teamName: localByProject.get(p.name)?.slug ?? p.name, isAlive, runId: p.name };
|
|
3359
3804
|
})
|
|
3360
3805
|
);
|
|
3361
3806
|
} catch {
|
|
@@ -3366,7 +3811,8 @@ app.get('/api/teams/runtime/alive', async () => {
|
|
|
3366
3811
|
// process-alive — 查询 cc-connect project 在线状态
|
|
3367
3812
|
app.get<{ Params: { name: string } }>('/api/teams/:name/process-alive', async (request) => {
|
|
3368
3813
|
try {
|
|
3369
|
-
const
|
|
3814
|
+
const bindProject = await resolveRouteCcProjectName(request.params.name);
|
|
3815
|
+
const p = await cc.getProject(bindProject);
|
|
3370
3816
|
return Array.isArray(p.platforms) && p.platforms.some((pl) => pl.connected);
|
|
3371
3817
|
} catch {
|
|
3372
3818
|
return false;
|
|
@@ -3380,8 +3826,15 @@ app.post<{ Params: { name: string }; Body: { text?: string; message?: string } }
|
|
|
3380
3826
|
try {
|
|
3381
3827
|
const text = request.body?.text ?? request.body?.message ?? '';
|
|
3382
3828
|
if (text) {
|
|
3829
|
+
let targetProject = request.params.name;
|
|
3830
|
+
try {
|
|
3831
|
+
const manifest = await svc.readTeamManifest(request.params.name);
|
|
3832
|
+
targetProject = manifest.bindProject || request.params.name;
|
|
3833
|
+
} catch {
|
|
3834
|
+
// request.params.name may already be a cc-connect project name.
|
|
3835
|
+
}
|
|
3383
3836
|
await sendHarnessMessageViaBridge({
|
|
3384
|
-
teamName:
|
|
3837
|
+
teamName: targetProject,
|
|
3385
3838
|
text,
|
|
3386
3839
|
});
|
|
3387
3840
|
}
|
|
@@ -3628,6 +4081,21 @@ async function applyTeamConfigUpdate(
|
|
|
3628
4081
|
const platformAllowFrom = body.platformAllowFrom
|
|
3629
4082
|
? normalizePlatformAllowFrom(body.platformAllowFrom)
|
|
3630
4083
|
: undefined;
|
|
4084
|
+
const platformAllowChat = body.platformAllowChat
|
|
4085
|
+
? normalizePlatformAllowFrom(body.platformAllowChat)
|
|
4086
|
+
: undefined;
|
|
4087
|
+
|
|
4088
|
+
// Validate agent type CLI availability before saving
|
|
4089
|
+
if (agentType && agentType !== 'claudecode') {
|
|
4090
|
+
try {
|
|
4091
|
+
const { execSync } = await import('node:child_process');
|
|
4092
|
+
execSync(`which ${agentType}`, { stdio: 'pipe', timeout: 5000 });
|
|
4093
|
+
} catch {
|
|
4094
|
+
throw new Error(
|
|
4095
|
+
`${agentType} CLI 未安装,无法切换到 ${agentType} 模式。请先安装对应的 CLI 工具。`
|
|
4096
|
+
);
|
|
4097
|
+
}
|
|
4098
|
+
}
|
|
3631
4099
|
|
|
3632
4100
|
const localPatch: Record<string, unknown> = {};
|
|
3633
4101
|
if (name) localPatch.displayName = name;
|
|
@@ -3640,6 +4108,7 @@ async function applyTeamConfigUpdate(
|
|
|
3640
4108
|
if (managedSources) localPatch.managedSources = managedSources;
|
|
3641
4109
|
if (disabledCommands) localPatch.disabledCommands = disabledCommands;
|
|
3642
4110
|
if (platformAllowFrom !== undefined) localPatch.platformAllowFrom = platformAllowFrom;
|
|
4111
|
+
if (platformAllowChat !== undefined) localPatch.platformAllowChat = platformAllowChat;
|
|
3643
4112
|
if (showContextIndicator !== undefined) localPatch.showContextIndicator = showContextIndicator;
|
|
3644
4113
|
if (replyFooter !== undefined) localPatch.replyFooter = replyFooter;
|
|
3645
4114
|
if (injectSender !== undefined) localPatch.injectSender = injectSender;
|
|
@@ -3671,19 +4140,26 @@ async function applyTeamConfigUpdate(
|
|
|
3671
4140
|
if (managedSources) ccPatch.admin_from = managedSources;
|
|
3672
4141
|
if (disabledCommands) ccPatch.disabled_commands = disabledCommands;
|
|
3673
4142
|
if (platformAllowFrom !== undefined) ccPatch.platform_allow_from = platformAllowFrom;
|
|
4143
|
+
if (platformAllowChat !== undefined) ccPatch.platform_allow_chat = platformAllowChat;
|
|
3674
4144
|
if (showContextIndicator !== undefined) ccPatch.show_context_indicator = showContextIndicator;
|
|
3675
4145
|
if (replyFooter !== undefined) ccPatch.reply_footer = replyFooter;
|
|
3676
4146
|
if (injectSender !== undefined) ccPatch.inject_sender = injectSender;
|
|
3677
4147
|
|
|
3678
4148
|
let ccSyncError: string | null = null;
|
|
4149
|
+
let bindProject: string;
|
|
4150
|
+
try {
|
|
4151
|
+
bindProject = await resolveRouteCcProjectName(teamName);
|
|
4152
|
+
} catch {
|
|
4153
|
+
bindProject = teamName;
|
|
4154
|
+
}
|
|
3679
4155
|
if (Object.keys(ccPatch).length > 0) {
|
|
3680
4156
|
try {
|
|
3681
4157
|
const updateResult = await cc.updateProject(
|
|
3682
|
-
|
|
4158
|
+
bindProject,
|
|
3683
4159
|
ccPatch as Parameters<CcConnectClient['updateProject']>[1]
|
|
3684
4160
|
);
|
|
3685
4161
|
if (updateResult.restart_required) {
|
|
3686
|
-
await cc.
|
|
4162
|
+
try { await cc.reload(); } catch { /* best effort */ }
|
|
3687
4163
|
}
|
|
3688
4164
|
} catch (err) {
|
|
3689
4165
|
ccSyncError = err instanceof Error ? err.message : String(err);
|
|
@@ -3691,7 +4167,7 @@ async function applyTeamConfigUpdate(
|
|
|
3691
4167
|
}
|
|
3692
4168
|
if (providerRefs !== undefined) {
|
|
3693
4169
|
try {
|
|
3694
|
-
await cc.setProviderRefs(
|
|
4170
|
+
await cc.setProviderRefs(bindProject, providerRefs);
|
|
3695
4171
|
} catch (err) {
|
|
3696
4172
|
ccSyncError = err instanceof Error ? err.message : String(err);
|
|
3697
4173
|
}
|
|
@@ -3720,7 +4196,8 @@ async function applyTeamConfigUpdate(
|
|
|
3720
4196
|
app.get<{ Params: { name: string } }>('/api/teams/:name/config', async (request, reply) => {
|
|
3721
4197
|
try {
|
|
3722
4198
|
const name = request.params.name;
|
|
3723
|
-
|
|
4199
|
+
let bindProject = await resolveRouteCcProjectName(name);
|
|
4200
|
+
const p = await cc.getProject(bindProject);
|
|
3724
4201
|
// local metadata overlay
|
|
3725
4202
|
let color = 'blue';
|
|
3726
4203
|
let description = '';
|
|
@@ -3732,6 +4209,7 @@ app.get<{ Params: { name: string } }>('/api/teams/:name/config', async (request,
|
|
|
3732
4209
|
let injectSender = false;
|
|
3733
4210
|
let permissionMode = 'default';
|
|
3734
4211
|
let platformAllowFrom: Record<string, string> = {};
|
|
4212
|
+
let platformAllowChat: Record<string, string> = {};
|
|
3735
4213
|
try {
|
|
3736
4214
|
const meta = await svc.readTeamManifest(name);
|
|
3737
4215
|
color = meta.color ?? color;
|
|
@@ -3744,6 +4222,7 @@ app.get<{ Params: { name: string } }>('/api/teams/:name/config', async (request,
|
|
|
3744
4222
|
injectSender = meta.injectSender ?? injectSender;
|
|
3745
4223
|
permissionMode = meta.permissionMode ?? permissionMode;
|
|
3746
4224
|
platformAllowFrom = normalizePlatformAllowFrom(meta.platformAllowFrom);
|
|
4225
|
+
platformAllowChat = normalizePlatformAllowFrom(meta.platformAllowChat);
|
|
3747
4226
|
} catch {
|
|
3748
4227
|
/* ok */
|
|
3749
4228
|
}
|
|
@@ -3780,12 +4259,21 @@ app.get<{ Params: { name: string } }>('/api/teams/:name/config', async (request,
|
|
|
3780
4259
|
}
|
|
3781
4260
|
return platformAllowFrom;
|
|
3782
4261
|
})();
|
|
4262
|
+
const resolvedPlatformAllowChat = (() => {
|
|
4263
|
+
const normalized = normalizePlatformAllowFrom(
|
|
4264
|
+
(projectSettings as Record<string, unknown>).platform_allow_chat
|
|
4265
|
+
);
|
|
4266
|
+
if (Object.keys(normalized).length > 0) {
|
|
4267
|
+
return normalized;
|
|
4268
|
+
}
|
|
4269
|
+
return platformAllowChat;
|
|
4270
|
+
})();
|
|
3783
4271
|
const resolvedPermissionMode =
|
|
3784
4272
|
typeof p.agent_mode === 'string' && p.agent_mode.trim().length > 0
|
|
3785
4273
|
? p.agent_mode.trim()
|
|
3786
4274
|
: permissionMode;
|
|
3787
4275
|
const [providerRefs, globalProviders] = await Promise.all([
|
|
3788
|
-
cc.getProviderRefs(
|
|
4276
|
+
cc.getProviderRefs(bindProject).catch(() => []),
|
|
3789
4277
|
cc.listProviders().catch(() => []),
|
|
3790
4278
|
]);
|
|
3791
4279
|
return {
|
|
@@ -3803,6 +4291,7 @@ app.get<{ Params: { name: string } }>('/api/teams/:name/config', async (request,
|
|
|
3803
4291
|
injectSender: resolvedInjectSender,
|
|
3804
4292
|
permissionMode: resolvedPermissionMode,
|
|
3805
4293
|
platformAllowFrom: resolvedPlatformAllowFrom,
|
|
4294
|
+
platformAllowChat: resolvedPlatformAllowChat,
|
|
3806
4295
|
providerRefs,
|
|
3807
4296
|
globalProviders,
|
|
3808
4297
|
settings: {
|
|
@@ -4089,25 +4578,87 @@ app.get<{ Params: { name: string; taskId: string } }>(
|
|
|
4089
4578
|
async () => ({ lines: [] })
|
|
4090
4579
|
);
|
|
4091
4580
|
|
|
4092
|
-
// member-stats
|
|
4581
|
+
// member-stats — aggregate from local JSONL session summaries
|
|
4093
4582
|
app.get<{ Params: { name: string; memberName: string } }>(
|
|
4094
4583
|
'/api/teams/:name/member-stats/:memberName',
|
|
4095
|
-
async () =>
|
|
4096
|
-
|
|
4097
|
-
|
|
4098
|
-
|
|
4099
|
-
|
|
4100
|
-
|
|
4101
|
-
|
|
4102
|
-
|
|
4103
|
-
|
|
4104
|
-
|
|
4105
|
-
|
|
4106
|
-
|
|
4107
|
-
|
|
4108
|
-
|
|
4109
|
-
|
|
4110
|
-
|
|
4584
|
+
async (request) => {
|
|
4585
|
+
try {
|
|
4586
|
+
const team = await svc.readTeamManifest(request.params.name);
|
|
4587
|
+
const workDir = team.workDir || team.bindProject || request.params.name;
|
|
4588
|
+
const sessions = await localSessionScanner.scanSummaries(workDir, request.params.name);
|
|
4589
|
+
|
|
4590
|
+
let inputTokens = 0;
|
|
4591
|
+
let outputTokens = 0;
|
|
4592
|
+
let cacheReadTokens = 0;
|
|
4593
|
+
let messageCount = 0;
|
|
4594
|
+
let totalDurationMs = 0;
|
|
4595
|
+
|
|
4596
|
+
let earliestStart: string | null = null;
|
|
4597
|
+
let latestEnd: string | null = null;
|
|
4598
|
+
|
|
4599
|
+
for (const s of sessions) {
|
|
4600
|
+
inputTokens += s.inputTokens;
|
|
4601
|
+
outputTokens += s.outputTokens;
|
|
4602
|
+
cacheReadTokens += s.cacheReadTokens;
|
|
4603
|
+
messageCount += s.messageCount;
|
|
4604
|
+
|
|
4605
|
+
if (s.startTime && (!earliestStart || s.startTime < earliestStart)) {
|
|
4606
|
+
earliestStart = s.startTime;
|
|
4607
|
+
}
|
|
4608
|
+
if (s.endTime && (!latestEnd || s.endTime > latestEnd)) {
|
|
4609
|
+
latestEnd = s.endTime;
|
|
4610
|
+
}
|
|
4611
|
+
}
|
|
4612
|
+
|
|
4613
|
+
if (earliestStart && latestEnd) {
|
|
4614
|
+
totalDurationMs = Date.parse(latestEnd) - Date.parse(earliestStart);
|
|
4615
|
+
if (totalDurationMs < 0) totalDurationMs = 0;
|
|
4616
|
+
}
|
|
4617
|
+
|
|
4618
|
+
// Count completed tasks from the team's task board
|
|
4619
|
+
let tasksCompleted = 0;
|
|
4620
|
+
try {
|
|
4621
|
+
const tasks = await svc['workspace'].readTasks(team.slug || request.params.name);
|
|
4622
|
+
tasksCompleted = tasks.filter((t) => t.status === 'done').length;
|
|
4623
|
+
} catch {
|
|
4624
|
+
// board may not exist yet
|
|
4625
|
+
}
|
|
4626
|
+
|
|
4627
|
+
return {
|
|
4628
|
+
linesAdded: 0,
|
|
4629
|
+
linesRemoved: 0,
|
|
4630
|
+
filesTouched: [],
|
|
4631
|
+
fileStats: {},
|
|
4632
|
+
toolUsage: {},
|
|
4633
|
+
inputTokens,
|
|
4634
|
+
outputTokens,
|
|
4635
|
+
cacheReadTokens,
|
|
4636
|
+
costUsd: 0,
|
|
4637
|
+
tasksCompleted,
|
|
4638
|
+
messageCount,
|
|
4639
|
+
totalDurationMs,
|
|
4640
|
+
sessionCount: sessions.length,
|
|
4641
|
+
computedAt: new Date().toISOString(),
|
|
4642
|
+
};
|
|
4643
|
+
} catch {
|
|
4644
|
+
return {
|
|
4645
|
+
linesAdded: 0,
|
|
4646
|
+
linesRemoved: 0,
|
|
4647
|
+
filesTouched: [],
|
|
4648
|
+
fileStats: {},
|
|
4649
|
+
toolUsage: {},
|
|
4650
|
+
inputTokens: 0,
|
|
4651
|
+
outputTokens: 0,
|
|
4652
|
+
cacheReadTokens: 0,
|
|
4653
|
+
costUsd: 0,
|
|
4654
|
+
tasksCompleted: 0,
|
|
4655
|
+
messageCount: 0,
|
|
4656
|
+
totalDurationMs: 0,
|
|
4657
|
+
sessionCount: 0,
|
|
4658
|
+
computedAt: new Date().toISOString(),
|
|
4659
|
+
};
|
|
4660
|
+
}
|
|
4661
|
+
}
|
|
4111
4662
|
);
|
|
4112
4663
|
|
|
4113
4664
|
// tool-approval stubs
|
|
@@ -4610,6 +5161,100 @@ app.post('/api/telemetry/scan', async (request, reply) => {
|
|
|
4610
5161
|
}
|
|
4611
5162
|
});
|
|
4612
5163
|
|
|
5164
|
+
// GET /api/telemetry/conversations → local Feishu/Lark conversation telemetry
|
|
5165
|
+
app.get<{
|
|
5166
|
+
Querystring: {
|
|
5167
|
+
teamName?: string;
|
|
5168
|
+
platform?: string;
|
|
5169
|
+
from?: string;
|
|
5170
|
+
to?: string;
|
|
5171
|
+
identityType?: 'person' | 'group' | 'unknown';
|
|
5172
|
+
identityId?: string;
|
|
5173
|
+
includeContent?: 'none' | 'summary' | 'full' | string;
|
|
5174
|
+
includeToolResults?: string;
|
|
5175
|
+
includeSystemMessages?: string;
|
|
5176
|
+
limit?: string;
|
|
5177
|
+
offset?: string;
|
|
5178
|
+
};
|
|
5179
|
+
}>('/api/telemetry/conversations', async (request, reply) => {
|
|
5180
|
+
try {
|
|
5181
|
+
const result = await conversationTelemetry.getConversations({
|
|
5182
|
+
teamName: request.query.teamName,
|
|
5183
|
+
platform: request.query.platform,
|
|
5184
|
+
from: request.query.from,
|
|
5185
|
+
to: request.query.to,
|
|
5186
|
+
identityType: request.query.identityType,
|
|
5187
|
+
identityId: request.query.identityId,
|
|
5188
|
+
includeContent: shouldIncludeContent(request.query.includeContent),
|
|
5189
|
+
includeToolResults: request.query.includeToolResults !== 'false',
|
|
5190
|
+
includeSystemMessages: request.query.includeSystemMessages !== 'false',
|
|
5191
|
+
limit: request.query.limit ? Number(request.query.limit) : undefined,
|
|
5192
|
+
offset: request.query.offset ? Number(request.query.offset) : undefined,
|
|
5193
|
+
});
|
|
5194
|
+
return result;
|
|
5195
|
+
} catch (err) {
|
|
5196
|
+
return reply.code(500).send({ error: String(err) });
|
|
5197
|
+
}
|
|
5198
|
+
});
|
|
5199
|
+
|
|
5200
|
+
// GET /api/telemetry/conversations/export → export local conversation telemetry
|
|
5201
|
+
app.get<{
|
|
5202
|
+
Querystring: {
|
|
5203
|
+
format?: 'csv' | 'json' | 'markdown' | 'plaintext' | string;
|
|
5204
|
+
teamName?: string;
|
|
5205
|
+
platform?: string;
|
|
5206
|
+
from?: string;
|
|
5207
|
+
to?: string;
|
|
5208
|
+
identityType?: 'person' | 'group' | 'unknown';
|
|
5209
|
+
identityId?: string;
|
|
5210
|
+
includeContent?: 'none' | 'summary' | 'full' | string;
|
|
5211
|
+
includeToolResults?: string;
|
|
5212
|
+
includeSystemMessages?: string;
|
|
5213
|
+
};
|
|
5214
|
+
}>('/api/telemetry/conversations/export', async (request, reply) => {
|
|
5215
|
+
try {
|
|
5216
|
+
const requestedFormat = request.query.format;
|
|
5217
|
+
const format =
|
|
5218
|
+
requestedFormat === 'json' ||
|
|
5219
|
+
requestedFormat === 'markdown' ||
|
|
5220
|
+
requestedFormat === 'plaintext' ||
|
|
5221
|
+
requestedFormat === 'csv'
|
|
5222
|
+
? requestedFormat
|
|
5223
|
+
: 'csv';
|
|
5224
|
+
const result = await conversationTelemetry.exportConversations(format, {
|
|
5225
|
+
teamName: request.query.teamName,
|
|
5226
|
+
platform: request.query.platform,
|
|
5227
|
+
from: request.query.from,
|
|
5228
|
+
to: request.query.to,
|
|
5229
|
+
identityType: request.query.identityType,
|
|
5230
|
+
identityId: request.query.identityId,
|
|
5231
|
+
includeContent: shouldIncludeContent(request.query.includeContent),
|
|
5232
|
+
includeToolResults: request.query.includeToolResults !== 'false',
|
|
5233
|
+
includeSystemMessages: request.query.includeSystemMessages !== 'false',
|
|
5234
|
+
});
|
|
5235
|
+
return result;
|
|
5236
|
+
} catch (err) {
|
|
5237
|
+
return reply.code(500).send({ error: String(err) });
|
|
5238
|
+
}
|
|
5239
|
+
});
|
|
5240
|
+
|
|
5241
|
+
// GET /api/telemetry/conversations/:sessionId → local conversation telemetry detail
|
|
5242
|
+
app.get<{
|
|
5243
|
+
Params: { sessionId: string };
|
|
5244
|
+
Querystring: { teamName?: string; platform?: string };
|
|
5245
|
+
}>('/api/telemetry/conversations/:sessionId', async (request, reply) => {
|
|
5246
|
+
try {
|
|
5247
|
+
const result = await conversationTelemetry.getConversationDetail(request.params.sessionId, {
|
|
5248
|
+
...request.query,
|
|
5249
|
+
includeContent: 'full',
|
|
5250
|
+
});
|
|
5251
|
+
if (!result) return reply.code(404).send({ error: 'Conversation not found' });
|
|
5252
|
+
return result;
|
|
5253
|
+
} catch (err) {
|
|
5254
|
+
return reply.code(500).send({ error: String(err) });
|
|
5255
|
+
}
|
|
5256
|
+
});
|
|
5257
|
+
|
|
4613
5258
|
// GET /api/telemetry/status → current telemetry status (full stats)
|
|
4614
5259
|
app.get('/api/telemetry/status', async (request, reply) => {
|
|
4615
5260
|
try {
|
|
@@ -4697,6 +5342,13 @@ app.get('/api/teams/review/git-file-log', async () => ({ log: [] }));
|
|
|
4697
5342
|
// ===========================================================================
|
|
4698
5343
|
|
|
4699
5344
|
app.get('/api/events', (request, reply) => {
|
|
5345
|
+
try {
|
|
5346
|
+
assertTrustedBrowserOrigin(request);
|
|
5347
|
+
} catch (err) {
|
|
5348
|
+
reply.code(403).send({ error: err instanceof Error ? err.message : String(err) });
|
|
5349
|
+
return;
|
|
5350
|
+
}
|
|
5351
|
+
|
|
4700
5352
|
reply.raw.writeHead(200, {
|
|
4701
5353
|
'Content-Type': 'text/event-stream; charset=utf-8',
|
|
4702
5354
|
'Cache-Control': 'no-cache, no-transform',
|