@yancyyu/openhermit 1.6.28 → 1.6.30
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-DsQt4FHy.js +52 -0
- package/dist-renderer/assets/{TeamGraphOverlay-Ba5njic5.js → TeamGraphOverlay-BjZC53xf.js} +1 -1
- package/dist-renderer/assets/{_basePickBy-BvnK-OC1.js → _basePickBy-CrWocIjq.js} +1 -1
- package/dist-renderer/assets/{_baseUniq-DmFYXx9G.js → _baseUniq-B6d8ysWi.js} +1 -1
- package/dist-renderer/assets/{arc-DX4ZQFY4.js → arc-DAIYCFP8.js} +1 -1
- package/dist-renderer/assets/{architectureDiagram-VXUJARFQ-DfYr3vEN.js → architectureDiagram-VXUJARFQ-B3UudXJh.js} +1 -1
- package/dist-renderer/assets/{blockDiagram-VD42YOAC-DuXdVeWn.js → blockDiagram-VD42YOAC-DbptKQ4W.js} +1 -1
- package/dist-renderer/assets/{c4Diagram-YG6GDRKO-Bw2nixXe.js → c4Diagram-YG6GDRKO-C4WQuZpV.js} +1 -1
- package/dist-renderer/assets/channel-DbjZvWii.js +1 -0
- package/dist-renderer/assets/{chunk-4BX2VUAB-DLiNGQoE.js → chunk-4BX2VUAB-Dp7fVpI_.js} +1 -1
- package/dist-renderer/assets/{chunk-55IACEB6-B1L_8VIF.js → chunk-55IACEB6-B8KGfbAy.js} +1 -1
- package/dist-renderer/assets/{chunk-B4BG7PRW-DaZMWKGk.js → chunk-B4BG7PRW-BG1oJrjA.js} +1 -1
- package/dist-renderer/assets/{chunk-DI55MBZ5-ku-dflJG.js → chunk-DI55MBZ5-DRmxNjht.js} +1 -1
- package/dist-renderer/assets/{chunk-FMBD7UC4-DV-mF1dP.js → chunk-FMBD7UC4-D6VLvy16.js} +1 -1
- package/dist-renderer/assets/{chunk-QN33PNHL-ByGcDFQ0.js → chunk-QN33PNHL-DZou1667.js} +1 -1
- package/dist-renderer/assets/{chunk-QZHKN3VN-7dv-Min8.js → chunk-QZHKN3VN-CghmasSh.js} +1 -1
- package/dist-renderer/assets/{chunk-TZMSLE5B-WdXL5fTu.js → chunk-TZMSLE5B-B7apcMPK.js} +1 -1
- package/dist-renderer/assets/classDiagram-2ON5EDUG-D_FGxxsl.js +1 -0
- package/dist-renderer/assets/classDiagram-v2-WZHVMYZB-D_FGxxsl.js +1 -0
- package/dist-renderer/assets/clone-CJ1kxO2J.js +1 -0
- package/dist-renderer/assets/{cose-bilkent-S5V4N54A-CNcsvqPl.js → cose-bilkent-S5V4N54A-05e5uQDp.js} +1 -1
- package/dist-renderer/assets/{dagre-6UL2VRFP-DBNx4qqx.js → dagre-6UL2VRFP-B06bRykF.js} +1 -1
- package/dist-renderer/assets/{diagram-PSM6KHXK-BfVlT6sT.js → diagram-PSM6KHXK-CY7VYQ7c.js} +1 -1
- package/dist-renderer/assets/{diagram-QEK2KX5R-HvVjs0K6.js → diagram-QEK2KX5R-BjKEH7dD.js} +1 -1
- package/dist-renderer/assets/{diagram-S2PKOQOG-DYb_KnWS.js → diagram-S2PKOQOG-Bf4ELS1_.js} +1 -1
- package/dist-renderer/assets/{erDiagram-Q2GNP2WA-Ba-IgI5G.js → erDiagram-Q2GNP2WA-DJ753_L9.js} +1 -1
- package/dist-renderer/assets/{flowDiagram-NV44I4VS-2iDN8Kpj.js → flowDiagram-NV44I4VS-B71S-lC-.js} +1 -1
- package/dist-renderer/assets/{ganttDiagram-JELNMOA3-Byjf8Fa3.js → ganttDiagram-JELNMOA3-C_U42mSZ.js} +1 -1
- package/dist-renderer/assets/{gitGraphDiagram-V2S2FVAM-DbKvfZ_j.js → gitGraphDiagram-V2S2FVAM-DKUJU4Ns.js} +1 -1
- package/dist-renderer/assets/{graph-Enirf-f8.js → graph-DY3qbzqj.js} +1 -1
- package/dist-renderer/assets/{index-DY1zqsb6.js → index-BlOrAXp3.js} +551 -537
- package/dist-renderer/assets/{index-AjxP_rE_.js → index-Bs27J5gB.js} +1 -1
- package/dist-renderer/assets/{index-CtlzGepK.js → index-C8B_nKOF.js} +1 -1
- package/dist-renderer/assets/index-CmZPUEhS.css +1 -0
- package/dist-renderer/assets/{index-COZPUWJW.js → index-DLKyDr4T.js} +1 -1
- package/dist-renderer/assets/{index-DdhqolqE.js → index-Dhsk3_DD.js} +1 -1
- package/dist-renderer/assets/{index-ChR1D6ZF.js → index-GpUvV2xs.js} +1 -1
- package/dist-renderer/assets/{infoDiagram-HS3SLOUP-D6uicwz1.js → infoDiagram-HS3SLOUP-BNs0y3IG.js} +1 -1
- package/dist-renderer/assets/{journeyDiagram-XKPGCS4Q-DqwZsXlQ.js → journeyDiagram-XKPGCS4Q-CqPnw4UV.js} +1 -1
- package/dist-renderer/assets/{kanban-definition-3W4ZIXB7-fCDVhVUm.js → kanban-definition-3W4ZIXB7-SLlzcUJ2.js} +1 -1
- package/dist-renderer/assets/{layout-CPFgj98r.js → layout-BZLlNmbr.js} +1 -1
- package/dist-renderer/assets/{linear-CYiQ7Y3M.js → linear-qz6v45xy.js} +1 -1
- package/dist-renderer/assets/{mindmap-definition-VGOIOE7T-D31dS2KE.js → mindmap-definition-VGOIOE7T-B1-kmEWV.js} +1 -1
- package/dist-renderer/assets/{pieDiagram-ADFJNKIX-BOsCJfds.js → pieDiagram-ADFJNKIX-B8a02iNx.js} +1 -1
- package/dist-renderer/assets/{quadrantDiagram-AYHSOK5B-CYTVQCfr.js → quadrantDiagram-AYHSOK5B-BKv1Xfou.js} +1 -1
- package/dist-renderer/assets/{requirementDiagram-UZGBJVZJ-CODCFpkt.js → requirementDiagram-UZGBJVZJ-B3DUpZi2.js} +1 -1
- package/dist-renderer/assets/{sankeyDiagram-TZEHDZUN-Z4ce9ZtZ.js → sankeyDiagram-TZEHDZUN-DmPzuTsy.js} +1 -1
- package/dist-renderer/assets/{sequenceDiagram-WL72ISMW-CmS9TxhW.js → sequenceDiagram-WL72ISMW-Bo7RelRb.js} +1 -1
- package/dist-renderer/assets/{stateDiagram-FKZM4ZOC-o9k-ns3q.js → stateDiagram-FKZM4ZOC-1epX98gV.js} +1 -1
- package/dist-renderer/assets/{stateDiagram-v2-4FDKWEC3-CxHMyEt1.js → stateDiagram-v2-4FDKWEC3-03Ym9PTr.js} +1 -1
- package/dist-renderer/assets/{timeline-definition-IT6M3QCI-B6T3zrde.js → timeline-definition-IT6M3QCI-r6isC62H.js} +1 -1
- package/dist-renderer/assets/{treemap-GDKQZRPO-CVd5GNDw.js → treemap-GDKQZRPO-CGKpOUF2.js} +1 -1
- package/dist-renderer/assets/{xychartDiagram-PRI3JC2R-CleBrdqc.js → xychartDiagram-PRI3JC2R-t4-rwdAw.js} +1 -1
- package/dist-renderer/index.html +2 -2
- package/package.json +4 -1
- package/src/main/ipc/extensions.ts +353 -0
- package/src/main/server.ts +907 -184
- package/src/main/services/extensions/ExtensionFacadeService.ts +135 -0
- package/src/main/services/extensions/catalog/GlamaMcpEnrichmentService.ts +190 -0
- package/src/main/services/extensions/catalog/McpCatalogAggregator.ts +150 -0
- package/src/main/services/extensions/catalog/OfficialMcpRegistryService.ts +381 -0
- package/src/main/services/extensions/catalog/PluginCatalogService.ts +392 -0
- package/src/main/services/extensions/credentials/CredentialService.ts +343 -0
- package/src/main/services/extensions/install/McpInstallService.ts +407 -0
- package/src/main/services/extensions/install/PluginInstallService.ts +198 -0
- package/src/main/services/extensions/runtime/ClaudeCodeAdapter.ts +199 -0
- package/src/main/services/extensions/runtime/CodexAdapter.ts +100 -0
- package/src/main/services/extensions/runtime/CursorAdapter.ts +154 -0
- package/src/main/services/extensions/runtime/ExtensionsRuntimeAdapter.ts +172 -0
- package/src/main/services/extensions/runtime/GeminiAdapter.ts +91 -0
- package/src/main/services/extensions/runtime/HarnessInstallAdapter.ts +49 -0
- package/src/main/services/extensions/runtime/McpConfigStateReader.ts +209 -0
- package/src/main/services/extensions/runtime/OpenCodeAdapter.ts +91 -0
- package/src/main/services/extensions/runtime/adapterRegistry.ts +54 -0
- package/src/main/services/extensions/runtime/mcpDiagnosticsParser.ts +214 -0
- package/src/main/services/extensions/runtime/mcpRuntimeJson.ts +45 -0
- package/src/main/services/extensions/skills/SkillImportService.ts +155 -0
- package/src/main/services/extensions/skills/SkillMetadataParser.ts +323 -0
- package/src/main/services/extensions/skills/SkillPlanService.ts +411 -0
- package/src/main/services/extensions/skills/SkillReviewService.ts +73 -0
- package/src/main/services/extensions/skills/SkillRootsResolver.ts +49 -0
- package/src/main/services/extensions/skills/SkillScaffoldService.ts +89 -0
- package/src/main/services/extensions/skills/SkillScanner.ts +117 -0
- package/src/main/services/extensions/skills/SkillValidator.ts +69 -0
- package/src/main/services/extensions/skills/SkillsCatalogService.ts +92 -0
- package/src/main/services/extensions/skills/SkillsMutationService.ts +146 -0
- package/src/main/services/extensions/skills/SkillsWatcherService.ts +134 -0
- package/src/main/services/extensions/state/McpInstallationStateService.ts +42 -0
- package/src/main/services/extensions/state/PluginInstallationStateService.ts +281 -0
- package/src/main/services/identity/AgentTeamsIdentityStore.ts +218 -0
- package/src/main/services/runtime/providerAwareCliEnv.ts +60 -0
- package/src/main/services/session-intelligence/UsageTelemetryService.ts +33 -18
- package/src/main/services/team/ClaudeBinaryResolver.ts +469 -0
- package/src/main/services/team/ClaudeDoctorProbe.ts +0 -0
- package/src/main/services/team/cliFlavor.ts +54 -0
- package/src/main/services/teams-mvp/CollaborationBoardService.ts +310 -0
- package/src/main/services/teams-mvp/TaskDispatchService.ts +883 -95
- package/src/main/services/teams-mvp/TeamProvisioningService.ts +58 -19
- package/src/main/services/teams-mvp/TeamWorkspaceService.ts +25 -2
- package/src/main/services/teams-mvp/index.ts +3 -0
- package/src/main/utils/atomicWrite.ts +72 -0
- package/src/main/utils/childProcess.ts +554 -0
- package/src/main/utils/cliEnv.ts +54 -0
- package/src/main/utils/cliPathMerge.ts +97 -0
- package/src/main/utils/pathDecoder.ts +664 -0
- package/src/main/utils/pathValidation.ts +432 -0
- package/src/main/utils/shellEnv.ts +331 -0
- package/src/renderer/App.tsx +5 -0
- package/src/renderer/api/httpClient.ts +128 -0
- package/src/renderer/components/extensions/ExtensionStoreView.tsx +59 -34
- package/src/renderer/components/extensions/ExtensionsSubTabTrigger.tsx +1 -1
- package/src/renderer/components/extensions/common/ExtensionToast.tsx +141 -0
- package/src/renderer/components/extensions/common/HarnessSelector.tsx +71 -0
- package/src/renderer/components/extensions/env/EnvVarPanel.tsx +335 -0
- package/src/renderer/components/extensions/env/ProjectEnvPanel.tsx +239 -0
- package/src/renderer/components/extensions/mcp/CustomMcpServerDialog.tsx +14 -223
- package/src/renderer/components/extensions/mcp/McpServerDetailDialog.tsx +11 -0
- package/src/renderer/components/extensions/mcp/McpServersPanel.tsx +51 -1
- package/src/renderer/components/extensions/skills/SkillsPanel.tsx +1 -126
- package/src/renderer/components/layout/PaneContent.tsx +2 -0
- package/src/renderer/components/layout/SortableTab.tsx +1 -0
- package/src/renderer/components/layout/TabBarActions.tsx +12 -12
- package/src/renderer/components/schedules/SchedulesView.tsx +54 -22
- package/src/renderer/components/settings/sections/AdvancedSection.tsx +1 -1
- package/src/renderer/components/settings/sections/HarnessSection.tsx +2 -6
- package/src/renderer/components/settings/sections/TaskBusSection.tsx +144 -84
- package/src/renderer/components/sidebar/SidebarSessions.tsx +23 -0
- package/src/renderer/components/sidebar/WorkspaceBrowser.tsx +1 -7
- package/src/renderer/components/tasks/TasksView.tsx +343 -0
- package/src/renderer/components/team/HarnessSelect.tsx +71 -0
- package/src/renderer/components/team/TeamDetailView.tsx +55 -98
- package/src/renderer/components/team/dialogs/CreateTeamDialog.tsx +21 -12
- package/src/renderer/components/team/dialogs/EditTeamDialog.tsx +8 -13
- package/src/renderer/components/team/dialogs/LaunchTeamDialog.tsx +1 -1
- package/src/renderer/components/team/editor/EditorContextMenu.tsx +8 -23
- package/src/renderer/components/team/editor/EditorFileTree.tsx +0 -4
- package/src/renderer/components/team/editor/EditorSelectionMenu.tsx +1 -8
- package/src/renderer/components/team/editor/ProjectEditorOverlay.tsx +0 -10
- package/src/renderer/components/team/kanban/KanbanBoard.tsx +31 -65
- package/src/renderer/components/team/members/MemberDetailDialog.tsx +8 -33
- package/src/renderer/components/team/messages/MessageComposer.tsx +39 -3
- package/src/renderer/components/team/messages/MessagesPanel.tsx +100 -26
- package/src/renderer/components/team/messages/StatusBlock.tsx +2 -24
- package/src/renderer/components/team/schedule/ScheduleEmptyState.tsx +1 -1
- package/src/renderer/components/terminal/TerminalPanel.tsx +156 -0
- package/src/renderer/components/ui/MentionableTextarea.tsx +0 -1
- package/src/renderer/hooks/useExtensionsTabState.ts +2 -2
- package/src/renderer/store/slices/extensionsSlice.ts +42 -107
- package/src/renderer/store/slices/scheduleSlice.ts +21 -0
- package/src/renderer/store/slices/teamSlice.ts +67 -25
- package/src/renderer/types/tabs.ts +1 -0
- package/src/shared/types/api.ts +58 -0
- package/src/shared/types/extensions/index.ts +1 -0
- package/src/shared/types/extensions/mcp.ts +2 -0
- package/src/shared/types/extensions/plugin.ts +2 -1
- package/src/shared/types/extensions/skill.ts +7 -0
- package/src/shared/types/team.ts +104 -1
- package/src/shared/utils/providerExtensionCapabilities.ts +1 -1
- package/dist-renderer/assets/ProjectEditorOverlay-A4DZTvSy.js +0 -57
- package/dist-renderer/assets/channel-Pre42N5O.js +0 -1
- package/dist-renderer/assets/classDiagram-2ON5EDUG-CdJsTJsj.js +0 -1
- package/dist-renderer/assets/classDiagram-v2-WZHVMYZB-CdJsTJsj.js +0 -1
- package/dist-renderer/assets/clone-BjQBiNfj.js +0 -1
- package/dist-renderer/assets/index-BIOJremZ.css +0 -1
- package/src/features/recent-projects/main/adapters/input/http/registerRecentProjectsHttp.ts +0 -30
- package/src/features/recent-projects/main/adapters/output/presenters/DashboardRecentProjectsPresenter.ts +0 -27
- package/src/features/recent-projects/main/adapters/output/sources/ClaudeRecentProjectsSourceAdapter.ts +0 -91
- package/src/features/recent-projects/main/adapters/output/sources/CodexRecentProjectsSourceAdapter.ts +0 -326
- package/src/features/recent-projects/main/composition/createRecentProjectsFeature.ts +0 -43
- package/src/features/recent-projects/main/index.ts +0 -3
- package/src/features/recent-projects/main/infrastructure/cache/InMemoryRecentProjectsCache.ts +0 -34
- package/src/features/recent-projects/main/infrastructure/codex/CodexAppServerClient.ts +0 -116
- package/src/features/recent-projects/main/infrastructure/identity/RecentProjectIdentityResolver.ts +0 -20
- package/src/features/recent-projects/main/infrastructure/identity/normalizeIdentityPath.ts +0 -10
- package/src/renderer/components/extensions/apikeys/ApiKeyCard.tsx +0 -143
- package/src/renderer/components/extensions/apikeys/ApiKeyFormDialog.tsx +0 -282
- package/src/renderer/components/extensions/apikeys/ApiKeysPanel.tsx +0 -280
package/src/main/server.ts
CHANGED
|
@@ -43,11 +43,19 @@ import cors from '@fastify/cors';
|
|
|
43
43
|
import staticPlugin from '@fastify/static';
|
|
44
44
|
import Fastify from 'fastify';
|
|
45
45
|
|
|
46
|
+
import {
|
|
47
|
+
CROSS_TEAM_SENT_SOURCE,
|
|
48
|
+
CROSS_TEAM_SOURCE,
|
|
49
|
+
formatCrossTeamText,
|
|
50
|
+
} from '@shared/constants/crossTeam';
|
|
51
|
+
import type { CcAgentType } from '../shared/types/ccConnect';
|
|
46
52
|
import { CcConnectBridge } from './services/ccConnect/CcConnectBridge';
|
|
47
53
|
import { CcConnectClient } from './services/ccConnect/CcConnectClient';
|
|
48
54
|
import { TeamProvisioningService } from './services/teams-mvp';
|
|
49
55
|
import { TaskDispatchService } from './services/teams-mvp/TaskDispatchService';
|
|
50
|
-
import
|
|
56
|
+
import { CollaborationBoardService } from './services/teams-mvp/CollaborationBoardService';
|
|
57
|
+
import type { TaskBusConfig, TeamLaunchRequest } from '@shared/types/team';
|
|
58
|
+
import type { TeamManifest } from './services/teams-mvp/TeamWorkspaceService';
|
|
51
59
|
import { UpdateService } from './services/UpdateService';
|
|
52
60
|
import {
|
|
53
61
|
startTelemetry,
|
|
@@ -74,6 +82,7 @@ const HERMIT_HOME = process.env.HERMIT_HOME ?? path.join(os.homedir(), '.hermit'
|
|
|
74
82
|
const HERMIT_CONFIG_FILE = path.join(HERMIT_HOME, 'config.json');
|
|
75
83
|
const HERMIT_APP_CONFIG_FILE = path.join(HERMIT_HOME, 'app-config.json');
|
|
76
84
|
const HERMIT_CC_CONNECT_CONFIG_FILE = path.join(HERMIT_HOME, 'cc-connect', 'config.toml');
|
|
85
|
+
const HERMIT_SETTINGS_FILE = path.join(HERMIT_HOME, 'settings.json');
|
|
77
86
|
|
|
78
87
|
interface HermitConfig {
|
|
79
88
|
ccBaseUrl: string;
|
|
@@ -189,7 +198,46 @@ const bridge = new CcConnectBridge({
|
|
|
189
198
|
bridgeToken: runtimeConfig.ccBridgeToken || runtimeConfig.ccToken,
|
|
190
199
|
});
|
|
191
200
|
const svc = new TeamProvisioningService(cc, bridge);
|
|
192
|
-
const
|
|
201
|
+
const collabBoard = new CollaborationBoardService();
|
|
202
|
+
const taskDispatch = new TaskDispatchService(svc['workspace'], collabBoard);
|
|
203
|
+
|
|
204
|
+
// Broadcast collab board changes via SSE
|
|
205
|
+
taskDispatch.onCollabChange = (dispatchId, status, fromTeam, toTeam) => {
|
|
206
|
+
broadcastSse('collab-change', { dispatchId, status, fromTeam, toTeam });
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
async function readSavedTaskBusConfig(): Promise<TaskBusConfig | null> {
|
|
210
|
+
try {
|
|
211
|
+
const raw = await fs.readFile(HERMIT_SETTINGS_FILE, 'utf-8');
|
|
212
|
+
const settings = JSON.parse(raw) as { taskBus?: TaskBusConfig };
|
|
213
|
+
return settings.taskBus ?? null;
|
|
214
|
+
} catch {
|
|
215
|
+
return null;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
async function initializeTaskBusFromSettings(): Promise<void> {
|
|
220
|
+
const config = await readSavedTaskBusConfig();
|
|
221
|
+
if (!config) return;
|
|
222
|
+
|
|
223
|
+
if (config.telemetry?.enabled) {
|
|
224
|
+
await startTelemetry(config).catch((err) => {
|
|
225
|
+
app.log.warn({ err }, 'telemetry startup failed');
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (!config.enabled) {
|
|
230
|
+
taskDispatch.dispose();
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
taskDispatch.dispose();
|
|
235
|
+
try {
|
|
236
|
+
await taskDispatch.start(config);
|
|
237
|
+
} catch (err) {
|
|
238
|
+
app.log.warn({ err }, 'Redis connection failed on startup — task bus disabled');
|
|
239
|
+
}
|
|
240
|
+
}
|
|
193
241
|
|
|
194
242
|
function normalizeStringArray(value: unknown): string[] {
|
|
195
243
|
if (!Array.isArray(value)) {
|
|
@@ -200,6 +248,25 @@ function normalizeStringArray(value: unknown): string[] {
|
|
|
200
248
|
.filter((entry) => entry.length > 0);
|
|
201
249
|
}
|
|
202
250
|
|
|
251
|
+
async function resolveTeamSlugForMention(rawName: string): Promise<string | null> {
|
|
252
|
+
const normalized = rawName.trim().replace(/^@/, '');
|
|
253
|
+
if (!normalized) return null;
|
|
254
|
+
try {
|
|
255
|
+
await svc.readTeamManifest(normalized);
|
|
256
|
+
return normalized;
|
|
257
|
+
} catch {
|
|
258
|
+
// Try display name / case-insensitive slug match.
|
|
259
|
+
}
|
|
260
|
+
const lower = normalized.toLowerCase();
|
|
261
|
+
const teams = await svc.listTeams().catch(() => []);
|
|
262
|
+
const matched = teams.find((team) => {
|
|
263
|
+
const slug = team.slug.toLowerCase();
|
|
264
|
+
const displayName = (team.displayName ?? '').toLowerCase();
|
|
265
|
+
return slug === lower || displayName === lower;
|
|
266
|
+
});
|
|
267
|
+
return matched?.slug ?? null;
|
|
268
|
+
}
|
|
269
|
+
|
|
203
270
|
function normalizePlatformAllowFrom(value: unknown): Record<string, string> {
|
|
204
271
|
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
205
272
|
return {};
|
|
@@ -238,19 +305,19 @@ bridge.on('reply', (msg) => {
|
|
|
238
305
|
const sessionKey: string = (msg as { session_key?: string }).session_key ?? '';
|
|
239
306
|
const teamName = resolveTeamFromSessionKey(sessionKey) ?? sessionKey;
|
|
240
307
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
.appendMessage(teamName, {
|
|
308
|
+
void (async () => {
|
|
309
|
+
// 先落盘再广播,否则前端可能在 appendFile 完成前刷新到旧 feed。
|
|
310
|
+
await svc.appendMessage(teamName, {
|
|
244
311
|
from: teamName,
|
|
245
312
|
to: 'user',
|
|
246
313
|
role: 'agent',
|
|
247
314
|
content: (msg as { content?: string }).content ?? '',
|
|
248
315
|
meta: { sessionKey },
|
|
249
|
-
})
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
316
|
+
});
|
|
317
|
+
broadcastSse('team-change', { type: 'inbox', teamName });
|
|
318
|
+
})().catch((err) => {
|
|
319
|
+
app.log.warn({ err, teamName, sessionKey }, 'bridge reply persistence failed');
|
|
320
|
+
});
|
|
254
321
|
});
|
|
255
322
|
|
|
256
323
|
bridge.on('reply_stream', (msg) => {
|
|
@@ -261,18 +328,20 @@ bridge.on('reply_stream', (msg) => {
|
|
|
261
328
|
if (done) {
|
|
262
329
|
// 流式结束,存储完整回复
|
|
263
330
|
const fullText = (msg as { full_text?: string }).full_text ?? '';
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
.appendMessage(teamName, {
|
|
331
|
+
void (async () => {
|
|
332
|
+
if (fullText) {
|
|
333
|
+
await svc.appendMessage(teamName, {
|
|
267
334
|
from: teamName,
|
|
268
335
|
to: 'user',
|
|
269
336
|
role: 'agent',
|
|
270
337
|
content: fullText,
|
|
271
338
|
meta: { sessionKey },
|
|
272
|
-
})
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
broadcastSse('team-change', { type: 'inbox', teamName });
|
|
342
|
+
})().catch((err) => {
|
|
343
|
+
app.log.warn({ err, teamName, sessionKey }, 'bridge stream reply persistence failed');
|
|
344
|
+
});
|
|
276
345
|
} else {
|
|
277
346
|
broadcastSse('team-change', { type: 'lead-message', teamName });
|
|
278
347
|
}
|
|
@@ -1119,6 +1188,7 @@ function toTeamTask(task: {
|
|
|
1119
1188
|
updatedAt: string;
|
|
1120
1189
|
order: number;
|
|
1121
1190
|
teamSlug: string;
|
|
1191
|
+
dispatchMeta?: import('@shared/types/team').DispatchMeta;
|
|
1122
1192
|
}) {
|
|
1123
1193
|
const statusMap: Record<string, string> = {
|
|
1124
1194
|
todo: 'pending',
|
|
@@ -1135,6 +1205,7 @@ function toTeamTask(task: {
|
|
|
1135
1205
|
createdAt: task.createdAt,
|
|
1136
1206
|
updatedAt: task.updatedAt,
|
|
1137
1207
|
result: task.result ?? undefined,
|
|
1208
|
+
dispatchMeta: task.dispatchMeta,
|
|
1138
1209
|
};
|
|
1139
1210
|
}
|
|
1140
1211
|
|
|
@@ -1379,42 +1450,69 @@ app.get('/api/harnesses', async () => {
|
|
|
1379
1450
|
});
|
|
1380
1451
|
|
|
1381
1452
|
// ===========================================================================
|
|
1382
|
-
//
|
|
1383
|
-
// POST /api/teams/:name/launch →
|
|
1453
|
+
// 团队启动 — 直接通过 cc-connect 激活 project/runtime
|
|
1454
|
+
// POST /api/teams/:name/launch → 补建 project(如缺失)并 restart cc-connect
|
|
1384
1455
|
// POST /api/teams/:name/stop → 无需操作(cc-connect 自管理),返回 ok
|
|
1385
1456
|
// ===========================================================================
|
|
1386
1457
|
|
|
1387
|
-
app.post<{ Params: { name: string } }>(
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
let isOnline = false;
|
|
1391
|
-
let projectExists = false;
|
|
1458
|
+
app.post<{ Params: { name: string }; Body: Partial<TeamLaunchRequest> }>(
|
|
1459
|
+
'/api/teams/:name/launch',
|
|
1460
|
+
async (request, reply) => {
|
|
1392
1461
|
try {
|
|
1393
|
-
const
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1462
|
+
const name = request.params.name;
|
|
1463
|
+
const body = request.body ?? {};
|
|
1464
|
+
let manifest: TeamManifest | null = null;
|
|
1465
|
+
try {
|
|
1466
|
+
manifest = await svc.readTeamManifest(name);
|
|
1467
|
+
} catch {
|
|
1468
|
+
// Team may only exist in cc-connect.
|
|
1469
|
+
}
|
|
1470
|
+
const bindProject = manifest?.bindProject ?? name;
|
|
1471
|
+
const workDir = body.cwd ?? manifest?.workDir ?? '';
|
|
1472
|
+
const harness = manifest?.harness ?? 'claudecode';
|
|
1473
|
+
const platformType = manifest?.platform ?? 'bridge';
|
|
1474
|
+
const platformOptions = manifest?.platformOptions ?? {};
|
|
1475
|
+
let isOnline = false;
|
|
1476
|
+
let projectExists = false;
|
|
1477
|
+
try {
|
|
1478
|
+
const p = await cc.getProject(bindProject);
|
|
1479
|
+
projectExists = true;
|
|
1480
|
+
isOnline = Array.isArray(p.platforms) && p.platforms.some((pl) => pl.connected);
|
|
1481
|
+
} catch {
|
|
1482
|
+
/* project 不存在 */
|
|
1483
|
+
}
|
|
1399
1484
|
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1485
|
+
if (!isOnline) {
|
|
1486
|
+
if (!projectExists) {
|
|
1487
|
+
if (!workDir) {
|
|
1488
|
+
return reply.code(400).send({ error: '团队缺少项目路径,无法启动 cc-connect project' });
|
|
1489
|
+
}
|
|
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();
|
|
1503
|
+
}
|
|
1504
|
+
}
|
|
1505
|
+
|
|
1506
|
+
return {
|
|
1507
|
+
runId: `cc-connect:${bindProject}:${Date.now()}`,
|
|
1508
|
+
ok: true,
|
|
1509
|
+
data: { teamName: name, bindProject, projectExists, isOnline: true },
|
|
1510
|
+
};
|
|
1511
|
+
} catch (err) {
|
|
1512
|
+
return reply.code(404).send(reply500(err));
|
|
1513
|
+
}
|
|
1416
1514
|
}
|
|
1417
|
-
|
|
1515
|
+
);
|
|
1418
1516
|
|
|
1419
1517
|
app.post<{ Params: { name: string } }>('/api/teams/:name/stop', async (request) => {
|
|
1420
1518
|
const name = request.params.name;
|
|
@@ -1653,25 +1751,85 @@ const MCP_TOOLS = [
|
|
|
1653
1751
|
},
|
|
1654
1752
|
{
|
|
1655
1753
|
name: 'list_teams',
|
|
1656
|
-
description:
|
|
1754
|
+
description:
|
|
1755
|
+
'只读:列出所有可用团队(本地和远程)及能力信息。跨团队派发由 Hermit 平台根据用户 @团队 自动处理,agent 不应自行派发。',
|
|
1657
1756
|
inputSchema: {
|
|
1658
1757
|
type: 'object',
|
|
1659
1758
|
properties: {},
|
|
1660
1759
|
},
|
|
1661
1760
|
},
|
|
1662
1761
|
{
|
|
1663
|
-
name: '
|
|
1664
|
-
description:
|
|
1762
|
+
name: 'accept_task',
|
|
1763
|
+
description: '接受来自另一个团队的任务请求。在本地创建任务并通知发起方。',
|
|
1764
|
+
inputSchema: {
|
|
1765
|
+
type: 'object',
|
|
1766
|
+
properties: {
|
|
1767
|
+
team_slug: { type: 'string', description: '你的团队 slug(接收方)' },
|
|
1768
|
+
dispatch_id: { type: 'string', description: '任务派发 ID' },
|
|
1769
|
+
},
|
|
1770
|
+
required: ['team_slug', 'dispatch_id'],
|
|
1771
|
+
},
|
|
1772
|
+
},
|
|
1773
|
+
{
|
|
1774
|
+
name: 'reject_task',
|
|
1775
|
+
description: '拒绝来自另一个团队的任务请求。通知发起方并附原因。',
|
|
1665
1776
|
inputSchema: {
|
|
1666
1777
|
type: 'object',
|
|
1667
1778
|
properties: {
|
|
1668
|
-
team_slug: { type: 'string', description: '
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
description: { type: 'string', description: '任务描述(可选)' },
|
|
1672
|
-
prompt: { type: 'string', description: '给目标团队的执行指令(可选)' },
|
|
1779
|
+
team_slug: { type: 'string', description: '你的团队 slug(接收方)' },
|
|
1780
|
+
dispatch_id: { type: 'string', description: '任务派发 ID' },
|
|
1781
|
+
reason: { type: 'string', description: '拒绝原因(可选)' },
|
|
1673
1782
|
},
|
|
1674
|
-
required: ['team_slug', '
|
|
1783
|
+
required: ['team_slug', 'dispatch_id'],
|
|
1784
|
+
},
|
|
1785
|
+
},
|
|
1786
|
+
{
|
|
1787
|
+
name: 'list_pending_requests',
|
|
1788
|
+
description: '列出当前团队待处理的任务请求(尚未接受或拒绝的)。',
|
|
1789
|
+
inputSchema: {
|
|
1790
|
+
type: 'object',
|
|
1791
|
+
properties: {
|
|
1792
|
+
team_slug: { type: 'string', description: '团队 slug' },
|
|
1793
|
+
},
|
|
1794
|
+
required: ['team_slug'],
|
|
1795
|
+
},
|
|
1796
|
+
},
|
|
1797
|
+
{
|
|
1798
|
+
name: 'deliver_task',
|
|
1799
|
+
description: '交付任务结果。完成任务后调用此工具,将结果发送给发起方审核。',
|
|
1800
|
+
inputSchema: {
|
|
1801
|
+
type: 'object',
|
|
1802
|
+
properties: {
|
|
1803
|
+
team_slug: { type: 'string', description: '你的团队 slug(接收方/执行方)' },
|
|
1804
|
+
dispatch_id: { type: 'string', description: '任务派发 ID' },
|
|
1805
|
+
result: { type: 'string', description: '交付结果描述' },
|
|
1806
|
+
},
|
|
1807
|
+
required: ['team_slug', 'dispatch_id', 'result'],
|
|
1808
|
+
},
|
|
1809
|
+
},
|
|
1810
|
+
{
|
|
1811
|
+
name: 'approve_task',
|
|
1812
|
+
description: '审核通过任务交付。发起方对交付结果满意时调用。',
|
|
1813
|
+
inputSchema: {
|
|
1814
|
+
type: 'object',
|
|
1815
|
+
properties: {
|
|
1816
|
+
team_slug: { type: 'string', description: '你的团队 slug(发起方/审核方)' },
|
|
1817
|
+
dispatch_id: { type: 'string', description: '任务派发 ID' },
|
|
1818
|
+
},
|
|
1819
|
+
required: ['team_slug', 'dispatch_id'],
|
|
1820
|
+
},
|
|
1821
|
+
},
|
|
1822
|
+
{
|
|
1823
|
+
name: 'reject_result',
|
|
1824
|
+
description: '退回任务交付结果,要求修改。附上反馈意见。超过 3 次退回需要人工介入。',
|
|
1825
|
+
inputSchema: {
|
|
1826
|
+
type: 'object',
|
|
1827
|
+
properties: {
|
|
1828
|
+
team_slug: { type: 'string', description: '你的团队 slug(发起方/审核方)' },
|
|
1829
|
+
dispatch_id: { type: 'string', description: '任务派发 ID' },
|
|
1830
|
+
feedback: { type: 'string', description: '退回反馈(需要修改的内容)' },
|
|
1831
|
+
},
|
|
1832
|
+
required: ['team_slug', 'dispatch_id', 'feedback'],
|
|
1675
1833
|
},
|
|
1676
1834
|
},
|
|
1677
1835
|
];
|
|
@@ -1703,20 +1861,37 @@ async function executeMcpTool(
|
|
|
1703
1861
|
}
|
|
1704
1862
|
|
|
1705
1863
|
if (toolName === 'list_teams') {
|
|
1706
|
-
const teams = await taskDispatch.
|
|
1864
|
+
const teams = await taskDispatch.discoverTeams();
|
|
1707
1865
|
return text(teams);
|
|
1708
1866
|
}
|
|
1709
1867
|
|
|
1710
|
-
if (toolName === '
|
|
1711
|
-
const result = await taskDispatch.
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1868
|
+
if (toolName === 'accept_task') {
|
|
1869
|
+
const result = await taskDispatch.acceptTask(args.team_slug, args.dispatch_id);
|
|
1870
|
+
return text(result);
|
|
1871
|
+
}
|
|
1872
|
+
|
|
1873
|
+
if (toolName === 'reject_task') {
|
|
1874
|
+
await taskDispatch.rejectTask(args.team_slug, args.dispatch_id, args.reason);
|
|
1875
|
+
return text({ ok: true, message: 'Task rejected' });
|
|
1876
|
+
}
|
|
1877
|
+
|
|
1878
|
+
if (toolName === 'list_pending_requests') {
|
|
1879
|
+
const requests = taskDispatch.listPendingRequests(args.team_slug);
|
|
1880
|
+
return text(requests);
|
|
1881
|
+
}
|
|
1882
|
+
|
|
1883
|
+
if (toolName === 'deliver_task') {
|
|
1884
|
+
const result = await taskDispatch.deliverTask(args.team_slug, args.dispatch_id, args.result);
|
|
1885
|
+
return text(result);
|
|
1886
|
+
}
|
|
1887
|
+
|
|
1888
|
+
if (toolName === 'approve_task') {
|
|
1889
|
+
const result = await taskDispatch.approveTask(args.team_slug, args.dispatch_id);
|
|
1890
|
+
return text(result);
|
|
1891
|
+
}
|
|
1892
|
+
|
|
1893
|
+
if (toolName === 'reject_result') {
|
|
1894
|
+
const result = await taskDispatch.rejectResult(args.team_slug, args.dispatch_id, args.feedback);
|
|
1720
1895
|
return text(result);
|
|
1721
1896
|
}
|
|
1722
1897
|
|
|
@@ -2976,12 +3151,13 @@ app.get<{ Params: { name: string }; Querystring: { cursor?: string; limit?: stri
|
|
|
2976
3151
|
const newestFirstMessages = [...msgs].reverse();
|
|
2977
3152
|
const pageSlice = newestFirstMessages.slice(offset, offset + limit);
|
|
2978
3153
|
const page = pageSlice.map((m) => {
|
|
2979
|
-
const
|
|
3154
|
+
const explicitSessionKey =
|
|
2980
3155
|
typeof m.meta?.sessionKey === 'string'
|
|
2981
3156
|
? m.meta.sessionKey
|
|
2982
3157
|
: typeof m.meta?.session_key === 'string'
|
|
2983
3158
|
? m.meta.session_key
|
|
2984
3159
|
: undefined;
|
|
3160
|
+
const sessionKey = explicitSessionKey ?? buildFallbackSessionKey(name);
|
|
2985
3161
|
const session = sessionKey ? sessionByKey.get(sessionKey) : undefined;
|
|
2986
3162
|
return {
|
|
2987
3163
|
messageId: m.id,
|
|
@@ -2990,7 +3166,18 @@ app.get<{ Params: { name: string }; Querystring: { cursor?: string; limit?: stri
|
|
|
2990
3166
|
text: m.content,
|
|
2991
3167
|
timestamp: m.ts,
|
|
2992
3168
|
read: true,
|
|
2993
|
-
source:
|
|
3169
|
+
source:
|
|
3170
|
+
typeof m.meta?.source === 'string'
|
|
3171
|
+
? m.meta.source
|
|
3172
|
+
: ((m.role === 'user' ? 'user_sent' : 'inbox') as string),
|
|
3173
|
+
taskRefs: Array.isArray(m.meta?.taskRefs) ? m.meta.taskRefs : undefined,
|
|
3174
|
+
summary: typeof m.meta?.summary === 'string' ? m.meta.summary : undefined,
|
|
3175
|
+
conversationId:
|
|
3176
|
+
typeof m.meta?.conversationId === 'string' ? m.meta.conversationId : undefined,
|
|
3177
|
+
replyToConversationId:
|
|
3178
|
+
typeof m.meta?.replyToConversationId === 'string'
|
|
3179
|
+
? m.meta.replyToConversationId
|
|
3180
|
+
: undefined,
|
|
2994
3181
|
session: sessionKey
|
|
2995
3182
|
? {
|
|
2996
3183
|
id: session?.id,
|
|
@@ -3678,32 +3865,77 @@ app.delete<{ Params: { name: string } }>('/api/teams/:name/draft', async () => (
|
|
|
3678
3865
|
// send-message — 从 Hermit 会话面板注入到 harness,不使用 Management /send(那会回发到 IM)。
|
|
3679
3866
|
app.post<{
|
|
3680
3867
|
Params: { name: string };
|
|
3681
|
-
Body: {
|
|
3868
|
+
Body: {
|
|
3869
|
+
member?: string;
|
|
3870
|
+
text?: string;
|
|
3871
|
+
content?: string;
|
|
3872
|
+
summary?: string;
|
|
3873
|
+
sessionKey?: string;
|
|
3874
|
+
messageId?: string;
|
|
3875
|
+
};
|
|
3682
3876
|
}>('/api/teams/:name/send-message', async (request, reply) => {
|
|
3683
3877
|
const teamName = request.params.name;
|
|
3684
3878
|
const text = request.body?.text ?? request.body?.content ?? '';
|
|
3685
3879
|
if (!text.trim()) return { ok: true, messageId: null };
|
|
3686
3880
|
|
|
3687
|
-
const
|
|
3881
|
+
const requestedMessageId =
|
|
3882
|
+
typeof request.body?.messageId === 'string' ? request.body.messageId.trim() : '';
|
|
3883
|
+
const msgId =
|
|
3884
|
+
requestedMessageId || `hermit-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
3688
3885
|
|
|
3689
|
-
|
|
3886
|
+
const crossTeamDirective = text.trim().match(/^@([^\s]+)\s+([\s\S]+)$/);
|
|
3887
|
+
if (crossTeamDirective) {
|
|
3888
|
+
const targetTeam = await resolveTeamSlugForMention(crossTeamDirective[1] ?? '');
|
|
3889
|
+
const subject = crossTeamDirective[2]?.trim();
|
|
3890
|
+
if (targetTeam && subject && targetTeam !== teamName) {
|
|
3891
|
+
try {
|
|
3892
|
+
const sourceMsg = await svc.appendMessage(teamName, {
|
|
3893
|
+
from: 'user',
|
|
3894
|
+
to: targetTeam,
|
|
3895
|
+
role: 'user',
|
|
3896
|
+
content: text,
|
|
3897
|
+
meta: { source: CROSS_TEAM_SENT_SOURCE },
|
|
3898
|
+
});
|
|
3899
|
+
const result = await taskDispatch.dispatchTask(
|
|
3900
|
+
teamName,
|
|
3901
|
+
{
|
|
3902
|
+
subject,
|
|
3903
|
+
description: text,
|
|
3904
|
+
prompt: subject,
|
|
3905
|
+
},
|
|
3906
|
+
targetTeam,
|
|
3907
|
+
{ deadlineMinutes: 10, needsHumanReview: true }
|
|
3908
|
+
);
|
|
3909
|
+
broadcastSse('team-change', { type: 'inbox', teamName });
|
|
3910
|
+
broadcastSse('collab-change', {
|
|
3911
|
+
dispatchId: result.dispatchId,
|
|
3912
|
+
status: result.status,
|
|
3913
|
+
fromTeam: teamName,
|
|
3914
|
+
toTeam: targetTeam,
|
|
3915
|
+
});
|
|
3916
|
+
return {
|
|
3917
|
+
ok: result.status !== 'failed',
|
|
3918
|
+
deliveredToInbox: true,
|
|
3919
|
+
messageId: sourceMsg.id,
|
|
3920
|
+
dispatchId: result.dispatchId,
|
|
3921
|
+
status: result.status,
|
|
3922
|
+
message: result.message,
|
|
3923
|
+
runtimeDelivery: {
|
|
3924
|
+
attempted: true,
|
|
3925
|
+
delivered: result.status !== 'failed',
|
|
3926
|
+
},
|
|
3927
|
+
};
|
|
3928
|
+
} catch (err) {
|
|
3929
|
+
request.log.warn({ err, teamName, targetTeam }, 'cross-team directive dispatch failed');
|
|
3930
|
+
}
|
|
3931
|
+
}
|
|
3932
|
+
}
|
|
3933
|
+
|
|
3934
|
+
// 使用固定格式 session key,保证 reply 事件能正确映射回 teamName。
|
|
3935
|
+
// UI 消息先落盘并广播,bridge 投递放后台执行,避免 bridge 重连窗口卡住发送按钮。
|
|
3690
3936
|
const requestedSessionKey =
|
|
3691
3937
|
typeof request.body?.sessionKey === 'string' ? request.body.sessionKey.trim() : '';
|
|
3692
|
-
|
|
3693
|
-
|
|
3694
|
-
try {
|
|
3695
|
-
sessionKey = await sendHarnessMessageViaBridge({
|
|
3696
|
-
teamName,
|
|
3697
|
-
text,
|
|
3698
|
-
sessionKey,
|
|
3699
|
-
msgId,
|
|
3700
|
-
});
|
|
3701
|
-
} catch (err) {
|
|
3702
|
-
return reply.code(502).send({
|
|
3703
|
-
ok: false,
|
|
3704
|
-
error: err instanceof Error ? err.message : '发送到 harness 失败',
|
|
3705
|
-
});
|
|
3706
|
-
}
|
|
3938
|
+
const sessionKey = requestedSessionKey || buildFallbackSessionKey(teamName);
|
|
3707
3939
|
|
|
3708
3940
|
// 本地存储用户消息
|
|
3709
3941
|
const userMsg = await svc
|
|
@@ -3719,13 +3951,24 @@ app.post<{
|
|
|
3719
3951
|
// 广播 SSE 让前端触发消息刷新
|
|
3720
3952
|
broadcastSse('team-change', { type: 'inbox', teamName });
|
|
3721
3953
|
|
|
3954
|
+
const bridgeWasConnected = bridge.connected;
|
|
3955
|
+
void sendHarnessMessageViaBridge({
|
|
3956
|
+
teamName,
|
|
3957
|
+
text,
|
|
3958
|
+
sessionKey,
|
|
3959
|
+
msgId,
|
|
3960
|
+
}).catch((err) => {
|
|
3961
|
+
request.log.warn({ err, teamName, sessionKey }, 'send-message bridge delivery failed');
|
|
3962
|
+
broadcastSse('team-change', { type: 'inbox', teamName });
|
|
3963
|
+
});
|
|
3964
|
+
|
|
3722
3965
|
return {
|
|
3723
3966
|
ok: true,
|
|
3724
3967
|
deliveredToInbox: true,
|
|
3725
3968
|
messageId: userMsg?.id ?? msgId,
|
|
3726
3969
|
runtimeDelivery: {
|
|
3727
3970
|
attempted: true,
|
|
3728
|
-
delivered:
|
|
3971
|
+
delivered: bridgeWasConnected,
|
|
3729
3972
|
},
|
|
3730
3973
|
};
|
|
3731
3974
|
});
|
|
@@ -3880,59 +4123,57 @@ app.post('/api/teams/tool-approval/read-file', async () => ({ content: '' }));
|
|
|
3880
4123
|
app.post('/api/teams/validate-cli-args', async () => ({ valid: true, args: [], errors: [] }));
|
|
3881
4124
|
|
|
3882
4125
|
// cross-team task dispatch endpoints
|
|
4126
|
+
// Agent collaboration: accept a task request
|
|
3883
4127
|
app.post<{
|
|
3884
|
-
Body: {
|
|
3885
|
-
|
|
3886
|
-
|
|
3887
|
-
|
|
3888
|
-
|
|
3889
|
-
|
|
3890
|
-
|
|
3891
|
-
|
|
3892
|
-
|
|
3893
|
-
|
|
3894
|
-
|
|
3895
|
-
|
|
3896
|
-
|
|
3897
|
-
|
|
3898
|
-
|
|
3899
|
-
return { ok: true, dispatchId: result.dispatchId, status: result.status };
|
|
4128
|
+
Body: { team_slug: string; dispatch_id: string };
|
|
4129
|
+
}>('/api/cross-team/accept', async (request) => {
|
|
4130
|
+
const { team_slug, dispatch_id } = request.body ?? {};
|
|
4131
|
+
if (!team_slug || !dispatch_id) {
|
|
4132
|
+
return { ok: false, error: 'team_slug and dispatch_id are required' };
|
|
4133
|
+
}
|
|
4134
|
+
try {
|
|
4135
|
+
const result = await taskDispatch.acceptTask(team_slug, dispatch_id);
|
|
4136
|
+
return { ok: true, taskId: result.taskId };
|
|
4137
|
+
} catch (err) {
|
|
4138
|
+
return {
|
|
4139
|
+
ok: false,
|
|
4140
|
+
error: err instanceof Error ? err.message : String(err),
|
|
4141
|
+
};
|
|
4142
|
+
}
|
|
3900
4143
|
});
|
|
3901
4144
|
|
|
3902
|
-
|
|
3903
|
-
|
|
3904
|
-
|
|
3905
|
-
|
|
3906
|
-
|
|
4145
|
+
// Agent collaboration: reject a task request
|
|
4146
|
+
app.post<{
|
|
4147
|
+
Body: { team_slug: string; dispatch_id: string; reason?: string };
|
|
4148
|
+
}>('/api/cross-team/reject', async (request) => {
|
|
4149
|
+
const { team_slug, dispatch_id, reason } = request.body ?? {};
|
|
4150
|
+
if (!team_slug || !dispatch_id) {
|
|
4151
|
+
return { ok: false, error: 'team_slug and dispatch_id are required' };
|
|
4152
|
+
}
|
|
3907
4153
|
try {
|
|
3908
|
-
|
|
3909
|
-
|
|
3910
|
-
|
|
3911
|
-
|
|
3912
|
-
|
|
3913
|
-
|
|
3914
|
-
|
|
3915
|
-
Array.isArray(detail.platforms) && detail.platforms.some((pl: any) => pl.connected);
|
|
3916
|
-
} catch {
|
|
3917
|
-
/* degraded */
|
|
3918
|
-
}
|
|
3919
|
-
return { name: p.name, isAlive };
|
|
3920
|
-
})
|
|
3921
|
-
);
|
|
3922
|
-
aliveSet = new Set(states.filter((s) => s.isAlive).map((s) => s.name));
|
|
3923
|
-
} catch {
|
|
3924
|
-
/* cc-connect unavailable */
|
|
4154
|
+
await taskDispatch.rejectTask(team_slug, dispatch_id, reason);
|
|
4155
|
+
return { ok: true };
|
|
4156
|
+
} catch (err) {
|
|
4157
|
+
return {
|
|
4158
|
+
ok: false,
|
|
4159
|
+
error: err instanceof Error ? err.message : String(err),
|
|
4160
|
+
};
|
|
3925
4161
|
}
|
|
4162
|
+
});
|
|
3926
4163
|
|
|
3927
|
-
|
|
3928
|
-
|
|
3929
|
-
|
|
3930
|
-
|
|
3931
|
-
|
|
3932
|
-
|
|
3933
|
-
|
|
3934
|
-
|
|
3935
|
-
|
|
4164
|
+
app.get<{ Querystring: { excludeTeam?: string } }>('/api/cross-team/targets', async (request) => {
|
|
4165
|
+
const excludeTeam = request.query.excludeTeam;
|
|
4166
|
+
const all = await taskDispatch.discoverTeams();
|
|
4167
|
+
const teams = excludeTeam ? all.filter((t) => t.slug !== excludeTeam) : all;
|
|
4168
|
+
return teams.map((t) => ({
|
|
4169
|
+
teamName: t.slug,
|
|
4170
|
+
displayName: t.displayName || t.slug,
|
|
4171
|
+
description: t.description,
|
|
4172
|
+
color: undefined,
|
|
4173
|
+
isOnline: t.status === 'online',
|
|
4174
|
+
location: t.location,
|
|
4175
|
+
harness: t.harness,
|
|
4176
|
+
}));
|
|
3936
4177
|
});
|
|
3937
4178
|
|
|
3938
4179
|
app.get<{ Params: { name: string } }>('/api/cross-team/outbox/:name', async (request) => {
|
|
@@ -3944,6 +4185,284 @@ app.get<{ Params: { name: string } }>('/api/cross-team/outbox/:name', async (req
|
|
|
3944
4185
|
return { pending };
|
|
3945
4186
|
});
|
|
3946
4187
|
|
|
4188
|
+
// Agent collaboration: discover teams with capabilities
|
|
4189
|
+
app.get('/api/cross-team/discover', async () => {
|
|
4190
|
+
const teams = await taskDispatch.discoverTeams();
|
|
4191
|
+
return { teams };
|
|
4192
|
+
});
|
|
4193
|
+
|
|
4194
|
+
// Agent collaboration: pending handshake requests for a team
|
|
4195
|
+
app.get<{ Params: { name: string } }>('/api/cross-team/pending-requests/:name', async (request) => {
|
|
4196
|
+
const teamSlug = request.params.name;
|
|
4197
|
+
const requests = taskDispatch.listPendingRequests(teamSlug);
|
|
4198
|
+
return { requests };
|
|
4199
|
+
});
|
|
4200
|
+
|
|
4201
|
+
// Agent collaboration: deliver task result
|
|
4202
|
+
app.post<{
|
|
4203
|
+
Body: { team_slug: string; dispatch_id: string; result: string };
|
|
4204
|
+
}>('/api/cross-team/deliver', async (request) => {
|
|
4205
|
+
const { team_slug, dispatch_id, result } = request.body ?? {};
|
|
4206
|
+
if (!team_slug || !dispatch_id || !result) {
|
|
4207
|
+
return { ok: false, error: 'team_slug, dispatch_id, and result are required' };
|
|
4208
|
+
}
|
|
4209
|
+
try {
|
|
4210
|
+
const res = await taskDispatch.deliverTask(team_slug, dispatch_id, result);
|
|
4211
|
+
return res;
|
|
4212
|
+
} catch (err) {
|
|
4213
|
+
return {
|
|
4214
|
+
ok: false,
|
|
4215
|
+
error: err instanceof Error ? err.message : String(err),
|
|
4216
|
+
};
|
|
4217
|
+
}
|
|
4218
|
+
});
|
|
4219
|
+
|
|
4220
|
+
// Agent collaboration: approve task result
|
|
4221
|
+
app.post<{
|
|
4222
|
+
Body: { team_slug: string; dispatch_id: string };
|
|
4223
|
+
}>('/api/cross-team/approve', async (request) => {
|
|
4224
|
+
const { team_slug, dispatch_id } = request.body ?? {};
|
|
4225
|
+
if (!team_slug || !dispatch_id) {
|
|
4226
|
+
return { ok: false, error: 'team_slug and dispatch_id are required' };
|
|
4227
|
+
}
|
|
4228
|
+
try {
|
|
4229
|
+
const res = await taskDispatch.approveTask(team_slug, dispatch_id);
|
|
4230
|
+
return res;
|
|
4231
|
+
} catch (err) {
|
|
4232
|
+
return {
|
|
4233
|
+
ok: false,
|
|
4234
|
+
error: err instanceof Error ? err.message : String(err),
|
|
4235
|
+
};
|
|
4236
|
+
}
|
|
4237
|
+
});
|
|
4238
|
+
|
|
4239
|
+
// Agent collaboration: reject (request revision) task result
|
|
4240
|
+
app.post<{
|
|
4241
|
+
Body: { team_slug: string; dispatch_id: string; feedback: string };
|
|
4242
|
+
}>('/api/cross-team/revision', async (request) => {
|
|
4243
|
+
const { team_slug, dispatch_id, feedback } = request.body ?? {};
|
|
4244
|
+
if (!team_slug || !dispatch_id || !feedback) {
|
|
4245
|
+
return { ok: false, error: 'team_slug, dispatch_id, and feedback are required' };
|
|
4246
|
+
}
|
|
4247
|
+
try {
|
|
4248
|
+
const res = await taskDispatch.rejectResult(team_slug, dispatch_id, feedback);
|
|
4249
|
+
return res;
|
|
4250
|
+
} catch (err) {
|
|
4251
|
+
return {
|
|
4252
|
+
ok: false,
|
|
4253
|
+
error: err instanceof Error ? err.message : String(err),
|
|
4254
|
+
};
|
|
4255
|
+
}
|
|
4256
|
+
});
|
|
4257
|
+
|
|
4258
|
+
// Collaboration board: list all collab tasks
|
|
4259
|
+
app.get('/api/collab/board', async () => {
|
|
4260
|
+
return { tasks: taskDispatch.getCollabBoard() };
|
|
4261
|
+
});
|
|
4262
|
+
|
|
4263
|
+
// Collaboration board: get single collab task
|
|
4264
|
+
app.get<{ Params: { dispatchId: string } }>('/api/collab/board/:dispatchId', async (request) => {
|
|
4265
|
+
const task = taskDispatch.getCollabTask(request.params.dispatchId);
|
|
4266
|
+
if (!task) return { ok: false, error: 'Not found' };
|
|
4267
|
+
return { task };
|
|
4268
|
+
});
|
|
4269
|
+
|
|
4270
|
+
app.get<{ Params: { dispatchId: string } }>(
|
|
4271
|
+
'/api/collab/board/:dispatchId/events',
|
|
4272
|
+
async (request) => {
|
|
4273
|
+
return { events: taskDispatch.getCollabTaskEvents(request.params.dispatchId) };
|
|
4274
|
+
}
|
|
4275
|
+
);
|
|
4276
|
+
|
|
4277
|
+
// Update /api/cross-team/send to support needsHumanReview
|
|
4278
|
+
app.post<{
|
|
4279
|
+
Body: {
|
|
4280
|
+
fromTeam: string;
|
|
4281
|
+
fromMember?: string;
|
|
4282
|
+
toTeam: string;
|
|
4283
|
+
text?: string;
|
|
4284
|
+
subject?: string;
|
|
4285
|
+
description?: string;
|
|
4286
|
+
prompt?: string;
|
|
4287
|
+
messageId?: string;
|
|
4288
|
+
sessionKey?: string;
|
|
4289
|
+
conversationId?: string;
|
|
4290
|
+
replyToConversationId?: string;
|
|
4291
|
+
taskRefs?: unknown[];
|
|
4292
|
+
actionMode?: string;
|
|
4293
|
+
summary?: string;
|
|
4294
|
+
chainDepth?: number;
|
|
4295
|
+
deadlineMinutes?: number;
|
|
4296
|
+
needsHumanReview?: boolean;
|
|
4297
|
+
};
|
|
4298
|
+
}>('/api/cross-team/send', async (request) => {
|
|
4299
|
+
const {
|
|
4300
|
+
fromTeam,
|
|
4301
|
+
fromMember,
|
|
4302
|
+
toTeam,
|
|
4303
|
+
text,
|
|
4304
|
+
subject,
|
|
4305
|
+
description,
|
|
4306
|
+
prompt,
|
|
4307
|
+
messageId,
|
|
4308
|
+
sessionKey,
|
|
4309
|
+
conversationId,
|
|
4310
|
+
replyToConversationId,
|
|
4311
|
+
taskRefs,
|
|
4312
|
+
actionMode,
|
|
4313
|
+
summary,
|
|
4314
|
+
chainDepth,
|
|
4315
|
+
deadlineMinutes,
|
|
4316
|
+
needsHumanReview,
|
|
4317
|
+
} = request.body ?? {};
|
|
4318
|
+
if (!fromTeam || !toTeam) return { ok: false, error: 'fromTeam and toTeam are required' };
|
|
4319
|
+
const resolvedToTeam = await resolveTeamSlugForMention(toTeam);
|
|
4320
|
+
if (!resolvedToTeam) return { ok: false, error: `Unknown target team: ${toTeam}` };
|
|
4321
|
+
|
|
4322
|
+
if (typeof text === 'string') {
|
|
4323
|
+
const trimmedText = text.trim();
|
|
4324
|
+
if (!trimmedText) return { ok: false, error: 'text is required' };
|
|
4325
|
+
|
|
4326
|
+
const depth = Number.isFinite(Number(chainDepth)) ? Number(chainDepth) : 0;
|
|
4327
|
+
const threadId = conversationId || messageId || `cross-team-${Date.now()}`;
|
|
4328
|
+
const sender = fromMember || 'user';
|
|
4329
|
+
const fromSessionKey =
|
|
4330
|
+
typeof sessionKey === 'string' && sessionKey.trim().length > 0
|
|
4331
|
+
? sessionKey.trim()
|
|
4332
|
+
: buildFallbackSessionKey(fromTeam);
|
|
4333
|
+
const toSessionKey = buildFallbackSessionKey(resolvedToTeam);
|
|
4334
|
+
const sentText = formatCrossTeamText(`${fromTeam}.${sender}`, depth, trimmedText, {
|
|
4335
|
+
conversationId: threadId,
|
|
4336
|
+
replyToConversationId,
|
|
4337
|
+
});
|
|
4338
|
+
const meta = {
|
|
4339
|
+
taskRefs,
|
|
4340
|
+
actionMode,
|
|
4341
|
+
summary,
|
|
4342
|
+
conversationId: threadId,
|
|
4343
|
+
replyToConversationId,
|
|
4344
|
+
chainDepth: depth,
|
|
4345
|
+
};
|
|
4346
|
+
|
|
4347
|
+
const outgoing = await svc.appendMessage(fromTeam, {
|
|
4348
|
+
from: `${fromTeam}.${sender}`,
|
|
4349
|
+
to: resolvedToTeam,
|
|
4350
|
+
role: 'user',
|
|
4351
|
+
content: trimmedText,
|
|
4352
|
+
meta: { ...meta, source: CROSS_TEAM_SENT_SOURCE, sessionKey: fromSessionKey },
|
|
4353
|
+
});
|
|
4354
|
+
|
|
4355
|
+
await svc.appendMessage(resolvedToTeam, {
|
|
4356
|
+
from: `${fromTeam}.${sender}`,
|
|
4357
|
+
to: resolvedToTeam,
|
|
4358
|
+
role: 'user',
|
|
4359
|
+
content: sentText,
|
|
4360
|
+
meta: {
|
|
4361
|
+
...meta,
|
|
4362
|
+
source: CROSS_TEAM_SOURCE,
|
|
4363
|
+
relayOfMessageId: outgoing.id,
|
|
4364
|
+
sessionKey: toSessionKey,
|
|
4365
|
+
},
|
|
4366
|
+
});
|
|
4367
|
+
|
|
4368
|
+
const existingTasks = await svc.readTasks(resolvedToTeam).catch(() => []);
|
|
4369
|
+
const existingTask = existingTasks.find((task) => task.dispatchMeta?.dispatchId === threadId);
|
|
4370
|
+
if (!existingTask) {
|
|
4371
|
+
const now = new Date().toISOString();
|
|
4372
|
+
await svc.createTask(resolvedToTeam, {
|
|
4373
|
+
title: summary || trimmedText.split(/\r?\n/, 1)[0]?.slice(0, 120) || '跨团队 @ 消息',
|
|
4374
|
+
description: trimmedText,
|
|
4375
|
+
status: 'todo',
|
|
4376
|
+
dispatchMeta: {
|
|
4377
|
+
dispatchId: threadId,
|
|
4378
|
+
originTeam: fromTeam,
|
|
4379
|
+
targetTeam: resolvedToTeam,
|
|
4380
|
+
status: 'pending_accept',
|
|
4381
|
+
dispatchedAt: now,
|
|
4382
|
+
receivedAt: now,
|
|
4383
|
+
},
|
|
4384
|
+
});
|
|
4385
|
+
}
|
|
4386
|
+
|
|
4387
|
+
broadcastSse('team-change', { type: 'inbox', teamName: fromTeam });
|
|
4388
|
+
broadcastSse('team-change', { type: 'inbox', teamName: resolvedToTeam });
|
|
4389
|
+
broadcastSse('team-change', { type: 'task', teamName: resolvedToTeam });
|
|
4390
|
+
|
|
4391
|
+
void sendHarnessMessageViaBridge({
|
|
4392
|
+
teamName: resolvedToTeam,
|
|
4393
|
+
text: sentText,
|
|
4394
|
+
}).catch((err) => {
|
|
4395
|
+
request.log.warn({ err }, 'cross-team runtime delivery failed after persistence');
|
|
4396
|
+
});
|
|
4397
|
+
|
|
4398
|
+
return {
|
|
4399
|
+
messageId: outgoing.id,
|
|
4400
|
+
deliveredToInbox: true,
|
|
4401
|
+
deduplicated: false,
|
|
4402
|
+
};
|
|
4403
|
+
}
|
|
4404
|
+
|
|
4405
|
+
if (!subject) return { ok: false, error: 'subject is required' };
|
|
4406
|
+
|
|
4407
|
+
const sentMessage = await svc.appendMessage(fromTeam, {
|
|
4408
|
+
from: fromMember ? `${fromTeam}.${fromMember}` : 'user',
|
|
4409
|
+
to: resolvedToTeam,
|
|
4410
|
+
role: 'user',
|
|
4411
|
+
content: `@${resolvedToTeam} ${subject}`,
|
|
4412
|
+
meta: {
|
|
4413
|
+
source: CROSS_TEAM_SENT_SOURCE,
|
|
4414
|
+
sessionKey,
|
|
4415
|
+
clientMessageId: messageId,
|
|
4416
|
+
},
|
|
4417
|
+
});
|
|
4418
|
+
broadcastSse('team-change', { type: 'inbox', teamName: fromTeam });
|
|
4419
|
+
|
|
4420
|
+
// Check collaboration toggle
|
|
4421
|
+
try {
|
|
4422
|
+
const configPath = path.join(os.homedir(), '.hermit', 'settings.json');
|
|
4423
|
+
const raw = await fs.readFile(configPath, 'utf-8');
|
|
4424
|
+
const settings = JSON.parse(raw);
|
|
4425
|
+
if (!settings.taskBus?.collaboration) {
|
|
4426
|
+
return {
|
|
4427
|
+
ok: false,
|
|
4428
|
+
error: 'Distributed collaboration is not enabled. Enable it in Settings → Task Bus.',
|
|
4429
|
+
};
|
|
4430
|
+
}
|
|
4431
|
+
} catch {
|
|
4432
|
+
return { ok: false, error: 'Could not read task bus configuration.' };
|
|
4433
|
+
}
|
|
4434
|
+
|
|
4435
|
+
const result = await taskDispatch.dispatchTask(
|
|
4436
|
+
fromTeam ?? 'unknown',
|
|
4437
|
+
{ subject, description, prompt },
|
|
4438
|
+
resolvedToTeam,
|
|
4439
|
+
{
|
|
4440
|
+
deadlineMinutes: deadlineMinutes ? Number(deadlineMinutes) : undefined,
|
|
4441
|
+
needsHumanReview,
|
|
4442
|
+
}
|
|
4443
|
+
);
|
|
4444
|
+
const ok = result.status !== 'failed';
|
|
4445
|
+
if (ok) {
|
|
4446
|
+
broadcastSse('team-change', { type: 'inbox', teamName: resolvedToTeam });
|
|
4447
|
+
void sendHarnessMessageViaBridge({
|
|
4448
|
+
teamName: resolvedToTeam,
|
|
4449
|
+
text: `[跨团队任务] ${fromTeam} 派发了任务:${subject}${description ? `\n\n${description}` : ''}`,
|
|
4450
|
+
}).catch((err) => {
|
|
4451
|
+
request.log.warn(
|
|
4452
|
+
{ err, fromTeam, resolvedToTeam },
|
|
4453
|
+
'cross-team task runtime delivery failed'
|
|
4454
|
+
);
|
|
4455
|
+
});
|
|
4456
|
+
}
|
|
4457
|
+
return {
|
|
4458
|
+
ok,
|
|
4459
|
+
messageId: sentMessage.id,
|
|
4460
|
+
dispatchId: result.dispatchId,
|
|
4461
|
+
status: result.status,
|
|
4462
|
+
message: result.message,
|
|
4463
|
+
};
|
|
4464
|
+
});
|
|
4465
|
+
|
|
3947
4466
|
// GET /api/settings/task-bus → full config including telemetry
|
|
3948
4467
|
app.get('/api/settings/task-bus', async () => {
|
|
3949
4468
|
const configPath = path.join(os.homedir(), '.hermit', 'settings.json');
|
|
@@ -3968,7 +4487,11 @@ app.get('/api/settings/task-bus', async () => {
|
|
|
3968
4487
|
|
|
3969
4488
|
// PUT /api/settings/task-bus → save config + start/stop telemetry
|
|
3970
4489
|
app.put<{ Body: TaskBusConfig }>('/api/settings/task-bus', async (request) => {
|
|
3971
|
-
const config =
|
|
4490
|
+
const config = (
|
|
4491
|
+
request.body && 'taskBus' in (request.body as unknown as Record<string, unknown>)
|
|
4492
|
+
? (request.body as unknown as { taskBus: TaskBusConfig }).taskBus
|
|
4493
|
+
: request.body
|
|
4494
|
+
) as TaskBusConfig;
|
|
3972
4495
|
const configPath = path.join(os.homedir(), '.hermit', 'settings.json');
|
|
3973
4496
|
let settings: Record<string, unknown> = {};
|
|
3974
4497
|
try {
|
|
@@ -3988,36 +4511,44 @@ app.put<{ Body: TaskBusConfig }>('/api/settings/task-bus', async (request) => {
|
|
|
3988
4511
|
await stopTelemetry();
|
|
3989
4512
|
}
|
|
3990
4513
|
|
|
3991
|
-
//
|
|
3992
|
-
|
|
3993
|
-
|
|
3994
|
-
|
|
3995
|
-
|
|
3996
|
-
|
|
3997
|
-
|
|
4514
|
+
// Keep CLAUDE.md team instructions aligned with the collaboration toggle.
|
|
4515
|
+
const syncTeamInstructions = async (enabled: boolean): Promise<void> => {
|
|
4516
|
+
const projects = await cc.listProjects();
|
|
4517
|
+
for (const p of projects) {
|
|
4518
|
+
let workDir = '';
|
|
4519
|
+
let slug = p.name;
|
|
4520
|
+
try {
|
|
4521
|
+
const meta = await svc.readTeamManifest(p.name);
|
|
4522
|
+
if (typeof meta.workDir === 'string') workDir = meta.workDir.trim();
|
|
4523
|
+
if (meta.slug) slug = meta.slug;
|
|
4524
|
+
} catch {
|
|
4525
|
+
/* no local manifest */
|
|
4526
|
+
}
|
|
4527
|
+
if (!workDir) {
|
|
3998
4528
|
try {
|
|
3999
|
-
const
|
|
4000
|
-
if (typeof
|
|
4001
|
-
if (meta.slug) slug = meta.slug;
|
|
4529
|
+
const detail = await cc.getProject(p.name);
|
|
4530
|
+
if (typeof detail.work_dir === 'string') workDir = detail.work_dir.trim();
|
|
4002
4531
|
} catch {
|
|
4003
|
-
|
|
4004
|
-
}
|
|
4005
|
-
if (!workDir) {
|
|
4006
|
-
try {
|
|
4007
|
-
const detail = await cc.getProject(p.name);
|
|
4008
|
-
if (typeof detail.work_dir === 'string') workDir = detail.work_dir.trim();
|
|
4009
|
-
} catch {
|
|
4010
|
-
// ignore
|
|
4011
|
-
}
|
|
4012
|
-
}
|
|
4013
|
-
if (workDir) {
|
|
4014
|
-
await svc.injectTeamInstructions(workDir, slug);
|
|
4532
|
+
// ignore
|
|
4015
4533
|
}
|
|
4016
4534
|
}
|
|
4017
|
-
|
|
4018
|
-
|
|
4535
|
+
if (!workDir) continue;
|
|
4536
|
+
if (enabled) {
|
|
4537
|
+
await svc.injectTeamInstructions(workDir, slug);
|
|
4538
|
+
} else {
|
|
4539
|
+
await svc.removeTeamInstructions(workDir);
|
|
4540
|
+
}
|
|
4019
4541
|
}
|
|
4542
|
+
};
|
|
4543
|
+
|
|
4544
|
+
const collaborationEnabled = config?.enabled === true && config?.collaboration === true;
|
|
4545
|
+
try {
|
|
4546
|
+
await syncTeamInstructions(collaborationEnabled);
|
|
4547
|
+
} catch (err) {
|
|
4548
|
+
request.log.warn({ err }, 'CLAUDE.md team instruction sync failed');
|
|
4549
|
+
}
|
|
4020
4550
|
|
|
4551
|
+
if (config?.enabled) {
|
|
4021
4552
|
// Reconnect TaskDispatchService with Redis (optional)
|
|
4022
4553
|
taskDispatch.dispose();
|
|
4023
4554
|
try {
|
|
@@ -4057,13 +4588,22 @@ app.post('/api/telemetry/scan', async (request, reply) => {
|
|
|
4057
4588
|
}
|
|
4058
4589
|
const result = await triggerScan(taskBus);
|
|
4059
4590
|
if (!result) {
|
|
4060
|
-
return reply.code(503).send({ error: '
|
|
4591
|
+
return reply.code(503).send({ error: 'Telemetry scan failed' });
|
|
4061
4592
|
}
|
|
4062
4593
|
return {
|
|
4063
4594
|
ok: true,
|
|
4064
|
-
|
|
4065
|
-
sessions: result.sessions.length,
|
|
4595
|
+
connected: taskBus.telemetry.uploadEnabled === true,
|
|
4066
4596
|
lastScan: new Date().toISOString(),
|
|
4597
|
+
sessions: result.aggregate.sessions,
|
|
4598
|
+
messages: result.aggregate.messages,
|
|
4599
|
+
tokensIn: result.aggregate.tokens.input,
|
|
4600
|
+
tokensOut: result.aggregate.tokens.output,
|
|
4601
|
+
cacheRead: result.aggregate.tokens.cacheRead,
|
|
4602
|
+
cacheCreation: result.aggregate.tokens.cacheCreation,
|
|
4603
|
+
activeDays: result.aggregate.activeDays,
|
|
4604
|
+
hourly: result.aggregate.hourly,
|
|
4605
|
+
projects: result.aggregate.projects,
|
|
4606
|
+
workSecondsByDay: result.aggregate.workSecondsByDay,
|
|
4067
4607
|
};
|
|
4068
4608
|
} catch (err) {
|
|
4069
4609
|
return reply.code(500).send({ error: String(err) });
|
|
@@ -4082,23 +4622,8 @@ app.get('/api/telemetry/status', async (request, reply) => {
|
|
|
4082
4622
|
// no settings
|
|
4083
4623
|
}
|
|
4084
4624
|
const taskBus = (settings.taskBus ?? {}) as TaskBusConfig;
|
|
4085
|
-
|
|
4086
|
-
|
|
4087
|
-
connected: false,
|
|
4088
|
-
lastScan: null,
|
|
4089
|
-
sessions: 0,
|
|
4090
|
-
messages: 0,
|
|
4091
|
-
tokensIn: 0,
|
|
4092
|
-
tokensOut: 0,
|
|
4093
|
-
cacheRead: 0,
|
|
4094
|
-
cacheCreation: 0,
|
|
4095
|
-
activeDays: 0,
|
|
4096
|
-
hourly: [],
|
|
4097
|
-
projects: [],
|
|
4098
|
-
workSecondsByDay: {},
|
|
4099
|
-
};
|
|
4100
|
-
}
|
|
4101
|
-
const status = await getTelemetryStatus(taskBus.redis);
|
|
4625
|
+
const redisCfg = taskBus.enabled ? taskBus.redis : undefined;
|
|
4626
|
+
const status = await getTelemetryStatus(redisCfg);
|
|
4102
4627
|
return (
|
|
4103
4628
|
status ?? {
|
|
4104
4629
|
connected: false,
|
|
@@ -4208,10 +4733,207 @@ app.get('/api/events', (request, reply) => {
|
|
|
4208
4733
|
|
|
4209
4734
|
const SSE_FALLBACK_RE = /^\/api\/(.*\/(events|stream|notifications\/stream))$/;
|
|
4210
4735
|
|
|
4211
|
-
|
|
4212
|
-
|
|
4213
|
-
|
|
4214
|
-
|
|
4736
|
+
// ── Extension Store routes (wired to extensionHandlers) ────────────────
|
|
4737
|
+
|
|
4738
|
+
import { extensionHandlers as ext, setSkillsWatcherEmitter } from './ipc/extensions';
|
|
4739
|
+
|
|
4740
|
+
// Broadcast skill file-watcher changes to connected frontends via SSE.
|
|
4741
|
+
setSkillsWatcherEmitter((event) => broadcastSse('skills:changed', event));
|
|
4742
|
+
|
|
4743
|
+
app.get('/api/extensions/plugins', async () => {
|
|
4744
|
+
const result = await ext.pluginGetAll();
|
|
4745
|
+
return result;
|
|
4746
|
+
});
|
|
4747
|
+
|
|
4748
|
+
app.get('/api/extensions/plugins/readme/:pluginId', async (request) => {
|
|
4749
|
+
const { pluginId } = request.params as { pluginId: string };
|
|
4750
|
+
const result = await ext.pluginGetReadme(pluginId);
|
|
4751
|
+
return result;
|
|
4752
|
+
});
|
|
4753
|
+
|
|
4754
|
+
app.post('/api/extensions/plugins/install', async (request) => {
|
|
4755
|
+
const body = request.body as Record<string, unknown>;
|
|
4756
|
+
const result = await ext.pluginInstall(body as any);
|
|
4757
|
+
return result;
|
|
4758
|
+
});
|
|
4759
|
+
|
|
4760
|
+
app.post('/api/extensions/plugins/uninstall', async (request) => {
|
|
4761
|
+
const body = request.body as Record<string, unknown>;
|
|
4762
|
+
const result = await ext.pluginUninstall(
|
|
4763
|
+
body.pluginId as string,
|
|
4764
|
+
body.scope as string,
|
|
4765
|
+
body.projectPath as string,
|
|
4766
|
+
body.harnessType as CcAgentType | undefined
|
|
4767
|
+
);
|
|
4768
|
+
return result;
|
|
4769
|
+
});
|
|
4770
|
+
|
|
4771
|
+
app.get('/api/extensions/mcp/search', async (request) => {
|
|
4772
|
+
const query = (request.query as Record<string, string>).q ?? '';
|
|
4773
|
+
const limit = Number((request.query as Record<string, string>).limit) || 20;
|
|
4774
|
+
const result = await ext.mcpSearch(query, limit);
|
|
4775
|
+
return result;
|
|
4776
|
+
});
|
|
4777
|
+
|
|
4778
|
+
app.get('/api/extensions/mcp/browse', async (request) => {
|
|
4779
|
+
const cursor = (request.query as Record<string, string>).cursor;
|
|
4780
|
+
const limit = Number((request.query as Record<string, string>).limit) || 20;
|
|
4781
|
+
const result = await ext.mcpBrowse(cursor || undefined, limit);
|
|
4782
|
+
return result;
|
|
4783
|
+
});
|
|
4784
|
+
|
|
4785
|
+
app.get('/api/extensions/mcp/installed', async (request) => {
|
|
4786
|
+
const projectPath = (request.query as Record<string, string>).projectPath;
|
|
4787
|
+
const result = await ext.mcpGetInstalled(projectPath);
|
|
4788
|
+
return result;
|
|
4789
|
+
});
|
|
4790
|
+
|
|
4791
|
+
app.get('/api/extensions/mcp/:registryId', async (request) => {
|
|
4792
|
+
const { registryId } = request.params as { registryId: string };
|
|
4793
|
+
const result = await ext.mcpGetById(registryId);
|
|
4794
|
+
return result;
|
|
4795
|
+
});
|
|
4796
|
+
|
|
4797
|
+
app.post('/api/extensions/mcp/install', async (request) => {
|
|
4798
|
+
const body = request.body as Record<string, unknown>;
|
|
4799
|
+
const result = await ext.mcpInstall(body as any);
|
|
4800
|
+
return result;
|
|
4801
|
+
});
|
|
4802
|
+
|
|
4803
|
+
app.post('/api/extensions/mcp/install-custom', async (request) => {
|
|
4804
|
+
const body = request.body as Record<string, unknown>;
|
|
4805
|
+
const result = await ext.mcpInstallCustom(body as any);
|
|
4806
|
+
return result;
|
|
4807
|
+
});
|
|
4808
|
+
|
|
4809
|
+
app.post('/api/extensions/mcp/uninstall', async (request) => {
|
|
4810
|
+
const body = request.body as Record<string, unknown>;
|
|
4811
|
+
const result = await ext.mcpUninstall(
|
|
4812
|
+
body.name as string,
|
|
4813
|
+
body.scope as string,
|
|
4814
|
+
body.projectPath as string,
|
|
4815
|
+
body.harnessType as CcAgentType | undefined
|
|
4816
|
+
);
|
|
4817
|
+
return result;
|
|
4818
|
+
});
|
|
4819
|
+
|
|
4820
|
+
app.get('/api/extensions/skills', async (request) => {
|
|
4821
|
+
const projectPath = (request.query as Record<string, string>).projectPath;
|
|
4822
|
+
const result = await ext.skillsList(projectPath);
|
|
4823
|
+
return result;
|
|
4824
|
+
});
|
|
4825
|
+
|
|
4826
|
+
app.get('/api/extensions/skills/:skillId', async (request) => {
|
|
4827
|
+
const { skillId } = request.params as { skillId: string };
|
|
4828
|
+
const projectPath = (request.query as Record<string, string>).projectPath;
|
|
4829
|
+
const result = await ext.skillsGetDetail(skillId, projectPath);
|
|
4830
|
+
return result;
|
|
4831
|
+
});
|
|
4832
|
+
|
|
4833
|
+
app.post('/api/extensions/skills/upsert', async (request) => {
|
|
4834
|
+
const result = await ext.skillsUpsert(request.body as any);
|
|
4835
|
+
return result;
|
|
4836
|
+
});
|
|
4837
|
+
|
|
4838
|
+
app.post('/api/extensions/skills/delete', async (request) => {
|
|
4839
|
+
const result = await ext.skillsDelete(request.body as any);
|
|
4840
|
+
return result;
|
|
4841
|
+
});
|
|
4842
|
+
|
|
4843
|
+
app.post('/api/extensions/skills/preview-upsert', async (request) => {
|
|
4844
|
+
return ext.skillsPreviewUpsert(request.body as any);
|
|
4845
|
+
});
|
|
4846
|
+
|
|
4847
|
+
app.post('/api/extensions/skills/apply-upsert', async (request) => {
|
|
4848
|
+
return ext.skillsApplyUpsert(request.body as any);
|
|
4849
|
+
});
|
|
4850
|
+
|
|
4851
|
+
app.post('/api/extensions/skills/preview-import', async (request) => {
|
|
4852
|
+
return ext.skillsPreviewImport(request.body as any);
|
|
4853
|
+
});
|
|
4854
|
+
|
|
4855
|
+
app.post('/api/extensions/skills/apply-import', async (request) => {
|
|
4856
|
+
return ext.skillsApplyImport(request.body as any);
|
|
4857
|
+
});
|
|
4858
|
+
|
|
4859
|
+
app.post('/api/extensions/skills/watching/start', async (request) => {
|
|
4860
|
+
const projectPath = (request.query as Record<string, string>).projectPath;
|
|
4861
|
+
return ext.skillsStartWatching(projectPath);
|
|
4862
|
+
});
|
|
4863
|
+
|
|
4864
|
+
app.post('/api/extensions/skills/watching/stop', async (request) => {
|
|
4865
|
+
const { watchId } = (request.body ?? {}) as { watchId?: string };
|
|
4866
|
+
return ext.skillsStopWatching(watchId as string);
|
|
4867
|
+
});
|
|
4868
|
+
|
|
4869
|
+
app.get('/api/extensions/credentials/status', async () => {
|
|
4870
|
+
const result = await ext.credentialsStatus();
|
|
4871
|
+
return result;
|
|
4872
|
+
});
|
|
4873
|
+
|
|
4874
|
+
app.get('/api/extensions/credentials/mcp/:mcpName', async (request) => {
|
|
4875
|
+
const { mcpName } = request.params as { mcpName: string };
|
|
4876
|
+
const result = await ext.credentialsGetMcp(mcpName);
|
|
4877
|
+
return result;
|
|
4878
|
+
});
|
|
4879
|
+
|
|
4880
|
+
app.post('/api/extensions/credentials/mcp', async (request) => {
|
|
4881
|
+
const body = request.body as Record<string, unknown>;
|
|
4882
|
+
const result = await ext.credentialsSaveMcp(
|
|
4883
|
+
body.mcpName as string,
|
|
4884
|
+
body.envValues as Record<string, string>
|
|
4885
|
+
);
|
|
4886
|
+
return result;
|
|
4887
|
+
});
|
|
4888
|
+
|
|
4889
|
+
app.get('/api/extensions/credentials/project-env', async (request) => {
|
|
4890
|
+
const projectPath = (request.query as Record<string, string>).projectPath;
|
|
4891
|
+
if (!projectPath) return { error: 'projectPath required' };
|
|
4892
|
+
const result = await ext.credentialsGetProjectEnv(projectPath);
|
|
4893
|
+
return result;
|
|
4894
|
+
});
|
|
4895
|
+
|
|
4896
|
+
app.post('/api/extensions/credentials/project-env', async (request) => {
|
|
4897
|
+
const body = request.body as Record<string, unknown>;
|
|
4898
|
+
const result = await ext.credentialsSaveProjectEnv(
|
|
4899
|
+
body.projectPath as string,
|
|
4900
|
+
body.vars as Record<string, string>
|
|
4901
|
+
);
|
|
4902
|
+
return result;
|
|
4903
|
+
});
|
|
4904
|
+
|
|
4905
|
+
app.post('/api/extensions/credentials/scan-required', async (request) => {
|
|
4906
|
+
const body = request.body as Record<string, unknown>;
|
|
4907
|
+
const result = await ext.credentialsScanRequired(
|
|
4908
|
+
body.projectPath as string,
|
|
4909
|
+
body.mcpServers as any,
|
|
4910
|
+
body.skillReqs as any
|
|
4911
|
+
);
|
|
4912
|
+
return result;
|
|
4913
|
+
});
|
|
4914
|
+
|
|
4915
|
+
app.get('/api/extensions/credentials/resolve-agent-env', async (request) => {
|
|
4916
|
+
const projectPath = (request.query as Record<string, string>).projectPath;
|
|
4917
|
+
if (!projectPath) return { error: 'projectPath required' };
|
|
4918
|
+
const result = await ext.credentialsResolveAgentEnv(projectPath);
|
|
4919
|
+
return result;
|
|
4920
|
+
});
|
|
4921
|
+
|
|
4922
|
+
app.get('/api/extensions/credentials/skill-env', async (request) => {
|
|
4923
|
+
const folderName = (request.query as Record<string, string>).folderName;
|
|
4924
|
+
if (!folderName) return { error: 'folderName required' };
|
|
4925
|
+
const result = await ext.credentialsGetSkillGlobalEnv(folderName);
|
|
4926
|
+
return result;
|
|
4927
|
+
});
|
|
4928
|
+
|
|
4929
|
+
app.post('/api/extensions/credentials/skill-env', async (request) => {
|
|
4930
|
+
const body = request.body as Record<string, unknown>;
|
|
4931
|
+
const result = await ext.credentialsSaveSkillGlobalEnv(
|
|
4932
|
+
body.folderName as string,
|
|
4933
|
+
body.vars as Record<string, string>
|
|
4934
|
+
);
|
|
4935
|
+
return result;
|
|
4936
|
+
});
|
|
4215
4937
|
|
|
4216
4938
|
app.setNotFoundHandler((request, reply) => {
|
|
4217
4939
|
const u = request.url;
|
|
@@ -4287,6 +5009,7 @@ function reply500(err: unknown) {
|
|
|
4287
5009
|
|
|
4288
5010
|
// 启动 cc-connect Bridge WebSocket 连接(注册 platform=hermit adapter)
|
|
4289
5011
|
bridge.start();
|
|
5012
|
+
await initializeTaskBusFromSettings();
|
|
4290
5013
|
|
|
4291
5014
|
try {
|
|
4292
5015
|
await app.listen({ host: HOST, port: PORT });
|