@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.
Files changed (243) hide show
  1. package/dist-renderer/assets/ProjectEditorOverlay-CemDOX-3.js +58 -0
  2. package/dist-renderer/assets/{TeamGraphOverlay-ZEDfZyHb.js → TeamGraphOverlay-hPY770Db.js} +1 -1
  3. package/dist-renderer/assets/{_basePickBy-CIhniz70.js → _basePickBy-BHHrJT1i.js} +1 -1
  4. package/dist-renderer/assets/{_baseUniq-cKAW4Q8I.js → _baseUniq-CWErBtke.js} +1 -1
  5. package/dist-renderer/assets/{arc-YmNsoDXW.js → arc-C_o2_Uv8.js} +1 -1
  6. package/dist-renderer/assets/{architectureDiagram-VXUJARFQ-DHEls2sX.js → architectureDiagram-VXUJARFQ-DUW0LI3t.js} +1 -1
  7. package/dist-renderer/assets/{blockDiagram-VD42YOAC-Bpwf1Sbg.js → blockDiagram-VD42YOAC-CWbCE9hQ.js} +1 -1
  8. package/dist-renderer/assets/{c4Diagram-YG6GDRKO-B0IaQ4w5.js → c4Diagram-YG6GDRKO-BjLadrfV.js} +1 -1
  9. package/dist-renderer/assets/channel-DyP9YlCF.js +1 -0
  10. package/dist-renderer/assets/{chunk-4BX2VUAB-DLk-hcFc.js → chunk-4BX2VUAB-CPnvjZl9.js} +1 -1
  11. package/dist-renderer/assets/{chunk-55IACEB6-1XRmX_Zm.js → chunk-55IACEB6-OlL47yXQ.js} +1 -1
  12. package/dist-renderer/assets/{chunk-B4BG7PRW-1waH1DAD.js → chunk-B4BG7PRW-DTasjbm8.js} +1 -1
  13. package/dist-renderer/assets/{chunk-DI55MBZ5-BqpZBtrN.js → chunk-DI55MBZ5-C5_Xaqkk.js} +1 -1
  14. package/dist-renderer/assets/{chunk-FMBD7UC4-Bly7vVym.js → chunk-FMBD7UC4-NdoM4DMR.js} +1 -1
  15. package/dist-renderer/assets/{chunk-QN33PNHL-Ci2QWBAs.js → chunk-QN33PNHL-C8Fybejy.js} +1 -1
  16. package/dist-renderer/assets/{chunk-QZHKN3VN-YCqFW7d-.js → chunk-QZHKN3VN-E98TYFXJ.js} +1 -1
  17. package/dist-renderer/assets/{chunk-TZMSLE5B-B0xGXInl.js → chunk-TZMSLE5B-h4lFgkIq.js} +1 -1
  18. package/dist-renderer/assets/classDiagram-2ON5EDUG-BqffFTae.js +1 -0
  19. package/dist-renderer/assets/classDiagram-v2-WZHVMYZB-BqffFTae.js +1 -0
  20. package/dist-renderer/assets/clone-MPcKWs2O.js +1 -0
  21. package/dist-renderer/assets/{cose-bilkent-S5V4N54A-DxcFNQKT.js → cose-bilkent-S5V4N54A-DtQ7fkrs.js} +1 -1
  22. package/dist-renderer/assets/{dagre-6UL2VRFP-DPo_RfZY.js → dagre-6UL2VRFP-CN-nL_z4.js} +1 -1
  23. package/dist-renderer/assets/{diagram-PSM6KHXK-U3hQsFe4.js → diagram-PSM6KHXK-DVJtqmm-.js} +1 -1
  24. package/dist-renderer/assets/{diagram-QEK2KX5R-OrwrAy0V.js → diagram-QEK2KX5R-DlxHxyXh.js} +1 -1
  25. package/dist-renderer/assets/{diagram-S2PKOQOG-CXATPWVw.js → diagram-S2PKOQOG-7dpzO6x6.js} +1 -1
  26. package/dist-renderer/assets/{erDiagram-Q2GNP2WA-B0e8AfMF.js → erDiagram-Q2GNP2WA-GP1TqsHi.js} +1 -1
  27. package/dist-renderer/assets/{flowDiagram-NV44I4VS-CXfzA4jJ.js → flowDiagram-NV44I4VS-C7ZLETuH.js} +1 -1
  28. package/dist-renderer/assets/{ganttDiagram-JELNMOA3-CMr08qVl.js → ganttDiagram-JELNMOA3-CvPB68dH.js} +1 -1
  29. package/dist-renderer/assets/{gitGraphDiagram-V2S2FVAM-vYFHpPmy.js → gitGraphDiagram-V2S2FVAM-B5yOm3w7.js} +1 -1
  30. package/dist-renderer/assets/{graph-DOe5j8dH.js → graph-smeyY1YZ.js} +1 -1
  31. package/dist-renderer/assets/{index-BySQS7AB.js → index-BJx8XvG1.js} +1 -1
  32. package/dist-renderer/assets/{index-C_okzZXP.js → index-CQaXUAua.js} +1 -1
  33. package/dist-renderer/assets/{index-VJ-MM9xa.js → index-CajRpxO2.js} +1 -1
  34. package/dist-renderer/assets/{index-V7dAKPqd.js → index-ChG4rE-E.js} +587 -705
  35. package/dist-renderer/assets/index-DUd0uw9C.css +32 -0
  36. package/dist-renderer/assets/{index-CzWxVCRL.js → index-IhmXZWqf.js} +1 -1
  37. package/dist-renderer/assets/{index-B2Dy7M2G.js → index-x_JkoDRH.js} +1 -1
  38. package/dist-renderer/assets/{infoDiagram-HS3SLOUP-D_WubR0B.js → infoDiagram-HS3SLOUP-D-hWRQGY.js} +1 -1
  39. package/dist-renderer/assets/{journeyDiagram-XKPGCS4Q-w9ca-1TI.js → journeyDiagram-XKPGCS4Q-Bb6W8rUG.js} +1 -1
  40. package/dist-renderer/assets/{kanban-definition-3W4ZIXB7-Jg9p6_pN.js → kanban-definition-3W4ZIXB7-CnHdUX0q.js} +1 -1
  41. package/dist-renderer/assets/{layout-B-z3y17c.js → layout-pqss_zkI.js} +1 -1
  42. package/dist-renderer/assets/{linear-D-RTX5UW.js → linear-B1mFITNh.js} +1 -1
  43. package/dist-renderer/assets/{mindmap-definition-VGOIOE7T-CDQmHOYP.js → mindmap-definition-VGOIOE7T-DTD9q7-D.js} +1 -1
  44. package/dist-renderer/assets/{pieDiagram-ADFJNKIX-D_odsQL7.js → pieDiagram-ADFJNKIX-Df3mhrn7.js} +1 -1
  45. package/dist-renderer/assets/{quadrantDiagram-AYHSOK5B-BRsmYWSA.js → quadrantDiagram-AYHSOK5B-B1FZ09vH.js} +1 -1
  46. package/dist-renderer/assets/{requirementDiagram-UZGBJVZJ-ChNE_BOV.js → requirementDiagram-UZGBJVZJ-aEO78thZ.js} +1 -1
  47. package/dist-renderer/assets/{sankeyDiagram-TZEHDZUN-C8FtpwKc.js → sankeyDiagram-TZEHDZUN-6Ui--jp-.js} +1 -1
  48. package/dist-renderer/assets/{sequenceDiagram-WL72ISMW-DmLCzNcc.js → sequenceDiagram-WL72ISMW-DF4Q1cAM.js} +1 -1
  49. package/dist-renderer/assets/splashScene-D0YB9uxm.js +17 -0
  50. package/dist-renderer/assets/{stateDiagram-FKZM4ZOC-WJBm4bhu.js → stateDiagram-FKZM4ZOC-BqA2BI8C.js} +1 -1
  51. package/dist-renderer/assets/stateDiagram-v2-4FDKWEC3-Cs2ZtUD2.js +1 -0
  52. package/dist-renderer/assets/{timeline-definition-IT6M3QCI-BXs_hOJs.js → timeline-definition-IT6M3QCI-DoOkw_A8.js} +1 -1
  53. package/dist-renderer/assets/{treemap-GDKQZRPO-o04MA0G9.js → treemap-GDKQZRPO-DUe26QdD.js} +1 -1
  54. package/dist-renderer/assets/{xychartDiagram-PRI3JC2R-Czj69XRd.js → xychartDiagram-PRI3JC2R-BKCnj5Xn.js} +1 -1
  55. package/dist-renderer/index.html +20 -53
  56. package/package.json +25 -18
  57. package/src/main/ipc/extensions.ts +2 -1
  58. package/src/main/server.ts +873 -221
  59. package/src/main/services/extensions/ExtensionFacadeService.ts +2 -5
  60. package/src/main/services/extensions/catalog/PluginCatalogService.ts +4 -2
  61. package/src/main/services/session-intelligence/ConversationTelemetryService.ts +1101 -0
  62. package/src/main/services/session-intelligence/LocalSessionScanner.ts +512 -0
  63. package/src/main/services/session-intelligence/SessionUsageParser.ts +4 -4
  64. package/src/main/services/system-manager/SystemManagerConfigService.ts +122 -0
  65. package/src/main/services/system-manager/SystemManagerPtyService.ts +233 -0
  66. package/src/main/services/system-manager/WorkflowPromptService.ts +75 -0
  67. package/src/main/services/teams-mvp/TaskDispatchService.ts +5 -6
  68. package/src/main/services/teams-mvp/TeamProvisioningService.ts +39 -2
  69. package/src/main/services/teams-mvp/TeamWorkspaceService.ts +22 -4
  70. package/src/main/utils/teamProjectResolution.ts +15 -0
  71. package/src/renderer/App.tsx +8 -4
  72. package/src/renderer/api/httpClient.ts +68 -18
  73. package/src/renderer/api/providers.ts +23 -2
  74. package/src/renderer/assets/participant-avatars/01.svg +3 -0
  75. package/src/renderer/assets/participant-avatars/02.svg +3 -0
  76. package/src/renderer/assets/participant-avatars/03.svg +3 -0
  77. package/src/renderer/assets/participant-avatars/04.svg +3 -0
  78. package/src/renderer/assets/participant-avatars/05.svg +3 -0
  79. package/src/renderer/assets/participant-avatars/06.svg +3 -0
  80. package/src/renderer/assets/participant-avatars/07.svg +3 -0
  81. package/src/renderer/assets/participant-avatars/08.svg +3 -0
  82. package/src/renderer/assets/participant-avatars/09.svg +3 -0
  83. package/src/renderer/assets/participant-avatars/10.svg +3 -0
  84. package/src/renderer/assets/participant-avatars/11.svg +3 -0
  85. package/src/renderer/assets/participant-avatars/12.svg +3 -0
  86. package/src/renderer/assets/participant-avatars/13.svg +3 -0
  87. package/src/renderer/components/chat/ChatHistoryItem.tsx +1 -1
  88. package/src/renderer/components/chat/items/SubagentItem.tsx +2 -2
  89. package/src/renderer/components/chat/viewers/MermaidDiagram.tsx +2 -2
  90. package/src/renderer/components/common/ErrorBoundary.tsx +1 -1
  91. package/src/renderer/components/common/TerminalPane.tsx +213 -0
  92. package/src/renderer/components/dashboard/CliStatusBanner.tsx +7 -7
  93. package/src/renderer/components/dashboard/DashboardView.tsx +9 -36
  94. package/src/renderer/components/extensions/ExtensionStoreView.tsx +7 -126
  95. package/src/renderer/components/extensions/ExtensionsSubTabTrigger.tsx +1 -1
  96. package/src/renderer/components/extensions/common/ExtensionToast.tsx +3 -3
  97. package/src/renderer/components/extensions/common/SourceBadge.tsx +1 -1
  98. package/src/renderer/components/extensions/mcp/McpLibraryEnableDialog.tsx +305 -0
  99. package/src/renderer/components/extensions/mcp/McpLibraryEntryDialog.tsx +418 -0
  100. package/src/renderer/components/extensions/mcp/McpLibraryPanel.tsx +404 -0
  101. package/src/renderer/components/extensions/plugins/CategoryChips.tsx +1 -1
  102. package/src/renderer/components/extensions/plugins/PluginCard.tsx +6 -6
  103. package/src/renderer/components/extensions/plugins/PluginDetailDialog.tsx +2 -2
  104. package/src/renderer/components/extensions/plugins/PluginsPanel.tsx +34 -21
  105. package/src/renderer/components/extensions/skills/SkillEditorDialog.tsx +1 -1
  106. package/src/renderer/components/extensions/skills/SkillsLibraryPanel.tsx +335 -0
  107. package/src/renderer/components/layout/PaneContent.tsx +8 -1
  108. package/src/renderer/components/layout/PaneResizeHandle.tsx +2 -2
  109. package/src/renderer/components/layout/Sidebar.tsx +13 -56
  110. package/src/renderer/components/layout/SortableTab.tsx +22 -33
  111. package/src/renderer/components/layout/TabBar.tsx +1 -1
  112. package/src/renderer/components/layout/TabContextMenu.tsx +1 -1
  113. package/src/renderer/components/report/sections/CostSection.tsx +2 -2
  114. package/src/renderer/components/report/sections/InsightsSection.tsx +1 -1
  115. package/src/renderer/components/runtime/ProviderRuntimeBackendSelector.tsx +2 -2
  116. package/src/renderer/components/runtime/ProviderRuntimeSettingsDialog.tsx +768 -157
  117. package/src/renderer/components/schedules/SchedulesView.tsx +51 -462
  118. package/src/renderer/components/schedules/calendar/CalendarDayView.tsx +173 -0
  119. package/src/renderer/components/schedules/calendar/CalendarEventBlock.tsx +113 -0
  120. package/src/renderer/components/schedules/calendar/CalendarHeader.tsx +148 -0
  121. package/src/renderer/components/schedules/calendar/CalendarMonthView.tsx +142 -0
  122. package/src/renderer/components/schedules/calendar/CalendarWeekView.tsx +219 -0
  123. package/src/renderer/components/schedules/calendar/ScheduleCalendarBoard.tsx +41 -0
  124. package/src/renderer/components/schedules/calendar/TeamGanttView.tsx +405 -0
  125. package/src/renderer/components/schedules/calendar/computeOccurrences.ts +234 -0
  126. package/src/renderer/components/schedules/calendar/index.ts +2 -0
  127. package/src/renderer/components/schedules/calendar/types.ts +44 -0
  128. package/src/renderer/components/search/CommandPalette.tsx +4 -4
  129. package/src/renderer/components/settings/SettingsTabs.tsx +50 -55
  130. package/src/renderer/components/settings/SettingsView.tsx +30 -35
  131. package/src/renderer/components/settings/components/SettingsSectionHeader.tsx +5 -1
  132. package/src/renderer/components/settings/components/SettingsSelect.tsx +5 -3
  133. package/src/renderer/components/settings/components/SettingsToggle.tsx +2 -2
  134. package/src/renderer/components/settings/sections/AdvancedSection.tsx +11 -42
  135. package/src/renderer/components/settings/sections/CliStatusSection.tsx +72 -113
  136. package/src/renderer/components/settings/sections/ConfigEditorDialog.tsx +1 -1
  137. package/src/renderer/components/settings/sections/GeneralSection.tsx +11 -3
  138. package/src/renderer/components/settings/sections/HarnessSection.tsx +18 -14
  139. package/src/renderer/components/settings/sections/PlatformsSection.tsx +3 -3
  140. package/src/renderer/components/settings/sections/TaskBusSection.tsx +33 -40
  141. package/src/renderer/components/settings/sections/index.ts +0 -1
  142. package/src/renderer/components/sidebar/SessionFiltersPopover.tsx +1 -1
  143. package/src/renderer/components/sidebar/SessionItem.tsx +3 -3
  144. package/src/renderer/components/sidebar/SidebarSessions.tsx +184 -6
  145. package/src/renderer/components/sidebar/SidebarTaskItem.tsx +4 -4
  146. package/src/renderer/components/sidebar/WorkspaceBrowser.tsx +40 -5
  147. package/src/renderer/components/splash/splashScene.ts +121 -929
  148. package/src/renderer/components/system-manager/FolderBrowser.tsx +163 -0
  149. package/src/renderer/components/system-manager/SystemManagerView.tsx +351 -0
  150. package/src/renderer/components/tasks/TasksView.tsx +112 -134
  151. package/src/renderer/components/team/CcSessionsSection.tsx +431 -89
  152. package/src/renderer/components/team/ClaudeLogsFilterPopover.tsx +1 -1
  153. package/src/renderer/components/team/ClaudeLogsPanel.tsx +1 -1
  154. package/src/renderer/components/team/CollapsibleTeamSection.tsx +17 -32
  155. package/src/renderer/components/team/ProcessesSection.tsx +2 -2
  156. package/src/renderer/components/team/TaskTooltip.tsx +2 -2
  157. package/src/renderer/components/team/TeamDetailView.tsx +319 -123
  158. package/src/renderer/components/team/TeamListFilterPopover.tsx +1 -1
  159. package/src/renderer/components/team/TeamListView.tsx +109 -124
  160. package/src/renderer/components/team/TeamSessionsSection.tsx +6 -6
  161. package/src/renderer/components/team/UnreadCommentsBadge.tsx +1 -1
  162. package/src/renderer/components/team/activity/ActivityItem.tsx +9 -9
  163. package/src/renderer/components/team/activity/ActivityTimeline.tsx +5 -5
  164. package/src/renderer/components/team/activity/LeadThoughtsGroup.tsx +3 -3
  165. package/src/renderer/components/team/activity/ReplyQuoteBlock.tsx +4 -4
  166. package/src/renderer/components/team/dialogs/CreateTaskDialog.tsx +4 -4
  167. package/src/renderer/components/team/dialogs/CreateTeamDialog.tsx +84 -306
  168. package/src/renderer/components/team/dialogs/EditTeamDialog.tsx +259 -342
  169. package/src/renderer/components/team/dialogs/GlobalTaskDetailDialog.tsx +1 -1
  170. package/src/renderer/components/team/dialogs/LaunchTeamDialog.tsx +18 -16
  171. package/src/renderer/components/team/dialogs/PlatformBindingDialog.tsx +221 -0
  172. package/src/renderer/components/team/dialogs/PlatformManualForm.tsx +8 -1
  173. package/src/renderer/components/team/dialogs/PlatformSetupQR.tsx +5 -5
  174. package/src/renderer/components/team/dialogs/RuntimeConfigDialog.tsx +361 -0
  175. package/src/renderer/components/team/dialogs/SendMessageDialog.tsx +6 -6
  176. package/src/renderer/components/team/dialogs/SkipPermissionsCheckbox.tsx +6 -6
  177. package/src/renderer/components/team/dialogs/StatusHistoryTimeline.tsx +1 -1
  178. package/src/renderer/components/team/dialogs/TaskAttachments.tsx +1 -1
  179. package/src/renderer/components/team/dialogs/TaskCommentInput.tsx +6 -6
  180. package/src/renderer/components/team/dialogs/TaskCommentsSection.tsx +4 -4
  181. package/src/renderer/components/team/dialogs/TaskDetailDialog.tsx +3 -3
  182. package/src/renderer/components/team/dialogs/platformMeta.ts +122 -11
  183. package/src/renderer/components/team/dialogs/useTeamEditForm.ts +17 -5
  184. package/src/renderer/components/team/editor/EditorFileTree.tsx +4 -4
  185. package/src/renderer/components/team/editor/EditorSearchPanel.tsx +1 -1
  186. package/src/renderer/components/team/editor/MarkdownSplitView.tsx +1 -1
  187. package/src/renderer/components/team/editor/NewFileDialog.tsx +1 -1
  188. package/src/renderer/components/team/editor/ProjectEditorOverlay.tsx +1 -1
  189. package/src/renderer/components/team/editor/SearchInFilesPanel.tsx +1 -1
  190. package/src/renderer/components/team/kanban/KanbanBoard.tsx +9 -9
  191. package/src/renderer/components/team/kanban/KanbanFilterPopover.tsx +4 -4
  192. package/src/renderer/components/team/kanban/KanbanSearchInput.tsx +1 -1
  193. package/src/renderer/components/team/kanban/KanbanSortPopover.tsx +5 -5
  194. package/src/renderer/components/team/kanban/KanbanTaskCard.tsx +4 -4
  195. package/src/renderer/components/team/members/MemberCard.tsx +14 -47
  196. package/src/renderer/components/team/members/MemberDetailDialog.tsx +3 -95
  197. package/src/renderer/components/team/members/MemberDetailStats.tsx +50 -65
  198. package/src/renderer/components/team/members/MemberDraftRow.tsx +1 -1
  199. package/src/renderer/components/team/members/MemberStatsTab.tsx +2 -2
  200. package/src/renderer/components/team/members/MemberWorkspaceTab.tsx +1 -1
  201. package/src/renderer/components/team/messages/MessageComposer.tsx +10 -112
  202. package/src/renderer/components/team/messages/MessagesFilterPopover.tsx +1 -1
  203. package/src/renderer/components/team/messages/MessagesPanel.tsx +136 -119
  204. package/src/renderer/components/team/review/ChangeReviewDialog.tsx +1 -1
  205. package/src/renderer/components/team/schedule/ScheduleStatusBadge.tsx +3 -3
  206. package/src/renderer/components/team/sidebar/TeamSidebarRail.tsx +4 -4
  207. package/src/renderer/components/team/tasks/TaskRow.tsx +1 -1
  208. package/src/renderer/components/team/tools/AddMcpInline.tsx +27 -17
  209. package/src/renderer/components/team/tools/McpChip.tsx +6 -3
  210. package/src/renderer/components/team/tools/SkillChip.tsx +3 -3
  211. package/src/renderer/components/team/tools/ToolsSection.tsx +418 -70
  212. package/src/renderer/components/ui/MemberSelect.tsx +2 -2
  213. package/src/renderer/components/ui/MentionSuggestionList.tsx +2 -2
  214. package/src/renderer/components/ui/MentionableTextarea.tsx +3 -3
  215. package/src/renderer/hooks/useExtensionsTabState.ts +3 -114
  216. package/src/renderer/index.css +56 -39
  217. package/src/renderer/index.html +17 -50
  218. package/src/renderer/store/index.ts +2 -1
  219. package/src/renderer/store/slices/scheduleSlice.ts +1 -1
  220. package/src/renderer/store/slices/teamSlice.ts +45 -168
  221. package/src/renderer/utils/claudeCodeOnlyProviders.ts +3 -10
  222. package/src/renderer/utils/memberHelpers.ts +5 -17
  223. package/src/renderer/utils/openCodeRuntimeDeliveryDiagnostics.ts +4 -2
  224. package/src/renderer/utils/providerSlashCommands.ts +0 -5
  225. package/src/renderer/utils/scheduleFormatters.ts +3 -1
  226. package/src/renderer/utils/teamMessageFiltering.ts +14 -1
  227. package/src/renderer/utils/teamModelAvailability.ts +18 -2
  228. package/src/shared/types/api.ts +121 -2
  229. package/src/shared/types/ccConnect.ts +2 -0
  230. package/src/shared/types/index.ts +3 -0
  231. package/src/shared/types/systemManager.ts +49 -0
  232. package/src/shared/types/team.ts +29 -0
  233. package/src/shared/types/terminal.ts +4 -2
  234. package/src/shared/utils/extensionNormalizers.ts +15 -8
  235. package/src/shared/utils/providerExtensionCapabilities.ts +2 -2
  236. package/dist-renderer/assets/ProjectEditorOverlay-lJZi-9Hp.js +0 -52
  237. package/dist-renderer/assets/channel-yIlSKy0e.js +0 -1
  238. package/dist-renderer/assets/classDiagram-2ON5EDUG-24fHez0s.js +0 -1
  239. package/dist-renderer/assets/classDiagram-v2-WZHVMYZB-24fHez0s.js +0 -1
  240. package/dist-renderer/assets/clone-BTNuUva-.js +0 -1
  241. package/dist-renderer/assets/index-Bi6nrZ4z.css +0 -1
  242. package/dist-renderer/assets/splashScene-C8lWNnm4.js +0 -1
  243. package/dist-renderer/assets/stateDiagram-v2-4FDKWEC3-_m6iPPUR.js +0 -1
@@ -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 type { TaskBusConfig, TeamLaunchRequest } from '@shared/types/team';
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
- /* ignore parse errors */
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
- const parsed = JSON.parse(content) as unknown;
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: process.env.CORS_ORIGIN?.split(',') ?? true,
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
- upstream.headers.get('content-type') ?? 'application/json; charset=utf-8'
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
- // GET /api/teams从 cc-connect 读取 project 列表,合并本地元数据(displayName 等)
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 cc.listProjects();
793
- const summaries = await Promise.all(
794
- projects.map(async (p) => {
795
- // platforms 从 listProjects 返回的是 string[],有 platform 即认为在线
796
- const isOnline = Array.isArray(p.platforms) && p.platforms.length > 0;
797
-
798
- // 尝试从本地元数据读取 displayName 等信息
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
- // 兼容仅存在于 cc-connect 的团队:回退读取 project 详情拿 work_dir。
820
- if (!workDir) {
821
- try {
822
- const detail = await cc.getProject(p.name);
823
- if (typeof detail.work_dir === 'string') {
824
- workDir = detail.work_dir.trim();
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
- return {
832
- teamName: p.name,
833
- displayName,
834
- description,
835
- color,
836
- memberCount: 1,
837
- members: [{ name: p.name, role: 'agent', agentId: p.agent_type, color }],
838
- taskCount: 0,
839
- lastActivity: null,
840
- isAlive: isOnline,
841
- harness: p.agent_type,
842
- bindProject: p.name,
843
- workDir,
844
- projectPath: workDir || undefined,
845
- sessionsCount: p.sessions_count,
846
- heartbeatEnabled: p.heartbeat_enabled,
847
- pendingDelete,
848
- restartRequired,
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
- // 直接调用 cc-connect add-platform(project 自动创建)
879
- const platformType = (body.platform as string) ?? 'feishu';
880
- const result = await cc.createProject(name, harness, workDir, platformType, {});
881
- try {
882
- await svc.createTeam({
883
- displayName,
884
- bindProject: name,
885
- harness,
886
- workDir,
887
- color: typeof body.color === 'string' ? body.color : undefined,
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 { ok: true, teamName: name, runId: null };
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
- const p = await cc.getProject(name);
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(name).catch(() => []),
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: name,
1460
+ bindProject,
1045
1461
  collaboration,
1046
1462
  description,
1047
- workDir: p.work_dir ?? workDir,
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: name,
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 === 'default' || teamName === 'my-project') {
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 data = await cc.getHeartbeat(request.params.name);
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 cc.resumeHeartbeat(request.params.name);
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 cc.pauseHeartbeat(request.params.name);
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 cc.pauseHeartbeat(request.params.name);
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 cc.resumeHeartbeat(request.params.name);
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 cc.updateProject(request.params.name, request.body as Record<string, unknown>);
1411
- const data = await cc.getHeartbeat(request.params.name);
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
- const result = await cc.createProject(
1491
- bindProject,
1492
- harness,
1493
- workDir,
1494
- platformType,
1495
- platformOptions as Record<string, string>
1496
- );
1497
- if (result.restart_required) {
1498
- await cc.restart();
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: true },
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
- // Stop = delete project from cc-connect (this is the only way to stop agents)
1520
- await cc.stopProject(name);
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
- app.log.warn({ err }, 'failed to read app config, using defaults');
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(name).catch(() => []);
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 — cc-connect project sessions 获取,转换为前端 Session 格式
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 sessions = await cc.listSessions(request.params.name);
3268
- const sessionsByKey = new Map<string, (typeof sessions)[number]>();
3269
- const sessionScore = (session: (typeof sessions)[number]): number => {
3270
- const updatedAt = Date.parse(session.updated_at);
3271
- return (
3272
- (session.live ? 1_000_000_000_000_000 : 0) +
3273
- (session.active ? 1_000_000_000_000 : 0) +
3274
- (session.history_count ?? 0) * 1_000_000 +
3275
- (session.agent_type ? 10_000 : 0) +
3276
- (Number.isFinite(updatedAt) ? updatedAt / 1_000_000 : 0)
3277
- );
3278
- };
3279
- for (const session of sessions) {
3280
- const existing = sessionsByKey.get(session.session_key);
3281
- if (!existing || sessionScore(session) > sessionScore(existing)) {
3282
- sessionsByKey.set(session.session_key, session);
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 [...sessionsByKey.values()].map((s) => ({
3287
- id: s.id,
3288
- title: s.user_name || s.chat_name || s.name || s.session_key,
3289
- projectId: request.params.name,
3290
- sessionKey: s.session_key,
3291
- platform: s.platform,
3292
- userName: s.user_name ?? null,
3293
- chatName: s.chat_name ?? null,
3294
- active: s.active,
3295
- live: s.live,
3296
- historyCount: s.history_count,
3297
- createdAt: s.created_at,
3298
- updatedAt: s.updated_at,
3299
- lastMessage: s.last_message
3300
- ? {
3301
- role: s.last_message.role,
3302
- content: s.last_message.content,
3303
- timestamp: s.last_message.timestamp,
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 — 通过 cc-connect API 获取会话详情(含历史消息)
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 historyLimit = request.query.history_limit
3755
+ async (request, reply) => {
3756
+ const limit = request.query.history_limit
3317
3757
  ? parseInt(request.query.history_limit, 10)
3318
3758
  : 500;
3319
- const detail = await cc.getSession(request.params.name, request.params.sessionId, historyLimit);
3320
- return mapCcSessionDetail(detail);
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 detail = await cc.getSession(request.params.name, request.params.sessionId, 1);
3330
- await sendHarnessMessageViaBridge({
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 cc.listProjects();
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 p = await cc.getProject(request.params.name);
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: request.params.name,
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
- teamName,
4158
+ bindProject,
3683
4159
  ccPatch as Parameters<CcConnectClient['updateProject']>[1]
3684
4160
  );
3685
4161
  if (updateResult.restart_required) {
3686
- await cc.restart();
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(teamName, providerRefs);
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
- const p = await cc.getProject(name);
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(name).catch(() => []),
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
- linesAdded: 0,
4097
- linesRemoved: 0,
4098
- filesTouched: [],
4099
- fileStats: {},
4100
- toolUsage: {},
4101
- inputTokens: 0,
4102
- outputTokens: 0,
4103
- cacheReadTokens: 0,
4104
- costUsd: 0,
4105
- tasksCompleted: 0,
4106
- messageCount: 0,
4107
- totalDurationMs: 0,
4108
- sessionCount: 0,
4109
- computedAt: new Date().toISOString(),
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',