@yancyyu/openhermit 1.5.8 → 1.5.9

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 (105) hide show
  1. package/README.md +10 -1
  2. package/dist-renderer/assets/{ProjectEditorOverlay-BNoDw9T1.js → ProjectEditorOverlay-C5D83Zxv.js} +1 -1
  3. package/dist-renderer/assets/{TeamGraphOverlay-CfGRKQIu.js → TeamGraphOverlay-ajzuM1-u.js} +1 -1
  4. package/dist-renderer/assets/{_basePickBy-Ct8Hm5_h.js → _basePickBy-C9H2zmVj.js} +1 -1
  5. package/dist-renderer/assets/{_baseUniq-BofrAFBx.js → _baseUniq-CpGZGemc.js} +1 -1
  6. package/dist-renderer/assets/{arc-AbJgatzR.js → arc-CbGBDw-m.js} +1 -1
  7. package/dist-renderer/assets/{architectureDiagram-VXUJARFQ-gpniCJVk.js → architectureDiagram-VXUJARFQ-nuKXUIpb.js} +1 -1
  8. package/dist-renderer/assets/{blockDiagram-VD42YOAC-aBbbmONC.js → blockDiagram-VD42YOAC-DHUUE7Jc.js} +1 -1
  9. package/dist-renderer/assets/{c4Diagram-YG6GDRKO-DJio1IsU.js → c4Diagram-YG6GDRKO-OILHhqLM.js} +1 -1
  10. package/dist-renderer/assets/channel-DpUKLLrj.js +1 -0
  11. package/dist-renderer/assets/{chunk-4BX2VUAB-D1_HKao2.js → chunk-4BX2VUAB-dqNpZaQ8.js} +1 -1
  12. package/dist-renderer/assets/{chunk-55IACEB6-NAmVxF4k.js → chunk-55IACEB6-BCoSJQM-.js} +1 -1
  13. package/dist-renderer/assets/{chunk-B4BG7PRW-Ce829laz.js → chunk-B4BG7PRW-BLbg8yVR.js} +1 -1
  14. package/dist-renderer/assets/{chunk-DI55MBZ5-Ct2Le12y.js → chunk-DI55MBZ5-CUUWOs1Q.js} +1 -1
  15. package/dist-renderer/assets/{chunk-FMBD7UC4-Cie3DzKk.js → chunk-FMBD7UC4-D9geTN5P.js} +1 -1
  16. package/dist-renderer/assets/{chunk-QN33PNHL-4f5Yb50e.js → chunk-QN33PNHL-BGT8-BRX.js} +1 -1
  17. package/dist-renderer/assets/{chunk-QZHKN3VN-D9ranl9c.js → chunk-QZHKN3VN-CC8ebGaM.js} +1 -1
  18. package/dist-renderer/assets/{chunk-TZMSLE5B-bdGZWlEy.js → chunk-TZMSLE5B-CajekcT6.js} +1 -1
  19. package/dist-renderer/assets/classDiagram-2ON5EDUG-LL85aSlz.js +1 -0
  20. package/dist-renderer/assets/classDiagram-v2-WZHVMYZB-LL85aSlz.js +1 -0
  21. package/dist-renderer/assets/clone-BHWsFzFA.js +1 -0
  22. package/dist-renderer/assets/{cose-bilkent-S5V4N54A-C6tvfcVi.js → cose-bilkent-S5V4N54A-C_x7hSy3.js} +1 -1
  23. package/dist-renderer/assets/{dagre-6UL2VRFP-B-4qcZam.js → dagre-6UL2VRFP-C4Y1k4DZ.js} +1 -1
  24. package/dist-renderer/assets/{diagram-PSM6KHXK-CwT3TLjx.js → diagram-PSM6KHXK-oRIeULoh.js} +1 -1
  25. package/dist-renderer/assets/{diagram-QEK2KX5R-BWH6-ZFd.js → diagram-QEK2KX5R-DwSqw5HF.js} +1 -1
  26. package/dist-renderer/assets/{diagram-S2PKOQOG-DfpPnfi1.js → diagram-S2PKOQOG-DqjGYje2.js} +1 -1
  27. package/dist-renderer/assets/{erDiagram-Q2GNP2WA-BFbEFR4x.js → erDiagram-Q2GNP2WA-CEV5bBgg.js} +1 -1
  28. package/dist-renderer/assets/{flowDiagram-NV44I4VS-Dg3cf5hW.js → flowDiagram-NV44I4VS-BQQkrRyu.js} +1 -1
  29. package/dist-renderer/assets/{ganttDiagram-JELNMOA3-B21y55W5.js → ganttDiagram-JELNMOA3-CLy4WR1G.js} +1 -1
  30. package/dist-renderer/assets/{gitGraphDiagram-V2S2FVAM-BDV3BJzn.js → gitGraphDiagram-V2S2FVAM-6W3ioQu_.js} +1 -1
  31. package/dist-renderer/assets/{graph-BfaZ4hZt.js → graph-BnLKQvhH.js} +1 -1
  32. package/dist-renderer/assets/{index-CCqtDawH.js → index-B4aiRxoU.js} +1 -1
  33. package/dist-renderer/assets/{index-pMg_LlsS.js → index-B8lKqPVq.js} +1 -1
  34. package/dist-renderer/assets/{index-CZltVMDP.js → index-BRuhNKyU.js} +12 -12
  35. package/dist-renderer/assets/{index-BMXHMpkG.js → index-BufvLVIl.js} +1 -1
  36. package/dist-renderer/assets/{index-Ct0-y9TF.js → index-C1xShqKH.js} +1 -1
  37. package/dist-renderer/assets/{index-CVMSpK8C.js → index-zIOLLI7O.js} +1 -1
  38. package/dist-renderer/assets/{infoDiagram-HS3SLOUP-DvMlS0CL.js → infoDiagram-HS3SLOUP-BoBweEEY.js} +1 -1
  39. package/dist-renderer/assets/{journeyDiagram-XKPGCS4Q-DIyMluRv.js → journeyDiagram-XKPGCS4Q-DLL0V5oP.js} +1 -1
  40. package/dist-renderer/assets/{kanban-definition-3W4ZIXB7-CVOx8f-7.js → kanban-definition-3W4ZIXB7-HsFtEDG3.js} +1 -1
  41. package/dist-renderer/assets/{layout-BPKIXUf4.js → layout-ClIooAAq.js} +1 -1
  42. package/dist-renderer/assets/{linear-CScZGLr2.js → linear-r3RJcj8y.js} +1 -1
  43. package/dist-renderer/assets/{mindmap-definition-VGOIOE7T-CmDQ7Wo6.js → mindmap-definition-VGOIOE7T-BA_P1U4V.js} +1 -1
  44. package/dist-renderer/assets/{pieDiagram-ADFJNKIX-DbVClin-.js → pieDiagram-ADFJNKIX-CzPAfkTB.js} +1 -1
  45. package/dist-renderer/assets/{quadrantDiagram-AYHSOK5B-CAB0MYcW.js → quadrantDiagram-AYHSOK5B-PvdPWzFJ.js} +1 -1
  46. package/dist-renderer/assets/{requirementDiagram-UZGBJVZJ-w2Lfpg3T.js → requirementDiagram-UZGBJVZJ-CHqIL_Od.js} +1 -1
  47. package/dist-renderer/assets/{sankeyDiagram-TZEHDZUN-kvG1QoKY.js → sankeyDiagram-TZEHDZUN-ConzpACM.js} +1 -1
  48. package/dist-renderer/assets/{sequenceDiagram-WL72ISMW-DCVBQ23J.js → sequenceDiagram-WL72ISMW-Zryq4oxP.js} +1 -1
  49. package/dist-renderer/assets/{stateDiagram-FKZM4ZOC-ItZ0JBvq.js → stateDiagram-FKZM4ZOC-BA9V7NHF.js} +1 -1
  50. package/dist-renderer/assets/{stateDiagram-v2-4FDKWEC3-Hpmw4dMm.js → stateDiagram-v2-4FDKWEC3-CGnaujD-.js} +1 -1
  51. package/dist-renderer/assets/{timeline-definition-IT6M3QCI-BzSFaAjV.js → timeline-definition-IT6M3QCI-DPs2ZjMm.js} +1 -1
  52. package/dist-renderer/assets/{treemap-GDKQZRPO-fSz4hQn0.js → treemap-GDKQZRPO-B0lzrLxb.js} +1 -1
  53. package/dist-renderer/assets/{xychartDiagram-PRI3JC2R-CT1kaGlv.js → xychartDiagram-PRI3JC2R-CINGmMxX.js} +1 -1
  54. package/dist-renderer/index.html +1 -1
  55. package/package.json +2 -1
  56. package/src/main/server.ts +993 -764
  57. package/src/main/services/UpdateService.ts +4 -1
  58. package/src/main/services/ccConnect/CcConnectBridge.ts +1 -8
  59. package/src/main/services/ccConnect/CcConnectClient.ts +7 -2
  60. package/src/main/services/teams-mvp/TeamProvisioningService.ts +14 -4
  61. package/src/main/services/teams-mvp/TeamWorkspaceService.ts +11 -6
  62. package/src/renderer/App.tsx +18 -7
  63. package/src/renderer/api/httpClient.ts +136 -42
  64. package/src/renderer/components/chat/ChatHistory.tsx +11 -8
  65. package/src/renderer/components/dashboard/DashboardView.tsx +4 -2
  66. package/src/renderer/components/extensions/ExtensionStoreView.tsx +2 -7
  67. package/src/renderer/components/layout/Sidebar.tsx +3 -1
  68. package/src/renderer/components/schedules/SchedulesView.tsx +15 -13
  69. package/src/renderer/components/settings/SettingsTabs.tsx +2 -1
  70. package/src/renderer/components/settings/hooks/useSettingsHandlers.ts +4 -5
  71. package/src/renderer/components/settings/sections/AdvancedSection.tsx +19 -4
  72. package/src/renderer/components/settings/sections/CliStatusSection.tsx +63 -59
  73. package/src/renderer/components/settings/sections/GeneralSection.tsx +5 -11
  74. package/src/renderer/components/settings/sections/HarnessSection.tsx +30 -15
  75. package/src/renderer/components/settings/sections/PlatformsSection.tsx +110 -51
  76. package/src/renderer/components/sidebar/SidebarSessions.tsx +100 -67
  77. package/src/renderer/components/sidebar/WorkspaceBrowser.tsx +26 -43
  78. package/src/renderer/components/team/CcSessionsSection.tsx +34 -14
  79. package/src/renderer/components/team/TeamDetailView.tsx +150 -148
  80. package/src/renderer/components/team/TeamEmptyState.tsx +27 -16
  81. package/src/renderer/components/team/TeamListView.tsx +4 -2
  82. package/src/renderer/components/team/activity/ActivityItem.tsx +6 -1
  83. package/src/renderer/components/team/dialogs/CreateTeamDialog.tsx +282 -75
  84. package/src/renderer/components/team/dialogs/EditTeamDialog.tsx +2 -1
  85. package/src/renderer/components/team/dialogs/PlatformManualForm.tsx +64 -21
  86. package/src/renderer/components/team/dialogs/PlatformSetupQR.tsx +68 -19
  87. package/src/renderer/components/team/dialogs/ProjectPathSelector.tsx +20 -16
  88. package/src/renderer/components/team/dialogs/platformMeta.ts +66 -11
  89. package/src/renderer/components/team/editor/EditorFileTree.tsx +9 -7
  90. package/src/renderer/components/team/kanban/KanbanBoard.tsx +7 -10
  91. package/src/renderer/components/team/kanban/KanbanTaskCard.tsx +1 -3
  92. package/src/renderer/components/team/members/MemberDetailDialog.tsx +1 -5
  93. package/src/renderer/components/team/messages/MessageComposer.tsx +3 -1
  94. package/src/renderer/components/team/messages/MessagesPanel.tsx +34 -26
  95. package/src/renderer/components/team/schedule/CcCronScheduleDialog.tsx +1 -3
  96. package/src/renderer/components/team/schedule/ScheduleSection.tsx +9 -10
  97. package/src/renderer/store/slices/scheduleSlice.ts +4 -1
  98. package/src/renderer/store/slices/teamSlice.ts +3 -1
  99. package/src/shared/types/api.ts +70 -21
  100. package/src/shared/utils/leadDetection.ts +5 -1
  101. package/tsconfig.json +26 -0
  102. package/dist-renderer/assets/channel-CZ8sd5Xf.js +0 -1
  103. package/dist-renderer/assets/classDiagram-2ON5EDUG-CMcfSKj5.js +0 -1
  104. package/dist-renderer/assets/classDiagram-v2-WZHVMYZB-CMcfSKj5.js +0 -1
  105. package/dist-renderer/assets/clone-CMuwA8RV.js +0 -1
@@ -108,7 +108,10 @@ function loadConfig(): HermitConfig {
108
108
  const tomlBridgeToken = readCcConnectTomlToken('bridge');
109
109
  const defaults: HermitConfig = {
110
110
  ccBaseUrl: process.env.CC_CONNECT_BASE_URL ?? 'http://127.0.0.1:9820',
111
- ccToken: process.env.CC_CONNECT_TOKEN || process.env.CC_CONNECT_MANAGEMENT_TOKEN || tomlManagementToken,
111
+ ccToken:
112
+ process.env.CC_CONNECT_TOKEN ||
113
+ process.env.CC_CONNECT_MANAGEMENT_TOKEN ||
114
+ tomlManagementToken,
112
115
  ccBridgeUrl: process.env.CC_CONNECT_BRIDGE_URL ?? 'ws://127.0.0.1:9810/bridge/ws',
113
116
  ccBridgeToken:
114
117
  process.env.CC_CONNECT_BRIDGE_TOKEN ||
@@ -123,7 +126,9 @@ function loadConfig(): HermitConfig {
123
126
  const raw = JSON.parse(readFileSync(HERMIT_CONFIG_FILE, 'utf-8')) as Partial<HermitConfig>;
124
127
  merged = { ...defaults, ...raw };
125
128
  }
126
- } catch { /* ignore parse errors */ }
129
+ } catch {
130
+ /* ignore parse errors */
131
+ }
127
132
  if (!merged.ccBridgeToken.trim()) {
128
133
  merged = { ...merged, ccBridgeToken: tomlBridgeToken || merged.ccToken };
129
134
  }
@@ -190,10 +195,10 @@ function normalizePlatformAllowFrom(value: unknown): Record<string, string> {
190
195
  return {};
191
196
  }
192
197
  const entries = Object.entries(value as Record<string, unknown>)
193
- .map(([platform, allowFrom]) => [
194
- platform.trim(),
195
- typeof allowFrom === 'string' ? allowFrom.trim() : '',
196
- ] as const)
198
+ .map(
199
+ ([platform, allowFrom]) =>
200
+ [platform.trim(), typeof allowFrom === 'string' ? allowFrom.trim() : ''] as const
201
+ )
197
202
  .filter(([platform, allowFrom]) => platform.length > 0 && allowFrom.length > 0);
198
203
  return Object.fromEntries(entries);
199
204
  }
@@ -224,13 +229,15 @@ bridge.on('reply', (msg) => {
224
229
  const teamName = resolveTeamFromSessionKey(sessionKey) ?? sessionKey;
225
230
 
226
231
  // 存储 agent 回复到本地
227
- svc.appendMessage(teamName, {
228
- from: teamName,
229
- to: 'user',
230
- role: 'agent',
231
- content: (msg as { content?: string }).content ?? '',
232
- meta: { sessionKey },
233
- }).catch(() => {});
232
+ svc
233
+ .appendMessage(teamName, {
234
+ from: teamName,
235
+ to: 'user',
236
+ role: 'agent',
237
+ content: (msg as { content?: string }).content ?? '',
238
+ meta: { sessionKey },
239
+ })
240
+ .catch(() => {});
234
241
 
235
242
  // 广播 inbox 事件 — 前端收到后会调 scheduleTrackedTeamMessageRefresh 重拉消息
236
243
  broadcastSse('team-change', { type: 'inbox', teamName });
@@ -245,13 +252,15 @@ bridge.on('reply_stream', (msg) => {
245
252
  // 流式结束,存储完整回复
246
253
  const fullText = (msg as { full_text?: string }).full_text ?? '';
247
254
  if (fullText) {
248
- svc.appendMessage(teamName, {
249
- from: teamName,
250
- to: 'user',
251
- role: 'agent',
252
- content: fullText,
253
- meta: { sessionKey },
254
- }).catch(() => {});
255
+ svc
256
+ .appendMessage(teamName, {
257
+ from: teamName,
258
+ to: 'user',
259
+ role: 'agent',
260
+ content: fullText,
261
+ meta: { sessionKey },
262
+ })
263
+ .catch(() => {});
255
264
  }
256
265
  broadcastSse('team-change', { type: 'inbox', teamName });
257
266
  } else {
@@ -306,7 +315,11 @@ await app.register(cors, {
306
315
  // /api/v1/* → cc-connect /api/v1/* (兼容旧 renderer 直接打 /api/v1 的代码)
307
316
  // ===========================================================================
308
317
 
309
- async function proxyToCcConnect(request: import('fastify').FastifyRequest, reply: import('fastify').FastifyReply, stripPrefix: string) {
318
+ async function proxyToCcConnect(
319
+ request: import('fastify').FastifyRequest,
320
+ reply: import('fastify').FastifyReply,
321
+ stripPrefix: string
322
+ ) {
310
323
  const baseUrl = runtimeConfig.ccBaseUrl.replace(/\/+$/, '');
311
324
  const token = runtimeConfig.ccToken;
312
325
 
@@ -338,7 +351,10 @@ async function proxyToCcConnect(request: import('fastify').FastifyRequest, reply
338
351
  const body = Buffer.from(await upstream.arrayBuffer());
339
352
  return reply
340
353
  .code(upstream.status)
341
- .header('Content-Type', upstream.headers.get('content-type') ?? 'application/json; charset=utf-8')
354
+ .header(
355
+ 'Content-Type',
356
+ upstream.headers.get('content-type') ?? 'application/json; charset=utf-8'
357
+ )
342
358
  .send(body);
343
359
  }
344
360
 
@@ -500,13 +516,22 @@ function writeCcConnectConfig(updates: Record<string, unknown>): void {
500
516
  // Update [management] section
501
517
  if (updates.management_enabled !== undefined) {
502
518
  const val = updates.management_enabled ? 'true' : 'false';
503
- raw = raw.replace(/(\[management\][^\n]*\n(?:[^\[]*)?)(enabled\s*=\s*)(true|false)/s, (match, prefix, key) => `${prefix}${key}${val}`);
519
+ raw = raw.replace(
520
+ /(\[management\][^\n]*\n(?:[^\[]*)?)(enabled\s*=\s*)(true|false)/s,
521
+ (match, prefix, key) => `${prefix}${key}${val}`
522
+ );
504
523
  }
505
524
  if (updates.management_port !== undefined) {
506
- raw = raw.replace(/(\[management\][^\n]*\n(?:[^\[]*)?)(port\s*=\s*)\d+/s, `$1$2${updates.management_port}`);
525
+ raw = raw.replace(
526
+ /(\[management\][^\n]*\n(?:[^\[]*)?)(port\s*=\s*)\d+/s,
527
+ `$1$2${updates.management_port}`
528
+ );
507
529
  }
508
530
  if (updates.management_token !== undefined) {
509
- raw = raw.replace(/(\[management\][^\n]*\n(?:[^\[]*)?)(token\s*=\s*)"[^"]*"/s, `$1$2"${updates.management_token}"`);
531
+ raw = raw.replace(
532
+ /(\[management\][^\n]*\n(?:[^\[]*)?)(token\s*=\s*)"[^"]*"/s,
533
+ `$1$2"${updates.management_token}"`
534
+ );
510
535
  }
511
536
 
512
537
  // Update [bridge] section
@@ -515,25 +540,40 @@ function writeCcConnectConfig(updates: Record<string, unknown>): void {
515
540
  raw = raw.replace(/(\[bridge\][^\n]*\n(?:[^\[]*)?)(enabled\s*=\s*)(true|false)/s, `$1$2${val}`);
516
541
  }
517
542
  if (updates.bridge_port !== undefined) {
518
- raw = raw.replace(/(\[bridge\][^\n]*\n(?:[^\[]*)?)(port\s*=\s*)\d+/s, `$1$2${updates.bridge_port}`);
543
+ raw = raw.replace(
544
+ /(\[bridge\][^\n]*\n(?:[^\[]*)?)(port\s*=\s*)\d+/s,
545
+ `$1$2${updates.bridge_port}`
546
+ );
519
547
  }
520
548
  if (updates.bridge_token !== undefined) {
521
- raw = raw.replace(/(\[bridge\][^\n]*\n(?:[^\[]*)?)(token\s*=\s*)"[^"]*"/s, `$1$2"${updates.bridge_token}"`);
549
+ raw = raw.replace(
550
+ /(\[bridge\][^\n]*\n(?:[^\[]*)?)(token\s*=\s*)"[^"]*"/s,
551
+ `$1$2"${updates.bridge_token}"`
552
+ );
522
553
  }
523
554
 
524
555
  // Update [log] section
525
556
  if (updates.log_level !== undefined) {
526
- raw = raw.replace(/(\[log\][^\n]*\n(?:[^\[]*)?)(level\s*=\s*)"[^"]*"/s, `$1$2"${updates.log_level}"`);
557
+ raw = raw.replace(
558
+ /(\[log\][^\n]*\n(?:[^\[]*)?)(level\s*=\s*)"[^"]*"/s,
559
+ `$1$2"${updates.log_level}"`
560
+ );
527
561
  }
528
562
 
529
563
  // Update [display] section
530
564
  if (updates.display_thinking !== undefined) {
531
565
  const val = updates.display_thinking ? 'true' : 'false';
532
- raw = raw.replace(/(\[display\][^\n]*\n(?:[^\[]*)?)(thinking_messages\s*=\s*)(true|false)/s, `$1$2${val}`);
566
+ raw = raw.replace(
567
+ /(\[display\][^\n]*\n(?:[^\[]*)?)(thinking_messages\s*=\s*)(true|false)/s,
568
+ `$1$2${val}`
569
+ );
533
570
  }
534
571
  if (updates.display_tool !== undefined) {
535
572
  const val = updates.display_tool ? 'true' : 'false';
536
- raw = raw.replace(/(\[display\][^\n]*\n(?:[^\[]*)?)(tool_messages\s*=\s*)(true|false)/s, `$1$2${val}`);
573
+ raw = raw.replace(
574
+ /(\[display\][^\n]*\n(?:[^\[]*)?)(tool_messages\s*=\s*)(true|false)/s,
575
+ `$1$2${val}`
576
+ );
537
577
  }
538
578
 
539
579
  writeFileSync(configFile, raw, 'utf-8');
@@ -559,7 +599,11 @@ app.post<{ Body: Record<string, unknown> }>('/api/cc-config', async (request, re
559
599
  writeCcConnectConfig(updates);
560
600
 
561
601
  // If management port/token changed, notify user to restart cc-connect
562
- const needsRestart = 'management_port' in updates || 'management_token' in updates || 'bridge_port' in updates || 'bridge_token' in updates;
602
+ const needsRestart =
603
+ 'management_port' in updates ||
604
+ 'management_token' in updates ||
605
+ 'bridge_port' in updates ||
606
+ 'bridge_token' in updates;
563
607
 
564
608
  return {
565
609
  ok: true,
@@ -654,62 +698,68 @@ app.post('/api/cc-reload', async () => {
654
698
  app.get('/api/teams', async () => {
655
699
  try {
656
700
  const projects = await cc.listProjects();
657
- return await Promise.all(projects.map(async (p) => {
658
- // platforms listProjects 返回的是 string[],有 platform 即认为在线
659
- const isOnline = Array.isArray(p.platforms) && p.platforms.length > 0;
660
-
661
- // 尝试从本地元数据读取 displayName 等信息
662
- let displayName = p.name;
663
- let color = 'blue';
664
- let description = `${p.agent_type} · ${p.platforms?.join(', ') ?? ''}`;
665
- let workDir = '';
666
- let pendingDelete = false;
667
- let restartRequired = false;
668
- try {
669
- const meta = await svc.readTeamManifest(p.name);
670
- if (meta.displayName) displayName = meta.displayName;
671
- if (meta.color) color = meta.color;
672
- if (meta.description) description = meta.description;
673
- pendingDelete = meta.pendingDelete === true;
674
- restartRequired = meta.restartRequired === true;
675
- if (typeof meta.workDir === 'string') {
676
- workDir = meta.workDir.trim();
677
- }
678
- } catch { /* no local manifest, use defaults */ }
679
-
680
- // 兼容仅存在于 cc-connect 的团队:回退读取 project 详情拿 work_dir。
681
- if (!workDir) {
701
+ return await Promise.all(
702
+ projects.map(async (p) => {
703
+ // platforms listProjects 返回的是 string[],有 platform 即认为在线
704
+ const isOnline = Array.isArray(p.platforms) && p.platforms.length > 0;
705
+
706
+ // 尝试从本地元数据读取 displayName 等信息
707
+ let displayName = p.name;
708
+ let color = 'blue';
709
+ let description = `${p.agent_type} · ${p.platforms?.join(', ') ?? ''}`;
710
+ let workDir = '';
711
+ let pendingDelete = false;
712
+ let restartRequired = false;
682
713
  try {
683
- const detail = await cc.getProject(p.name);
684
- if (typeof detail.work_dir === 'string') {
685
- workDir = detail.work_dir.trim();
714
+ const meta = await svc.readTeamManifest(p.name);
715
+ if (meta.displayName) displayName = meta.displayName;
716
+ if (meta.color) color = meta.color;
717
+ if (meta.description) description = meta.description;
718
+ pendingDelete = meta.pendingDelete === true;
719
+ restartRequired = meta.restartRequired === true;
720
+ if (typeof meta.workDir === 'string') {
721
+ workDir = meta.workDir.trim();
686
722
  }
687
723
  } catch {
688
- // ignore detail read failure, keep empty path
724
+ /* no local manifest, use defaults */
689
725
  }
690
- }
691
-
692
- return {
693
- teamName: p.name,
694
- displayName,
695
- description,
696
- color,
697
- memberCount: 1,
698
- members: [{ name: p.name, role: 'agent', agentId: p.agent_type, color }],
699
- taskCount: 0,
700
- lastActivity: null,
701
- isAlive: isOnline,
702
- harness: p.agent_type,
703
- bindProject: p.name,
704
- workDir,
705
- projectPath: workDir || undefined,
706
- sessionsCount: p.sessions_count,
707
- heartbeatEnabled: p.heartbeat_enabled,
708
- pendingDelete,
709
- restartRequired,
710
- };
711
- }));
712
- } catch { return []; }
726
+
727
+ // 兼容仅存在于 cc-connect 的团队:回退读取 project 详情拿 work_dir。
728
+ if (!workDir) {
729
+ try {
730
+ const detail = await cc.getProject(p.name);
731
+ if (typeof detail.work_dir === 'string') {
732
+ workDir = detail.work_dir.trim();
733
+ }
734
+ } catch {
735
+ // ignore detail read failure, keep empty path
736
+ }
737
+ }
738
+
739
+ return {
740
+ teamName: p.name,
741
+ displayName,
742
+ description,
743
+ color,
744
+ memberCount: 1,
745
+ members: [{ name: p.name, role: 'agent', agentId: p.agent_type, color }],
746
+ taskCount: 0,
747
+ lastActivity: null,
748
+ isAlive: isOnline,
749
+ harness: p.agent_type,
750
+ bindProject: p.name,
751
+ workDir,
752
+ projectPath: workDir || undefined,
753
+ sessionsCount: p.sessions_count,
754
+ heartbeatEnabled: p.heartbeat_enabled,
755
+ pendingDelete,
756
+ restartRequired,
757
+ };
758
+ })
759
+ );
760
+ } catch {
761
+ return [];
762
+ }
713
763
  });
714
764
 
715
765
  // POST /api/teams/create → 直接在 cc-connect 创建 project
@@ -757,7 +807,7 @@ app.post('/api/teams/create', async (request, reply) => {
757
807
  // GET /api/teams/:name/data → TeamViewSnapshot (cc-connect project 为主,本地 tasks 为辅)
758
808
  app.get<{ Params: { name: string } }>('/api/teams/:name/data', async (request, reply) => {
759
809
  const { name } = request.params;
760
-
810
+
761
811
  // 本地元数据(始终尝试读取)
762
812
  let displayName = name; // 默认使用 team ID
763
813
  let color = 'blue';
@@ -799,7 +849,9 @@ app.get<{ Params: { name: string } }>('/api/teams/:name/data', async (request, r
799
849
  if (meta.platformAllowFrom) {
800
850
  platformAllowFrom = normalizePlatformAllowFrom(meta.platformAllowFrom);
801
851
  }
802
- } catch { /* no local manifest */ }
852
+ } catch {
853
+ /* no local manifest */
854
+ }
803
855
 
804
856
  // 本地任务
805
857
  const rawTasks = activeTasks(await svc.readTasks(name).catch(() => []));
@@ -865,15 +917,17 @@ app.get<{ Params: { name: string } }>('/api/teams/:name/data', async (request, r
865
917
  members: [{ name: displayName, role: 'lead' }],
866
918
  },
867
919
  tasks: teamTasks,
868
- members: [{
869
- name: displayName,
870
- agentId: p.agent_type,
871
- agentType: p.agent_type,
872
- role: 'lead',
873
- color,
874
- currentTaskId: null,
875
- taskCount: teamTasks.length,
876
- }],
920
+ members: [
921
+ {
922
+ name: displayName,
923
+ agentId: p.agent_type,
924
+ agentType: p.agent_type,
925
+ role: 'lead',
926
+ color,
927
+ currentTaskId: null,
928
+ taskCount: teamTasks.length,
929
+ },
930
+ ],
877
931
  kanbanState: { teamName: name, reviewers: [], tasks: {} },
878
932
  processes: [],
879
933
  isAlive: isOnline,
@@ -917,15 +971,17 @@ app.get<{ Params: { name: string } }>('/api/teams/:name/data', async (request, r
917
971
  members: [{ name: displayName, role: 'lead' }],
918
972
  },
919
973
  tasks: teamTasks,
920
- members: [{
921
- name: displayName,
922
- agentId: harness,
923
- agentType: harness,
924
- role: 'lead',
925
- color,
926
- currentTaskId: null,
927
- taskCount: teamTasks.length,
928
- }],
974
+ members: [
975
+ {
976
+ name: displayName,
977
+ agentId: harness,
978
+ agentType: harness,
979
+ role: 'lead',
980
+ color,
981
+ currentTaskId: null,
982
+ taskCount: teamTasks.length,
983
+ },
984
+ ],
929
985
  kanbanState: { teamName: name, reviewers: [], tasks: {} },
930
986
  processes: [],
931
987
  isAlive: false,
@@ -951,20 +1007,22 @@ app.get<{ Params: { name: string } }>('/api/teams/:name/data', async (request, r
951
1007
  });
952
1008
 
953
1009
  // PATCH /api/teams/:name — 更新团队元数据
954
- app.patch<{ Params: { name: string }; Body: { displayName?: string; color?: string; description?: string } }>(
955
- '/api/teams/:name', async (request, reply) => {
956
- try {
957
- const updated = await svc.updateTeam(request.params.name, request.body ?? {});
958
- return { ok: true, data: updated };
959
- } catch (err) {
960
- return reply.code(404).send(reply500(err));
961
- }
1010
+ app.patch<{
1011
+ Params: { name: string };
1012
+ Body: { displayName?: string; color?: string; description?: string };
1013
+ }>('/api/teams/:name', async (request, reply) => {
1014
+ try {
1015
+ const updated = await svc.updateTeam(request.params.name, request.body ?? {});
1016
+ return { ok: true, data: updated };
1017
+ } catch (err) {
1018
+ return reply.code(404).send(reply500(err));
962
1019
  }
963
- );
1020
+ });
964
1021
 
965
1022
  // DELETE /api/teams/:name
966
1023
  app.delete<{ Params: { name: string }; Querystring: { deleteFiles?: string } }>(
967
- '/api/teams/:name', async (request, reply) => {
1024
+ '/api/teams/:name',
1025
+ async (request, reply) => {
968
1026
  const teamName = request.params.name;
969
1027
  try {
970
1028
  try {
@@ -973,7 +1031,7 @@ app.delete<{ Params: { name: string }; Querystring: { deleteFiles?: string } }>(
973
1031
  pendingDelete: true,
974
1032
  restartRequired: result.restart_required === true,
975
1033
  });
976
- } catch (err) {
1034
+ } catch (err) {
977
1035
  request.log.warn({ err, teamName }, 'delete cc-connect project failed or project missing');
978
1036
  }
979
1037
  if (request.query.deleteFiles === 'true') {
@@ -1000,8 +1058,24 @@ function toTaskStatus(s: string): 'todo' | 'doing' | 'done' {
1000
1058
  }
1001
1059
 
1002
1060
  /** internal Task → TeamTask shape (for UI consumption) */
1003
- function toTeamTask(task: { id: string; title?: string; subject?: string; description?: string; status: string; assignee?: string | null; result?: string | null; createdAt: string; updatedAt: string; order: number; teamSlug: string }) {
1004
- const statusMap: Record<string, string> = { todo: 'pending', doing: 'in_progress', done: 'completed' };
1061
+ function toTeamTask(task: {
1062
+ id: string;
1063
+ title?: string;
1064
+ subject?: string;
1065
+ description?: string;
1066
+ status: string;
1067
+ assignee?: string | null;
1068
+ result?: string | null;
1069
+ createdAt: string;
1070
+ updatedAt: string;
1071
+ order: number;
1072
+ teamSlug: string;
1073
+ }) {
1074
+ const statusMap: Record<string, string> = {
1075
+ todo: 'pending',
1076
+ doing: 'in_progress',
1077
+ done: 'completed',
1078
+ };
1005
1079
  return {
1006
1080
  id: task.id,
1007
1081
  displayId: task.id.slice(0, 8),
@@ -1057,11 +1131,14 @@ app.get<{ Params: { name: string } }>('/api/teams/:name/tasks', async (request)
1057
1131
  try {
1058
1132
  const tasks = activeTasks(await svc.readTasks(request.params.name));
1059
1133
  return tasks.map(toTeamTask);
1060
- } catch { return []; }
1134
+ } catch {
1135
+ return [];
1136
+ }
1061
1137
  });
1062
1138
 
1063
1139
  app.post<{ Params: { name: string }; Body: Record<string, unknown> }>(
1064
- '/api/teams/:name/tasks', async (request, reply) => {
1140
+ '/api/teams/:name/tasks',
1141
+ async (request, reply) => {
1065
1142
  const body = request.body ?? {};
1066
1143
  // 支持 subject(TeamTask)或 title(内部)
1067
1144
  const title = (body.subject ?? body.title) as string | undefined;
@@ -1073,16 +1150,17 @@ app.post<{ Params: { name: string }; Body: Record<string, unknown> }>(
1073
1150
  status: body.status ? toTaskStatus(body.status as string) : 'todo',
1074
1151
  });
1075
1152
  if (task.assignee) {
1076
- svc.dispatchTask(request.params.name, task).catch((err) =>
1077
- request.log.warn({ err }, 'dispatchTask failed')
1078
- );
1153
+ svc
1154
+ .dispatchTask(request.params.name, task)
1155
+ .catch((err) => request.log.warn({ err }, 'dispatchTask failed'));
1079
1156
  }
1080
1157
  return toTeamTask(task);
1081
1158
  }
1082
1159
  );
1083
1160
 
1084
1161
  app.patch<{ Params: { name: string; id: string }; Body: Record<string, unknown> }>(
1085
- '/api/teams/:name/tasks/:id', async (request) => {
1162
+ '/api/teams/:name/tasks/:id',
1163
+ async (request) => {
1086
1164
  const body = request.body ?? {};
1087
1165
  const patch: Record<string, unknown> = {};
1088
1166
  if (body.subject !== undefined) patch.title = body.subject;
@@ -1094,22 +1172,23 @@ app.patch<{ Params: { name: string; id: string }; Body: Record<string, unknown>
1094
1172
  if (body.result !== undefined) patch.result = body.result;
1095
1173
  const task = await svc.patchTask(request.params.name, request.params.id, patch);
1096
1174
  if (patch.assignee && task.assignee) {
1097
- svc.dispatchTask(request.params.name, task).catch((err) =>
1098
- request.log.warn({ err }, 'dispatchTask failed')
1099
- );
1175
+ svc
1176
+ .dispatchTask(request.params.name, task)
1177
+ .catch((err) => request.log.warn({ err }, 'dispatchTask failed'));
1100
1178
  }
1101
1179
  return toTeamTask(task);
1102
1180
  }
1103
1181
  );
1104
1182
 
1105
1183
  app.delete<{ Params: { name: string; id: string } }>(
1106
- '/api/teams/:name/tasks/:id', async (request, reply) => {
1184
+ '/api/teams/:name/tasks/:id',
1185
+ async (request, reply) => {
1107
1186
  try {
1108
1187
  await svc.patchTask(request.params.name, request.params.id, {
1109
1188
  status: 'done',
1110
1189
  result: '__deleted__',
1111
1190
  });
1112
- return { ok: true };
1191
+ return { ok: true };
1113
1192
  } catch {
1114
1193
  return reply.code(404).send({ error: 'not found' });
1115
1194
  }
@@ -1121,7 +1200,8 @@ app.delete<{ Params: { name: string; id: string } }>(
1121
1200
  // ===========================================================================
1122
1201
 
1123
1202
  app.patch<{ Params: { name: string }; Body: { collaboration: boolean } }>(
1124
- '/api/teams/:name/collaboration', async (request, reply) => {
1203
+ '/api/teams/:name/collaboration',
1204
+ async (request, reply) => {
1125
1205
  const { collaboration } = request.body ?? {};
1126
1206
  if (typeof collaboration !== 'boolean') {
1127
1207
  return reply.code(400).send({ error: 'collaboration must be boolean' });
@@ -1149,55 +1229,90 @@ app.get<{ Params: { name: string } }>('/api/teams/:name/heartbeat', async (reque
1149
1229
  try {
1150
1230
  const data = await cc.getHeartbeat(request.params.name);
1151
1231
  return { ok: true, data };
1152
- } catch (err) { return reply.code(404).send(reply500(err)); }
1153
- });
1154
-
1155
- app.post<{ Params: { name: string } }>('/api/teams/:name/heartbeat/enable', async (request, reply) => {
1156
- try {
1157
- await cc.resumeHeartbeat(request.params.name);
1158
- return { ok: true };
1159
- } catch (err) { return reply.code(500).send(reply500(err)); }
1232
+ } catch (err) {
1233
+ return reply.code(404).send(reply500(err));
1234
+ }
1160
1235
  });
1161
1236
 
1162
- app.post<{ Params: { name: string } }>('/api/teams/:name/heartbeat/disable', async (request, reply) => {
1163
- try {
1164
- await cc.pauseHeartbeat(request.params.name);
1165
- return { ok: true };
1166
- } catch (err) { return reply.code(500).send(reply500(err)); }
1167
- });
1237
+ app.post<{ Params: { name: string } }>(
1238
+ '/api/teams/:name/heartbeat/enable',
1239
+ async (request, reply) => {
1240
+ try {
1241
+ await cc.resumeHeartbeat(request.params.name);
1242
+ return { ok: true };
1243
+ } catch (err) {
1244
+ return reply.code(500).send(reply500(err));
1245
+ }
1246
+ }
1247
+ );
1168
1248
 
1169
- app.post<{ Params: { name: string } }>('/api/teams/:name/heartbeat/pause', async (request, reply) => {
1170
- try {
1171
- await cc.pauseHeartbeat(request.params.name);
1172
- return { ok: true };
1173
- } catch (err) { return reply.code(500).send(reply500(err)); }
1174
- });
1249
+ app.post<{ Params: { name: string } }>(
1250
+ '/api/teams/:name/heartbeat/disable',
1251
+ async (request, reply) => {
1252
+ try {
1253
+ await cc.pauseHeartbeat(request.params.name);
1254
+ return { ok: true };
1255
+ } catch (err) {
1256
+ return reply.code(500).send(reply500(err));
1257
+ }
1258
+ }
1259
+ );
1175
1260
 
1176
- app.post<{ Params: { name: string } }>('/api/teams/:name/heartbeat/resume', async (request, reply) => {
1177
- try {
1178
- await cc.resumeHeartbeat(request.params.name);
1179
- return { ok: true };
1180
- } catch (err) { return reply.code(500).send(reply500(err)); }
1181
- });
1261
+ app.post<{ Params: { name: string } }>(
1262
+ '/api/teams/:name/heartbeat/pause',
1263
+ async (request, reply) => {
1264
+ try {
1265
+ await cc.pauseHeartbeat(request.params.name);
1266
+ return { ok: true };
1267
+ } catch (err) {
1268
+ return reply.code(500).send(reply500(err));
1269
+ }
1270
+ }
1271
+ );
1182
1272
 
1183
- app.patch<{ Params: { name: string }; Body: { interval_mins?: number; only_when_idle?: boolean; silent?: boolean } }>(
1184
- '/api/teams/:name/heartbeat', async (request, reply) => {
1273
+ app.post<{ Params: { name: string } }>(
1274
+ '/api/teams/:name/heartbeat/resume',
1275
+ async (request, reply) => {
1185
1276
  try {
1186
- await cc.updateProject(request.params.name, request.body as Record<string, unknown>);
1187
- const data = await cc.getHeartbeat(request.params.name);
1188
- return { ok: true, data };
1189
- } catch (err) { return reply.code(500).send(reply500(err)); }
1277
+ await cc.resumeHeartbeat(request.params.name);
1278
+ return { ok: true };
1279
+ } catch (err) {
1280
+ return reply.code(500).send(reply500(err));
1281
+ }
1190
1282
  }
1191
1283
  );
1192
1284
 
1285
+ app.patch<{
1286
+ Params: { name: string };
1287
+ Body: { interval_mins?: number; only_when_idle?: boolean; silent?: boolean };
1288
+ }>('/api/teams/:name/heartbeat', async (request, reply) => {
1289
+ try {
1290
+ await cc.updateProject(request.params.name, request.body as Record<string, unknown>);
1291
+ const data = await cc.getHeartbeat(request.params.name);
1292
+ return { ok: true, data };
1293
+ } catch (err) {
1294
+ return reply.code(500).send(reply500(err));
1295
+ }
1296
+ });
1297
+
1193
1298
  // ===========================================================================
1194
1299
  // Harness 列表 — 从 cc-connect projects 提取已用 agent_type,合并固定枚举
1195
1300
  // GET /api/harnesses
1196
1301
  // ===========================================================================
1197
1302
 
1198
1303
  const CC_AGENT_TYPES = [
1199
- 'claudecode', 'codex', 'cursor', 'gemini', 'iflow',
1200
- 'kimi', 'devin', 'opencode', 'qoder', 'pi', 'acp', 'tmux',
1304
+ 'claudecode',
1305
+ 'codex',
1306
+ 'cursor',
1307
+ 'gemini',
1308
+ 'iflow',
1309
+ 'kimi',
1310
+ 'devin',
1311
+ 'opencode',
1312
+ 'qoder',
1313
+ 'pi',
1314
+ 'acp',
1315
+ 'tmux',
1201
1316
  ] as const;
1202
1317
 
1203
1318
  app.get('/api/harnesses', async () => {
@@ -1229,7 +1344,9 @@ app.post<{ Params: { name: string } }>('/api/teams/:name/launch', async (request
1229
1344
  const p = await cc.getProject(name);
1230
1345
  projectExists = true;
1231
1346
  isOnline = Array.isArray(p.platforms) && p.platforms.some((pl) => pl.connected);
1232
- } catch { /* project 不存在 */ }
1347
+ } catch {
1348
+ /* project 不存在 */
1349
+ }
1233
1350
 
1234
1351
  return {
1235
1352
  ok: true,
@@ -1239,11 +1356,15 @@ app.post<{ Params: { name: string } }>('/api/teams/:name/launch', async (request
1239
1356
  projectExists,
1240
1357
  isOnline,
1241
1358
  message: projectExists
1242
- ? isOnline ? '团队在线' : '团队 project 存在但无活跃连接'
1359
+ ? isOnline
1360
+ ? '团队在线'
1361
+ : '团队 project 存在但无活跃连接'
1243
1362
  : `cc-connect 中不存在 project "${name}"`,
1244
1363
  },
1245
1364
  };
1246
- } catch (err) { return reply.code(404).send(reply500(err)); }
1365
+ } catch (err) {
1366
+ return reply.code(404).send(reply500(err));
1367
+ }
1247
1368
  });
1248
1369
 
1249
1370
  app.post<{ Params: { name: string } }>('/api/teams/:name/stop', async (request) => {
@@ -1263,104 +1384,131 @@ app.post<{ Params: { name: string } }>('/api/teams/:name/stop', async (request)
1263
1384
  // Feishu/Lark setup
1264
1385
  app.post('/api/setup/feishu/begin', async (request, reply) => {
1265
1386
  try {
1266
- const result = await (await fetch(`${runtimeConfig.ccBaseUrl}/api/v1/setup/feishu/begin`, {
1267
- method: 'POST',
1268
- headers: {
1269
- 'Content-Type': 'application/json',
1270
- ...(runtimeConfig.ccToken ? { Authorization: `Bearer ${runtimeConfig.ccToken}` } : {}),
1271
- },
1272
- body: JSON.stringify(request.body ?? {}),
1273
- })).json();
1387
+ const result = await (
1388
+ await fetch(`${runtimeConfig.ccBaseUrl}/api/v1/setup/feishu/begin`, {
1389
+ method: 'POST',
1390
+ headers: {
1391
+ 'Content-Type': 'application/json',
1392
+ ...(runtimeConfig.ccToken ? { Authorization: `Bearer ${runtimeConfig.ccToken}` } : {}),
1393
+ },
1394
+ body: JSON.stringify(request.body ?? {}),
1395
+ })
1396
+ ).json();
1274
1397
  return result;
1275
- } catch (err) { return reply500(err); }
1398
+ } catch (err) {
1399
+ return reply500(err);
1400
+ }
1276
1401
  });
1277
1402
 
1278
1403
  app.post('/api/setup/feishu/poll', async (request, reply) => {
1279
1404
  try {
1280
- const result = await (await fetch(`${runtimeConfig.ccBaseUrl}/api/v1/setup/feishu/poll`, {
1281
- method: 'POST',
1282
- headers: {
1283
- 'Content-Type': 'application/json',
1284
- ...(runtimeConfig.ccToken ? { Authorization: `Bearer ${runtimeConfig.ccToken}` } : {}),
1285
- },
1286
- body: JSON.stringify(request.body ?? {}),
1287
- })).json();
1405
+ const result = await (
1406
+ await fetch(`${runtimeConfig.ccBaseUrl}/api/v1/setup/feishu/poll`, {
1407
+ method: 'POST',
1408
+ headers: {
1409
+ 'Content-Type': 'application/json',
1410
+ ...(runtimeConfig.ccToken ? { Authorization: `Bearer ${runtimeConfig.ccToken}` } : {}),
1411
+ },
1412
+ body: JSON.stringify(request.body ?? {}),
1413
+ })
1414
+ ).json();
1288
1415
  return result;
1289
- } catch (err) { return reply500(err); }
1416
+ } catch (err) {
1417
+ return reply500(err);
1418
+ }
1290
1419
  });
1291
1420
 
1292
1421
  app.post('/api/setup/feishu/save', async (request, reply) => {
1293
1422
  try {
1294
- const result = await (await fetch(`${runtimeConfig.ccBaseUrl}/api/v1/setup/feishu/save`, {
1295
- method: 'POST',
1296
- headers: {
1297
- 'Content-Type': 'application/json',
1298
- ...(runtimeConfig.ccToken ? { Authorization: `Bearer ${runtimeConfig.ccToken}` } : {}),
1299
- },
1300
- body: JSON.stringify(request.body ?? {}),
1301
- })).json();
1423
+ const result = await (
1424
+ await fetch(`${runtimeConfig.ccBaseUrl}/api/v1/setup/feishu/save`, {
1425
+ method: 'POST',
1426
+ headers: {
1427
+ 'Content-Type': 'application/json',
1428
+ ...(runtimeConfig.ccToken ? { Authorization: `Bearer ${runtimeConfig.ccToken}` } : {}),
1429
+ },
1430
+ body: JSON.stringify(request.body ?? {}),
1431
+ })
1432
+ ).json();
1302
1433
  return result;
1303
- } catch (err) { return reply500(err); }
1434
+ } catch (err) {
1435
+ return reply500(err);
1436
+ }
1304
1437
  });
1305
1438
 
1306
1439
  // Weixin setup
1307
1440
  app.post('/api/setup/weixin/begin', async (request, reply) => {
1308
1441
  try {
1309
- const result = await (await fetch(`${runtimeConfig.ccBaseUrl}/api/v1/setup/weixin/begin`, {
1310
- method: 'POST',
1311
- headers: {
1312
- 'Content-Type': 'application/json',
1313
- ...(runtimeConfig.ccToken ? { Authorization: `Bearer ${runtimeConfig.ccToken}` } : {}),
1314
- },
1315
- body: JSON.stringify(request.body ?? {}),
1316
- })).json();
1442
+ const result = await (
1443
+ await fetch(`${runtimeConfig.ccBaseUrl}/api/v1/setup/weixin/begin`, {
1444
+ method: 'POST',
1445
+ headers: {
1446
+ 'Content-Type': 'application/json',
1447
+ ...(runtimeConfig.ccToken ? { Authorization: `Bearer ${runtimeConfig.ccToken}` } : {}),
1448
+ },
1449
+ body: JSON.stringify(request.body ?? {}),
1450
+ })
1451
+ ).json();
1317
1452
  return result;
1318
- } catch (err) { return reply500(err); }
1453
+ } catch (err) {
1454
+ return reply500(err);
1455
+ }
1319
1456
  });
1320
1457
 
1321
1458
  app.post('/api/setup/weixin/poll', async (request, reply) => {
1322
1459
  try {
1323
- const result = await (await fetch(`${runtimeConfig.ccBaseUrl}/api/v1/setup/weixin/poll`, {
1324
- method: 'POST',
1325
- headers: {
1326
- 'Content-Type': 'application/json',
1327
- ...(runtimeConfig.ccToken ? { Authorization: `Bearer ${runtimeConfig.ccToken}` } : {}),
1328
- },
1329
- body: JSON.stringify(request.body ?? {}),
1330
- })).json();
1460
+ const result = await (
1461
+ await fetch(`${runtimeConfig.ccBaseUrl}/api/v1/setup/weixin/poll`, {
1462
+ method: 'POST',
1463
+ headers: {
1464
+ 'Content-Type': 'application/json',
1465
+ ...(runtimeConfig.ccToken ? { Authorization: `Bearer ${runtimeConfig.ccToken}` } : {}),
1466
+ },
1467
+ body: JSON.stringify(request.body ?? {}),
1468
+ })
1469
+ ).json();
1331
1470
  return result;
1332
- } catch (err) { return reply500(err); }
1471
+ } catch (err) {
1472
+ return reply500(err);
1473
+ }
1333
1474
  });
1334
1475
 
1335
1476
  app.post('/api/setup/weixin/save', async (request, reply) => {
1336
1477
  try {
1337
- const result = await (await fetch(`${runtimeConfig.ccBaseUrl}/api/v1/setup/weixin/save`, {
1338
- method: 'POST',
1339
- headers: {
1340
- 'Content-Type': 'application/json',
1341
- ...(runtimeConfig.ccToken ? { Authorization: `Bearer ${runtimeConfig.ccToken}` } : {}),
1342
- },
1343
- body: JSON.stringify(request.body ?? {}),
1344
- })).json();
1478
+ const result = await (
1479
+ await fetch(`${runtimeConfig.ccBaseUrl}/api/v1/setup/weixin/save`, {
1480
+ method: 'POST',
1481
+ headers: {
1482
+ 'Content-Type': 'application/json',
1483
+ ...(runtimeConfig.ccToken ? { Authorization: `Bearer ${runtimeConfig.ccToken}` } : {}),
1484
+ },
1485
+ body: JSON.stringify(request.body ?? {}),
1486
+ })
1487
+ ).json();
1345
1488
  return result;
1346
- } catch (err) { return reply500(err); }
1489
+ } catch (err) {
1490
+ return reply500(err);
1491
+ }
1347
1492
  });
1348
1493
 
1349
1494
  // Generic platform add (manual credential form)
1350
- app.post<{ Params: { name: string }; Body: { type: string; options?: Record<string, unknown>; work_dir?: string; agent_type?: string } }>(
1351
- '/api/projects/:name/add-platform', async (request, reply) => {
1352
- try {
1353
- const result = await cc.createProject(
1354
- request.params.name,
1355
- request.body.agent_type ?? 'claudecode',
1356
- request.body.work_dir ?? '',
1357
- request.body.type,
1358
- (request.body.options ?? {}) as Record<string, string>
1359
- );
1360
- return result;
1361
- } catch (err) { return reply500(err); }
1495
+ app.post<{
1496
+ Params: { name: string };
1497
+ Body: { type: string; options?: Record<string, unknown>; work_dir?: string; agent_type?: string };
1498
+ }>('/api/projects/:name/add-platform', async (request, reply) => {
1499
+ try {
1500
+ const result = await cc.createProject(
1501
+ request.params.name,
1502
+ request.body.agent_type ?? 'claudecode',
1503
+ request.body.work_dir ?? '',
1504
+ request.body.type,
1505
+ (request.body.options ?? {}) as Record<string, string>
1506
+ );
1507
+ return result;
1508
+ } catch (err) {
1509
+ return reply500(err);
1362
1510
  }
1363
- );
1511
+ });
1364
1512
 
1365
1513
  // ===========================================================================
1366
1514
  // 组织图 API — GET /api/graph
@@ -1393,11 +1541,15 @@ app.get('/api/graph', async () => {
1393
1541
  });
1394
1542
  }
1395
1543
  }
1396
- } catch { /* skip */ }
1544
+ } catch {
1545
+ /* skip */
1546
+ }
1397
1547
  }
1398
1548
 
1399
1549
  return { ok: true, data: { nodes, edges } };
1400
- } catch (err) { return reply500(err); }
1550
+ } catch (err) {
1551
+ return reply500(err);
1552
+ }
1401
1553
  });
1402
1554
 
1403
1555
  // ===========================================================================
@@ -1497,7 +1649,9 @@ async function executeMcpTool(
1497
1649
  assignee: args.assignee ?? null,
1498
1650
  });
1499
1651
  if (task.assignee) {
1500
- svc.dispatchTask(args.team_slug, task).catch(() => { /* best-effort */ });
1652
+ svc.dispatchTask(args.team_slug, task).catch(() => {
1653
+ /* best-effort */
1654
+ });
1501
1655
  }
1502
1656
  return text(task);
1503
1657
  }
@@ -1516,12 +1670,14 @@ app.get('/mcp', (request, reply) => {
1516
1670
 
1517
1671
  // MCP initialize 握手
1518
1672
  const endpoint = `http://${request.hostname}/mcp`;
1519
- reply.raw.write(
1520
- `event: endpoint\ndata: ${JSON.stringify({ endpoint })}\n\n`
1521
- );
1673
+ reply.raw.write(`event: endpoint\ndata: ${JSON.stringify({ endpoint })}\n\n`);
1522
1674
 
1523
1675
  const ka = setInterval(() => {
1524
- try { reply.raw.write(': keep-alive\n\n'); } catch { clearInterval(ka); }
1676
+ try {
1677
+ reply.raw.write(': keep-alive\n\n');
1678
+ } catch {
1679
+ clearInterval(ka);
1680
+ }
1525
1681
  }, 15000);
1526
1682
 
1527
1683
  request.raw.on('close', () => clearInterval(ka));
@@ -1529,56 +1685,59 @@ app.get('/mcp', (request, reply) => {
1529
1685
  });
1530
1686
 
1531
1687
  // POST /mcp — JSON-RPC 请求处理
1532
- app.post<{ Body: { jsonrpc?: string; id?: unknown; method?: string; params?: Record<string, unknown> } }>(
1533
- '/mcp',
1534
- async (request, reply) => {
1535
- const { id, method, params = {} } = request.body ?? {};
1688
+ app.post<{
1689
+ Body: { jsonrpc?: string; id?: unknown; method?: string; params?: Record<string, unknown> };
1690
+ }>('/mcp', async (request, reply) => {
1691
+ const { id, method, params = {} } = request.body ?? {};
1692
+
1693
+ // MCP initialize
1694
+ if (method === 'initialize') {
1695
+ return {
1696
+ jsonrpc: '2.0',
1697
+ id,
1698
+ result: {
1699
+ protocolVersion: '2024-11-05',
1700
+ capabilities: { tools: {} },
1701
+ serverInfo: { name: 'hermit-tasks', version: '1.0.0' },
1702
+ },
1703
+ };
1704
+ }
1536
1705
 
1537
- // MCP initialize
1538
- if (method === 'initialize') {
1706
+ // MCP tools/list
1707
+ if (method === 'tools/list') {
1708
+ return { jsonrpc: '2.0', id, result: { tools: MCP_TOOLS } };
1709
+ }
1710
+
1711
+ // MCP tools/call
1712
+ if (method === 'tools/call') {
1713
+ const toolName = params.name as string;
1714
+ const toolArgs = (params.arguments ?? {}) as Record<string, string>;
1715
+ try {
1716
+ const content = await executeMcpTool(toolName, toolArgs);
1717
+ return { jsonrpc: '2.0', id, result: { content } };
1718
+ } catch (err) {
1539
1719
  return {
1540
1720
  jsonrpc: '2.0',
1541
1721
  id,
1542
1722
  result: {
1543
- protocolVersion: '2024-11-05',
1544
- capabilities: { tools: {} },
1545
- serverInfo: { name: 'hermit-tasks', version: '1.0.0' },
1723
+ content: [
1724
+ { type: 'text', text: `Error: ${err instanceof Error ? err.message : String(err)}` },
1725
+ ],
1726
+ isError: true,
1546
1727
  },
1547
1728
  };
1548
1729
  }
1730
+ }
1549
1731
 
1550
- // MCP tools/list
1551
- if (method === 'tools/list') {
1552
- return { jsonrpc: '2.0', id, result: { tools: MCP_TOOLS } };
1553
- }
1554
-
1555
- // MCP tools/call
1556
- if (method === 'tools/call') {
1557
- const toolName = params.name as string;
1558
- const toolArgs = (params.arguments ?? {}) as Record<string, string>;
1559
- try {
1560
- const content = await executeMcpTool(toolName, toolArgs);
1561
- return { jsonrpc: '2.0', id, result: { content } };
1562
- } catch (err) {
1563
- return {
1564
- jsonrpc: '2.0',
1565
- id,
1566
- result: {
1567
- content: [{ type: 'text', text: `Error: ${err instanceof Error ? err.message : String(err)}` }],
1568
- isError: true,
1569
- },
1570
- };
1571
- }
1572
- }
1573
-
1574
- // notifications/initialized — 无需响应
1575
- if (method === 'notifications/initialized') {
1576
- return reply.code(204).send();
1577
- }
1578
-
1579
- return reply.code(400).send({ jsonrpc: '2.0', id, error: { code: -32601, message: 'Method not found' } });
1732
+ // notifications/initialized — 无需响应
1733
+ if (method === 'notifications/initialized') {
1734
+ return reply.code(204).send();
1580
1735
  }
1581
- );
1736
+
1737
+ return reply
1738
+ .code(400)
1739
+ .send({ jsonrpc: '2.0', id, error: { code: -32601, message: 'Method not found' } });
1740
+ });
1582
1741
 
1583
1742
  // ===========================================================================
1584
1743
  // Hermit 主仓 UI 首屏强依赖的几个 stub(占位实现)
@@ -1646,7 +1805,7 @@ const DEFAULT_APP_CONFIG = {
1646
1805
  enabled: true,
1647
1806
  soundEnabled: true,
1648
1807
  ignoredRegex: [] as string[],
1649
- ignoredRepositories: [] as string[],
1808
+ ignoredRepositories: [] as string[],
1650
1809
  snoozedUntil: null as number | null,
1651
1810
  snoozeMinutes: 30,
1652
1811
  includeSubagentErrors: false,
@@ -1758,7 +1917,7 @@ app.post<{ Body: { section?: unknown; data?: unknown } }>('/api/config/update',
1758
1917
  })
1759
1918
  : current;
1760
1919
  return {
1761
- success: true,
1920
+ success: true,
1762
1921
  data: writeAppConfig(next),
1763
1922
  };
1764
1923
  });
@@ -1776,7 +1935,15 @@ type InMemoryScheduleRun = {
1776
1935
  id: string;
1777
1936
  scheduleId: string;
1778
1937
  teamName: string;
1779
- status: 'pending' | 'warming_up' | 'warm' | 'running' | 'completed' | 'failed' | 'failed_interrupted' | 'cancelled';
1938
+ status:
1939
+ | 'pending'
1940
+ | 'warming_up'
1941
+ | 'warm'
1942
+ | 'running'
1943
+ | 'completed'
1944
+ | 'failed'
1945
+ | 'failed_interrupted'
1946
+ | 'cancelled';
1780
1947
  scheduledFor: string;
1781
1948
  startedAt: string;
1782
1949
  warmUpCompletedAt?: string;
@@ -1810,7 +1977,9 @@ function buildFallbackSessionKey(teamName: string): string {
1810
1977
  return `hermit:${teamName}:session`;
1811
1978
  }
1812
1979
 
1813
- async function waitForHarnessBridgeConnected(timeoutMs = HARNESS_BRIDGE_CONNECT_TIMEOUT_MS): Promise<void> {
1980
+ async function waitForHarnessBridgeConnected(
1981
+ timeoutMs = HARNESS_BRIDGE_CONNECT_TIMEOUT_MS
1982
+ ): Promise<void> {
1814
1983
  if (bridge.connected) return;
1815
1984
  bridge.start();
1816
1985
  if (bridge.connected) return;
@@ -1859,30 +2028,32 @@ async function resolveTeamWorkDirs(teamNames: string[]): Promise<Map<string, str
1859
2028
  const uniqueTeamNames = [...new Set(teamNames.filter((name) => name.trim().length > 0))];
1860
2029
  const results = new Map<string, string>();
1861
2030
 
1862
- await Promise.all(uniqueTeamNames.map(async (teamName) => {
1863
- let cwd = '';
1864
- try {
1865
- const meta = await svc.readTeamManifest(teamName);
1866
- if (typeof meta.workDir === 'string') {
1867
- cwd = meta.workDir.trim();
1868
- }
1869
- } catch {
1870
- // ignore
1871
- }
1872
-
1873
- if (!cwd) {
2031
+ await Promise.all(
2032
+ uniqueTeamNames.map(async (teamName) => {
2033
+ let cwd = '';
1874
2034
  try {
1875
- const detail = await cc.getProject(teamName);
1876
- if (typeof detail.work_dir === 'string') {
1877
- cwd = detail.work_dir.trim();
2035
+ const meta = await svc.readTeamManifest(teamName);
2036
+ if (typeof meta.workDir === 'string') {
2037
+ cwd = meta.workDir.trim();
1878
2038
  }
1879
2039
  } catch {
1880
2040
  // ignore
1881
2041
  }
1882
- }
1883
2042
 
1884
- results.set(teamName, cwd);
1885
- }));
2043
+ if (!cwd) {
2044
+ try {
2045
+ const detail = await cc.getProject(teamName);
2046
+ if (typeof detail.work_dir === 'string') {
2047
+ cwd = detail.work_dir.trim();
2048
+ }
2049
+ } catch {
2050
+ // ignore
2051
+ }
2052
+ }
2053
+
2054
+ results.set(teamName, cwd);
2055
+ })
2056
+ );
1886
2057
 
1887
2058
  return results;
1888
2059
  }
@@ -2010,14 +2181,17 @@ app.post<{ Body: Record<string, unknown> }>('/api/schedules', async (request, re
2010
2181
  : DEFAULT_SCHEDULE_MAX_TURNS;
2011
2182
 
2012
2183
  const launchConfig =
2013
- body.launchConfig && typeof body.launchConfig === 'object' && !Array.isArray(body.launchConfig)
2184
+ body.launchConfig &&
2185
+ typeof body.launchConfig === 'object' &&
2186
+ !Array.isArray(body.launchConfig)
2014
2187
  ? (body.launchConfig as Record<string, unknown>)
2015
2188
  : {};
2016
2189
  const prompt = typeof launchConfig.prompt === 'string' ? launchConfig.prompt.trim() : '';
2017
2190
  const cwd = typeof launchConfig.cwd === 'string' ? launchConfig.cwd.trim() : '';
2018
- const sessionKey = typeof launchConfig.session_key === 'string' && launchConfig.session_key.trim().length > 0
2019
- ? launchConfig.session_key.trim()
2020
- : buildFallbackSessionKey(teamName);
2191
+ const sessionKey =
2192
+ typeof launchConfig.session_key === 'string' && launchConfig.session_key.trim().length > 0
2193
+ ? launchConfig.session_key.trim()
2194
+ : buildFallbackSessionKey(teamName);
2021
2195
 
2022
2196
  if (!teamName || !cronExpression || !prompt) {
2023
2197
  return reply
@@ -2111,12 +2285,16 @@ app.delete<{ Params: { id: string } }>('/api/schedules/:id', async (request, rep
2111
2285
  jobs = await cc.listCronJobs();
2112
2286
  listedJobs = true;
2113
2287
  } catch (listErr) {
2114
- request.log.warn({ err: listErr, scheduleId: requestedId }, 'list cron jobs before delete failed');
2288
+ request.log.warn(
2289
+ { err: listErr, scheduleId: requestedId },
2290
+ 'list cron jobs before delete failed'
2291
+ );
2115
2292
  }
2116
2293
  const target = findCronJobByRouteId(jobs, requestedId);
2117
2294
  if (target) {
2118
2295
  resolvedId = target.id;
2119
- resolvedTeamName = 'project' in target && typeof target.project === 'string' ? target.project : '';
2296
+ resolvedTeamName =
2297
+ 'project' in target && typeof target.project === 'string' ? target.project : '';
2120
2298
  } else if (
2121
2299
  listedJobs &&
2122
2300
  !jobs.some((job) => job.id === normalizedId || job.id.startsWith(normalizedId))
@@ -2216,7 +2394,11 @@ app.post<{ Params: { id: string } }>('/api/schedules/:id/trigger', async (reques
2216
2394
  let run: InMemoryScheduleRun;
2217
2395
 
2218
2396
  try {
2219
- await cc.sendMessage(job.project, job.session_key || buildFallbackSessionKey(job.project), job.prompt);
2397
+ await cc.sendMessage(
2398
+ job.project,
2399
+ job.session_key || buildFallbackSessionKey(job.project),
2400
+ job.prompt
2401
+ );
2220
2402
  run = {
2221
2403
  id: runId,
2222
2404
  scheduleId: job.id,
@@ -2280,20 +2462,22 @@ app.get<{ Params: { id: string } }>('/api/schedules/:id/runs', async (request) =
2280
2462
  const job = jobs.find((item) => item.id === scheduleId);
2281
2463
  const lastRunAt = normalizeCronLastRun(job?.last_run);
2282
2464
  if (!job || !lastRunAt) return [];
2283
- return [{
2284
- id: `last-${scheduleId}`,
2285
- scheduleId,
2286
- teamName: job.project,
2287
- status: 'completed',
2288
- scheduledFor: lastRunAt,
2289
- startedAt: lastRunAt,
2290
- executionStartedAt: lastRunAt,
2291
- completedAt: lastRunAt,
2292
- durationMs: 0,
2293
- exitCode: 0,
2294
- retryCount: 0,
2295
- summary: 'Last run from cc-connect',
2296
- }];
2465
+ return [
2466
+ {
2467
+ id: `last-${scheduleId}`,
2468
+ scheduleId,
2469
+ teamName: job.project,
2470
+ status: 'completed',
2471
+ scheduledFor: lastRunAt,
2472
+ startedAt: lastRunAt,
2473
+ executionStartedAt: lastRunAt,
2474
+ completedAt: lastRunAt,
2475
+ durationMs: 0,
2476
+ exitCode: 0,
2477
+ retryCount: 0,
2478
+ summary: 'Last run from cc-connect',
2479
+ },
2480
+ ];
2297
2481
  } catch {
2298
2482
  return [];
2299
2483
  }
@@ -2312,23 +2496,29 @@ app.get<{ Params: { id: string; runId: string } }>(
2312
2496
  );
2313
2497
 
2314
2498
  // Browse directories — returns subdirectories at the given path
2315
- app.post<{ Body: { path?: string; limit?: number } }>('/api/config/browse-folders', async (request) => {
2316
- const { path: dirPath, limit = 200 } = request.body ?? {};
2317
- const target = dirPath && dirPath.trim() ? dirPath.trim() : os.homedir();
2318
-
2319
- try {
2320
- const entries = readdirSync(target, { withFileTypes: true });
2321
- const dirs = entries
2322
- .filter((e) => e.isDirectory() && !e.name.startsWith('.'))
2323
- .slice(0, limit)
2324
- .map((e) => path.join(target, e.name));
2325
- return { success: true, data: { path: target, dirs, hasParent: target !== path.dirname(target) } };
2326
- } catch {
2327
- return { success: false, error: `无法访问目录: ${target}` };
2328
- }
2329
- });
2499
+ app.post<{ Body: { path?: string; limit?: number } }>(
2500
+ '/api/config/browse-folders',
2501
+ async (request) => {
2502
+ const { path: dirPath, limit = 200 } = request.body ?? {};
2503
+ const target = dirPath && dirPath.trim() ? dirPath.trim() : os.homedir();
2330
2504
 
2331
- // POST /api/workspace/list — 文件目录浏览
2505
+ try {
2506
+ const entries = readdirSync(target, { withFileTypes: true });
2507
+ const dirs = entries
2508
+ .filter((e) => e.isDirectory() && !e.name.startsWith('.'))
2509
+ .slice(0, limit)
2510
+ .map((e) => path.join(target, e.name));
2511
+ return {
2512
+ success: true,
2513
+ data: { path: target, dirs, hasParent: target !== path.dirname(target) },
2514
+ };
2515
+ } catch {
2516
+ return { success: false, error: `无法访问目录: ${target}` };
2517
+ }
2518
+ }
2519
+ );
2520
+
2521
+ // POST /api/workspace/list — 文件目录浏览
2332
2522
  app.post<{ Body: { dirPath?: string } }>('/api/workspace/list', async (request) => {
2333
2523
  const { dirPath } = request.body ?? {};
2334
2524
  const target = dirPath && dirPath.trim() ? dirPath.trim() : os.homedir();
@@ -2345,7 +2535,9 @@ app.post<{ Body: { dirPath?: string } }>('/api/workspace/list', async (request)
2345
2535
  try {
2346
2536
  const stat = statSync(fullPath);
2347
2537
  size = stat.size;
2348
- } catch { /* ignore */ }
2538
+ } catch {
2539
+ /* ignore */
2540
+ }
2349
2541
  return {
2350
2542
  name: e.name,
2351
2543
  isDirectory,
@@ -2391,7 +2583,9 @@ function resolveEditorPath(root: string, rawPath: unknown): string {
2391
2583
  throw new Error('filePath/dirPath 参数不能为空');
2392
2584
  }
2393
2585
  const requested = rawPath.trim();
2394
- const resolved = path.resolve(path.isAbsolute(requested) ? requested : path.join(root, requested));
2586
+ const resolved = path.resolve(
2587
+ path.isAbsolute(requested) ? requested : path.join(root, requested)
2588
+ );
2395
2589
  if (!isPathInsideRoot(root, resolved)) {
2396
2590
  throw new Error('路径超出项目根目录');
2397
2591
  }
@@ -2432,9 +2626,7 @@ app.get<{ Querystring: { root?: unknown; dirPath?: unknown; maxEntries?: string
2432
2626
  async (request, reply) => {
2433
2627
  try {
2434
2628
  const root = resolveEditorRoot(request.query.root);
2435
- const dirPath = request.query.dirPath
2436
- ? resolveEditorPath(root, request.query.dirPath)
2437
- : root;
2629
+ const dirPath = request.query.dirPath ? resolveEditorPath(root, request.query.dirPath) : root;
2438
2630
  const maxEntriesRaw = Number.parseInt(request.query.maxEntries ?? '', 10);
2439
2631
  const maxEntries = Number.isFinite(maxEntriesRaw)
2440
2632
  ? Math.min(Math.max(maxEntriesRaw, 1), MAX_EDITOR_DIR_ENTRIES)
@@ -2500,31 +2692,30 @@ app.get<{ Querystring: { root?: unknown; filePath?: unknown } }>(
2500
2692
  }
2501
2693
  );
2502
2694
 
2503
- app.post<{ Body: { root?: unknown; filePath?: unknown; content?: unknown; baselineMtimeMs?: unknown } }>(
2504
- '/api/editor/writeFile',
2505
- async (request, reply) => {
2506
- try {
2507
- const root = resolveEditorRoot(request.body?.root);
2508
- const filePath = resolveEditorPath(root, request.body?.filePath);
2509
- const content = request.body?.content;
2510
- if (typeof content !== 'string') {
2511
- throw new Error('content 必须是字符串');
2512
- }
2513
- const baselineRaw = request.body?.baselineMtimeMs;
2514
- if (typeof baselineRaw === 'number' && Number.isFinite(baselineRaw)) {
2515
- const currentMtime = statSync(filePath).mtimeMs;
2516
- if (Math.abs(currentMtime - baselineRaw) > 1) {
2517
- throw new Error('CONFLICT: file changed on disk');
2518
- }
2695
+ app.post<{
2696
+ Body: { root?: unknown; filePath?: unknown; content?: unknown; baselineMtimeMs?: unknown };
2697
+ }>('/api/editor/writeFile', async (request, reply) => {
2698
+ try {
2699
+ const root = resolveEditorRoot(request.body?.root);
2700
+ const filePath = resolveEditorPath(root, request.body?.filePath);
2701
+ const content = request.body?.content;
2702
+ if (typeof content !== 'string') {
2703
+ throw new Error('content 必须是字符串');
2704
+ }
2705
+ const baselineRaw = request.body?.baselineMtimeMs;
2706
+ if (typeof baselineRaw === 'number' && Number.isFinite(baselineRaw)) {
2707
+ const currentMtime = statSync(filePath).mtimeMs;
2708
+ if (Math.abs(currentMtime - baselineRaw) > 1) {
2709
+ throw new Error('CONFLICT: file changed on disk');
2519
2710
  }
2520
- writeFileSync(filePath, content, 'utf-8');
2521
- const st = statSync(filePath);
2522
- return { mtimeMs: st.mtimeMs, size: st.size };
2523
- } catch (err) {
2524
- return sendEditorError(reply, err);
2525
2711
  }
2712
+ writeFileSync(filePath, content, 'utf-8');
2713
+ const st = statSync(filePath);
2714
+ return { mtimeMs: st.mtimeMs, size: st.size };
2715
+ } catch (err) {
2716
+ return sendEditorError(reply, err);
2526
2717
  }
2527
- );
2718
+ });
2528
2719
 
2529
2720
  app.post<{ Body: { root?: unknown; parentDir?: unknown; fileName?: unknown } }>(
2530
2721
  '/api/editor/createFile',
@@ -2532,7 +2723,8 @@ app.post<{ Body: { root?: unknown; parentDir?: unknown; fileName?: unknown } }>(
2532
2723
  try {
2533
2724
  const root = resolveEditorRoot(request.body?.root);
2534
2725
  const parentDir = resolveEditorPath(root, request.body?.parentDir);
2535
- const fileName = typeof request.body?.fileName === 'string' ? request.body.fileName.trim() : '';
2726
+ const fileName =
2727
+ typeof request.body?.fileName === 'string' ? request.body.fileName.trim() : '';
2536
2728
  if (!fileName) {
2537
2729
  throw new Error('fileName 不能为空');
2538
2730
  }
@@ -2565,16 +2757,19 @@ app.post<{ Body: { root?: unknown; parentDir?: unknown; dirName?: unknown } }>(
2565
2757
  }
2566
2758
  );
2567
2759
 
2568
- app.post<{ Body: { root?: unknown; filePath?: unknown } }>('/api/editor/deleteFile', async (request, reply) => {
2569
- try {
2570
- const root = resolveEditorRoot(request.body?.root);
2571
- const filePath = resolveEditorPath(root, request.body?.filePath);
2572
- rmSync(filePath, { recursive: true, force: false });
2573
- return { deletedPath: filePath };
2574
- } catch (err) {
2575
- return sendEditorError(reply, err);
2760
+ app.post<{ Body: { root?: unknown; filePath?: unknown } }>(
2761
+ '/api/editor/deleteFile',
2762
+ async (request, reply) => {
2763
+ try {
2764
+ const root = resolveEditorRoot(request.body?.root);
2765
+ const filePath = resolveEditorPath(root, request.body?.filePath);
2766
+ rmSync(filePath, { recursive: true, force: false });
2767
+ return { deletedPath: filePath };
2768
+ } catch (err) {
2769
+ return sendEditorError(reply, err);
2770
+ }
2576
2771
  }
2577
- });
2772
+ );
2578
2773
 
2579
2774
  app.post<{ Body: { root?: unknown; sourcePath?: unknown; destDir?: unknown } }>(
2580
2775
  '/api/editor/moveFile',
@@ -2680,7 +2875,8 @@ app.get('/api/editor/search', async () => ({ results: [], totalMatches: 0, trunc
2680
2875
 
2681
2876
  // 消息分页 — store 期望 MessagesPage 结构
2682
2877
  app.get<{ Params: { name: string }; Querystring: { cursor?: string; limit?: string } }>(
2683
- '/api/teams/:name/messages', async (request) => {
2878
+ '/api/teams/:name/messages',
2879
+ async (request) => {
2684
2880
  const { name } = request.params;
2685
2881
  const requestedLimit = Number(request.query.limit ?? 50);
2686
2882
  const limit = Math.min(
@@ -2708,13 +2904,13 @@ app.get<{ Params: { name: string }; Querystring: { cursor?: string; limit?: stri
2708
2904
  : undefined;
2709
2905
  const session = sessionKey ? sessionByKey.get(sessionKey) : undefined;
2710
2906
  return {
2711
- messageId: m.id,
2712
- from: m.from,
2713
- to: m.to,
2714
- text: m.content,
2715
- timestamp: m.ts,
2716
- read: true,
2717
- source: (m.role === 'user' ? 'user_sent' : 'inbox') as string,
2907
+ messageId: m.id,
2908
+ from: m.from,
2909
+ to: m.to,
2910
+ text: m.content,
2911
+ timestamp: m.ts,
2912
+ read: true,
2913
+ source: (m.role === 'user' ? 'user_sent' : 'inbox') as string,
2718
2914
  session: sessionKey
2719
2915
  ? {
2720
2916
  id: session?.id,
@@ -2747,101 +2943,94 @@ app.get<{ Params: { name: string }; Querystring: { cursor?: string; limit?: stri
2747
2943
 
2748
2944
  // 消息 head(messages-head 不是标准路由,storeok调 getMessagesPage 的同路由带 limit)
2749
2945
  // member-activity-meta
2750
- app.get<{ Params: { name: string } }>(
2751
- '/api/teams/:name/member-activity-meta', async (request) => {
2752
- const { name } = request.params;
2753
- return {
2754
- teamName: name,
2755
- computedAt: new Date().toISOString(),
2756
- members: {},
2757
- feedRevision: '0',
2758
- };
2759
- }
2760
- );
2946
+ app.get<{ Params: { name: string } }>('/api/teams/:name/member-activity-meta', async (request) => {
2947
+ const { name } = request.params;
2948
+ return {
2949
+ teamName: name,
2950
+ computedAt: new Date().toISOString(),
2951
+ members: {},
2952
+ feedRevision: '0',
2953
+ };
2954
+ });
2761
2955
 
2762
2956
  // member-activity — GET /api/teams/:name/member-activity
2763
- app.get<{ Params: { name: string } }>(
2764
- '/api/teams/:name/member-activity', async (request) => {
2765
- const { name } = request.params;
2766
- return {
2767
- teamName: name,
2768
- computedAt: new Date().toISOString(),
2769
- members: {},
2770
- feedRevision: '0',
2771
- };
2772
- }
2773
- );
2957
+ app.get<{ Params: { name: string } }>('/api/teams/:name/member-activity', async (request) => {
2958
+ const { name } = request.params;
2959
+ return {
2960
+ teamName: name,
2961
+ computedAt: new Date().toISOString(),
2962
+ members: {},
2963
+ feedRevision: '0',
2964
+ };
2965
+ });
2774
2966
 
2775
2967
  // member-spawn-statuses — GET /api/teams/:name/member-spawn-statuses
2776
- app.get<{ Params: { name: string } }>(
2777
- '/api/teams/:name/member-spawn-statuses', async (request) => {
2778
- const { name } = request.params;
2779
- return {
2780
- statuses: {},
2781
- runId: null,
2782
- };
2783
- }
2784
- );
2968
+ app.get<{ Params: { name: string } }>('/api/teams/:name/member-spawn-statuses', async (request) => {
2969
+ const { name } = request.params;
2970
+ return {
2971
+ statuses: {},
2972
+ runId: null,
2973
+ };
2974
+ });
2785
2975
 
2786
2976
  // agent-runtime — GET /api/teams/:name/agent-runtime
2787
- app.get<{ Params: { name: string } }>(
2788
- '/api/teams/:name/agent-runtime', async (request) => {
2789
- const { name } = request.params;
2790
- return {
2791
- teamName: name,
2792
- updatedAt: new Date().toISOString(),
2793
- runId: null,
2794
- members: {},
2795
- };
2796
- }
2797
- );
2977
+ app.get<{ Params: { name: string } }>('/api/teams/:name/agent-runtime', async (request) => {
2978
+ const { name } = request.params;
2979
+ return {
2980
+ teamName: name,
2981
+ updatedAt: new Date().toISOString(),
2982
+ runId: null,
2983
+ members: {},
2984
+ };
2985
+ });
2798
2986
 
2799
2987
  // lead-activity — GET /api/teams/:name/lead-activity
2800
- app.get<{ Params: { name: string } }>(
2801
- '/api/teams/:name/lead-activity', async () => {
2802
- return { state: 'offline', updatedAt: new Date().toISOString() };
2803
- }
2804
- );
2988
+ app.get<{ Params: { name: string } }>('/api/teams/:name/lead-activity', async () => {
2989
+ return { state: 'offline', updatedAt: new Date().toISOString() };
2990
+ });
2805
2991
 
2806
2992
  // lead-context — GET /api/teams/:name/lead-context
2807
- app.get<{ Params: { name: string } }>(
2808
- '/api/teams/:name/lead-context', async () => {
2809
- return { usage: null };
2810
- }
2811
- );
2993
+ app.get<{ Params: { name: string } }>('/api/teams/:name/lead-context', async () => {
2994
+ return { usage: null };
2995
+ });
2812
2996
 
2813
2997
  // sessions — 从 cc-connect project sessions 获取,转换为前端 Session 格式
2814
- app.get<{ Params: { name: string } }>(
2815
- '/api/teams/:name/sessions', async (request) => {
2816
- try {
2817
- const sessions = await cc.listSessions(request.params.name);
2818
- return sessions.map((s) => ({
2819
- id: s.id,
2820
- title: s.user_name || s.chat_name || s.name || s.session_key,
2821
- projectId: request.params.name,
2822
- sessionKey: s.session_key,
2823
- platform: s.platform,
2824
- userName: s.user_name ?? null,
2825
- chatName: s.chat_name ?? null,
2826
- active: s.active,
2827
- live: s.live,
2828
- historyCount: s.history_count,
2829
- createdAt: s.created_at,
2830
- updatedAt: s.updated_at,
2831
- lastMessage: s.last_message ? {
2832
- role: s.last_message.role,
2833
- content: s.last_message.content,
2834
- timestamp: s.last_message.timestamp,
2835
- } : null,
2836
- }));
2837
- } catch { return []; }
2998
+ app.get<{ Params: { name: string } }>('/api/teams/:name/sessions', async (request) => {
2999
+ try {
3000
+ const sessions = await cc.listSessions(request.params.name);
3001
+ return sessions.map((s) => ({
3002
+ id: s.id,
3003
+ title: s.user_name || s.chat_name || s.name || s.session_key,
3004
+ projectId: request.params.name,
3005
+ sessionKey: s.session_key,
3006
+ platform: s.platform,
3007
+ userName: s.user_name ?? null,
3008
+ chatName: s.chat_name ?? null,
3009
+ active: s.active,
3010
+ live: s.live,
3011
+ historyCount: s.history_count,
3012
+ createdAt: s.created_at,
3013
+ updatedAt: s.updated_at,
3014
+ lastMessage: s.last_message
3015
+ ? {
3016
+ role: s.last_message.role,
3017
+ content: s.last_message.content,
3018
+ timestamp: s.last_message.timestamp,
3019
+ }
3020
+ : null,
3021
+ }));
3022
+ } catch {
3023
+ return [];
2838
3024
  }
2839
- );
3025
+ });
2840
3026
 
2841
3027
  // GET session detail — 通过 cc-connect API 获取会话详情(含历史消息)
2842
3028
  app.get<{ Params: { name: string; sessionId: string }; Querystring: { history_limit?: string } }>(
2843
- '/api/teams/:name/sessions/:sessionId', async (request) => {
2844
- const historyLimit = request.query.history_limit ? parseInt(request.query.history_limit, 10) : 500;
3029
+ '/api/teams/:name/sessions/:sessionId',
3030
+ async (request) => {
3031
+ const historyLimit = request.query.history_limit
3032
+ ? parseInt(request.query.history_limit, 10)
3033
+ : 500;
2845
3034
  const detail = await cc.getSession(request.params.name, request.params.sessionId, historyLimit);
2846
3035
  return mapCcSessionDetail(detail);
2847
3036
  }
@@ -2849,12 +3038,15 @@ app.get<{ Params: { name: string; sessionId: string }; Querystring: { history_li
2849
3038
 
2850
3039
  // DELETE session — 通过 cc-connect API 删除指定 session
2851
3040
  app.delete<{ Params: { name: string; sessionId: string } }>(
2852
- '/api/teams/:name/sessions/:sessionId', async (request, reply) => {
3041
+ '/api/teams/:name/sessions/:sessionId',
3042
+ async (request, reply) => {
2853
3043
  try {
2854
3044
  await cc.deleteSession(request.params.name, request.params.sessionId);
2855
3045
  return { ok: true };
2856
3046
  } catch (err) {
2857
- return reply.code(500).send({ ok: false, error: err instanceof Error ? err.message : String(err) });
3047
+ return reply
3048
+ .code(500)
3049
+ .send({ ok: false, error: err instanceof Error ? err.message : String(err) });
2858
3050
  }
2859
3051
  }
2860
3052
  );
@@ -2863,30 +3055,37 @@ app.delete<{ Params: { name: string; sessionId: string } }>(
2863
3055
  app.get('/api/teams/runtime/alive', async () => {
2864
3056
  try {
2865
3057
  const projects = await cc.listProjects();
2866
- return await Promise.all(projects.map(async (p) => {
2867
- let isAlive = false;
2868
- try {
2869
- const detail = await cc.getProject(p.name);
2870
- isAlive = Array.isArray(detail.platforms) && detail.platforms.some((pl) => pl.connected);
2871
- } catch { /* degraded */ }
2872
- return { teamName: p.name, isAlive, runId: p.name };
2873
- }));
2874
- } catch { return []; }
3058
+ return await Promise.all(
3059
+ projects.map(async (p) => {
3060
+ let isAlive = false;
3061
+ try {
3062
+ const detail = await cc.getProject(p.name);
3063
+ isAlive = Array.isArray(detail.platforms) && detail.platforms.some((pl) => pl.connected);
3064
+ } catch {
3065
+ /* degraded */
3066
+ }
3067
+ return { teamName: p.name, isAlive, runId: p.name };
3068
+ })
3069
+ );
3070
+ } catch {
3071
+ return [];
3072
+ }
2875
3073
  });
2876
3074
 
2877
3075
  // process-alive — 查询 cc-connect project 在线状态
2878
- app.get<{ Params: { name: string } }>(
2879
- '/api/teams/:name/process-alive', async (request) => {
2880
- try {
2881
- const p = await cc.getProject(request.params.name);
2882
- return Array.isArray(p.platforms) && p.platforms.some((pl) => pl.connected);
2883
- } catch { return false; }
3076
+ app.get<{ Params: { name: string } }>('/api/teams/:name/process-alive', async (request) => {
3077
+ try {
3078
+ const p = await cc.getProject(request.params.name);
3079
+ return Array.isArray(p.platforms) && p.platforms.some((pl) => pl.connected);
3080
+ } catch {
3081
+ return false;
2884
3082
  }
2885
- );
3083
+ });
2886
3084
 
2887
3085
  // process-send — 从 Hermit UI 注入到 harness,不回发到 IM 平台。
2888
3086
  app.post<{ Params: { name: string }; Body: { text?: string; message?: string } }>(
2889
- '/api/teams/:name/process-send', async (request, reply) => {
3087
+ '/api/teams/:name/process-send',
3088
+ async (request, reply) => {
2890
3089
  try {
2891
3090
  const text = request.body?.text ?? request.body?.message ?? '';
2892
3091
  if (text) {
@@ -2906,28 +3105,22 @@ app.post<{ Params: { name: string }; Body: { text?: string; message?: string } }
2906
3105
  );
2907
3106
 
2908
3107
  // saved-request — 新版无此概念
2909
- app.get<{ Params: { name: string } }>(
2910
- '/api/teams/:name/saved-request', async () => null
2911
- );
3108
+ app.get<{ Params: { name: string } }>('/api/teams/:name/saved-request', async () => null);
2912
3109
 
2913
3110
  // kanban state — 返回空看板状态
2914
- app.get<{ Params: { name: string } }>(
2915
- '/api/teams/:name/kanban', async (request) => ({
2916
- teamName: request.params.name,
2917
- reviewers: [],
2918
- tasks: {},
2919
- })
2920
- );
3111
+ app.get<{ Params: { name: string } }>('/api/teams/:name/kanban', async (request) => ({
3112
+ teamName: request.params.name,
3113
+ reviewers: [],
3114
+ tasks: {},
3115
+ }));
2921
3116
 
2922
3117
  // task-change-presence — 返回 {}
2923
- app.get<{ Params: { name: string } }>(
2924
- '/api/teams/:name/task-change-presence', async () => ({})
2925
- );
3118
+ app.get<{ Params: { name: string } }>('/api/teams/:name/task-change-presence', async () => ({}));
2926
3119
 
2927
3120
  // kanban column order — no-op
2928
- app.post<{ Params: { name: string } }>(
2929
- '/api/teams/:name/kanban-column-order', async () => ({ ok: true })
2930
- );
3121
+ app.post<{ Params: { name: string } }>('/api/teams/:name/kanban-column-order', async () => ({
3122
+ ok: true,
3123
+ }));
2931
3124
 
2932
3125
  // teams/tasks (全局任务列表 — 跨所有团队)
2933
3126
  app.get('/api/teams/tasks', async () => {
@@ -2938,52 +3131,69 @@ app.get('/api/teams/tasks', async () => {
2938
3131
  try {
2939
3132
  const tasks = activeTasks(await svc.readTasks(p.name));
2940
3133
  allTasks.push(...tasks.map(toTeamTask));
2941
- } catch { /* skip */ }
3134
+ } catch {
3135
+ /* skip */
3136
+ }
2942
3137
  }
2943
3138
  return allTasks;
2944
- } catch { return []; }
3139
+ } catch {
3140
+ return [];
3141
+ }
2945
3142
  });
2946
3143
 
2947
3144
  // 团队任务子操作 — 全部委托给 svc.patchTask
2948
3145
  app.post<{ Params: { name: string; id: string } }>(
2949
- '/api/teams/:name/tasks/:id/request-review', async (request) => {
3146
+ '/api/teams/:name/tasks/:id/request-review',
3147
+ async (request) => {
2950
3148
  try {
2951
3149
  const task = await svc.patchTask(request.params.name, request.params.id, { status: 'done' });
2952
3150
  return { ok: true, data: toTeamTask(task) };
2953
- } catch { return { ok: true }; }
3151
+ } catch {
3152
+ return { ok: true };
3153
+ }
2954
3154
  }
2955
3155
  );
2956
3156
  app.patch<{ Params: { name: string; id: string }; Body: Record<string, unknown> }>(
2957
- '/api/teams/:name/tasks/:id/kanban', async (request) => {
3157
+ '/api/teams/:name/tasks/:id/kanban',
3158
+ async (request) => {
2958
3159
  // kanban metadata — stored in board.json via patchTask (no-op for now, column tracked client-side)
2959
3160
  return { ok: true };
2960
3161
  }
2961
3162
  );
2962
3163
  app.patch<{ Params: { name: string; id: string }; Body: { status?: string } }>(
2963
- '/api/teams/:name/tasks/:id/status', async (request) => {
3164
+ '/api/teams/:name/tasks/:id/status',
3165
+ async (request) => {
2964
3166
  try {
2965
3167
  const { status } = request.body ?? {};
2966
3168
  const task = await svc.patchTask(request.params.name, request.params.id, {
2967
3169
  status: status ? toTaskStatus(status) : undefined,
2968
3170
  });
2969
3171
  return toTeamTask(task);
2970
- } catch { return { ok: true }; }
3172
+ } catch {
3173
+ return { ok: true };
3174
+ }
2971
3175
  }
2972
3176
  );
2973
3177
  app.patch<{ Params: { name: string; id: string }; Body: { owner?: string } }>(
2974
- '/api/teams/:name/tasks/:id/owner', async (request) => {
3178
+ '/api/teams/:name/tasks/:id/owner',
3179
+ async (request) => {
2975
3180
  try {
2976
3181
  const body = request.body ?? {};
2977
- const task = await svc.patchTask(request.params.name, request.params.id, { assignee: body.owner ?? null });
3182
+ const task = await svc.patchTask(request.params.name, request.params.id, {
3183
+ assignee: body.owner ?? null,
3184
+ });
2978
3185
  if (task.assignee) {
2979
3186
  svc.dispatchTask(request.params.name, task).catch(() => {});
2980
3187
  }
2981
3188
  return toTeamTask(task);
2982
- } catch { return { ok: true }; }
3189
+ } catch {
3190
+ return { ok: true };
3191
+ }
2983
3192
  }
2984
3193
  );
2985
3194
  app.patch<{ Params: { name: string; id: string }; Body: Record<string, unknown> }>(
2986
- '/api/teams/:name/tasks/:id/fields', async (request) => {
3195
+ '/api/teams/:name/tasks/:id/fields',
3196
+ async (request) => {
2987
3197
  try {
2988
3198
  const body = request.body ?? {};
2989
3199
  const patch: Record<string, unknown> = {};
@@ -2991,11 +3201,14 @@ app.patch<{ Params: { name: string; id: string }; Body: Record<string, unknown>
2991
3201
  if (body.description !== undefined) patch.description = body.description;
2992
3202
  const task = await svc.patchTask(request.params.name, request.params.id, patch);
2993
3203
  return toTeamTask(task);
2994
- } catch { return { ok: true }; }
3204
+ } catch {
3205
+ return { ok: true };
3206
+ }
2995
3207
  }
2996
3208
  );
2997
3209
  app.post<{ Params: { name: string; id: string } }>(
2998
- '/api/teams/:name/tasks/:id/start', async (request) => {
3210
+ '/api/teams/:name/tasks/:id/start',
3211
+ async (request) => {
2999
3212
  try {
3000
3213
  const task = await svc.patchTask(request.params.name, request.params.id, { status: 'doing' });
3001
3214
  if (task.assignee) {
@@ -3003,11 +3216,14 @@ app.post<{ Params: { name: string; id: string } }>(
3003
3216
  return { notifiedOwner: true };
3004
3217
  }
3005
3218
  return { notifiedOwner: false };
3006
- } catch { return { notifiedOwner: false }; }
3219
+ } catch {
3220
+ return { notifiedOwner: false };
3221
+ }
3007
3222
  }
3008
3223
  );
3009
3224
  app.post<{ Params: { name: string; id: string } }>(
3010
- '/api/teams/:name/tasks/:id/start-by-user', async (request) => {
3225
+ '/api/teams/:name/tasks/:id/start-by-user',
3226
+ async (request) => {
3011
3227
  try {
3012
3228
  const task = await svc.patchTask(request.params.name, request.params.id, { status: 'doing' });
3013
3229
  if (task.assignee) {
@@ -3015,13 +3231,19 @@ app.post<{ Params: { name: string; id: string } }>(
3015
3231
  return { notifiedOwner: true };
3016
3232
  }
3017
3233
  return { notifiedOwner: false };
3018
- } catch { return { notifiedOwner: false }; }
3234
+ } catch {
3235
+ return { notifiedOwner: false };
3236
+ }
3019
3237
  }
3020
3238
  );
3021
3239
  app.post<{ Params: { name: string; id: string } }>(
3022
- '/api/teams/:name/tasks/:id/soft-delete', async (request, reply) => {
3240
+ '/api/teams/:name/tasks/:id/soft-delete',
3241
+ async (request, reply) => {
3023
3242
  try {
3024
- await svc.patchTask(request.params.name, request.params.id, { status: 'done', result: '__deleted__' });
3243
+ await svc.patchTask(request.params.name, request.params.id, {
3244
+ status: 'done',
3245
+ result: '__deleted__',
3246
+ });
3025
3247
  return { ok: true };
3026
3248
  } catch (err) {
3027
3249
  return reply.code(404).send(reply500(err));
@@ -3029,7 +3251,8 @@ app.post<{ Params: { name: string; id: string } }>(
3029
3251
  }
3030
3252
  );
3031
3253
  app.post<{ Params: { name: string; id: string } }>(
3032
- '/api/teams/:name/tasks/:id/restore', async (request, reply) => {
3254
+ '/api/teams/:name/tasks/:id/restore',
3255
+ async (request, reply) => {
3033
3256
  try {
3034
3257
  await svc.patchTask(request.params.name, request.params.id, { status: 'todo', result: null });
3035
3258
  return { ok: true };
@@ -3038,54 +3261,55 @@ app.post<{ Params: { name: string; id: string } }>(
3038
3261
  }
3039
3262
  }
3040
3263
  );
3041
- app.get<{ Params: { name: string } }>(
3042
- '/api/teams/:name/deleted-tasks', async (request) => {
3043
- try {
3044
- const tasks = await svc.readTasks(request.params.name);
3045
- return tasks.filter((t) => t.result === '__deleted__').map(toTeamTask);
3046
- } catch { return []; }
3264
+ app.get<{ Params: { name: string } }>('/api/teams/:name/deleted-tasks', async (request) => {
3265
+ try {
3266
+ const tasks = await svc.readTasks(request.params.name);
3267
+ return tasks.filter((t) => t.result === '__deleted__').map(toTeamTask);
3268
+ } catch {
3269
+ return [];
3047
3270
  }
3048
- );
3271
+ });
3049
3272
  app.post<{ Params: { name: string; id: string }; Body: { text?: string } }>(
3050
- '/api/teams/:name/tasks/:id/comments', async () => ({ ok: true })
3273
+ '/api/teams/:name/tasks/:id/comments',
3274
+ async () => ({ ok: true })
3051
3275
  );
3052
3276
  app.post<{ Params: { name: string; id: string } }>(
3053
- '/api/teams/:name/tasks/:id/clarification', async () => ({ ok: true })
3277
+ '/api/teams/:name/tasks/:id/clarification',
3278
+ async () => ({ ok: true })
3054
3279
  );
3055
3280
  app.post<{ Params: { name: string; id: string } }>(
3056
- '/api/teams/:name/tasks/:id/relationships', async () => ({ ok: true })
3281
+ '/api/teams/:name/tasks/:id/relationships',
3282
+ async () => ({ ok: true })
3057
3283
  );
3058
3284
 
3059
-
3060
3285
  // 成员相关 stubs
3061
- app.post<{ Params: { name: string } }>(
3062
- '/api/teams/:name/members', async () => ({ ok: true })
3063
- );
3286
+ app.post<{ Params: { name: string } }>('/api/teams/:name/members', async () => ({ ok: true }));
3064
3287
  app.delete<{ Params: { name: string; memberName: string } }>(
3065
- '/api/teams/:name/members/:memberName', async () => ({ ok: true })
3288
+ '/api/teams/:name/members/:memberName',
3289
+ async () => ({ ok: true })
3066
3290
  );
3067
3291
  app.patch<{ Params: { name: string; memberName: string } }>(
3068
- '/api/teams/:name/members/:memberName/role', async () => ({ ok: true })
3292
+ '/api/teams/:name/members/:memberName/role',
3293
+ async () => ({ ok: true })
3069
3294
  );
3070
3295
  app.post<{ Params: { name: string; memberName: string } }>(
3071
- '/api/teams/:name/members/:memberName/restart', async () => ({ ok: true })
3296
+ '/api/teams/:name/members/:memberName/restart',
3297
+ async () => ({ ok: true })
3072
3298
  );
3073
3299
  app.post<{ Params: { name: string; memberName: string } }>(
3074
- '/api/teams/:name/members/:memberName/skip-launch', async () => ({ ok: true })
3300
+ '/api/teams/:name/members/:memberName/skip-launch',
3301
+ async () => ({ ok: true })
3075
3302
  );
3076
3303
 
3077
3304
  // claude logs
3078
- app.get<{ Params: { name: string } }>(
3079
- '/api/teams/:name/claude-logs', async () => ({ logs: [], total: 0 })
3080
- );
3305
+ app.get<{ Params: { name: string } }>('/api/teams/:name/claude-logs', async () => ({
3306
+ logs: [],
3307
+ total: 0,
3308
+ }));
3081
3309
 
3082
3310
  // restore / permanent delete
3083
- app.post<{ Params: { name: string } }>(
3084
- '/api/teams/:name/restore', async () => ({ ok: true })
3085
- );
3086
- app.delete<{ Params: { name: string } }>(
3087
- '/api/teams/:name/permanent', async () => ({ ok: true })
3088
- );
3311
+ app.post<{ Params: { name: string } }>('/api/teams/:name/restore', async () => ({ ok: true }));
3312
+ app.delete<{ Params: { name: string } }>('/api/teams/:name/permanent', async () => ({ ok: true }));
3089
3313
 
3090
3314
  // config operations
3091
3315
  async function applyTeamConfigUpdate(
@@ -3097,11 +3321,9 @@ async function applyTeamConfigUpdate(
3097
3321
  const color = typeof body.color === 'string' ? body.color.trim() : '';
3098
3322
  const agentType = typeof body.agentType === 'string' ? body.agentType.trim() : '';
3099
3323
  const workDir = typeof body.workDir === 'string' ? body.workDir.trim() : '';
3100
- const permissionMode =
3101
- typeof body.permissionMode === 'string' ? body.permissionMode.trim() : '';
3324
+ const permissionMode = typeof body.permissionMode === 'string' ? body.permissionMode.trim() : '';
3102
3325
  const language = typeof body.language === 'string' ? body.language.trim() : '';
3103
- const managedSources =
3104
- typeof body.managedSources === 'string' ? body.managedSources.trim() : '';
3326
+ const managedSources = typeof body.managedSources === 'string' ? body.managedSources.trim() : '';
3105
3327
  const showContextIndicator =
3106
3328
  typeof body.showContextIndicator === 'boolean' ? body.showContextIndicator : undefined;
3107
3329
  const replyFooter = typeof body.replyFooter === 'boolean' ? body.replyFooter : undefined;
@@ -3193,133 +3415,131 @@ async function applyTeamConfigUpdate(
3193
3415
  };
3194
3416
  }
3195
3417
 
3196
- app.get<{ Params: { name: string } }>(
3197
- '/api/teams/:name/config', async (request, reply) => {
3418
+ app.get<{ Params: { name: string } }>('/api/teams/:name/config', async (request, reply) => {
3419
+ try {
3420
+ const name = request.params.name;
3421
+ const p = await cc.getProject(name);
3422
+ // local metadata overlay
3423
+ let color = 'blue';
3424
+ let description = '';
3425
+ let language = '';
3426
+ let managedSources = '*';
3427
+ let disabledCommands: string[] = [];
3428
+ let showContextIndicator = false;
3429
+ let replyFooter = false;
3430
+ let injectSender = false;
3431
+ let permissionMode = 'default';
3432
+ let platformAllowFrom: Record<string, string> = {};
3198
3433
  try {
3199
- const name = request.params.name;
3200
- const p = await cc.getProject(name);
3201
- // local metadata overlay
3202
- let color = 'blue';
3203
- let description = '';
3204
- let language = '';
3205
- let managedSources = '*';
3206
- let disabledCommands: string[] = [];
3207
- let showContextIndicator = false;
3208
- let replyFooter = false;
3209
- let injectSender = false;
3210
- let permissionMode = 'default';
3211
- let platformAllowFrom: Record<string, string> = {};
3212
- try {
3213
- const meta = await svc.readTeamManifest(name);
3214
- color = meta.color ?? color;
3215
- description = meta.description ?? description;
3216
- language = meta.language ?? language;
3217
- managedSources = meta.managedSources ?? managedSources;
3218
- disabledCommands = normalizeStringArray(meta.disabledCommands);
3219
- showContextIndicator = meta.showContextIndicator ?? showContextIndicator;
3220
- replyFooter = meta.replyFooter ?? replyFooter;
3221
- injectSender = meta.injectSender ?? injectSender;
3222
- permissionMode = meta.permissionMode ?? permissionMode;
3223
- platformAllowFrom = normalizePlatformAllowFrom(meta.platformAllowFrom);
3224
- } catch { /* ok */ }
3225
- const projectSettings = (p.settings ?? {}) as Record<string, unknown>;
3226
- const resolvedLanguage =
3227
- typeof projectSettings.language === 'string' && projectSettings.language.trim().length > 0
3228
- ? projectSettings.language.trim()
3229
- : language;
3230
- const resolvedManagedSources =
3231
- typeof projectSettings.admin_from === 'string' && projectSettings.admin_from.trim().length > 0
3232
- ? projectSettings.admin_from.trim()
3233
- : managedSources;
3234
- const resolvedDisabledCommands =
3235
- Array.isArray(projectSettings.disabled_commands) &&
3236
- normalizeStringArray(projectSettings.disabled_commands).length > 0
3237
- ? normalizeStringArray(projectSettings.disabled_commands)
3238
- : disabledCommands;
3239
- const resolvedShowContextIndicator =
3240
- typeof projectSettings.show_context_indicator === 'boolean'
3241
- ? projectSettings.show_context_indicator
3242
- : showContextIndicator;
3243
- const resolvedReplyFooter =
3244
- typeof projectSettings.reply_footer === 'boolean'
3245
- ? projectSettings.reply_footer
3246
- : replyFooter;
3247
- const resolvedInjectSender =
3248
- typeof projectSettings.inject_sender === 'boolean'
3249
- ? projectSettings.inject_sender
3250
- : injectSender;
3251
- const resolvedPlatformAllowFrom = (() => {
3252
- const normalized = normalizePlatformAllowFrom(projectSettings.platform_allow_from);
3253
- if (Object.keys(normalized).length > 0) {
3254
- return normalized;
3255
- }
3256
- return platformAllowFrom;
3257
- })();
3258
- const resolvedPermissionMode =
3259
- typeof p.agent_mode === 'string' && p.agent_mode.trim().length > 0
3260
- ? p.agent_mode.trim()
3261
- : permissionMode;
3262
- return {
3263
- name,
3264
- color,
3265
- projectPath: p.work_dir ?? '',
3266
- description,
3267
- agentType: p.agent_type,
3268
- workDir: p.work_dir ?? '',
3434
+ const meta = await svc.readTeamManifest(name);
3435
+ color = meta.color ?? color;
3436
+ description = meta.description ?? description;
3437
+ language = meta.language ?? language;
3438
+ managedSources = meta.managedSources ?? managedSources;
3439
+ disabledCommands = normalizeStringArray(meta.disabledCommands);
3440
+ showContextIndicator = meta.showContextIndicator ?? showContextIndicator;
3441
+ replyFooter = meta.replyFooter ?? replyFooter;
3442
+ injectSender = meta.injectSender ?? injectSender;
3443
+ permissionMode = meta.permissionMode ?? permissionMode;
3444
+ platformAllowFrom = normalizePlatformAllowFrom(meta.platformAllowFrom);
3445
+ } catch {
3446
+ /* ok */
3447
+ }
3448
+ const projectSettings = (p.settings ?? {}) as Record<string, unknown>;
3449
+ const resolvedLanguage =
3450
+ typeof projectSettings.language === 'string' && projectSettings.language.trim().length > 0
3451
+ ? projectSettings.language.trim()
3452
+ : language;
3453
+ const resolvedManagedSources =
3454
+ typeof projectSettings.admin_from === 'string' && projectSettings.admin_from.trim().length > 0
3455
+ ? projectSettings.admin_from.trim()
3456
+ : managedSources;
3457
+ const resolvedDisabledCommands =
3458
+ Array.isArray(projectSettings.disabled_commands) &&
3459
+ normalizeStringArray(projectSettings.disabled_commands).length > 0
3460
+ ? normalizeStringArray(projectSettings.disabled_commands)
3461
+ : disabledCommands;
3462
+ const resolvedShowContextIndicator =
3463
+ typeof projectSettings.show_context_indicator === 'boolean'
3464
+ ? projectSettings.show_context_indicator
3465
+ : showContextIndicator;
3466
+ const resolvedReplyFooter =
3467
+ typeof projectSettings.reply_footer === 'boolean'
3468
+ ? projectSettings.reply_footer
3469
+ : replyFooter;
3470
+ const resolvedInjectSender =
3471
+ typeof projectSettings.inject_sender === 'boolean'
3472
+ ? projectSettings.inject_sender
3473
+ : injectSender;
3474
+ const resolvedPlatformAllowFrom = (() => {
3475
+ const normalized = normalizePlatformAllowFrom(projectSettings.platform_allow_from);
3476
+ if (Object.keys(normalized).length > 0) {
3477
+ return normalized;
3478
+ }
3479
+ return platformAllowFrom;
3480
+ })();
3481
+ const resolvedPermissionMode =
3482
+ typeof p.agent_mode === 'string' && p.agent_mode.trim().length > 0
3483
+ ? p.agent_mode.trim()
3484
+ : permissionMode;
3485
+ return {
3486
+ name,
3487
+ color,
3488
+ projectPath: p.work_dir ?? '',
3489
+ description,
3490
+ agentType: p.agent_type,
3491
+ workDir: p.work_dir ?? '',
3492
+ language: resolvedLanguage,
3493
+ managedSources: resolvedManagedSources,
3494
+ disabledCommands: resolvedDisabledCommands,
3495
+ showContextIndicator: resolvedShowContextIndicator,
3496
+ replyFooter: resolvedReplyFooter,
3497
+ injectSender: resolvedInjectSender,
3498
+ permissionMode: resolvedPermissionMode,
3499
+ platformAllowFrom: resolvedPlatformAllowFrom,
3500
+ settings: {
3501
+ ...projectSettings,
3269
3502
  language: resolvedLanguage,
3270
- managedSources: resolvedManagedSources,
3271
- disabledCommands: resolvedDisabledCommands,
3272
- showContextIndicator: resolvedShowContextIndicator,
3273
- replyFooter: resolvedReplyFooter,
3274
- injectSender: resolvedInjectSender,
3275
- permissionMode: resolvedPermissionMode,
3276
- platformAllowFrom: resolvedPlatformAllowFrom,
3277
- settings: {
3278
- ...projectSettings,
3279
- language: resolvedLanguage,
3280
- admin_from: resolvedManagedSources,
3281
- disabled_commands: resolvedDisabledCommands,
3282
- show_context_indicator: resolvedShowContextIndicator,
3283
- reply_footer: resolvedReplyFooter,
3284
- inject_sender: resolvedInjectSender,
3285
- platform_allow_from: resolvedPlatformAllowFrom,
3286
- },
3287
- };
3288
- } catch { return reply.code(404).send({ error: 'not found' }); }
3503
+ admin_from: resolvedManagedSources,
3504
+ disabled_commands: resolvedDisabledCommands,
3505
+ show_context_indicator: resolvedShowContextIndicator,
3506
+ reply_footer: resolvedReplyFooter,
3507
+ inject_sender: resolvedInjectSender,
3508
+ platform_allow_from: resolvedPlatformAllowFrom,
3509
+ },
3510
+ };
3511
+ } catch {
3512
+ return reply.code(404).send({ error: 'not found' });
3289
3513
  }
3290
- );
3291
- app.patch<{ Params: { name: string } }>(
3292
- '/api/teams/:name/config', async (request, reply) => {
3293
- try {
3294
- const data = await applyTeamConfigUpdate(
3295
- request.params.name,
3296
- (request.body as Record<string, unknown>) ?? {}
3297
- );
3298
- return data;
3299
- } catch (err) {
3300
- return reply.code(400).send(reply500(err));
3301
- }
3514
+ });
3515
+ app.patch<{ Params: { name: string } }>('/api/teams/:name/config', async (request, reply) => {
3516
+ try {
3517
+ const data = await applyTeamConfigUpdate(
3518
+ request.params.name,
3519
+ (request.body as Record<string, unknown>) ?? {}
3520
+ );
3521
+ return data;
3522
+ } catch (err) {
3523
+ return reply.code(400).send(reply500(err));
3302
3524
  }
3303
- );
3525
+ });
3304
3526
 
3305
3527
  // provisioning stubs (新版无 provisioning 概念)
3306
3528
  app.post('/api/teams/provisioning/prepare', async () => ({
3307
3529
  runId: null,
3308
3530
  warnings: [],
3309
3531
  }));
3310
- app.get<{ Params: { runId: string } }>(
3311
- '/api/teams/provisioning/:runId', async () => ({
3312
- runId: '',
3313
- phase: 'done',
3314
- progress: 100,
3315
- message: '',
3316
- done: true,
3317
- error: null,
3318
- })
3319
- );
3320
- app.post<{ Params: { runId: string } }>(
3321
- '/api/teams/provisioning/:runId/cancel', async () => ({ ok: true })
3322
- );
3532
+ app.get<{ Params: { runId: string } }>('/api/teams/provisioning/:runId', async () => ({
3533
+ runId: '',
3534
+ phase: 'done',
3535
+ progress: 100,
3536
+ message: '',
3537
+ done: true,
3538
+ error: null,
3539
+ }));
3540
+ app.post<{ Params: { runId: string } }>('/api/teams/provisioning/:runId/cancel', async () => ({
3541
+ ok: true,
3542
+ }));
3323
3543
 
3324
3544
  // 团队创建已由上方 /api/teams/create 处理(cc-connect 直接调用)
3325
3545
 
@@ -3329,68 +3549,65 @@ app.post('/api/teams/templates/save', async () => ({ sources: [], templates: []
3329
3549
  app.post('/api/teams/templates/refresh', async () => ({ sources: [], templates: [] }));
3330
3550
 
3331
3551
  // replace members
3332
- app.put<{ Params: { name: string } }>(
3333
- '/api/teams/:name/members', async () => ({ ok: true })
3334
- );
3552
+ app.put<{ Params: { name: string } }>('/api/teams/:name/members', async () => ({ ok: true }));
3335
3553
 
3336
3554
  // draft
3337
- app.delete<{ Params: { name: string } }>(
3338
- '/api/teams/:name/draft', async () => ({ ok: true })
3339
- );
3555
+ app.delete<{ Params: { name: string } }>('/api/teams/:name/draft', async () => ({ ok: true }));
3340
3556
 
3341
3557
  // send-message — 从 Hermit 会话面板注入到 harness,不使用 Management /send(那会回发到 IM)。
3342
- app.post<{ Params: { name: string }; Body: { member?: string; text?: string; content?: string; summary?: string; sessionKey?: string } }>(
3343
- '/api/teams/:name/send-message', async (request, reply) => {
3344
- const teamName = request.params.name;
3345
- const text = request.body?.text ?? request.body?.content ?? '';
3346
- if (!text.trim()) return { ok: true, messageId: null };
3558
+ app.post<{
3559
+ Params: { name: string };
3560
+ Body: { member?: string; text?: string; content?: string; summary?: string; sessionKey?: string };
3561
+ }>('/api/teams/:name/send-message', async (request, reply) => {
3562
+ const teamName = request.params.name;
3563
+ const text = request.body?.text ?? request.body?.content ?? '';
3564
+ if (!text.trim()) return { ok: true, messageId: null };
3347
3565
 
3348
- const msgId = `hermit-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
3566
+ const msgId = `hermit-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
3349
3567
 
3350
- // 使用固定格式 session key,保证 reply 事件能正确映射回 teamName
3351
- const requestedSessionKey =
3352
- typeof request.body?.sessionKey === 'string' ? request.body.sessionKey.trim() : '';
3353
- let sessionKey = requestedSessionKey;
3568
+ // 使用固定格式 session key,保证 reply 事件能正确映射回 teamName
3569
+ const requestedSessionKey =
3570
+ typeof request.body?.sessionKey === 'string' ? request.body.sessionKey.trim() : '';
3571
+ let sessionKey = requestedSessionKey;
3354
3572
 
3355
- try {
3356
- sessionKey = await sendHarnessMessageViaBridge({
3357
- teamName,
3358
- text,
3359
- sessionKey,
3360
- msgId,
3361
- });
3362
- } catch (err) {
3363
- return reply.code(502).send({
3364
- ok: false,
3365
- error: err instanceof Error ? err.message : '发送到 harness 失败',
3366
- });
3367
- }
3573
+ try {
3574
+ sessionKey = await sendHarnessMessageViaBridge({
3575
+ teamName,
3576
+ text,
3577
+ sessionKey,
3578
+ msgId,
3579
+ });
3580
+ } catch (err) {
3581
+ return reply.code(502).send({
3582
+ ok: false,
3583
+ error: err instanceof Error ? err.message : '发送到 harness 失败',
3584
+ });
3585
+ }
3368
3586
 
3369
- // 本地存储用户消息
3370
- const userMsg = await svc.appendMessage(teamName, {
3587
+ // 本地存储用户消息
3588
+ const userMsg = await svc
3589
+ .appendMessage(teamName, {
3371
3590
  from: 'user',
3372
3591
  to: teamName,
3373
3592
  role: 'user',
3374
3593
  content: text,
3375
3594
  meta: { sessionKey },
3376
- }).catch(() => null);
3377
-
3378
- // 广播 SSE 让前端触发消息刷新
3379
- broadcastSse('team-change', { type: 'inbox', teamName });
3380
-
3381
- return {
3382
- ok: true,
3383
- deliveredToInbox: true,
3384
- messageId: userMsg?.id ?? msgId,
3385
- runtimeDelivery: {
3386
- attempted: true,
3387
- delivered: true,
3388
- },
3389
- };
3390
- }
3391
- );
3595
+ })
3596
+ .catch(() => null);
3392
3597
 
3598
+ // 广播 SSE 让前端触发消息刷新
3599
+ broadcastSse('team-change', { type: 'inbox', teamName });
3393
3600
 
3601
+ return {
3602
+ ok: true,
3603
+ deliveredToInbox: true,
3604
+ messageId: userMsg?.id ?? msgId,
3605
+ runtimeDelivery: {
3606
+ attempted: true,
3607
+ delivered: true,
3608
+ },
3609
+ };
3610
+ });
3394
3611
 
3395
3612
  // ===========================================================================
3396
3613
  // 路由别名 — 修正前端调用路径与服务端路径的不匹配
@@ -3398,52 +3615,57 @@ app.post<{ Params: { name: string }; Body: { member?: string; text?: string; con
3398
3615
 
3399
3616
  // requestReview: 前端调用 /tasks/:id/review,服务端原路由是 /tasks/:id/request-review
3400
3617
  app.post<{ Params: { name: string; id: string } }>(
3401
- '/api/teams/:name/tasks/:id/review', async (request) => {
3618
+ '/api/teams/:name/tasks/:id/review',
3619
+ async (request) => {
3402
3620
  try {
3403
3621
  const task = await svc.patchTask(request.params.name, request.params.id, { status: 'done' });
3404
3622
  return { ok: true, data: toTeamTask(task) };
3405
- } catch { return { ok: true }; }
3623
+ } catch {
3624
+ return { ok: true };
3625
+ }
3406
3626
  }
3407
3627
  );
3408
3628
 
3409
3629
  // updateKanban: 前端调用 PATCH /kanban/:taskId
3410
3630
  app.patch<{ Params: { name: string; id: string }; Body: Record<string, unknown> }>(
3411
- '/api/teams/:name/kanban/:id', async () => ({ ok: true })
3631
+ '/api/teams/:name/kanban/:id',
3632
+ async () => ({ ok: true })
3412
3633
  );
3413
3634
 
3414
3635
  // updateKanbanColumnOrder: 前端调用 PUT /kanban/column-order
3415
- app.put<{ Params: { name: string } }>(
3416
- '/api/teams/:name/kanban/column-order', async () => ({ ok: true })
3417
- );
3636
+ app.put<{ Params: { name: string } }>('/api/teams/:name/kanban/column-order', async () => ({
3637
+ ok: true,
3638
+ }));
3418
3639
 
3419
3640
  // updateConfig: 前端调用 PUT /config(服务端原有 PATCH,补充 PUT 别名)
3420
- app.put<{ Params: { name: string } }>(
3421
- '/api/teams/:name/config', async (request, reply) => {
3422
- try {
3423
- const data = await applyTeamConfigUpdate(
3424
- request.params.name,
3425
- (request.body as Record<string, unknown>) ?? {}
3426
- );
3427
- return data;
3428
- } catch (err) {
3429
- return reply.code(400).send(reply500(err));
3430
- }
3641
+ app.put<{ Params: { name: string } }>('/api/teams/:name/config', async (request, reply) => {
3642
+ try {
3643
+ const data = await applyTeamConfigUpdate(
3644
+ request.params.name,
3645
+ (request.body as Record<string, unknown>) ?? {}
3646
+ );
3647
+ return data;
3648
+ } catch (err) {
3649
+ return reply.code(400).send(reply500(err));
3431
3650
  }
3432
- );
3651
+ });
3433
3652
 
3434
3653
  // skipMemberForLaunch: 前端调用 /members/:memberName/skip
3435
3654
  app.post<{ Params: { name: string; memberName: string } }>(
3436
- '/api/teams/:name/members/:memberName/skip', async () => ({ ok: true })
3655
+ '/api/teams/:name/members/:memberName/skip',
3656
+ async () => ({ ok: true })
3437
3657
  );
3438
3658
 
3439
3659
  // setTaskClarification: 前端调用 POST /task-clarification/:taskId
3440
3660
  app.post<{ Params: { name: string; taskId: string } }>(
3441
- '/api/teams/:name/task-clarification/:taskId', async () => ({ ok: true })
3661
+ '/api/teams/:name/task-clarification/:taskId',
3662
+ async () => ({ ok: true })
3442
3663
  );
3443
3664
 
3444
3665
  // removeTaskRelationship: 前端调用 DELETE /tasks/:id/relationships
3445
3666
  app.delete<{ Params: { name: string; id: string } }>(
3446
- '/api/teams/:name/tasks/:id/relationships', async () => ({ ok: true })
3667
+ '/api/teams/:name/tasks/:id/relationships',
3668
+ async () => ({ ok: true })
3447
3669
  );
3448
3670
 
3449
3671
  // ===========================================================================
@@ -3455,52 +3677,58 @@ app.post('/api/teams/config', async () => ({ ok: true }));
3455
3677
 
3456
3678
  // kill-process
3457
3679
  app.post<{ Params: { name: string }; Body: { pid?: number } }>(
3458
- '/api/teams/:name/kill-process', async () => ({ ok: true })
3680
+ '/api/teams/:name/kill-process',
3681
+ async () => ({ ok: true })
3459
3682
  );
3460
3683
 
3461
3684
  // member-logs
3462
3685
  app.get<{ Params: { name: string; memberName: string } }>(
3463
- '/api/teams/:name/member-logs/:memberName', async () => []
3686
+ '/api/teams/:name/member-logs/:memberName',
3687
+ async () => []
3464
3688
  );
3465
3689
 
3466
3690
  // task-logs
3467
3691
  app.get<{ Params: { name: string; taskId: string } }>(
3468
- '/api/teams/:name/task-logs/:taskId', async () => []
3692
+ '/api/teams/:name/task-logs/:taskId',
3693
+ async () => []
3469
3694
  );
3470
3695
 
3471
3696
  // activity
3472
- app.get<{ Params: { name: string } }>(
3473
- '/api/teams/:name/activity', async () => []
3474
- );
3697
+ app.get<{ Params: { name: string } }>('/api/teams/:name/activity', async () => []);
3475
3698
 
3476
3699
  // task-activity-detail
3477
- app.get<{ Params: { name: string } }>(
3478
- '/api/teams/:name/task-activity-detail', async () => ({ entries: [] })
3479
- );
3700
+ app.get<{ Params: { name: string } }>('/api/teams/:name/task-activity-detail', async () => ({
3701
+ entries: [],
3702
+ }));
3480
3703
 
3481
3704
  // task-log-stream-summary
3482
3705
  app.get<{ Params: { name: string; taskId: string } }>(
3483
- '/api/teams/:name/task-log-stream-summary/:taskId', async () => ({ chunks: [] })
3706
+ '/api/teams/:name/task-log-stream-summary/:taskId',
3707
+ async () => ({ chunks: [] })
3484
3708
  );
3485
3709
 
3486
3710
  // task-log-stream
3487
3711
  app.get<{ Params: { name: string; taskId: string } }>(
3488
- '/api/teams/:name/task-log-stream/:taskId', async () => ({ chunks: [] })
3712
+ '/api/teams/:name/task-log-stream/:taskId',
3713
+ async () => ({ chunks: [] })
3489
3714
  );
3490
3715
 
3491
3716
  // exact-log-summaries
3492
3717
  app.get<{ Params: { name: string; taskId: string } }>(
3493
- '/api/teams/:name/exact-log-summaries/:taskId', async () => ({ logs: [] })
3718
+ '/api/teams/:name/exact-log-summaries/:taskId',
3719
+ async () => ({ logs: [] })
3494
3720
  );
3495
3721
 
3496
3722
  // exact-log-detail
3497
3723
  app.get<{ Params: { name: string; taskId: string } }>(
3498
- '/api/teams/:name/exact-log-detail/:taskId', async () => ({ lines: [] })
3724
+ '/api/teams/:name/exact-log-detail/:taskId',
3725
+ async () => ({ lines: [] })
3499
3726
  );
3500
3727
 
3501
3728
  // member-stats
3502
3729
  app.get<{ Params: { name: string; memberName: string } }>(
3503
- '/api/teams/:name/member-stats/:memberName', async () => ({
3730
+ '/api/teams/:name/member-stats/:memberName',
3731
+ async () => ({
3504
3732
  linesAdded: 0,
3505
3733
  linesRemoved: 0,
3506
3734
  filesTouched: [],
@@ -3519,12 +3747,12 @@ app.get<{ Params: { name: string; memberName: string } }>(
3519
3747
  );
3520
3748
 
3521
3749
  // tool-approval stubs
3522
- app.post<{ Params: { name: string } }>(
3523
- '/api/teams/:name/tool-approval/respond', async () => ({ ok: true })
3524
- );
3525
- app.post<{ Params: { name: string } }>(
3526
- '/api/teams/:name/tool-approval/settings', async () => ({ ok: true })
3527
- );
3750
+ app.post<{ Params: { name: string } }>('/api/teams/:name/tool-approval/respond', async () => ({
3751
+ ok: true,
3752
+ }));
3753
+ app.post<{ Params: { name: string } }>('/api/teams/:name/tool-approval/settings', async () => ({
3754
+ ok: true,
3755
+ }));
3528
3756
  app.post('/api/teams/tool-approval/read-file', async () => ({ content: '' }));
3529
3757
 
3530
3758
  // validate-cli-args
@@ -3533,13 +3761,12 @@ app.post('/api/teams/validate-cli-args', async () => ({ valid: true, args: [], e
3533
3761
  // cross-team stubs
3534
3762
  app.post('/api/cross-team/send', async () => ({ ok: true }));
3535
3763
  app.get('/api/cross-team/targets', async () => []);
3536
- app.get<{ Params: { name: string } }>(
3537
- '/api/cross-team/outbox/:name', async () => []
3538
- );
3764
+ app.get<{ Params: { name: string } }>('/api/cross-team/outbox/:name', async () => []);
3539
3765
 
3540
3766
  // review stubs
3541
3767
  app.get<{ Params: { name: string; memberName: string } }>(
3542
- '/api/teams/:name/review/agent-changes/:memberName', async (request) => ({
3768
+ '/api/teams/:name/review/agent-changes/:memberName',
3769
+ async (request) => ({
3543
3770
  teamName: request.params.name,
3544
3771
  memberName: request.params.memberName,
3545
3772
  files: [],
@@ -3550,17 +3777,19 @@ app.get<{ Params: { name: string; memberName: string } }>(
3550
3777
  })
3551
3778
  );
3552
3779
  app.get<{ Params: { name: string; taskId: string } }>(
3553
- '/api/teams/:name/review/task-changes/:taskId', async () => ({ changes: [] })
3780
+ '/api/teams/:name/review/task-changes/:taskId',
3781
+ async () => ({ changes: [] })
3554
3782
  );
3555
3783
  app.get<{ Params: { name: string; memberName: string } }>(
3556
- '/api/teams/:name/review/change-stats/:memberName', async () => ({ stats: {} })
3557
- );
3558
- app.get<{ Params: { name: string } }>(
3559
- '/api/teams/:name/review/file-content', async () => ({ content: '' })
3560
- );
3561
- app.post<{ Params: { name: string } }>(
3562
- '/api/teams/:name/review/apply-decisions', async () => ({ ok: true })
3784
+ '/api/teams/:name/review/change-stats/:memberName',
3785
+ async () => ({ stats: {} })
3563
3786
  );
3787
+ app.get<{ Params: { name: string } }>('/api/teams/:name/review/file-content', async () => ({
3788
+ content: '',
3789
+ }));
3790
+ app.post<{ Params: { name: string } }>('/api/teams/:name/review/apply-decisions', async () => ({
3791
+ ok: true,
3792
+ }));
3564
3793
  app.post('/api/teams/review/check-conflict', async () => ({ conflict: false }));
3565
3794
  app.post('/api/teams/review/preview-reject', async () => ({ preview: '' }));
3566
3795
  app.post('/api/teams/review/save-edited-file', async () => ({ ok: true }));