@yancyyu/openhermit 1.6.28 → 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.
Files changed (96) hide show
  1. package/dist-renderer/assets/ProjectEditorOverlay-CQm6jUR1.js +52 -0
  2. package/dist-renderer/assets/{TeamGraphOverlay-Ba5njic5.js → TeamGraphOverlay-h0WDfifv.js} +1 -1
  3. package/dist-renderer/assets/{_basePickBy-BvnK-OC1.js → _basePickBy-CgG_tjgX.js} +1 -1
  4. package/dist-renderer/assets/{_baseUniq-DmFYXx9G.js → _baseUniq-DwPTU9lP.js} +1 -1
  5. package/dist-renderer/assets/{arc-DX4ZQFY4.js → arc-7nIrGRzY.js} +1 -1
  6. package/dist-renderer/assets/{architectureDiagram-VXUJARFQ-DfYr3vEN.js → architectureDiagram-VXUJARFQ-BYhA6Ev2.js} +1 -1
  7. package/dist-renderer/assets/{blockDiagram-VD42YOAC-DuXdVeWn.js → blockDiagram-VD42YOAC-BVpZUGDg.js} +1 -1
  8. package/dist-renderer/assets/{c4Diagram-YG6GDRKO-Bw2nixXe.js → c4Diagram-YG6GDRKO-DsdreMQ9.js} +1 -1
  9. package/dist-renderer/assets/channel-C0SqeFU7.js +1 -0
  10. package/dist-renderer/assets/{chunk-4BX2VUAB-DLiNGQoE.js → chunk-4BX2VUAB-CcoAs7Jd.js} +1 -1
  11. package/dist-renderer/assets/{chunk-55IACEB6-B1L_8VIF.js → chunk-55IACEB6-CGGAOoXd.js} +1 -1
  12. package/dist-renderer/assets/{chunk-B4BG7PRW-DaZMWKGk.js → chunk-B4BG7PRW-FhpTEPvD.js} +1 -1
  13. package/dist-renderer/assets/{chunk-DI55MBZ5-ku-dflJG.js → chunk-DI55MBZ5-DoYySbm1.js} +1 -1
  14. package/dist-renderer/assets/{chunk-FMBD7UC4-DV-mF1dP.js → chunk-FMBD7UC4-e9l2tGHG.js} +1 -1
  15. package/dist-renderer/assets/{chunk-QN33PNHL-ByGcDFQ0.js → chunk-QN33PNHL-DeiXVTCy.js} +1 -1
  16. package/dist-renderer/assets/{chunk-QZHKN3VN-7dv-Min8.js → chunk-QZHKN3VN-DC2UJLJM.js} +1 -1
  17. package/dist-renderer/assets/{chunk-TZMSLE5B-WdXL5fTu.js → chunk-TZMSLE5B-BHFD9eZI.js} +1 -1
  18. package/dist-renderer/assets/classDiagram-2ON5EDUG-DWew1HpM.js +1 -0
  19. package/dist-renderer/assets/classDiagram-v2-WZHVMYZB-DWew1HpM.js +1 -0
  20. package/dist-renderer/assets/clone-Dm-k63Yr.js +1 -0
  21. package/dist-renderer/assets/{cose-bilkent-S5V4N54A-CNcsvqPl.js → cose-bilkent-S5V4N54A-BdybQraU.js} +1 -1
  22. package/dist-renderer/assets/{dagre-6UL2VRFP-DBNx4qqx.js → dagre-6UL2VRFP-DdF3pwM3.js} +1 -1
  23. package/dist-renderer/assets/{diagram-PSM6KHXK-BfVlT6sT.js → diagram-PSM6KHXK-B9Ldd3nh.js} +1 -1
  24. package/dist-renderer/assets/{diagram-QEK2KX5R-HvVjs0K6.js → diagram-QEK2KX5R-XEqkrbpu.js} +1 -1
  25. package/dist-renderer/assets/{diagram-S2PKOQOG-DYb_KnWS.js → diagram-S2PKOQOG-CipwtY59.js} +1 -1
  26. package/dist-renderer/assets/{erDiagram-Q2GNP2WA-Ba-IgI5G.js → erDiagram-Q2GNP2WA-BB-2ISGo.js} +1 -1
  27. package/dist-renderer/assets/{flowDiagram-NV44I4VS-2iDN8Kpj.js → flowDiagram-NV44I4VS-B8XmJ0u2.js} +1 -1
  28. package/dist-renderer/assets/{ganttDiagram-JELNMOA3-Byjf8Fa3.js → ganttDiagram-JELNMOA3-D-8XglBb.js} +1 -1
  29. package/dist-renderer/assets/{gitGraphDiagram-V2S2FVAM-DbKvfZ_j.js → gitGraphDiagram-V2S2FVAM-DL4ChakD.js} +1 -1
  30. package/dist-renderer/assets/{graph-Enirf-f8.js → graph-BiFNoBjP.js} +1 -1
  31. package/dist-renderer/assets/{index-AjxP_rE_.js → index-6m1ZAymG.js} +1 -1
  32. package/dist-renderer/assets/index-BhellmRb.css +1 -0
  33. package/dist-renderer/assets/{index-DY1zqsb6.js → index-BowUl0Jb.js} +540 -536
  34. package/dist-renderer/assets/{index-CtlzGepK.js → index-Dp3kJTEe.js} +1 -1
  35. package/dist-renderer/assets/{index-COZPUWJW.js → index-TOpt_T7A.js} +1 -1
  36. package/dist-renderer/assets/{index-DdhqolqE.js → index-qNBNjW4K.js} +1 -1
  37. package/dist-renderer/assets/{index-ChR1D6ZF.js → index-vAykq1H1.js} +1 -1
  38. package/dist-renderer/assets/{infoDiagram-HS3SLOUP-D6uicwz1.js → infoDiagram-HS3SLOUP-DRIBfHDi.js} +1 -1
  39. package/dist-renderer/assets/{journeyDiagram-XKPGCS4Q-DqwZsXlQ.js → journeyDiagram-XKPGCS4Q-BOMiigU4.js} +1 -1
  40. package/dist-renderer/assets/{kanban-definition-3W4ZIXB7-fCDVhVUm.js → kanban-definition-3W4ZIXB7-DDxeyjod.js} +1 -1
  41. package/dist-renderer/assets/{layout-CPFgj98r.js → layout-DNANbrI4.js} +1 -1
  42. package/dist-renderer/assets/{linear-CYiQ7Y3M.js → linear-DxEJi1yT.js} +1 -1
  43. package/dist-renderer/assets/{mindmap-definition-VGOIOE7T-D31dS2KE.js → mindmap-definition-VGOIOE7T-nBfGriW8.js} +1 -1
  44. package/dist-renderer/assets/{pieDiagram-ADFJNKIX-BOsCJfds.js → pieDiagram-ADFJNKIX-Din5j6sV.js} +1 -1
  45. package/dist-renderer/assets/{quadrantDiagram-AYHSOK5B-CYTVQCfr.js → quadrantDiagram-AYHSOK5B-DMVK2BEQ.js} +1 -1
  46. package/dist-renderer/assets/{requirementDiagram-UZGBJVZJ-CODCFpkt.js → requirementDiagram-UZGBJVZJ-6SC94Gg_.js} +1 -1
  47. package/dist-renderer/assets/{sankeyDiagram-TZEHDZUN-Z4ce9ZtZ.js → sankeyDiagram-TZEHDZUN-CD2gghhu.js} +1 -1
  48. package/dist-renderer/assets/{sequenceDiagram-WL72ISMW-CmS9TxhW.js → sequenceDiagram-WL72ISMW-BnhkN7nZ.js} +1 -1
  49. package/dist-renderer/assets/{stateDiagram-FKZM4ZOC-o9k-ns3q.js → stateDiagram-FKZM4ZOC-Bn8XdYX-.js} +1 -1
  50. package/dist-renderer/assets/{stateDiagram-v2-4FDKWEC3-CxHMyEt1.js → stateDiagram-v2-4FDKWEC3-1b6sI1_g.js} +1 -1
  51. package/dist-renderer/assets/{timeline-definition-IT6M3QCI-B6T3zrde.js → timeline-definition-IT6M3QCI-CNs3RPoa.js} +1 -1
  52. package/dist-renderer/assets/treemap-GDKQZRPO-DU_yr827.js +162 -0
  53. package/dist-renderer/assets/{xychartDiagram-PRI3JC2R-CleBrdqc.js → xychartDiagram-PRI3JC2R-B8o5J2f3.js} +1 -1
  54. package/dist-renderer/index.html +2 -2
  55. package/package.json +1 -1
  56. package/src/main/server.ts +699 -179
  57. package/src/main/services/session-intelligence/UsageTelemetryService.ts +33 -18
  58. package/src/main/services/teams-mvp/CollaborationBoardService.ts +310 -0
  59. package/src/main/services/teams-mvp/TaskDispatchService.ts +880 -95
  60. package/src/main/services/teams-mvp/TeamProvisioningService.ts +58 -19
  61. package/src/main/services/teams-mvp/TeamWorkspaceService.ts +25 -2
  62. package/src/main/services/teams-mvp/index.ts +3 -0
  63. package/src/renderer/App.tsx +5 -0
  64. package/src/renderer/api/httpClient.ts +67 -0
  65. package/src/renderer/components/layout/PaneContent.tsx +2 -0
  66. package/src/renderer/components/layout/SortableTab.tsx +1 -0
  67. package/src/renderer/components/layout/TabBarActions.tsx +12 -12
  68. package/src/renderer/components/schedules/SchedulesView.tsx +54 -22
  69. package/src/renderer/components/settings/sections/AdvancedSection.tsx +1 -1
  70. package/src/renderer/components/settings/sections/TaskBusSection.tsx +129 -79
  71. package/src/renderer/components/tasks/TasksView.tsx +343 -0
  72. package/src/renderer/components/team/TeamDetailView.tsx +20 -98
  73. package/src/renderer/components/team/dialogs/LaunchTeamDialog.tsx +1 -1
  74. package/src/renderer/components/team/editor/EditorContextMenu.tsx +8 -23
  75. package/src/renderer/components/team/editor/EditorFileTree.tsx +0 -4
  76. package/src/renderer/components/team/editor/EditorSelectionMenu.tsx +1 -8
  77. package/src/renderer/components/team/editor/ProjectEditorOverlay.tsx +0 -10
  78. package/src/renderer/components/team/kanban/KanbanBoard.tsx +5 -1
  79. package/src/renderer/components/team/members/MemberDetailDialog.tsx +8 -33
  80. package/src/renderer/components/team/messages/MessageComposer.tsx +39 -3
  81. package/src/renderer/components/team/messages/MessagesPanel.tsx +72 -2
  82. package/src/renderer/components/team/messages/StatusBlock.tsx +2 -24
  83. package/src/renderer/components/team/schedule/ScheduleEmptyState.tsx +1 -1
  84. package/src/renderer/components/ui/MentionableTextarea.tsx +0 -1
  85. package/src/renderer/store/slices/scheduleSlice.ts +21 -0
  86. package/src/renderer/store/slices/teamSlice.ts +59 -23
  87. package/src/renderer/types/tabs.ts +1 -0
  88. package/src/shared/types/api.ts +29 -0
  89. package/src/shared/types/team.ts +104 -1
  90. package/dist-renderer/assets/ProjectEditorOverlay-A4DZTvSy.js +0 -57
  91. package/dist-renderer/assets/channel-Pre42N5O.js +0 -1
  92. package/dist-renderer/assets/classDiagram-2ON5EDUG-CdJsTJsj.js +0 -1
  93. package/dist-renderer/assets/classDiagram-v2-WZHVMYZB-CdJsTJsj.js +0 -1
  94. package/dist-renderer/assets/clone-BjQBiNfj.js +0 -1
  95. package/dist-renderer/assets/index-BIOJremZ.css +0 -1
  96. package/dist-renderer/assets/treemap-GDKQZRPO-CVd5GNDw.js +0 -162
@@ -43,11 +43,18 @@ 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 type { TaskBusConfig } from '@shared/types/team';
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';
52
59
  import {
53
60
  startTelemetry,
@@ -74,6 +81,7 @@ const HERMIT_HOME = process.env.HERMIT_HOME ?? path.join(os.homedir(), '.hermit'
74
81
  const HERMIT_CONFIG_FILE = path.join(HERMIT_HOME, 'config.json');
75
82
  const HERMIT_APP_CONFIG_FILE = path.join(HERMIT_HOME, 'app-config.json');
76
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');
77
85
 
78
86
  interface HermitConfig {
79
87
  ccBaseUrl: string;
@@ -189,7 +197,42 @@ const bridge = new CcConnectBridge({
189
197
  bridgeToken: runtimeConfig.ccBridgeToken || runtimeConfig.ccToken,
190
198
  });
191
199
  const svc = new TeamProvisioningService(cc, bridge);
192
- const taskDispatch = new TaskDispatchService(svc['workspace']);
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
+ }
193
236
 
194
237
  function normalizeStringArray(value: unknown): string[] {
195
238
  if (!Array.isArray(value)) {
@@ -200,6 +243,25 @@ function normalizeStringArray(value: unknown): string[] {
200
243
  .filter((entry) => entry.length > 0);
201
244
  }
202
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
+
203
265
  function normalizePlatformAllowFrom(value: unknown): Record<string, string> {
204
266
  if (!value || typeof value !== 'object' || Array.isArray(value)) {
205
267
  return {};
@@ -238,19 +300,19 @@ bridge.on('reply', (msg) => {
238
300
  const sessionKey: string = (msg as { session_key?: string }).session_key ?? '';
239
301
  const teamName = resolveTeamFromSessionKey(sessionKey) ?? sessionKey;
240
302
 
241
- // 存储 agent 回复到本地
242
- svc
243
- .appendMessage(teamName, {
303
+ void (async () => {
304
+ // 先落盘再广播,否则前端可能在 appendFile 完成前刷新到旧 feed。
305
+ await svc.appendMessage(teamName, {
244
306
  from: teamName,
245
307
  to: 'user',
246
308
  role: 'agent',
247
309
  content: (msg as { content?: string }).content ?? '',
248
310
  meta: { sessionKey },
249
- })
250
- .catch(() => {});
251
-
252
- // 广播 inbox 事件 前端收到后会调 scheduleTrackedTeamMessageRefresh 重拉消息
253
- broadcastSse('team-change', { type: 'inbox', teamName });
311
+ });
312
+ broadcastSse('team-change', { type: 'inbox', teamName });
313
+ })().catch((err) => {
314
+ app.log.warn({ err, teamName, sessionKey }, 'bridge reply persistence failed');
315
+ });
254
316
  });
255
317
 
256
318
  bridge.on('reply_stream', (msg) => {
@@ -261,18 +323,20 @@ bridge.on('reply_stream', (msg) => {
261
323
  if (done) {
262
324
  // 流式结束,存储完整回复
263
325
  const fullText = (msg as { full_text?: string }).full_text ?? '';
264
- if (fullText) {
265
- svc
266
- .appendMessage(teamName, {
326
+ void (async () => {
327
+ if (fullText) {
328
+ await svc.appendMessage(teamName, {
267
329
  from: teamName,
268
330
  to: 'user',
269
331
  role: 'agent',
270
332
  content: fullText,
271
333
  meta: { sessionKey },
272
- })
273
- .catch(() => {});
274
- }
275
- broadcastSse('team-change', { type: 'inbox', teamName });
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
+ });
276
340
  } else {
277
341
  broadcastSse('team-change', { type: 'lead-message', teamName });
278
342
  }
@@ -1119,6 +1183,7 @@ function toTeamTask(task: {
1119
1183
  updatedAt: string;
1120
1184
  order: number;
1121
1185
  teamSlug: string;
1186
+ dispatchMeta?: import('@shared/types/team').DispatchMeta;
1122
1187
  }) {
1123
1188
  const statusMap: Record<string, string> = {
1124
1189
  todo: 'pending',
@@ -1135,6 +1200,7 @@ function toTeamTask(task: {
1135
1200
  createdAt: task.createdAt,
1136
1201
  updatedAt: task.updatedAt,
1137
1202
  result: task.result ?? undefined,
1203
+ dispatchMeta: task.dispatchMeta,
1138
1204
  };
1139
1205
  }
1140
1206
 
@@ -1379,42 +1445,69 @@ app.get('/api/harnesses', async () => {
1379
1445
  });
1380
1446
 
1381
1447
  // ===========================================================================
1382
- // 团队启动验活直接复用 cc-connect heartbeat / project 状态
1383
- // POST /api/teams/:name/launch → 校验 bindProject 是否存在+在线,返回状态
1448
+ // 团队启动直接通过 cc-connect 激活 project/runtime
1449
+ // POST /api/teams/:name/launch → 补建 project(如缺失)并 restart cc-connect
1384
1450
  // POST /api/teams/:name/stop → 无需操作(cc-connect 自管理),返回 ok
1385
1451
  // ===========================================================================
1386
1452
 
1387
- app.post<{ Params: { name: string } }>('/api/teams/:name/launch', async (request, reply) => {
1388
- try {
1389
- const name = request.params.name;
1390
- let isOnline = false;
1391
- let projectExists = false;
1453
+ app.post<{ Params: { name: string }; Body: Partial<TeamLaunchRequest> }>(
1454
+ '/api/teams/:name/launch',
1455
+ async (request, reply) => {
1392
1456
  try {
1393
- const p = await cc.getProject(name);
1394
- projectExists = true;
1395
- isOnline = Array.isArray(p.platforms) && p.platforms.some((pl) => pl.connected);
1396
- } catch {
1397
- /* project 不存在 */
1398
- }
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
+ }
1399
1479
 
1400
- return {
1401
- ok: true,
1402
- data: {
1403
- teamName: name,
1404
- bindProject: name,
1405
- projectExists,
1406
- isOnline,
1407
- message: projectExists
1408
- ? isOnline
1409
- ? '团队在线'
1410
- : '团队 project 存在但无活跃连接'
1411
- : `cc-connect 中不存在 project "${name}"`,
1412
- },
1413
- };
1414
- } catch (err) {
1415
- return reply.code(404).send(reply500(err));
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
+ }
1416
1509
  }
1417
- });
1510
+ );
1418
1511
 
1419
1512
  app.post<{ Params: { name: string } }>('/api/teams/:name/stop', async (request) => {
1420
1513
  const name = request.params.name;
@@ -1653,25 +1746,85 @@ const MCP_TOOLS = [
1653
1746
  },
1654
1747
  {
1655
1748
  name: 'list_teams',
1656
- description: '列出所有可用的团队(本地和远程)。用于发现可以派发任务的目标团队。',
1749
+ description:
1750
+ '只读:列出所有可用团队(本地和远程)及能力信息。跨团队派发由 Hermit 平台根据用户 @团队 自动处理,agent 不应自行派发。',
1657
1751
  inputSchema: {
1658
1752
  type: 'object',
1659
1753
  properties: {},
1660
1754
  },
1661
1755
  },
1662
1756
  {
1663
- name: 'dispatch_task',
1664
- description: `将任务派发给另一个团队。当前团队通过 team_slug 参数指定。${taskDispatch.dispatchRulesText}`,
1757
+ name: 'accept_task',
1758
+ description: '接受来自另一个团队的任务请求。在本地创建任务并通知发起方。',
1665
1759
  inputSchema: {
1666
1760
  type: 'object',
1667
1761
  properties: {
1668
- team_slug: { type: 'string', description: '当前团队 slug(即发出派发的团队)' },
1669
- target_team: { type: 'string', description: '目标团队 slug(可通过 list_teams 查询)' },
1670
- subject: { type: 'string', description: '任务标题' },
1671
- description: { type: 'string', description: '任务描述(可选)' },
1672
- prompt: { type: 'string', description: '给目标团队的执行指令(可选)' },
1762
+ team_slug: { type: 'string', description: '你的团队 slug(接收方)' },
1763
+ dispatch_id: { type: 'string', description: '任务派发 ID' },
1673
1764
  },
1674
- required: ['team_slug', 'target_team', 'subject'],
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: '审核通过任务交付。发起方对交付结果满意时调用。',
1808
+ inputSchema: {
1809
+ type: 'object',
1810
+ properties: {
1811
+ team_slug: { type: 'string', description: '你的团队 slug(发起方/审核方)' },
1812
+ dispatch_id: { type: 'string', description: '任务派发 ID' },
1813
+ },
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'],
1675
1828
  },
1676
1829
  },
1677
1830
  ];
@@ -1703,20 +1856,37 @@ async function executeMcpTool(
1703
1856
  }
1704
1857
 
1705
1858
  if (toolName === 'list_teams') {
1706
- const teams = await taskDispatch.listTeams();
1859
+ const teams = await taskDispatch.discoverTeams();
1707
1860
  return text(teams);
1708
1861
  }
1709
1862
 
1710
- if (toolName === 'dispatch_task') {
1711
- const result = await taskDispatch.dispatchTask(
1712
- args.team_slug,
1713
- {
1714
- subject: args.subject,
1715
- description: args.description,
1716
- prompt: args.prompt,
1717
- },
1718
- args.target_team
1719
- );
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);
1720
1890
  return text(result);
1721
1891
  }
1722
1892
 
@@ -2976,12 +3146,13 @@ app.get<{ Params: { name: string }; Querystring: { cursor?: string; limit?: stri
2976
3146
  const newestFirstMessages = [...msgs].reverse();
2977
3147
  const pageSlice = newestFirstMessages.slice(offset, offset + limit);
2978
3148
  const page = pageSlice.map((m) => {
2979
- const sessionKey =
3149
+ const explicitSessionKey =
2980
3150
  typeof m.meta?.sessionKey === 'string'
2981
3151
  ? m.meta.sessionKey
2982
3152
  : typeof m.meta?.session_key === 'string'
2983
3153
  ? m.meta.session_key
2984
3154
  : undefined;
3155
+ const sessionKey = explicitSessionKey ?? buildFallbackSessionKey(name);
2985
3156
  const session = sessionKey ? sessionByKey.get(sessionKey) : undefined;
2986
3157
  return {
2987
3158
  messageId: m.id,
@@ -2990,7 +3161,18 @@ app.get<{ Params: { name: string }; Querystring: { cursor?: string; limit?: stri
2990
3161
  text: m.content,
2991
3162
  timestamp: m.ts,
2992
3163
  read: true,
2993
- source: (m.role === 'user' ? 'user_sent' : 'inbox') as string,
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,
2994
3176
  session: sessionKey
2995
3177
  ? {
2996
3178
  id: session?.id,
@@ -3678,32 +3860,77 @@ app.delete<{ Params: { name: string } }>('/api/teams/:name/draft', async () => (
3678
3860
  // send-message — 从 Hermit 会话面板注入到 harness,不使用 Management /send(那会回发到 IM)。
3679
3861
  app.post<{
3680
3862
  Params: { name: string };
3681
- Body: { member?: string; text?: string; content?: string; summary?: string; sessionKey?: string };
3863
+ Body: {
3864
+ member?: string;
3865
+ text?: string;
3866
+ content?: string;
3867
+ summary?: string;
3868
+ sessionKey?: string;
3869
+ messageId?: string;
3870
+ };
3682
3871
  }>('/api/teams/:name/send-message', async (request, reply) => {
3683
3872
  const teamName = request.params.name;
3684
3873
  const text = request.body?.text ?? request.body?.content ?? '';
3685
3874
  if (!text.trim()) return { ok: true, messageId: null };
3686
3875
 
3687
- const msgId = `hermit-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
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)}`;
3688
3880
 
3689
- // 使用固定格式 session key,保证 reply 事件能正确映射回 teamName
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
+ }
3928
+
3929
+ // 使用固定格式 session key,保证 reply 事件能正确映射回 teamName。
3930
+ // UI 消息先落盘并广播,bridge 投递放后台执行,避免 bridge 重连窗口卡住发送按钮。
3690
3931
  const requestedSessionKey =
3691
3932
  typeof request.body?.sessionKey === 'string' ? request.body.sessionKey.trim() : '';
3692
- let sessionKey = requestedSessionKey;
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
- }
3933
+ const sessionKey = requestedSessionKey || buildFallbackSessionKey(teamName);
3707
3934
 
3708
3935
  // 本地存储用户消息
3709
3936
  const userMsg = await svc
@@ -3719,13 +3946,24 @@ app.post<{
3719
3946
  // 广播 SSE 让前端触发消息刷新
3720
3947
  broadcastSse('team-change', { type: 'inbox', teamName });
3721
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
+
3722
3960
  return {
3723
3961
  ok: true,
3724
3962
  deliveredToInbox: true,
3725
3963
  messageId: userMsg?.id ?? msgId,
3726
3964
  runtimeDelivery: {
3727
3965
  attempted: true,
3728
- delivered: true,
3966
+ delivered: bridgeWasConnected,
3729
3967
  },
3730
3968
  };
3731
3969
  });
@@ -3880,59 +4118,57 @@ app.post('/api/teams/tool-approval/read-file', async () => ({ content: '' }));
3880
4118
  app.post('/api/teams/validate-cli-args', async () => ({ valid: true, args: [], errors: [] }));
3881
4119
 
3882
4120
  // cross-team task dispatch endpoints
4121
+ // Agent collaboration: accept a task request
3883
4122
  app.post<{
3884
- Body: {
3885
- fromTeam: string;
3886
- toTeam: string;
3887
- subject: string;
3888
- description?: string;
3889
- prompt?: string;
3890
- };
3891
- }>('/api/cross-team/send', async (request) => {
3892
- const { fromTeam, toTeam, subject, description, prompt } = request.body ?? {};
3893
- if (!toTeam || !subject) return { ok: false, error: 'toTeam and subject are required' };
3894
- const result = await taskDispatch.dispatchTask(
3895
- fromTeam ?? 'unknown',
3896
- { subject, description, prompt },
3897
- toTeam
3898
- );
3899
- 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
+ }
3900
4138
  });
3901
4139
 
3902
- app.get<{ Querystring: { excludeTeam?: string } }>('/api/cross-team/targets', async (request) => {
3903
- const excludeTeam = request.query.excludeTeam;
3904
- // Fetch teams from workspace + alive status from cc-connect
3905
- const allTeams = await svc.listTeams();
3906
- let aliveSet = new Set<string>();
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
+ }
3907
4148
  try {
3908
- const projects = await cc.listProjects();
3909
- const states = await Promise.all(
3910
- projects.map(async (p) => {
3911
- let isAlive = false;
3912
- try {
3913
- const detail = await cc.getProject(p.name);
3914
- isAlive =
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 */
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
+ };
3925
4156
  }
4157
+ });
3926
4158
 
3927
- return allTeams
3928
- .filter((t) => t.slug !== excludeTeam && !t.pendingDelete)
3929
- .map((t) => ({
3930
- teamName: t.slug,
3931
- displayName: t.displayName || t.slug,
3932
- description: t.description,
3933
- color: t.color,
3934
- isOnline: aliveSet.has(t.bindProject),
3935
- }));
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
+ }));
3936
4172
  });
3937
4173
 
3938
4174
  app.get<{ Params: { name: string } }>('/api/cross-team/outbox/:name', async (request) => {
@@ -3944,6 +4180,284 @@ app.get<{ Params: { name: string } }>('/api/cross-team/outbox/:name', async (req
3944
4180
  return { pending };
3945
4181
  });
3946
4182
 
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
+
3947
4461
  // GET /api/settings/task-bus → full config including telemetry
3948
4462
  app.get('/api/settings/task-bus', async () => {
3949
4463
  const configPath = path.join(os.homedir(), '.hermit', 'settings.json');
@@ -3968,7 +4482,11 @@ app.get('/api/settings/task-bus', async () => {
3968
4482
 
3969
4483
  // PUT /api/settings/task-bus → save config + start/stop telemetry
3970
4484
  app.put<{ Body: TaskBusConfig }>('/api/settings/task-bus', async (request) => {
3971
- const config = request.body;
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;
3972
4490
  const configPath = path.join(os.homedir(), '.hermit', 'settings.json');
3973
4491
  let settings: Record<string, unknown> = {};
3974
4492
  try {
@@ -3988,36 +4506,44 @@ app.put<{ Body: TaskBusConfig }>('/api/settings/task-bus', async (request) => {
3988
4506
  await stopTelemetry();
3989
4507
  }
3990
4508
 
3991
- // Auto-inject CLAUDE.md instructions when enabling
3992
- if (config?.enabled) {
3993
- try {
3994
- const projects = await cc.listProjects();
3995
- for (const p of projects) {
3996
- let workDir = '';
3997
- let slug = p.name;
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) {
3998
4523
  try {
3999
- const meta = await svc.readTeamManifest(p.name);
4000
- if (typeof meta.workDir === 'string') workDir = meta.workDir.trim();
4001
- 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();
4002
4526
  } catch {
4003
- /* no local manifest */
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);
4527
+ // ignore
4015
4528
  }
4016
4529
  }
4017
- } catch (err) {
4018
- request.log.warn({ err }, 'CLAUDE.md injection failed');
4530
+ if (!workDir) continue;
4531
+ if (enabled) {
4532
+ await svc.injectTeamInstructions(workDir, slug);
4533
+ } else {
4534
+ await svc.removeTeamInstructions(workDir);
4535
+ }
4019
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
+ }
4020
4545
 
4546
+ if (config?.enabled) {
4021
4547
  // Reconnect TaskDispatchService with Redis (optional)
4022
4548
  taskDispatch.dispose();
4023
4549
  try {
@@ -4057,13 +4583,22 @@ app.post('/api/telemetry/scan', async (request, reply) => {
4057
4583
  }
4058
4584
  const result = await triggerScan(taskBus);
4059
4585
  if (!result) {
4060
- return reply.code(503).send({ error: 'Redis not available' });
4586
+ return reply.code(503).send({ error: 'Telemetry scan failed' });
4061
4587
  }
4062
4588
  return {
4063
4589
  ok: true,
4064
- ...result.aggregate,
4065
- sessions: result.sessions.length,
4590
+ connected: taskBus.telemetry.uploadEnabled === true,
4066
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,
4067
4602
  };
4068
4603
  } catch (err) {
4069
4604
  return reply.code(500).send({ error: String(err) });
@@ -4082,22 +4617,6 @@ app.get('/api/telemetry/status', async (request, reply) => {
4082
4617
  // no settings
4083
4618
  }
4084
4619
  const taskBus = (settings.taskBus ?? {}) as TaskBusConfig;
4085
- if (!taskBus.redis) {
4086
- return {
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
4620
  const status = await getTelemetryStatus(taskBus.redis);
4102
4621
  return (
4103
4622
  status ?? {
@@ -4287,6 +4806,7 @@ function reply500(err: unknown) {
4287
4806
 
4288
4807
  // 启动 cc-connect Bridge WebSocket 连接(注册 platform=hermit adapter)
4289
4808
  bridge.start();
4809
+ await initializeTaskBusFromSettings();
4290
4810
 
4291
4811
  try {
4292
4812
  await app.listen({ host: HOST, port: PORT });