@yancyyu/openhermit 1.6.27 → 1.6.29
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -1
- package/bin/hermit.mjs +2 -2
- package/dist-renderer/assets/ProjectEditorOverlay-CQm6jUR1.js +52 -0
- package/dist-renderer/assets/{TeamGraphOverlay-DVq8rt6_.js → TeamGraphOverlay-h0WDfifv.js} +1 -1
- package/dist-renderer/assets/{_basePickBy-ZbF0pKvS.js → _basePickBy-CgG_tjgX.js} +1 -1
- package/dist-renderer/assets/{_baseUniq-BBLBOeXc.js → _baseUniq-DwPTU9lP.js} +1 -1
- package/dist-renderer/assets/{arc-wGaEgkCf.js → arc-7nIrGRzY.js} +1 -1
- package/dist-renderer/assets/{architectureDiagram-VXUJARFQ-BpMkdC35.js → architectureDiagram-VXUJARFQ-BYhA6Ev2.js} +1 -1
- package/dist-renderer/assets/{blockDiagram-VD42YOAC-C8Z1xhG4.js → blockDiagram-VD42YOAC-BVpZUGDg.js} +1 -1
- package/dist-renderer/assets/{c4Diagram-YG6GDRKO-CJmlw9LA.js → c4Diagram-YG6GDRKO-DsdreMQ9.js} +1 -1
- package/dist-renderer/assets/channel-C0SqeFU7.js +1 -0
- package/dist-renderer/assets/{chunk-4BX2VUAB-CHPHiRPP.js → chunk-4BX2VUAB-CcoAs7Jd.js} +1 -1
- package/dist-renderer/assets/{chunk-55IACEB6-DyVohOQb.js → chunk-55IACEB6-CGGAOoXd.js} +1 -1
- package/dist-renderer/assets/{chunk-B4BG7PRW-p5bffh_R.js → chunk-B4BG7PRW-FhpTEPvD.js} +1 -1
- package/dist-renderer/assets/{chunk-DI55MBZ5-BnfGPSUu.js → chunk-DI55MBZ5-DoYySbm1.js} +1 -1
- package/dist-renderer/assets/{chunk-FMBD7UC4-B6SCKseX.js → chunk-FMBD7UC4-e9l2tGHG.js} +1 -1
- package/dist-renderer/assets/{chunk-QN33PNHL-L12RvLBR.js → chunk-QN33PNHL-DeiXVTCy.js} +1 -1
- package/dist-renderer/assets/{chunk-QZHKN3VN-DeH1Kxge.js → chunk-QZHKN3VN-DC2UJLJM.js} +1 -1
- package/dist-renderer/assets/{chunk-TZMSLE5B-BWnjzSlI.js → chunk-TZMSLE5B-BHFD9eZI.js} +1 -1
- package/dist-renderer/assets/classDiagram-2ON5EDUG-DWew1HpM.js +1 -0
- package/dist-renderer/assets/classDiagram-v2-WZHVMYZB-DWew1HpM.js +1 -0
- package/dist-renderer/assets/clone-Dm-k63Yr.js +1 -0
- package/dist-renderer/assets/{cose-bilkent-S5V4N54A-BtzoT5fu.js → cose-bilkent-S5V4N54A-BdybQraU.js} +1 -1
- package/dist-renderer/assets/{dagre-6UL2VRFP-CBBvuoUD.js → dagre-6UL2VRFP-DdF3pwM3.js} +1 -1
- package/dist-renderer/assets/{diagram-PSM6KHXK-Be9BAKws.js → diagram-PSM6KHXK-B9Ldd3nh.js} +1 -1
- package/dist-renderer/assets/{diagram-QEK2KX5R-BDS4PI_i.js → diagram-QEK2KX5R-XEqkrbpu.js} +1 -1
- package/dist-renderer/assets/{diagram-S2PKOQOG-2Rameaq7.js → diagram-S2PKOQOG-CipwtY59.js} +1 -1
- package/dist-renderer/assets/{erDiagram-Q2GNP2WA-CSIzCEZD.js → erDiagram-Q2GNP2WA-BB-2ISGo.js} +1 -1
- package/dist-renderer/assets/{flowDiagram-NV44I4VS-ForEIVM5.js → flowDiagram-NV44I4VS-B8XmJ0u2.js} +1 -1
- package/dist-renderer/assets/{ganttDiagram-JELNMOA3-BJrli_xr.js → ganttDiagram-JELNMOA3-D-8XglBb.js} +1 -1
- package/dist-renderer/assets/{gitGraphDiagram-V2S2FVAM-C_4GuLno.js → gitGraphDiagram-V2S2FVAM-DL4ChakD.js} +1 -1
- package/dist-renderer/assets/{graph-B1EAT_gw.js → graph-BiFNoBjP.js} +1 -1
- package/dist-renderer/assets/{index-eKRmS5kI.js → index-6m1ZAymG.js} +1 -1
- package/dist-renderer/assets/index-BhellmRb.css +1 -0
- package/dist-renderer/assets/{index-DYdseEwc.js → index-BowUl0Jb.js} +518 -514
- package/dist-renderer/assets/{index-DR602dwJ.js → index-Dp3kJTEe.js} +1 -1
- package/dist-renderer/assets/{index-Dwr5wu5x.js → index-TOpt_T7A.js} +1 -1
- package/dist-renderer/assets/{index-DOA_jbYb.js → index-qNBNjW4K.js} +1 -1
- package/dist-renderer/assets/{index-k4tnOFC5.js → index-vAykq1H1.js} +1 -1
- package/dist-renderer/assets/{infoDiagram-HS3SLOUP-DjI0uaMz.js → infoDiagram-HS3SLOUP-DRIBfHDi.js} +1 -1
- package/dist-renderer/assets/{journeyDiagram-XKPGCS4Q-jQ6Thae-.js → journeyDiagram-XKPGCS4Q-BOMiigU4.js} +1 -1
- package/dist-renderer/assets/{kanban-definition-3W4ZIXB7-CKw6InbL.js → kanban-definition-3W4ZIXB7-DDxeyjod.js} +1 -1
- package/dist-renderer/assets/{layout-Dad20y3V.js → layout-DNANbrI4.js} +1 -1
- package/dist-renderer/assets/{linear-vMgo_2Cv.js → linear-DxEJi1yT.js} +1 -1
- package/dist-renderer/assets/{mindmap-definition-VGOIOE7T-DYp6YoHL.js → mindmap-definition-VGOIOE7T-nBfGriW8.js} +1 -1
- package/dist-renderer/assets/{pieDiagram-ADFJNKIX-BytBecG9.js → pieDiagram-ADFJNKIX-Din5j6sV.js} +1 -1
- package/dist-renderer/assets/{quadrantDiagram-AYHSOK5B-RUaspLsc.js → quadrantDiagram-AYHSOK5B-DMVK2BEQ.js} +1 -1
- package/dist-renderer/assets/{requirementDiagram-UZGBJVZJ-rR2B1Use.js → requirementDiagram-UZGBJVZJ-6SC94Gg_.js} +1 -1
- package/dist-renderer/assets/{sankeyDiagram-TZEHDZUN-BJi5qYhq.js → sankeyDiagram-TZEHDZUN-CD2gghhu.js} +1 -1
- package/dist-renderer/assets/{sequenceDiagram-WL72ISMW-BM-wggUb.js → sequenceDiagram-WL72ISMW-BnhkN7nZ.js} +1 -1
- package/dist-renderer/assets/{stateDiagram-FKZM4ZOC-BqmcVjnj.js → stateDiagram-FKZM4ZOC-Bn8XdYX-.js} +1 -1
- package/dist-renderer/assets/{stateDiagram-v2-4FDKWEC3-By3JDVbB.js → stateDiagram-v2-4FDKWEC3-1b6sI1_g.js} +1 -1
- package/dist-renderer/assets/{timeline-definition-IT6M3QCI-szH0GUyk.js → timeline-definition-IT6M3QCI-CNs3RPoa.js} +1 -1
- package/dist-renderer/assets/treemap-GDKQZRPO-DU_yr827.js +162 -0
- package/dist-renderer/assets/{xychartDiagram-PRI3JC2R-dwDpvw0w.js → xychartDiagram-PRI3JC2R-B8o5J2f3.js} +1 -1
- package/dist-renderer/index.html +2 -2
- package/package.json +1 -1
- package/src/main/server.ts +800 -163
- package/src/main/services/session-intelligence/SessionUsageParser.ts +446 -0
- package/src/main/services/session-intelligence/UsageTelemetryService.ts +252 -0
- package/src/main/services/teams-mvp/CollaborationBoardService.ts +310 -0
- package/src/main/services/teams-mvp/TaskDispatchService.ts +880 -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/renderer/App.tsx +5 -0
- package/src/renderer/api/httpClient.ts +67 -0
- package/src/renderer/components/dashboard/DashboardView.tsx +6 -105
- 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/SettingsTabs.tsx +2 -2
- package/src/renderer/components/settings/sections/AdvancedSection.tsx +1 -1
- package/src/renderer/components/settings/sections/TaskBusSection.tsx +511 -81
- package/src/renderer/components/tasks/TasksView.tsx +343 -0
- package/src/renderer/components/team/TeamDetailView.tsx +20 -98
- 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 +5 -1
- 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 +72 -2
- package/src/renderer/components/team/messages/StatusBlock.tsx +2 -24
- package/src/renderer/components/team/schedule/ScheduleEmptyState.tsx +1 -1
- package/src/renderer/components/ui/MentionableTextarea.tsx +0 -1
- package/src/renderer/store/slices/scheduleSlice.ts +21 -0
- package/src/renderer/store/slices/teamSlice.ts +59 -23
- package/src/renderer/types/tabs.ts +1 -0
- package/src/shared/types/api.ts +29 -0
- package/src/shared/types/team.ts +109 -1
- package/dist-renderer/assets/ProjectEditorOverlay-BBwYdXPv.js +0 -57
- package/dist-renderer/assets/channel-DJUrwVrK.js +0 -1
- package/dist-renderer/assets/classDiagram-2ON5EDUG-blc3DrH7.js +0 -1
- package/dist-renderer/assets/classDiagram-v2-WZHVMYZB-blc3DrH7.js +0 -1
- package/dist-renderer/assets/clone-BftjWakJ.js +0 -1
- package/dist-renderer/assets/index-CWpFqEvz.css +0 -1
- package/dist-renderer/assets/treemap-GDKQZRPO-BCMlh-Ex.js +0 -162
package/src/main/server.ts
CHANGED
|
@@ -43,12 +43,25 @@ 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';
|
|
46
51
|
import { CcConnectBridge } from './services/ccConnect/CcConnectBridge';
|
|
47
52
|
import { CcConnectClient } from './services/ccConnect/CcConnectClient';
|
|
48
53
|
import { TeamProvisioningService } from './services/teams-mvp';
|
|
49
54
|
import { TaskDispatchService } from './services/teams-mvp/TaskDispatchService';
|
|
50
|
-
import
|
|
55
|
+
import { CollaborationBoardService } from './services/teams-mvp/CollaborationBoardService';
|
|
56
|
+
import type { TaskBusConfig, TeamLaunchRequest } from '@shared/types/team';
|
|
57
|
+
import type { TeamManifest } from './services/teams-mvp/TeamWorkspaceService';
|
|
51
58
|
import { UpdateService } from './services/UpdateService';
|
|
59
|
+
import {
|
|
60
|
+
startTelemetry,
|
|
61
|
+
stopTelemetry,
|
|
62
|
+
triggerScan,
|
|
63
|
+
getTelemetryStatus,
|
|
64
|
+
} from './services/session-intelligence/UsageTelemetryService';
|
|
52
65
|
|
|
53
66
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
54
67
|
const pkg = JSON.parse(readFileSync(path.join(__dirname, '../../package.json'), 'utf-8'));
|
|
@@ -68,6 +81,7 @@ const HERMIT_HOME = process.env.HERMIT_HOME ?? path.join(os.homedir(), '.hermit'
|
|
|
68
81
|
const HERMIT_CONFIG_FILE = path.join(HERMIT_HOME, 'config.json');
|
|
69
82
|
const HERMIT_APP_CONFIG_FILE = path.join(HERMIT_HOME, 'app-config.json');
|
|
70
83
|
const HERMIT_CC_CONNECT_CONFIG_FILE = path.join(HERMIT_HOME, 'cc-connect', 'config.toml');
|
|
84
|
+
const HERMIT_SETTINGS_FILE = path.join(HERMIT_HOME, 'settings.json');
|
|
71
85
|
|
|
72
86
|
interface HermitConfig {
|
|
73
87
|
ccBaseUrl: string;
|
|
@@ -183,7 +197,42 @@ const bridge = new CcConnectBridge({
|
|
|
183
197
|
bridgeToken: runtimeConfig.ccBridgeToken || runtimeConfig.ccToken,
|
|
184
198
|
});
|
|
185
199
|
const svc = new TeamProvisioningService(cc, bridge);
|
|
186
|
-
const
|
|
200
|
+
const collabBoard = new CollaborationBoardService();
|
|
201
|
+
const taskDispatch = new TaskDispatchService(svc['workspace'], collabBoard);
|
|
202
|
+
|
|
203
|
+
// Broadcast collab board changes via SSE
|
|
204
|
+
taskDispatch.onCollabChange = (dispatchId, status, fromTeam, toTeam) => {
|
|
205
|
+
broadcastSse('collab-change', { dispatchId, status, fromTeam, toTeam });
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
async function readSavedTaskBusConfig(): Promise<TaskBusConfig | null> {
|
|
209
|
+
try {
|
|
210
|
+
const raw = await fs.readFile(HERMIT_SETTINGS_FILE, 'utf-8');
|
|
211
|
+
const settings = JSON.parse(raw) as { taskBus?: TaskBusConfig };
|
|
212
|
+
return settings.taskBus ?? null;
|
|
213
|
+
} catch {
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
async function initializeTaskBusFromSettings(): Promise<void> {
|
|
219
|
+
const config = await readSavedTaskBusConfig();
|
|
220
|
+
if (!config) return;
|
|
221
|
+
|
|
222
|
+
if (config.telemetry?.enabled) {
|
|
223
|
+
await startTelemetry(config).catch((err) => {
|
|
224
|
+
app.log.warn({ err }, 'telemetry startup failed');
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (!config.enabled) {
|
|
229
|
+
taskDispatch.dispose();
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
taskDispatch.dispose();
|
|
234
|
+
await taskDispatch.start(config);
|
|
235
|
+
}
|
|
187
236
|
|
|
188
237
|
function normalizeStringArray(value: unknown): string[] {
|
|
189
238
|
if (!Array.isArray(value)) {
|
|
@@ -194,6 +243,25 @@ function normalizeStringArray(value: unknown): string[] {
|
|
|
194
243
|
.filter((entry) => entry.length > 0);
|
|
195
244
|
}
|
|
196
245
|
|
|
246
|
+
async function resolveTeamSlugForMention(rawName: string): Promise<string | null> {
|
|
247
|
+
const normalized = rawName.trim().replace(/^@/, '');
|
|
248
|
+
if (!normalized) return null;
|
|
249
|
+
try {
|
|
250
|
+
await svc.readTeamManifest(normalized);
|
|
251
|
+
return normalized;
|
|
252
|
+
} catch {
|
|
253
|
+
// Try display name / case-insensitive slug match.
|
|
254
|
+
}
|
|
255
|
+
const lower = normalized.toLowerCase();
|
|
256
|
+
const teams = await svc.listTeams().catch(() => []);
|
|
257
|
+
const matched = teams.find((team) => {
|
|
258
|
+
const slug = team.slug.toLowerCase();
|
|
259
|
+
const displayName = (team.displayName ?? '').toLowerCase();
|
|
260
|
+
return slug === lower || displayName === lower;
|
|
261
|
+
});
|
|
262
|
+
return matched?.slug ?? null;
|
|
263
|
+
}
|
|
264
|
+
|
|
197
265
|
function normalizePlatformAllowFrom(value: unknown): Record<string, string> {
|
|
198
266
|
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
199
267
|
return {};
|
|
@@ -232,19 +300,19 @@ bridge.on('reply', (msg) => {
|
|
|
232
300
|
const sessionKey: string = (msg as { session_key?: string }).session_key ?? '';
|
|
233
301
|
const teamName = resolveTeamFromSessionKey(sessionKey) ?? sessionKey;
|
|
234
302
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
.appendMessage(teamName, {
|
|
303
|
+
void (async () => {
|
|
304
|
+
// 先落盘再广播,否则前端可能在 appendFile 完成前刷新到旧 feed。
|
|
305
|
+
await svc.appendMessage(teamName, {
|
|
238
306
|
from: teamName,
|
|
239
307
|
to: 'user',
|
|
240
308
|
role: 'agent',
|
|
241
309
|
content: (msg as { content?: string }).content ?? '',
|
|
242
310
|
meta: { sessionKey },
|
|
243
|
-
})
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
311
|
+
});
|
|
312
|
+
broadcastSse('team-change', { type: 'inbox', teamName });
|
|
313
|
+
})().catch((err) => {
|
|
314
|
+
app.log.warn({ err, teamName, sessionKey }, 'bridge reply persistence failed');
|
|
315
|
+
});
|
|
248
316
|
});
|
|
249
317
|
|
|
250
318
|
bridge.on('reply_stream', (msg) => {
|
|
@@ -255,18 +323,20 @@ bridge.on('reply_stream', (msg) => {
|
|
|
255
323
|
if (done) {
|
|
256
324
|
// 流式结束,存储完整回复
|
|
257
325
|
const fullText = (msg as { full_text?: string }).full_text ?? '';
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
.appendMessage(teamName, {
|
|
326
|
+
void (async () => {
|
|
327
|
+
if (fullText) {
|
|
328
|
+
await svc.appendMessage(teamName, {
|
|
261
329
|
from: teamName,
|
|
262
330
|
to: 'user',
|
|
263
331
|
role: 'agent',
|
|
264
332
|
content: fullText,
|
|
265
333
|
meta: { sessionKey },
|
|
266
|
-
})
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
broadcastSse('team-change', { type: 'inbox', teamName });
|
|
337
|
+
})().catch((err) => {
|
|
338
|
+
app.log.warn({ err, teamName, sessionKey }, 'bridge stream reply persistence failed');
|
|
339
|
+
});
|
|
270
340
|
} else {
|
|
271
341
|
broadcastSse('team-change', { type: 'lead-message', teamName });
|
|
272
342
|
}
|
|
@@ -1113,6 +1183,7 @@ function toTeamTask(task: {
|
|
|
1113
1183
|
updatedAt: string;
|
|
1114
1184
|
order: number;
|
|
1115
1185
|
teamSlug: string;
|
|
1186
|
+
dispatchMeta?: import('@shared/types/team').DispatchMeta;
|
|
1116
1187
|
}) {
|
|
1117
1188
|
const statusMap: Record<string, string> = {
|
|
1118
1189
|
todo: 'pending',
|
|
@@ -1129,6 +1200,7 @@ function toTeamTask(task: {
|
|
|
1129
1200
|
createdAt: task.createdAt,
|
|
1130
1201
|
updatedAt: task.updatedAt,
|
|
1131
1202
|
result: task.result ?? undefined,
|
|
1203
|
+
dispatchMeta: task.dispatchMeta,
|
|
1132
1204
|
};
|
|
1133
1205
|
}
|
|
1134
1206
|
|
|
@@ -1373,42 +1445,69 @@ app.get('/api/harnesses', async () => {
|
|
|
1373
1445
|
});
|
|
1374
1446
|
|
|
1375
1447
|
// ===========================================================================
|
|
1376
|
-
//
|
|
1377
|
-
// POST /api/teams/:name/launch →
|
|
1448
|
+
// 团队启动 — 直接通过 cc-connect 激活 project/runtime
|
|
1449
|
+
// POST /api/teams/:name/launch → 补建 project(如缺失)并 restart cc-connect
|
|
1378
1450
|
// POST /api/teams/:name/stop → 无需操作(cc-connect 自管理),返回 ok
|
|
1379
1451
|
// ===========================================================================
|
|
1380
1452
|
|
|
1381
|
-
app.post<{ Params: { name: string } }>(
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
let isOnline = false;
|
|
1385
|
-
let projectExists = false;
|
|
1453
|
+
app.post<{ Params: { name: string }; Body: Partial<TeamLaunchRequest> }>(
|
|
1454
|
+
'/api/teams/:name/launch',
|
|
1455
|
+
async (request, reply) => {
|
|
1386
1456
|
try {
|
|
1387
|
-
const
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1457
|
+
const name = request.params.name;
|
|
1458
|
+
const body = request.body ?? {};
|
|
1459
|
+
let manifest: TeamManifest | null = null;
|
|
1460
|
+
try {
|
|
1461
|
+
manifest = await svc.readTeamManifest(name);
|
|
1462
|
+
} catch {
|
|
1463
|
+
// Team may only exist in cc-connect.
|
|
1464
|
+
}
|
|
1465
|
+
const bindProject = manifest?.bindProject ?? name;
|
|
1466
|
+
const workDir = body.cwd ?? manifest?.workDir ?? '';
|
|
1467
|
+
const harness = manifest?.harness ?? 'claudecode';
|
|
1468
|
+
const platformType = manifest?.platform ?? 'bridge';
|
|
1469
|
+
const platformOptions = manifest?.platformOptions ?? {};
|
|
1470
|
+
let isOnline = false;
|
|
1471
|
+
let projectExists = false;
|
|
1472
|
+
try {
|
|
1473
|
+
const p = await cc.getProject(bindProject);
|
|
1474
|
+
projectExists = true;
|
|
1475
|
+
isOnline = Array.isArray(p.platforms) && p.platforms.some((pl) => pl.connected);
|
|
1476
|
+
} catch {
|
|
1477
|
+
/* project 不存在 */
|
|
1478
|
+
}
|
|
1393
1479
|
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1480
|
+
if (!isOnline) {
|
|
1481
|
+
if (!projectExists) {
|
|
1482
|
+
if (!workDir) {
|
|
1483
|
+
return reply.code(400).send({ error: '团队缺少项目路径,无法启动 cc-connect project' });
|
|
1484
|
+
}
|
|
1485
|
+
const result = await cc.createProject(
|
|
1486
|
+
bindProject,
|
|
1487
|
+
harness,
|
|
1488
|
+
workDir,
|
|
1489
|
+
platformType,
|
|
1490
|
+
platformOptions as Record<string, string>
|
|
1491
|
+
);
|
|
1492
|
+
if (result.restart_required) {
|
|
1493
|
+
await cc.restart();
|
|
1494
|
+
}
|
|
1495
|
+
projectExists = true;
|
|
1496
|
+
} else {
|
|
1497
|
+
await cc.restart();
|
|
1498
|
+
}
|
|
1499
|
+
}
|
|
1500
|
+
|
|
1501
|
+
return {
|
|
1502
|
+
runId: `cc-connect:${bindProject}:${Date.now()}`,
|
|
1503
|
+
ok: true,
|
|
1504
|
+
data: { teamName: name, bindProject, projectExists, isOnline: true },
|
|
1505
|
+
};
|
|
1506
|
+
} catch (err) {
|
|
1507
|
+
return reply.code(404).send(reply500(err));
|
|
1508
|
+
}
|
|
1410
1509
|
}
|
|
1411
|
-
|
|
1510
|
+
);
|
|
1412
1511
|
|
|
1413
1512
|
app.post<{ Params: { name: string } }>('/api/teams/:name/stop', async (request) => {
|
|
1414
1513
|
const name = request.params.name;
|
|
@@ -1647,25 +1746,85 @@ const MCP_TOOLS = [
|
|
|
1647
1746
|
},
|
|
1648
1747
|
{
|
|
1649
1748
|
name: 'list_teams',
|
|
1650
|
-
description:
|
|
1749
|
+
description:
|
|
1750
|
+
'只读:列出所有可用团队(本地和远程)及能力信息。跨团队派发由 Hermit 平台根据用户 @团队 自动处理,agent 不应自行派发。',
|
|
1651
1751
|
inputSchema: {
|
|
1652
1752
|
type: 'object',
|
|
1653
1753
|
properties: {},
|
|
1654
1754
|
},
|
|
1655
1755
|
},
|
|
1656
1756
|
{
|
|
1657
|
-
name: '
|
|
1658
|
-
description:
|
|
1757
|
+
name: 'accept_task',
|
|
1758
|
+
description: '接受来自另一个团队的任务请求。在本地创建任务并通知发起方。',
|
|
1759
|
+
inputSchema: {
|
|
1760
|
+
type: 'object',
|
|
1761
|
+
properties: {
|
|
1762
|
+
team_slug: { type: 'string', description: '你的团队 slug(接收方)' },
|
|
1763
|
+
dispatch_id: { type: 'string', description: '任务派发 ID' },
|
|
1764
|
+
},
|
|
1765
|
+
required: ['team_slug', 'dispatch_id'],
|
|
1766
|
+
},
|
|
1767
|
+
},
|
|
1768
|
+
{
|
|
1769
|
+
name: 'reject_task',
|
|
1770
|
+
description: '拒绝来自另一个团队的任务请求。通知发起方并附原因。',
|
|
1771
|
+
inputSchema: {
|
|
1772
|
+
type: 'object',
|
|
1773
|
+
properties: {
|
|
1774
|
+
team_slug: { type: 'string', description: '你的团队 slug(接收方)' },
|
|
1775
|
+
dispatch_id: { type: 'string', description: '任务派发 ID' },
|
|
1776
|
+
reason: { type: 'string', description: '拒绝原因(可选)' },
|
|
1777
|
+
},
|
|
1778
|
+
required: ['team_slug', 'dispatch_id'],
|
|
1779
|
+
},
|
|
1780
|
+
},
|
|
1781
|
+
{
|
|
1782
|
+
name: 'list_pending_requests',
|
|
1783
|
+
description: '列出当前团队待处理的任务请求(尚未接受或拒绝的)。',
|
|
1784
|
+
inputSchema: {
|
|
1785
|
+
type: 'object',
|
|
1786
|
+
properties: {
|
|
1787
|
+
team_slug: { type: 'string', description: '团队 slug' },
|
|
1788
|
+
},
|
|
1789
|
+
required: ['team_slug'],
|
|
1790
|
+
},
|
|
1791
|
+
},
|
|
1792
|
+
{
|
|
1793
|
+
name: 'deliver_task',
|
|
1794
|
+
description: '交付任务结果。完成任务后调用此工具,将结果发送给发起方审核。',
|
|
1795
|
+
inputSchema: {
|
|
1796
|
+
type: 'object',
|
|
1797
|
+
properties: {
|
|
1798
|
+
team_slug: { type: 'string', description: '你的团队 slug(接收方/执行方)' },
|
|
1799
|
+
dispatch_id: { type: 'string', description: '任务派发 ID' },
|
|
1800
|
+
result: { type: 'string', description: '交付结果描述' },
|
|
1801
|
+
},
|
|
1802
|
+
required: ['team_slug', 'dispatch_id', 'result'],
|
|
1803
|
+
},
|
|
1804
|
+
},
|
|
1805
|
+
{
|
|
1806
|
+
name: 'approve_task',
|
|
1807
|
+
description: '审核通过任务交付。发起方对交付结果满意时调用。',
|
|
1659
1808
|
inputSchema: {
|
|
1660
1809
|
type: 'object',
|
|
1661
1810
|
properties: {
|
|
1662
|
-
team_slug: { type: 'string', description: '
|
|
1663
|
-
|
|
1664
|
-
subject: { type: 'string', description: '任务标题' },
|
|
1665
|
-
description: { type: 'string', description: '任务描述(可选)' },
|
|
1666
|
-
prompt: { type: 'string', description: '给目标团队的执行指令(可选)' },
|
|
1811
|
+
team_slug: { type: 'string', description: '你的团队 slug(发起方/审核方)' },
|
|
1812
|
+
dispatch_id: { type: 'string', description: '任务派发 ID' },
|
|
1667
1813
|
},
|
|
1668
|
-
required: ['team_slug', '
|
|
1814
|
+
required: ['team_slug', 'dispatch_id'],
|
|
1815
|
+
},
|
|
1816
|
+
},
|
|
1817
|
+
{
|
|
1818
|
+
name: 'reject_result',
|
|
1819
|
+
description: '退回任务交付结果,要求修改。附上反馈意见。超过 3 次退回需要人工介入。',
|
|
1820
|
+
inputSchema: {
|
|
1821
|
+
type: 'object',
|
|
1822
|
+
properties: {
|
|
1823
|
+
team_slug: { type: 'string', description: '你的团队 slug(发起方/审核方)' },
|
|
1824
|
+
dispatch_id: { type: 'string', description: '任务派发 ID' },
|
|
1825
|
+
feedback: { type: 'string', description: '退回反馈(需要修改的内容)' },
|
|
1826
|
+
},
|
|
1827
|
+
required: ['team_slug', 'dispatch_id', 'feedback'],
|
|
1669
1828
|
},
|
|
1670
1829
|
},
|
|
1671
1830
|
];
|
|
@@ -1697,20 +1856,37 @@ async function executeMcpTool(
|
|
|
1697
1856
|
}
|
|
1698
1857
|
|
|
1699
1858
|
if (toolName === 'list_teams') {
|
|
1700
|
-
const teams = await taskDispatch.
|
|
1859
|
+
const teams = await taskDispatch.discoverTeams();
|
|
1701
1860
|
return text(teams);
|
|
1702
1861
|
}
|
|
1703
1862
|
|
|
1704
|
-
if (toolName === '
|
|
1705
|
-
const result = await taskDispatch.
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1863
|
+
if (toolName === 'accept_task') {
|
|
1864
|
+
const result = await taskDispatch.acceptTask(args.team_slug, args.dispatch_id);
|
|
1865
|
+
return text(result);
|
|
1866
|
+
}
|
|
1867
|
+
|
|
1868
|
+
if (toolName === 'reject_task') {
|
|
1869
|
+
await taskDispatch.rejectTask(args.team_slug, args.dispatch_id, args.reason);
|
|
1870
|
+
return text({ ok: true, message: 'Task rejected' });
|
|
1871
|
+
}
|
|
1872
|
+
|
|
1873
|
+
if (toolName === 'list_pending_requests') {
|
|
1874
|
+
const requests = taskDispatch.listPendingRequests(args.team_slug);
|
|
1875
|
+
return text(requests);
|
|
1876
|
+
}
|
|
1877
|
+
|
|
1878
|
+
if (toolName === 'deliver_task') {
|
|
1879
|
+
const result = await taskDispatch.deliverTask(args.team_slug, args.dispatch_id, args.result);
|
|
1880
|
+
return text(result);
|
|
1881
|
+
}
|
|
1882
|
+
|
|
1883
|
+
if (toolName === 'approve_task') {
|
|
1884
|
+
const result = await taskDispatch.approveTask(args.team_slug, args.dispatch_id);
|
|
1885
|
+
return text(result);
|
|
1886
|
+
}
|
|
1887
|
+
|
|
1888
|
+
if (toolName === 'reject_result') {
|
|
1889
|
+
const result = await taskDispatch.rejectResult(args.team_slug, args.dispatch_id, args.feedback);
|
|
1714
1890
|
return text(result);
|
|
1715
1891
|
}
|
|
1716
1892
|
|
|
@@ -2970,12 +3146,13 @@ app.get<{ Params: { name: string }; Querystring: { cursor?: string; limit?: stri
|
|
|
2970
3146
|
const newestFirstMessages = [...msgs].reverse();
|
|
2971
3147
|
const pageSlice = newestFirstMessages.slice(offset, offset + limit);
|
|
2972
3148
|
const page = pageSlice.map((m) => {
|
|
2973
|
-
const
|
|
3149
|
+
const explicitSessionKey =
|
|
2974
3150
|
typeof m.meta?.sessionKey === 'string'
|
|
2975
3151
|
? m.meta.sessionKey
|
|
2976
3152
|
: typeof m.meta?.session_key === 'string'
|
|
2977
3153
|
? m.meta.session_key
|
|
2978
3154
|
: undefined;
|
|
3155
|
+
const sessionKey = explicitSessionKey ?? buildFallbackSessionKey(name);
|
|
2979
3156
|
const session = sessionKey ? sessionByKey.get(sessionKey) : undefined;
|
|
2980
3157
|
return {
|
|
2981
3158
|
messageId: m.id,
|
|
@@ -2984,7 +3161,18 @@ app.get<{ Params: { name: string }; Querystring: { cursor?: string; limit?: stri
|
|
|
2984
3161
|
text: m.content,
|
|
2985
3162
|
timestamp: m.ts,
|
|
2986
3163
|
read: true,
|
|
2987
|
-
source:
|
|
3164
|
+
source:
|
|
3165
|
+
typeof m.meta?.source === 'string'
|
|
3166
|
+
? m.meta.source
|
|
3167
|
+
: ((m.role === 'user' ? 'user_sent' : 'inbox') as string),
|
|
3168
|
+
taskRefs: Array.isArray(m.meta?.taskRefs) ? m.meta.taskRefs : undefined,
|
|
3169
|
+
summary: typeof m.meta?.summary === 'string' ? m.meta.summary : undefined,
|
|
3170
|
+
conversationId:
|
|
3171
|
+
typeof m.meta?.conversationId === 'string' ? m.meta.conversationId : undefined,
|
|
3172
|
+
replyToConversationId:
|
|
3173
|
+
typeof m.meta?.replyToConversationId === 'string'
|
|
3174
|
+
? m.meta.replyToConversationId
|
|
3175
|
+
: undefined,
|
|
2988
3176
|
session: sessionKey
|
|
2989
3177
|
? {
|
|
2990
3178
|
id: session?.id,
|
|
@@ -3672,32 +3860,77 @@ app.delete<{ Params: { name: string } }>('/api/teams/:name/draft', async () => (
|
|
|
3672
3860
|
// send-message — 从 Hermit 会话面板注入到 harness,不使用 Management /send(那会回发到 IM)。
|
|
3673
3861
|
app.post<{
|
|
3674
3862
|
Params: { name: string };
|
|
3675
|
-
Body: {
|
|
3863
|
+
Body: {
|
|
3864
|
+
member?: string;
|
|
3865
|
+
text?: string;
|
|
3866
|
+
content?: string;
|
|
3867
|
+
summary?: string;
|
|
3868
|
+
sessionKey?: string;
|
|
3869
|
+
messageId?: string;
|
|
3870
|
+
};
|
|
3676
3871
|
}>('/api/teams/:name/send-message', async (request, reply) => {
|
|
3677
3872
|
const teamName = request.params.name;
|
|
3678
3873
|
const text = request.body?.text ?? request.body?.content ?? '';
|
|
3679
3874
|
if (!text.trim()) return { ok: true, messageId: null };
|
|
3680
3875
|
|
|
3681
|
-
const
|
|
3876
|
+
const requestedMessageId =
|
|
3877
|
+
typeof request.body?.messageId === 'string' ? request.body.messageId.trim() : '';
|
|
3878
|
+
const msgId =
|
|
3879
|
+
requestedMessageId || `hermit-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
3880
|
+
|
|
3881
|
+
const crossTeamDirective = text.trim().match(/^@([^\s]+)\s+([\s\S]+)$/);
|
|
3882
|
+
if (crossTeamDirective) {
|
|
3883
|
+
const targetTeam = await resolveTeamSlugForMention(crossTeamDirective[1] ?? '');
|
|
3884
|
+
const subject = crossTeamDirective[2]?.trim();
|
|
3885
|
+
if (targetTeam && subject && targetTeam !== teamName) {
|
|
3886
|
+
try {
|
|
3887
|
+
const sourceMsg = await svc.appendMessage(teamName, {
|
|
3888
|
+
from: 'user',
|
|
3889
|
+
to: targetTeam,
|
|
3890
|
+
role: 'user',
|
|
3891
|
+
content: text,
|
|
3892
|
+
meta: { source: CROSS_TEAM_SENT_SOURCE },
|
|
3893
|
+
});
|
|
3894
|
+
const result = await taskDispatch.dispatchTask(
|
|
3895
|
+
teamName,
|
|
3896
|
+
{
|
|
3897
|
+
subject,
|
|
3898
|
+
description: text,
|
|
3899
|
+
prompt: subject,
|
|
3900
|
+
},
|
|
3901
|
+
targetTeam,
|
|
3902
|
+
{ deadlineMinutes: 10, needsHumanReview: true }
|
|
3903
|
+
);
|
|
3904
|
+
broadcastSse('team-change', { type: 'inbox', teamName });
|
|
3905
|
+
broadcastSse('collab-change', {
|
|
3906
|
+
dispatchId: result.dispatchId,
|
|
3907
|
+
status: result.status,
|
|
3908
|
+
fromTeam: teamName,
|
|
3909
|
+
toTeam: targetTeam,
|
|
3910
|
+
});
|
|
3911
|
+
return {
|
|
3912
|
+
ok: result.status !== 'failed',
|
|
3913
|
+
deliveredToInbox: true,
|
|
3914
|
+
messageId: sourceMsg.id,
|
|
3915
|
+
dispatchId: result.dispatchId,
|
|
3916
|
+
status: result.status,
|
|
3917
|
+
message: result.message,
|
|
3918
|
+
runtimeDelivery: {
|
|
3919
|
+
attempted: true,
|
|
3920
|
+
delivered: result.status !== 'failed',
|
|
3921
|
+
},
|
|
3922
|
+
};
|
|
3923
|
+
} catch (err) {
|
|
3924
|
+
request.log.warn({ err, teamName, targetTeam }, 'cross-team directive dispatch failed');
|
|
3925
|
+
}
|
|
3926
|
+
}
|
|
3927
|
+
}
|
|
3682
3928
|
|
|
3683
|
-
// 使用固定格式 session key,保证 reply 事件能正确映射回 teamName
|
|
3929
|
+
// 使用固定格式 session key,保证 reply 事件能正确映射回 teamName。
|
|
3930
|
+
// UI 消息先落盘并广播,bridge 投递放后台执行,避免 bridge 重连窗口卡住发送按钮。
|
|
3684
3931
|
const requestedSessionKey =
|
|
3685
3932
|
typeof request.body?.sessionKey === 'string' ? request.body.sessionKey.trim() : '';
|
|
3686
|
-
|
|
3687
|
-
|
|
3688
|
-
try {
|
|
3689
|
-
sessionKey = await sendHarnessMessageViaBridge({
|
|
3690
|
-
teamName,
|
|
3691
|
-
text,
|
|
3692
|
-
sessionKey,
|
|
3693
|
-
msgId,
|
|
3694
|
-
});
|
|
3695
|
-
} catch (err) {
|
|
3696
|
-
return reply.code(502).send({
|
|
3697
|
-
ok: false,
|
|
3698
|
-
error: err instanceof Error ? err.message : '发送到 harness 失败',
|
|
3699
|
-
});
|
|
3700
|
-
}
|
|
3933
|
+
const sessionKey = requestedSessionKey || buildFallbackSessionKey(teamName);
|
|
3701
3934
|
|
|
3702
3935
|
// 本地存储用户消息
|
|
3703
3936
|
const userMsg = await svc
|
|
@@ -3713,13 +3946,24 @@ app.post<{
|
|
|
3713
3946
|
// 广播 SSE 让前端触发消息刷新
|
|
3714
3947
|
broadcastSse('team-change', { type: 'inbox', teamName });
|
|
3715
3948
|
|
|
3949
|
+
const bridgeWasConnected = bridge.connected;
|
|
3950
|
+
void sendHarnessMessageViaBridge({
|
|
3951
|
+
teamName,
|
|
3952
|
+
text,
|
|
3953
|
+
sessionKey,
|
|
3954
|
+
msgId,
|
|
3955
|
+
}).catch((err) => {
|
|
3956
|
+
request.log.warn({ err, teamName, sessionKey }, 'send-message bridge delivery failed');
|
|
3957
|
+
broadcastSse('team-change', { type: 'inbox', teamName });
|
|
3958
|
+
});
|
|
3959
|
+
|
|
3716
3960
|
return {
|
|
3717
3961
|
ok: true,
|
|
3718
3962
|
deliveredToInbox: true,
|
|
3719
3963
|
messageId: userMsg?.id ?? msgId,
|
|
3720
3964
|
runtimeDelivery: {
|
|
3721
3965
|
attempted: true,
|
|
3722
|
-
delivered:
|
|
3966
|
+
delivered: bridgeWasConnected,
|
|
3723
3967
|
},
|
|
3724
3968
|
};
|
|
3725
3969
|
});
|
|
@@ -3874,59 +4118,57 @@ app.post('/api/teams/tool-approval/read-file', async () => ({ content: '' }));
|
|
|
3874
4118
|
app.post('/api/teams/validate-cli-args', async () => ({ valid: true, args: [], errors: [] }));
|
|
3875
4119
|
|
|
3876
4120
|
// cross-team task dispatch endpoints
|
|
4121
|
+
// Agent collaboration: accept a task request
|
|
3877
4122
|
app.post<{
|
|
3878
|
-
Body: {
|
|
3879
|
-
|
|
3880
|
-
|
|
3881
|
-
|
|
3882
|
-
|
|
3883
|
-
|
|
3884
|
-
|
|
3885
|
-
|
|
3886
|
-
|
|
3887
|
-
|
|
3888
|
-
|
|
3889
|
-
|
|
3890
|
-
|
|
3891
|
-
|
|
3892
|
-
|
|
3893
|
-
return { ok: true, dispatchId: result.dispatchId, status: result.status };
|
|
4123
|
+
Body: { team_slug: string; dispatch_id: string };
|
|
4124
|
+
}>('/api/cross-team/accept', async (request) => {
|
|
4125
|
+
const { team_slug, dispatch_id } = request.body ?? {};
|
|
4126
|
+
if (!team_slug || !dispatch_id) {
|
|
4127
|
+
return { ok: false, error: 'team_slug and dispatch_id are required' };
|
|
4128
|
+
}
|
|
4129
|
+
try {
|
|
4130
|
+
const result = await taskDispatch.acceptTask(team_slug, dispatch_id);
|
|
4131
|
+
return { ok: true, taskId: result.taskId };
|
|
4132
|
+
} catch (err) {
|
|
4133
|
+
return {
|
|
4134
|
+
ok: false,
|
|
4135
|
+
error: err instanceof Error ? err.message : String(err),
|
|
4136
|
+
};
|
|
4137
|
+
}
|
|
3894
4138
|
});
|
|
3895
4139
|
|
|
3896
|
-
|
|
3897
|
-
|
|
3898
|
-
|
|
3899
|
-
|
|
3900
|
-
|
|
4140
|
+
// Agent collaboration: reject a task request
|
|
4141
|
+
app.post<{
|
|
4142
|
+
Body: { team_slug: string; dispatch_id: string; reason?: string };
|
|
4143
|
+
}>('/api/cross-team/reject', async (request) => {
|
|
4144
|
+
const { team_slug, dispatch_id, reason } = request.body ?? {};
|
|
4145
|
+
if (!team_slug || !dispatch_id) {
|
|
4146
|
+
return { ok: false, error: 'team_slug and dispatch_id are required' };
|
|
4147
|
+
}
|
|
3901
4148
|
try {
|
|
3902
|
-
|
|
3903
|
-
|
|
3904
|
-
|
|
3905
|
-
|
|
3906
|
-
|
|
3907
|
-
|
|
3908
|
-
|
|
3909
|
-
Array.isArray(detail.platforms) && detail.platforms.some((pl: any) => pl.connected);
|
|
3910
|
-
} catch {
|
|
3911
|
-
/* degraded */
|
|
3912
|
-
}
|
|
3913
|
-
return { name: p.name, isAlive };
|
|
3914
|
-
})
|
|
3915
|
-
);
|
|
3916
|
-
aliveSet = new Set(states.filter((s) => s.isAlive).map((s) => s.name));
|
|
3917
|
-
} catch {
|
|
3918
|
-
/* cc-connect unavailable */
|
|
4149
|
+
await taskDispatch.rejectTask(team_slug, dispatch_id, reason);
|
|
4150
|
+
return { ok: true };
|
|
4151
|
+
} catch (err) {
|
|
4152
|
+
return {
|
|
4153
|
+
ok: false,
|
|
4154
|
+
error: err instanceof Error ? err.message : String(err),
|
|
4155
|
+
};
|
|
3919
4156
|
}
|
|
4157
|
+
});
|
|
3920
4158
|
|
|
3921
|
-
|
|
3922
|
-
|
|
3923
|
-
|
|
3924
|
-
|
|
3925
|
-
|
|
3926
|
-
|
|
3927
|
-
|
|
3928
|
-
|
|
3929
|
-
|
|
4159
|
+
app.get<{ Querystring: { excludeTeam?: string } }>('/api/cross-team/targets', async (request) => {
|
|
4160
|
+
const excludeTeam = request.query.excludeTeam;
|
|
4161
|
+
const all = await taskDispatch.discoverTeams();
|
|
4162
|
+
const teams = excludeTeam ? all.filter((t) => t.slug !== excludeTeam) : all;
|
|
4163
|
+
return teams.map((t) => ({
|
|
4164
|
+
teamName: t.slug,
|
|
4165
|
+
displayName: t.displayName || t.slug,
|
|
4166
|
+
description: t.description,
|
|
4167
|
+
color: undefined,
|
|
4168
|
+
isOnline: t.status === 'online',
|
|
4169
|
+
location: t.location,
|
|
4170
|
+
harness: t.harness,
|
|
4171
|
+
}));
|
|
3930
4172
|
});
|
|
3931
4173
|
|
|
3932
4174
|
app.get<{ Params: { name: string } }>('/api/cross-team/outbox/:name', async (request) => {
|
|
@@ -3938,20 +4180,313 @@ app.get<{ Params: { name: string } }>('/api/cross-team/outbox/:name', async (req
|
|
|
3938
4180
|
return { pending };
|
|
3939
4181
|
});
|
|
3940
4182
|
|
|
3941
|
-
//
|
|
4183
|
+
// Agent collaboration: discover teams with capabilities
|
|
4184
|
+
app.get('/api/cross-team/discover', async () => {
|
|
4185
|
+
const teams = await taskDispatch.discoverTeams();
|
|
4186
|
+
return { teams };
|
|
4187
|
+
});
|
|
4188
|
+
|
|
4189
|
+
// Agent collaboration: pending handshake requests for a team
|
|
4190
|
+
app.get<{ Params: { name: string } }>('/api/cross-team/pending-requests/:name', async (request) => {
|
|
4191
|
+
const teamSlug = request.params.name;
|
|
4192
|
+
const requests = taskDispatch.listPendingRequests(teamSlug);
|
|
4193
|
+
return { requests };
|
|
4194
|
+
});
|
|
4195
|
+
|
|
4196
|
+
// Agent collaboration: deliver task result
|
|
4197
|
+
app.post<{
|
|
4198
|
+
Body: { team_slug: string; dispatch_id: string; result: string };
|
|
4199
|
+
}>('/api/cross-team/deliver', async (request) => {
|
|
4200
|
+
const { team_slug, dispatch_id, result } = request.body ?? {};
|
|
4201
|
+
if (!team_slug || !dispatch_id || !result) {
|
|
4202
|
+
return { ok: false, error: 'team_slug, dispatch_id, and result are required' };
|
|
4203
|
+
}
|
|
4204
|
+
try {
|
|
4205
|
+
const res = await taskDispatch.deliverTask(team_slug, dispatch_id, result);
|
|
4206
|
+
return res;
|
|
4207
|
+
} catch (err) {
|
|
4208
|
+
return {
|
|
4209
|
+
ok: false,
|
|
4210
|
+
error: err instanceof Error ? err.message : String(err),
|
|
4211
|
+
};
|
|
4212
|
+
}
|
|
4213
|
+
});
|
|
4214
|
+
|
|
4215
|
+
// Agent collaboration: approve task result
|
|
4216
|
+
app.post<{
|
|
4217
|
+
Body: { team_slug: string; dispatch_id: string };
|
|
4218
|
+
}>('/api/cross-team/approve', async (request) => {
|
|
4219
|
+
const { team_slug, dispatch_id } = request.body ?? {};
|
|
4220
|
+
if (!team_slug || !dispatch_id) {
|
|
4221
|
+
return { ok: false, error: 'team_slug and dispatch_id are required' };
|
|
4222
|
+
}
|
|
4223
|
+
try {
|
|
4224
|
+
const res = await taskDispatch.approveTask(team_slug, dispatch_id);
|
|
4225
|
+
return res;
|
|
4226
|
+
} catch (err) {
|
|
4227
|
+
return {
|
|
4228
|
+
ok: false,
|
|
4229
|
+
error: err instanceof Error ? err.message : String(err),
|
|
4230
|
+
};
|
|
4231
|
+
}
|
|
4232
|
+
});
|
|
4233
|
+
|
|
4234
|
+
// Agent collaboration: reject (request revision) task result
|
|
4235
|
+
app.post<{
|
|
4236
|
+
Body: { team_slug: string; dispatch_id: string; feedback: string };
|
|
4237
|
+
}>('/api/cross-team/revision', async (request) => {
|
|
4238
|
+
const { team_slug, dispatch_id, feedback } = request.body ?? {};
|
|
4239
|
+
if (!team_slug || !dispatch_id || !feedback) {
|
|
4240
|
+
return { ok: false, error: 'team_slug, dispatch_id, and feedback are required' };
|
|
4241
|
+
}
|
|
4242
|
+
try {
|
|
4243
|
+
const res = await taskDispatch.rejectResult(team_slug, dispatch_id, feedback);
|
|
4244
|
+
return res;
|
|
4245
|
+
} catch (err) {
|
|
4246
|
+
return {
|
|
4247
|
+
ok: false,
|
|
4248
|
+
error: err instanceof Error ? err.message : String(err),
|
|
4249
|
+
};
|
|
4250
|
+
}
|
|
4251
|
+
});
|
|
4252
|
+
|
|
4253
|
+
// Collaboration board: list all collab tasks
|
|
4254
|
+
app.get('/api/collab/board', async () => {
|
|
4255
|
+
return { tasks: taskDispatch.getCollabBoard() };
|
|
4256
|
+
});
|
|
4257
|
+
|
|
4258
|
+
// Collaboration board: get single collab task
|
|
4259
|
+
app.get<{ Params: { dispatchId: string } }>('/api/collab/board/:dispatchId', async (request) => {
|
|
4260
|
+
const task = taskDispatch.getCollabTask(request.params.dispatchId);
|
|
4261
|
+
if (!task) return { ok: false, error: 'Not found' };
|
|
4262
|
+
return { task };
|
|
4263
|
+
});
|
|
4264
|
+
|
|
4265
|
+
app.get<{ Params: { dispatchId: string } }>(
|
|
4266
|
+
'/api/collab/board/:dispatchId/events',
|
|
4267
|
+
async (request) => {
|
|
4268
|
+
return { events: taskDispatch.getCollabTaskEvents(request.params.dispatchId) };
|
|
4269
|
+
}
|
|
4270
|
+
);
|
|
4271
|
+
|
|
4272
|
+
// Update /api/cross-team/send to support needsHumanReview
|
|
4273
|
+
app.post<{
|
|
4274
|
+
Body: {
|
|
4275
|
+
fromTeam: string;
|
|
4276
|
+
fromMember?: string;
|
|
4277
|
+
toTeam: string;
|
|
4278
|
+
text?: string;
|
|
4279
|
+
subject?: string;
|
|
4280
|
+
description?: string;
|
|
4281
|
+
prompt?: string;
|
|
4282
|
+
messageId?: string;
|
|
4283
|
+
sessionKey?: string;
|
|
4284
|
+
conversationId?: string;
|
|
4285
|
+
replyToConversationId?: string;
|
|
4286
|
+
taskRefs?: unknown[];
|
|
4287
|
+
actionMode?: string;
|
|
4288
|
+
summary?: string;
|
|
4289
|
+
chainDepth?: number;
|
|
4290
|
+
deadlineMinutes?: number;
|
|
4291
|
+
needsHumanReview?: boolean;
|
|
4292
|
+
};
|
|
4293
|
+
}>('/api/cross-team/send', async (request) => {
|
|
4294
|
+
const {
|
|
4295
|
+
fromTeam,
|
|
4296
|
+
fromMember,
|
|
4297
|
+
toTeam,
|
|
4298
|
+
text,
|
|
4299
|
+
subject,
|
|
4300
|
+
description,
|
|
4301
|
+
prompt,
|
|
4302
|
+
messageId,
|
|
4303
|
+
sessionKey,
|
|
4304
|
+
conversationId,
|
|
4305
|
+
replyToConversationId,
|
|
4306
|
+
taskRefs,
|
|
4307
|
+
actionMode,
|
|
4308
|
+
summary,
|
|
4309
|
+
chainDepth,
|
|
4310
|
+
deadlineMinutes,
|
|
4311
|
+
needsHumanReview,
|
|
4312
|
+
} = request.body ?? {};
|
|
4313
|
+
if (!fromTeam || !toTeam) return { ok: false, error: 'fromTeam and toTeam are required' };
|
|
4314
|
+
const resolvedToTeam = await resolveTeamSlugForMention(toTeam);
|
|
4315
|
+
if (!resolvedToTeam) return { ok: false, error: `Unknown target team: ${toTeam}` };
|
|
4316
|
+
|
|
4317
|
+
if (typeof text === 'string') {
|
|
4318
|
+
const trimmedText = text.trim();
|
|
4319
|
+
if (!trimmedText) return { ok: false, error: 'text is required' };
|
|
4320
|
+
|
|
4321
|
+
const depth = Number.isFinite(Number(chainDepth)) ? Number(chainDepth) : 0;
|
|
4322
|
+
const threadId = conversationId || messageId || `cross-team-${Date.now()}`;
|
|
4323
|
+
const sender = fromMember || 'user';
|
|
4324
|
+
const fromSessionKey =
|
|
4325
|
+
typeof sessionKey === 'string' && sessionKey.trim().length > 0
|
|
4326
|
+
? sessionKey.trim()
|
|
4327
|
+
: buildFallbackSessionKey(fromTeam);
|
|
4328
|
+
const toSessionKey = buildFallbackSessionKey(resolvedToTeam);
|
|
4329
|
+
const sentText = formatCrossTeamText(`${fromTeam}.${sender}`, depth, trimmedText, {
|
|
4330
|
+
conversationId: threadId,
|
|
4331
|
+
replyToConversationId,
|
|
4332
|
+
});
|
|
4333
|
+
const meta = {
|
|
4334
|
+
taskRefs,
|
|
4335
|
+
actionMode,
|
|
4336
|
+
summary,
|
|
4337
|
+
conversationId: threadId,
|
|
4338
|
+
replyToConversationId,
|
|
4339
|
+
chainDepth: depth,
|
|
4340
|
+
};
|
|
4341
|
+
|
|
4342
|
+
const outgoing = await svc.appendMessage(fromTeam, {
|
|
4343
|
+
from: `${fromTeam}.${sender}`,
|
|
4344
|
+
to: resolvedToTeam,
|
|
4345
|
+
role: 'user',
|
|
4346
|
+
content: trimmedText,
|
|
4347
|
+
meta: { ...meta, source: CROSS_TEAM_SENT_SOURCE, sessionKey: fromSessionKey },
|
|
4348
|
+
});
|
|
4349
|
+
|
|
4350
|
+
await svc.appendMessage(resolvedToTeam, {
|
|
4351
|
+
from: `${fromTeam}.${sender}`,
|
|
4352
|
+
to: resolvedToTeam,
|
|
4353
|
+
role: 'user',
|
|
4354
|
+
content: sentText,
|
|
4355
|
+
meta: {
|
|
4356
|
+
...meta,
|
|
4357
|
+
source: CROSS_TEAM_SOURCE,
|
|
4358
|
+
relayOfMessageId: outgoing.id,
|
|
4359
|
+
sessionKey: toSessionKey,
|
|
4360
|
+
},
|
|
4361
|
+
});
|
|
4362
|
+
|
|
4363
|
+
const existingTasks = await svc.readTasks(resolvedToTeam).catch(() => []);
|
|
4364
|
+
const existingTask = existingTasks.find((task) => task.dispatchMeta?.dispatchId === threadId);
|
|
4365
|
+
if (!existingTask) {
|
|
4366
|
+
const now = new Date().toISOString();
|
|
4367
|
+
await svc.createTask(resolvedToTeam, {
|
|
4368
|
+
title: summary || trimmedText.split(/\r?\n/, 1)[0]?.slice(0, 120) || '跨团队 @ 消息',
|
|
4369
|
+
description: trimmedText,
|
|
4370
|
+
status: 'todo',
|
|
4371
|
+
dispatchMeta: {
|
|
4372
|
+
dispatchId: threadId,
|
|
4373
|
+
originTeam: fromTeam,
|
|
4374
|
+
targetTeam: resolvedToTeam,
|
|
4375
|
+
status: 'pending_accept',
|
|
4376
|
+
dispatchedAt: now,
|
|
4377
|
+
receivedAt: now,
|
|
4378
|
+
},
|
|
4379
|
+
});
|
|
4380
|
+
}
|
|
4381
|
+
|
|
4382
|
+
broadcastSse('team-change', { type: 'inbox', teamName: fromTeam });
|
|
4383
|
+
broadcastSse('team-change', { type: 'inbox', teamName: resolvedToTeam });
|
|
4384
|
+
broadcastSse('team-change', { type: 'task', teamName: resolvedToTeam });
|
|
4385
|
+
|
|
4386
|
+
void sendHarnessMessageViaBridge({
|
|
4387
|
+
teamName: resolvedToTeam,
|
|
4388
|
+
text: sentText,
|
|
4389
|
+
}).catch((err) => {
|
|
4390
|
+
request.log.warn({ err }, 'cross-team runtime delivery failed after persistence');
|
|
4391
|
+
});
|
|
4392
|
+
|
|
4393
|
+
return {
|
|
4394
|
+
messageId: outgoing.id,
|
|
4395
|
+
deliveredToInbox: true,
|
|
4396
|
+
deduplicated: false,
|
|
4397
|
+
};
|
|
4398
|
+
}
|
|
4399
|
+
|
|
4400
|
+
if (!subject) return { ok: false, error: 'subject is required' };
|
|
4401
|
+
|
|
4402
|
+
const sentMessage = await svc.appendMessage(fromTeam, {
|
|
4403
|
+
from: fromMember ? `${fromTeam}.${fromMember}` : 'user',
|
|
4404
|
+
to: resolvedToTeam,
|
|
4405
|
+
role: 'user',
|
|
4406
|
+
content: `@${resolvedToTeam} ${subject}`,
|
|
4407
|
+
meta: {
|
|
4408
|
+
source: CROSS_TEAM_SENT_SOURCE,
|
|
4409
|
+
sessionKey,
|
|
4410
|
+
clientMessageId: messageId,
|
|
4411
|
+
},
|
|
4412
|
+
});
|
|
4413
|
+
broadcastSse('team-change', { type: 'inbox', teamName: fromTeam });
|
|
4414
|
+
|
|
4415
|
+
// Check collaboration toggle
|
|
4416
|
+
try {
|
|
4417
|
+
const configPath = path.join(os.homedir(), '.hermit', 'settings.json');
|
|
4418
|
+
const raw = await fs.readFile(configPath, 'utf-8');
|
|
4419
|
+
const settings = JSON.parse(raw);
|
|
4420
|
+
if (!settings.taskBus?.collaboration) {
|
|
4421
|
+
return {
|
|
4422
|
+
ok: false,
|
|
4423
|
+
error: 'Distributed collaboration is not enabled. Enable it in Settings → Task Bus.',
|
|
4424
|
+
};
|
|
4425
|
+
}
|
|
4426
|
+
} catch {
|
|
4427
|
+
return { ok: false, error: 'Could not read task bus configuration.' };
|
|
4428
|
+
}
|
|
4429
|
+
|
|
4430
|
+
const result = await taskDispatch.dispatchTask(
|
|
4431
|
+
fromTeam ?? 'unknown',
|
|
4432
|
+
{ subject, description, prompt },
|
|
4433
|
+
resolvedToTeam,
|
|
4434
|
+
{
|
|
4435
|
+
deadlineMinutes: deadlineMinutes ? Number(deadlineMinutes) : undefined,
|
|
4436
|
+
needsHumanReview,
|
|
4437
|
+
}
|
|
4438
|
+
);
|
|
4439
|
+
const ok = result.status !== 'failed';
|
|
4440
|
+
if (ok) {
|
|
4441
|
+
broadcastSse('team-change', { type: 'inbox', teamName: resolvedToTeam });
|
|
4442
|
+
void sendHarnessMessageViaBridge({
|
|
4443
|
+
teamName: resolvedToTeam,
|
|
4444
|
+
text: `[跨团队任务] ${fromTeam} 派发了任务:${subject}${description ? `\n\n${description}` : ''}`,
|
|
4445
|
+
}).catch((err) => {
|
|
4446
|
+
request.log.warn(
|
|
4447
|
+
{ err, fromTeam, resolvedToTeam },
|
|
4448
|
+
'cross-team task runtime delivery failed'
|
|
4449
|
+
);
|
|
4450
|
+
});
|
|
4451
|
+
}
|
|
4452
|
+
return {
|
|
4453
|
+
ok,
|
|
4454
|
+
messageId: sentMessage.id,
|
|
4455
|
+
dispatchId: result.dispatchId,
|
|
4456
|
+
status: result.status,
|
|
4457
|
+
message: result.message,
|
|
4458
|
+
};
|
|
4459
|
+
});
|
|
4460
|
+
|
|
4461
|
+
// GET /api/settings/task-bus → full config including telemetry
|
|
3942
4462
|
app.get('/api/settings/task-bus', async () => {
|
|
3943
4463
|
const configPath = path.join(os.homedir(), '.hermit', 'settings.json');
|
|
3944
4464
|
try {
|
|
3945
4465
|
const raw = await fs.readFile(configPath, 'utf-8');
|
|
3946
4466
|
const settings = JSON.parse(raw);
|
|
3947
|
-
return
|
|
4467
|
+
return (
|
|
4468
|
+
settings.taskBus ?? {
|
|
4469
|
+
enabled: false,
|
|
4470
|
+
redis: { host: '127.0.0.1', port: 6379 },
|
|
4471
|
+
telemetry: { enabled: false, platform: 'claudecode' },
|
|
4472
|
+
}
|
|
4473
|
+
);
|
|
3948
4474
|
} catch {
|
|
3949
|
-
return {
|
|
4475
|
+
return {
|
|
4476
|
+
enabled: false,
|
|
4477
|
+
redis: { host: '127.0.0.1', port: 6379 },
|
|
4478
|
+
telemetry: { enabled: false, platform: 'claudecode' },
|
|
4479
|
+
};
|
|
3950
4480
|
}
|
|
3951
4481
|
});
|
|
3952
4482
|
|
|
4483
|
+
// PUT /api/settings/task-bus → save config + start/stop telemetry
|
|
3953
4484
|
app.put<{ Body: TaskBusConfig }>('/api/settings/task-bus', async (request) => {
|
|
3954
|
-
const config =
|
|
4485
|
+
const config = (
|
|
4486
|
+
request.body && 'taskBus' in (request.body as unknown as Record<string, unknown>)
|
|
4487
|
+
? (request.body as unknown as { taskBus: TaskBusConfig }).taskBus
|
|
4488
|
+
: request.body
|
|
4489
|
+
) as TaskBusConfig;
|
|
3955
4490
|
const configPath = path.join(os.homedir(), '.hermit', 'settings.json');
|
|
3956
4491
|
let settings: Record<string, unknown> = {};
|
|
3957
4492
|
try {
|
|
@@ -3964,36 +4499,51 @@ app.put<{ Body: TaskBusConfig }>('/api/settings/task-bus', async (request) => {
|
|
|
3964
4499
|
await fs.mkdir(path.dirname(configPath), { recursive: true });
|
|
3965
4500
|
await fs.writeFile(configPath, JSON.stringify(settings, null, 2));
|
|
3966
4501
|
|
|
3967
|
-
//
|
|
3968
|
-
if (config?.enabled) {
|
|
3969
|
-
|
|
3970
|
-
|
|
3971
|
-
|
|
3972
|
-
|
|
3973
|
-
|
|
4502
|
+
// Sync telemetry service
|
|
4503
|
+
if (config.telemetry?.enabled) {
|
|
4504
|
+
await startTelemetry(config);
|
|
4505
|
+
} else {
|
|
4506
|
+
await stopTelemetry();
|
|
4507
|
+
}
|
|
4508
|
+
|
|
4509
|
+
// Keep CLAUDE.md team instructions aligned with the collaboration toggle.
|
|
4510
|
+
const syncTeamInstructions = async (enabled: boolean): Promise<void> => {
|
|
4511
|
+
const projects = await cc.listProjects();
|
|
4512
|
+
for (const p of projects) {
|
|
4513
|
+
let workDir = '';
|
|
4514
|
+
let slug = p.name;
|
|
4515
|
+
try {
|
|
4516
|
+
const meta = await svc.readTeamManifest(p.name);
|
|
4517
|
+
if (typeof meta.workDir === 'string') workDir = meta.workDir.trim();
|
|
4518
|
+
if (meta.slug) slug = meta.slug;
|
|
4519
|
+
} catch {
|
|
4520
|
+
/* no local manifest */
|
|
4521
|
+
}
|
|
4522
|
+
if (!workDir) {
|
|
3974
4523
|
try {
|
|
3975
|
-
const
|
|
3976
|
-
if (typeof
|
|
3977
|
-
if (meta.slug) slug = meta.slug;
|
|
4524
|
+
const detail = await cc.getProject(p.name);
|
|
4525
|
+
if (typeof detail.work_dir === 'string') workDir = detail.work_dir.trim();
|
|
3978
4526
|
} catch {
|
|
3979
|
-
|
|
3980
|
-
}
|
|
3981
|
-
if (!workDir) {
|
|
3982
|
-
try {
|
|
3983
|
-
const detail = await cc.getProject(p.name);
|
|
3984
|
-
if (typeof detail.work_dir === 'string') workDir = detail.work_dir.trim();
|
|
3985
|
-
} catch {
|
|
3986
|
-
// ignore
|
|
3987
|
-
}
|
|
3988
|
-
}
|
|
3989
|
-
if (workDir) {
|
|
3990
|
-
await svc.injectTeamInstructions(workDir, slug);
|
|
4527
|
+
// ignore
|
|
3991
4528
|
}
|
|
3992
4529
|
}
|
|
3993
|
-
|
|
3994
|
-
|
|
4530
|
+
if (!workDir) continue;
|
|
4531
|
+
if (enabled) {
|
|
4532
|
+
await svc.injectTeamInstructions(workDir, slug);
|
|
4533
|
+
} else {
|
|
4534
|
+
await svc.removeTeamInstructions(workDir);
|
|
4535
|
+
}
|
|
3995
4536
|
}
|
|
4537
|
+
};
|
|
4538
|
+
|
|
4539
|
+
const collaborationEnabled = config?.enabled === true && config?.collaboration === true;
|
|
4540
|
+
try {
|
|
4541
|
+
await syncTeamInstructions(collaborationEnabled);
|
|
4542
|
+
} catch (err) {
|
|
4543
|
+
request.log.warn({ err }, 'CLAUDE.md team instruction sync failed');
|
|
4544
|
+
}
|
|
3996
4545
|
|
|
4546
|
+
if (config?.enabled) {
|
|
3997
4547
|
// Reconnect TaskDispatchService with Redis (optional)
|
|
3998
4548
|
taskDispatch.dispose();
|
|
3999
4549
|
try {
|
|
@@ -4016,6 +4566,92 @@ app.put<{ Body: TaskBusConfig }>('/api/settings/task-bus', async (request) => {
|
|
|
4016
4566
|
return { ok: true, connected: false, message: 'Task bus disabled' };
|
|
4017
4567
|
});
|
|
4018
4568
|
|
|
4569
|
+
// POST /api/telemetry/scan → trigger manual scan
|
|
4570
|
+
app.post('/api/telemetry/scan', async (request, reply) => {
|
|
4571
|
+
try {
|
|
4572
|
+
const configPath = path.join(os.homedir(), '.hermit', 'settings.json');
|
|
4573
|
+
let settings: Record<string, unknown> = {};
|
|
4574
|
+
try {
|
|
4575
|
+
const raw = await fs.readFile(configPath, 'utf-8');
|
|
4576
|
+
settings = JSON.parse(raw);
|
|
4577
|
+
} catch {
|
|
4578
|
+
// no settings
|
|
4579
|
+
}
|
|
4580
|
+
const taskBus = (settings.taskBus ?? {}) as TaskBusConfig;
|
|
4581
|
+
if (!taskBus.telemetry?.enabled) {
|
|
4582
|
+
return reply.code(400).send({ error: 'Telemetry is not enabled' });
|
|
4583
|
+
}
|
|
4584
|
+
const result = await triggerScan(taskBus);
|
|
4585
|
+
if (!result) {
|
|
4586
|
+
return reply.code(503).send({ error: 'Telemetry scan failed' });
|
|
4587
|
+
}
|
|
4588
|
+
return {
|
|
4589
|
+
ok: true,
|
|
4590
|
+
connected: taskBus.telemetry.uploadEnabled === true,
|
|
4591
|
+
lastScan: new Date().toISOString(),
|
|
4592
|
+
sessions: result.aggregate.sessions,
|
|
4593
|
+
messages: result.aggregate.messages,
|
|
4594
|
+
tokensIn: result.aggregate.tokens.input,
|
|
4595
|
+
tokensOut: result.aggregate.tokens.output,
|
|
4596
|
+
cacheRead: result.aggregate.tokens.cacheRead,
|
|
4597
|
+
cacheCreation: result.aggregate.tokens.cacheCreation,
|
|
4598
|
+
activeDays: result.aggregate.activeDays,
|
|
4599
|
+
hourly: result.aggregate.hourly,
|
|
4600
|
+
projects: result.aggregate.projects,
|
|
4601
|
+
workSecondsByDay: result.aggregate.workSecondsByDay,
|
|
4602
|
+
};
|
|
4603
|
+
} catch (err) {
|
|
4604
|
+
return reply.code(500).send({ error: String(err) });
|
|
4605
|
+
}
|
|
4606
|
+
});
|
|
4607
|
+
|
|
4608
|
+
// GET /api/telemetry/status → current telemetry status (full stats)
|
|
4609
|
+
app.get('/api/telemetry/status', async (request, reply) => {
|
|
4610
|
+
try {
|
|
4611
|
+
const configPath = path.join(os.homedir(), '.hermit', 'settings.json');
|
|
4612
|
+
let settings: Record<string, unknown> = {};
|
|
4613
|
+
try {
|
|
4614
|
+
const raw = await fs.readFile(configPath, 'utf-8');
|
|
4615
|
+
settings = JSON.parse(raw);
|
|
4616
|
+
} catch {
|
|
4617
|
+
// no settings
|
|
4618
|
+
}
|
|
4619
|
+
const taskBus = (settings.taskBus ?? {}) as TaskBusConfig;
|
|
4620
|
+
const status = await getTelemetryStatus(taskBus.redis);
|
|
4621
|
+
return (
|
|
4622
|
+
status ?? {
|
|
4623
|
+
connected: false,
|
|
4624
|
+
lastScan: null,
|
|
4625
|
+
sessions: 0,
|
|
4626
|
+
messages: 0,
|
|
4627
|
+
tokensIn: 0,
|
|
4628
|
+
tokensOut: 0,
|
|
4629
|
+
cacheRead: 0,
|
|
4630
|
+
cacheCreation: 0,
|
|
4631
|
+
activeDays: 0,
|
|
4632
|
+
hourly: [],
|
|
4633
|
+
projects: [],
|
|
4634
|
+
workSecondsByDay: {},
|
|
4635
|
+
}
|
|
4636
|
+
);
|
|
4637
|
+
} catch {
|
|
4638
|
+
return {
|
|
4639
|
+
connected: false,
|
|
4640
|
+
lastScan: null,
|
|
4641
|
+
sessions: 0,
|
|
4642
|
+
messages: 0,
|
|
4643
|
+
tokensIn: 0,
|
|
4644
|
+
tokensOut: 0,
|
|
4645
|
+
cacheRead: 0,
|
|
4646
|
+
cacheCreation: 0,
|
|
4647
|
+
activeDays: 0,
|
|
4648
|
+
hourly: [],
|
|
4649
|
+
projects: [],
|
|
4650
|
+
workSecondsByDay: {},
|
|
4651
|
+
};
|
|
4652
|
+
}
|
|
4653
|
+
});
|
|
4654
|
+
|
|
4019
4655
|
app.get<{ Params: { name: string; memberName: string } }>(
|
|
4020
4656
|
'/api/teams/:name/review/agent-changes/:memberName',
|
|
4021
4657
|
async (request) => ({
|
|
@@ -4170,6 +4806,7 @@ function reply500(err: unknown) {
|
|
|
4170
4806
|
|
|
4171
4807
|
// 启动 cc-connect Bridge WebSocket 连接(注册 platform=hermit adapter)
|
|
4172
4808
|
bridge.start();
|
|
4809
|
+
await initializeTaskBusFromSettings();
|
|
4173
4810
|
|
|
4174
4811
|
try {
|
|
4175
4812
|
await app.listen({ host: HOST, port: PORT });
|