@yancyyu/openhermit 1.6.28 → 1.6.29

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. package/dist-renderer/assets/ProjectEditorOverlay-CQm6jUR1.js +52 -0
  2. package/dist-renderer/assets/{TeamGraphOverlay-Ba5njic5.js → TeamGraphOverlay-h0WDfifv.js} +1 -1
  3. package/dist-renderer/assets/{_basePickBy-BvnK-OC1.js → _basePickBy-CgG_tjgX.js} +1 -1
  4. package/dist-renderer/assets/{_baseUniq-DmFYXx9G.js → _baseUniq-DwPTU9lP.js} +1 -1
  5. package/dist-renderer/assets/{arc-DX4ZQFY4.js → arc-7nIrGRzY.js} +1 -1
  6. package/dist-renderer/assets/{architectureDiagram-VXUJARFQ-DfYr3vEN.js → architectureDiagram-VXUJARFQ-BYhA6Ev2.js} +1 -1
  7. package/dist-renderer/assets/{blockDiagram-VD42YOAC-DuXdVeWn.js → blockDiagram-VD42YOAC-BVpZUGDg.js} +1 -1
  8. package/dist-renderer/assets/{c4Diagram-YG6GDRKO-Bw2nixXe.js → c4Diagram-YG6GDRKO-DsdreMQ9.js} +1 -1
  9. package/dist-renderer/assets/channel-C0SqeFU7.js +1 -0
  10. package/dist-renderer/assets/{chunk-4BX2VUAB-DLiNGQoE.js → chunk-4BX2VUAB-CcoAs7Jd.js} +1 -1
  11. package/dist-renderer/assets/{chunk-55IACEB6-B1L_8VIF.js → chunk-55IACEB6-CGGAOoXd.js} +1 -1
  12. package/dist-renderer/assets/{chunk-B4BG7PRW-DaZMWKGk.js → chunk-B4BG7PRW-FhpTEPvD.js} +1 -1
  13. package/dist-renderer/assets/{chunk-DI55MBZ5-ku-dflJG.js → chunk-DI55MBZ5-DoYySbm1.js} +1 -1
  14. package/dist-renderer/assets/{chunk-FMBD7UC4-DV-mF1dP.js → chunk-FMBD7UC4-e9l2tGHG.js} +1 -1
  15. package/dist-renderer/assets/{chunk-QN33PNHL-ByGcDFQ0.js → chunk-QN33PNHL-DeiXVTCy.js} +1 -1
  16. package/dist-renderer/assets/{chunk-QZHKN3VN-7dv-Min8.js → chunk-QZHKN3VN-DC2UJLJM.js} +1 -1
  17. package/dist-renderer/assets/{chunk-TZMSLE5B-WdXL5fTu.js → chunk-TZMSLE5B-BHFD9eZI.js} +1 -1
  18. package/dist-renderer/assets/classDiagram-2ON5EDUG-DWew1HpM.js +1 -0
  19. package/dist-renderer/assets/classDiagram-v2-WZHVMYZB-DWew1HpM.js +1 -0
  20. package/dist-renderer/assets/clone-Dm-k63Yr.js +1 -0
  21. package/dist-renderer/assets/{cose-bilkent-S5V4N54A-CNcsvqPl.js → cose-bilkent-S5V4N54A-BdybQraU.js} +1 -1
  22. package/dist-renderer/assets/{dagre-6UL2VRFP-DBNx4qqx.js → dagre-6UL2VRFP-DdF3pwM3.js} +1 -1
  23. package/dist-renderer/assets/{diagram-PSM6KHXK-BfVlT6sT.js → diagram-PSM6KHXK-B9Ldd3nh.js} +1 -1
  24. package/dist-renderer/assets/{diagram-QEK2KX5R-HvVjs0K6.js → diagram-QEK2KX5R-XEqkrbpu.js} +1 -1
  25. package/dist-renderer/assets/{diagram-S2PKOQOG-DYb_KnWS.js → diagram-S2PKOQOG-CipwtY59.js} +1 -1
  26. package/dist-renderer/assets/{erDiagram-Q2GNP2WA-Ba-IgI5G.js → erDiagram-Q2GNP2WA-BB-2ISGo.js} +1 -1
  27. package/dist-renderer/assets/{flowDiagram-NV44I4VS-2iDN8Kpj.js → flowDiagram-NV44I4VS-B8XmJ0u2.js} +1 -1
  28. package/dist-renderer/assets/{ganttDiagram-JELNMOA3-Byjf8Fa3.js → ganttDiagram-JELNMOA3-D-8XglBb.js} +1 -1
  29. package/dist-renderer/assets/{gitGraphDiagram-V2S2FVAM-DbKvfZ_j.js → gitGraphDiagram-V2S2FVAM-DL4ChakD.js} +1 -1
  30. package/dist-renderer/assets/{graph-Enirf-f8.js → graph-BiFNoBjP.js} +1 -1
  31. package/dist-renderer/assets/{index-AjxP_rE_.js → index-6m1ZAymG.js} +1 -1
  32. package/dist-renderer/assets/index-BhellmRb.css +1 -0
  33. package/dist-renderer/assets/{index-DY1zqsb6.js → index-BowUl0Jb.js} +540 -536
  34. package/dist-renderer/assets/{index-CtlzGepK.js → index-Dp3kJTEe.js} +1 -1
  35. package/dist-renderer/assets/{index-COZPUWJW.js → index-TOpt_T7A.js} +1 -1
  36. package/dist-renderer/assets/{index-DdhqolqE.js → index-qNBNjW4K.js} +1 -1
  37. package/dist-renderer/assets/{index-ChR1D6ZF.js → index-vAykq1H1.js} +1 -1
  38. package/dist-renderer/assets/{infoDiagram-HS3SLOUP-D6uicwz1.js → infoDiagram-HS3SLOUP-DRIBfHDi.js} +1 -1
  39. package/dist-renderer/assets/{journeyDiagram-XKPGCS4Q-DqwZsXlQ.js → journeyDiagram-XKPGCS4Q-BOMiigU4.js} +1 -1
  40. package/dist-renderer/assets/{kanban-definition-3W4ZIXB7-fCDVhVUm.js → kanban-definition-3W4ZIXB7-DDxeyjod.js} +1 -1
  41. package/dist-renderer/assets/{layout-CPFgj98r.js → layout-DNANbrI4.js} +1 -1
  42. package/dist-renderer/assets/{linear-CYiQ7Y3M.js → linear-DxEJi1yT.js} +1 -1
  43. package/dist-renderer/assets/{mindmap-definition-VGOIOE7T-D31dS2KE.js → mindmap-definition-VGOIOE7T-nBfGriW8.js} +1 -1
  44. package/dist-renderer/assets/{pieDiagram-ADFJNKIX-BOsCJfds.js → pieDiagram-ADFJNKIX-Din5j6sV.js} +1 -1
  45. package/dist-renderer/assets/{quadrantDiagram-AYHSOK5B-CYTVQCfr.js → quadrantDiagram-AYHSOK5B-DMVK2BEQ.js} +1 -1
  46. package/dist-renderer/assets/{requirementDiagram-UZGBJVZJ-CODCFpkt.js → requirementDiagram-UZGBJVZJ-6SC94Gg_.js} +1 -1
  47. package/dist-renderer/assets/{sankeyDiagram-TZEHDZUN-Z4ce9ZtZ.js → sankeyDiagram-TZEHDZUN-CD2gghhu.js} +1 -1
  48. package/dist-renderer/assets/{sequenceDiagram-WL72ISMW-CmS9TxhW.js → sequenceDiagram-WL72ISMW-BnhkN7nZ.js} +1 -1
  49. package/dist-renderer/assets/{stateDiagram-FKZM4ZOC-o9k-ns3q.js → stateDiagram-FKZM4ZOC-Bn8XdYX-.js} +1 -1
  50. package/dist-renderer/assets/{stateDiagram-v2-4FDKWEC3-CxHMyEt1.js → stateDiagram-v2-4FDKWEC3-1b6sI1_g.js} +1 -1
  51. package/dist-renderer/assets/{timeline-definition-IT6M3QCI-B6T3zrde.js → timeline-definition-IT6M3QCI-CNs3RPoa.js} +1 -1
  52. package/dist-renderer/assets/treemap-GDKQZRPO-DU_yr827.js +162 -0
  53. package/dist-renderer/assets/{xychartDiagram-PRI3JC2R-CleBrdqc.js → xychartDiagram-PRI3JC2R-B8o5J2f3.js} +1 -1
  54. package/dist-renderer/index.html +2 -2
  55. package/package.json +1 -1
  56. package/src/main/server.ts +699 -179
  57. package/src/main/services/session-intelligence/UsageTelemetryService.ts +33 -18
  58. package/src/main/services/teams-mvp/CollaborationBoardService.ts +310 -0
  59. package/src/main/services/teams-mvp/TaskDispatchService.ts +880 -95
  60. package/src/main/services/teams-mvp/TeamProvisioningService.ts +58 -19
  61. package/src/main/services/teams-mvp/TeamWorkspaceService.ts +25 -2
  62. package/src/main/services/teams-mvp/index.ts +3 -0
  63. package/src/renderer/App.tsx +5 -0
  64. package/src/renderer/api/httpClient.ts +67 -0
  65. package/src/renderer/components/layout/PaneContent.tsx +2 -0
  66. package/src/renderer/components/layout/SortableTab.tsx +1 -0
  67. package/src/renderer/components/layout/TabBarActions.tsx +12 -12
  68. package/src/renderer/components/schedules/SchedulesView.tsx +54 -22
  69. package/src/renderer/components/settings/sections/AdvancedSection.tsx +1 -1
  70. package/src/renderer/components/settings/sections/TaskBusSection.tsx +129 -79
  71. package/src/renderer/components/tasks/TasksView.tsx +343 -0
  72. package/src/renderer/components/team/TeamDetailView.tsx +20 -98
  73. package/src/renderer/components/team/dialogs/LaunchTeamDialog.tsx +1 -1
  74. package/src/renderer/components/team/editor/EditorContextMenu.tsx +8 -23
  75. package/src/renderer/components/team/editor/EditorFileTree.tsx +0 -4
  76. package/src/renderer/components/team/editor/EditorSelectionMenu.tsx +1 -8
  77. package/src/renderer/components/team/editor/ProjectEditorOverlay.tsx +0 -10
  78. package/src/renderer/components/team/kanban/KanbanBoard.tsx +5 -1
  79. package/src/renderer/components/team/members/MemberDetailDialog.tsx +8 -33
  80. package/src/renderer/components/team/messages/MessageComposer.tsx +39 -3
  81. package/src/renderer/components/team/messages/MessagesPanel.tsx +72 -2
  82. package/src/renderer/components/team/messages/StatusBlock.tsx +2 -24
  83. package/src/renderer/components/team/schedule/ScheduleEmptyState.tsx +1 -1
  84. package/src/renderer/components/ui/MentionableTextarea.tsx +0 -1
  85. package/src/renderer/store/slices/scheduleSlice.ts +21 -0
  86. package/src/renderer/store/slices/teamSlice.ts +59 -23
  87. package/src/renderer/types/tabs.ts +1 -0
  88. package/src/shared/types/api.ts +29 -0
  89. package/src/shared/types/team.ts +104 -1
  90. package/dist-renderer/assets/ProjectEditorOverlay-A4DZTvSy.js +0 -57
  91. package/dist-renderer/assets/channel-Pre42N5O.js +0 -1
  92. package/dist-renderer/assets/classDiagram-2ON5EDUG-CdJsTJsj.js +0 -1
  93. package/dist-renderer/assets/classDiagram-v2-WZHVMYZB-CdJsTJsj.js +0 -1
  94. package/dist-renderer/assets/clone-BjQBiNfj.js +0 -1
  95. package/dist-renderer/assets/index-BIOJremZ.css +0 -1
  96. package/dist-renderer/assets/treemap-GDKQZRPO-CVd5GNDw.js +0 -162
@@ -202,7 +202,9 @@ export function TaskBusSection(): React.JSX.Element {
202
202
  const [connected, setConnected] = useState(false);
203
203
  const [message, setMessage] = useState<string | null>(null);
204
204
 
205
- const [telemetryEnabled, setTelemetryEnabled] = useState(false);
205
+ const [collectionEnabled, setCollectionEnabled] = useState(false);
206
+ const [uploadEnabled, setUploadEnabled] = useState(false);
207
+ const [collaborationEnabled, setCollaborationEnabled] = useState(false);
206
208
  const [telemetryPlatform, setTelemetryPlatform] = useState('claudecode');
207
209
  const [scanning, setScanning] = useState(false);
208
210
  const [telemetryStatus, setTelemetryStatus] = useState<TelemetryStatus | null>(null);
@@ -218,9 +220,11 @@ export function TaskBusSection(): React.JSX.Element {
218
220
  setPassword(data.redis.password ?? '');
219
221
  }
220
222
  if (data.telemetry) {
221
- setTelemetryEnabled(data.telemetry.enabled);
223
+ setCollectionEnabled(data.telemetry.enabled);
224
+ setUploadEnabled(data.telemetry.uploadEnabled ?? false);
222
225
  setTelemetryPlatform(data.telemetry.platform ?? 'claudecode');
223
226
  }
227
+ setCollaborationEnabled(data.collaboration ?? false);
224
228
  })
225
229
  .catch(() => {})
226
230
  .finally(() => setLoading(false));
@@ -235,7 +239,7 @@ export function TaskBusSection(): React.JSX.Element {
235
239
  .catch(() => {});
236
240
 
237
241
  const poll = setInterval(() => {
238
- if (telemetryEnabled) {
242
+ if (collectionEnabled) {
239
243
  fetch('/api/telemetry/status')
240
244
  .then((r) => r.json())
241
245
  .then((s: TelemetryStatus) => setTelemetryStatus(s))
@@ -243,19 +247,23 @@ export function TaskBusSection(): React.JSX.Element {
243
247
  }
244
248
  }, 30000);
245
249
  return () => clearInterval(poll);
246
- }, [telemetryEnabled]);
250
+ }, [collectionEnabled]);
247
251
 
248
252
  const buildConfig = (
249
253
  overrides: Partial<{
250
254
  enabled: boolean;
251
- telemetryEnabled: boolean;
255
+ collectionEnabled: boolean;
256
+ uploadEnabled: boolean;
257
+ collaborationEnabled: boolean;
252
258
  telemetryPlatform: string;
253
259
  }> = {}
254
260
  ): TaskBusConfig => ({
255
261
  enabled: overrides.enabled ?? enabled,
256
262
  redis: { host, port, password: password || undefined },
263
+ collaboration: overrides.collaborationEnabled ?? collaborationEnabled,
257
264
  telemetry: {
258
- enabled: overrides.telemetryEnabled ?? telemetryEnabled,
265
+ enabled: overrides.collectionEnabled ?? collectionEnabled,
266
+ uploadEnabled: overrides.uploadEnabled ?? uploadEnabled,
259
267
  platform: (overrides.telemetryPlatform ?? telemetryPlatform) as 'claudecode',
260
268
  },
261
269
  });
@@ -296,45 +304,58 @@ export function TaskBusSection(): React.JSX.Element {
296
304
  .catch(() => setMessage('操作失败'));
297
305
  };
298
306
 
299
- const toggleTelemetry = async (value: boolean) => {
307
+ const toggleCollection = async (value: boolean) => {
308
+ setCollectionEnabled(value);
309
+ const config = buildConfig({ collectionEnabled: value });
310
+ try {
311
+ await fetch('/api/settings/task-bus', {
312
+ method: 'PUT',
313
+ headers: { 'Content-Type': 'application/json' },
314
+ body: JSON.stringify(config),
315
+ });
316
+ if (value) triggerScan();
317
+ else setTelemetryStatus(null);
318
+ } catch {
319
+ setCollectionEnabled(!value);
320
+ setMessage('操作失败');
321
+ }
322
+ };
323
+
324
+ const toggleUpload = async (value: boolean) => {
300
325
  if (!value) {
301
- setTelemetryEnabled(false);
302
- const config = buildConfig({ telemetryEnabled: false });
326
+ setUploadEnabled(false);
327
+ const config = buildConfig({ uploadEnabled: false });
303
328
  fetch('/api/settings/task-bus', {
304
329
  method: 'PUT',
305
330
  headers: { 'Content-Type': 'application/json' },
306
331
  body: JSON.stringify(config),
307
332
  }).catch(() => setMessage('操作失败'));
308
- setTelemetryStatus(null);
309
333
  return;
310
334
  }
311
335
 
312
- // Optimistic update: toggle on immediately
313
- setTelemetryEnabled(true);
314
-
315
- // Test Redis if not already connected
336
+ // Upload requires Redis
316
337
  let redisReady = connected;
317
338
  if (!redisReady) {
318
339
  setMessage('正在测试 Redis 连接...');
319
340
  redisReady = await testRedisConnection();
320
341
  if (!redisReady) {
321
- setTelemetryEnabled(false);
342
+ setUploadEnabled(false);
322
343
  setMessage('Redis 连接失败,无法启用数据上报');
323
344
  return;
324
345
  }
325
346
  }
326
347
 
348
+ setUploadEnabled(true);
327
349
  setMessage(null);
328
- const config = buildConfig({ telemetryEnabled: true });
350
+ const config = buildConfig({ uploadEnabled: true });
329
351
  try {
330
352
  await fetch('/api/settings/task-bus', {
331
353
  method: 'PUT',
332
354
  headers: { 'Content-Type': 'application/json' },
333
355
  body: JSON.stringify(config),
334
356
  });
335
- triggerScan();
336
357
  } catch {
337
- setTelemetryEnabled(false);
358
+ setUploadEnabled(false);
338
359
  setMessage('操作失败');
339
360
  }
340
361
  };
@@ -349,7 +370,7 @@ export function TaskBusSection(): React.JSX.Element {
349
370
  setTelemetryStatus(result);
350
371
  }
351
372
  })
352
- .catch(() => setMessage('采集失败,请检查 Redis 连接'))
373
+ .catch(() => setMessage('采集失败,请检查本地 Claude Code 会话目录'))
353
374
  .finally(() => setScanning(false));
354
375
  };
355
376
 
@@ -362,6 +383,18 @@ export function TaskBusSection(): React.JSX.Element {
362
383
  }).catch(() => {});
363
384
  };
364
385
 
386
+ const toggleCollaboration = (value: boolean) => {
387
+ setCollaborationEnabled(value);
388
+ const config = buildConfig({ collaborationEnabled: value });
389
+ fetch('/api/settings/task-bus', {
390
+ method: 'PUT',
391
+ headers: { 'Content-Type': 'application/json' },
392
+ body: JSON.stringify(config),
393
+ })
394
+ .then(() => setMessage(value ? '已开启分布式团队协作' : '已关闭分布式团队协作'))
395
+ .catch(() => setMessage('操作失败'));
396
+ };
397
+
365
398
  if (loading) {
366
399
  return (
367
400
  <div className="flex items-center justify-center py-12">
@@ -372,11 +405,65 @@ export function TaskBusSection(): React.JSX.Element {
372
405
 
373
406
  return (
374
407
  <div>
408
+ <SettingsSectionHeader title="本地数据采集" icon={<BarChart3 size={12} />} />
409
+
410
+ <SettingRow
411
+ label="数据采集"
412
+ description="扫描本机 ~/.claude/projects 会话文件,采集使用指标;不需要 Redis,也不会上传对话内容"
413
+ >
414
+ <div className="flex items-center gap-2">
415
+ <select
416
+ value={telemetryPlatform}
417
+ onChange={(e) => {
418
+ const nextPlatform = e.target.value;
419
+ setTelemetryPlatform(nextPlatform);
420
+ saveTelemetryPlatform(nextPlatform);
421
+ }}
422
+ className="rounded-md border border-[var(--color-border)] bg-[var(--color-bg)] px-2 py-1 text-xs outline-none focus:border-indigo-500/50"
423
+ >
424
+ <option value="claudecode">Claude Code</option>
425
+ </select>
426
+ <SettingsToggle
427
+ enabled={collectionEnabled}
428
+ onChange={(value) => void toggleCollection(value)}
429
+ />
430
+ </div>
431
+ </SettingRow>
432
+
433
+ {collectionEnabled && (
434
+ <>
435
+ <div
436
+ className="flex items-center gap-3 border-b py-3"
437
+ style={{ borderColor: 'var(--color-border-subtle)' }}
438
+ >
439
+ <Button
440
+ size="sm"
441
+ variant="outline"
442
+ onClick={triggerScan}
443
+ disabled={scanning}
444
+ className="gap-1.5"
445
+ >
446
+ {scanning ? <Loader2 size={12} className="animate-spin" /> : <BarChart3 size={12} />}
447
+ {scanning ? '采集中...' : '立即采集'}
448
+ </Button>
449
+ <span className="text-[10px] text-[var(--color-text-muted)]">
450
+ 本地扫描,不依赖团队总线或 Redis。
451
+ </span>
452
+ </div>
453
+
454
+ {telemetryStatus && (
455
+ <div className="py-3">
456
+ <UsageDashboard status={telemetryStatus} />
457
+ </div>
458
+ )}
459
+ </>
460
+ )}
461
+
375
462
  <SettingsSectionHeader title="团队总线" icon={<Radio size={12} />} />
376
463
 
377
464
  <SettingRow
378
465
  label="启用团队总线"
379
- description="开启后自动为所有团队注入跨团队协作指令到 CLAUDE.md"
466
+ description="用于 Redis 连接、数据上报和跨团队协作;本地数据采集不依赖它"
380
467
  >
381
468
  <SettingsToggle enabled={enabled} onChange={toggle} />
382
469
  </SettingRow>
@@ -388,7 +475,9 @@ export function TaskBusSection(): React.JSX.Element {
388
475
  <div className="flex items-center gap-2 px-1 pb-2">
389
476
  <span className="text-sm font-medium text-red-500">*</span>
390
477
  <span className="text-sm font-medium">Redis</span>
391
- <span className="text-xs text-[var(--color-text-muted)]">(数据上报必填)</span>
478
+ <span className="text-xs text-[var(--color-text-muted)]">
479
+ (上报和跨团队协作必填)
480
+ </span>
392
481
  <div className="ml-auto flex items-center gap-2">
393
482
  {connected ? (
394
483
  <span className="flex items-center gap-1 text-xs text-emerald-500">
@@ -465,78 +554,39 @@ export function TaskBusSection(): React.JSX.Element {
465
554
  </div>
466
555
  </div>
467
556
 
468
- {/* 数据采集 - 不依赖 Redis */}
469
- <div style={{ borderColor: 'var(--color-border-subtle)' }}>
470
- <SettingRow
471
- label="数据采集"
472
- description="扫描本地 ~/.claude/projects 会话文件,采集使用指标(会话、消息、Token、工作时长)"
473
- >
474
- <div className="flex items-center gap-2">
475
- <select
476
- value={telemetryPlatform}
477
- onChange={(e) => {
478
- const nextPlatform = e.target.value;
479
- setTelemetryPlatform(nextPlatform);
480
- saveTelemetryPlatform(nextPlatform);
481
- }}
482
- className="rounded-md border border-[var(--color-border)] bg-[var(--color-bg)] px-2 py-1 text-xs outline-none focus:border-indigo-500/50"
483
- >
484
- <option value="claudecode">Claude Code</option>
485
- </select>
486
- <SettingsToggle
487
- enabled={telemetryEnabled}
488
- onChange={(value) => void toggleTelemetry(value)}
489
- />
490
- </div>
557
+ {/* 数据上报 - 依赖 Redis,最下面 */}
558
+ <div>
559
+ <SettingRow label="数据上报" description="将采集数据上报到 Redis,供团队看板使用">
560
+ <SettingsToggle
561
+ enabled={uploadEnabled}
562
+ onChange={(value) => void toggleUpload(value)}
563
+ />
491
564
  </SettingRow>
492
565
 
493
- {telemetryEnabled && (
494
- <>
495
- <div className="flex items-center gap-3 border-b py-3" style={{ borderColor: 'var(--color-border-subtle)' }}>
496
- <Button
497
- size="sm"
498
- variant="outline"
499
- onClick={triggerScan}
500
- disabled={scanning}
501
- className="gap-1.5"
502
- >
503
- {scanning ? (
504
- <Loader2 size={12} className="animate-spin" />
505
- ) : (
506
- <BarChart3 size={12} />
507
- )}
508
- {scanning ? '采集中...' : '立即采集'}
509
- </Button>
510
- <span className="text-[10px] text-[var(--color-text-muted)]">
511
- 扫描本地 ~/.claude/projects 下的会话文件
512
- </span>
513
- </div>
514
-
515
- {telemetryStatus && (
516
- <div className="py-3">
517
- <UsageDashboard status={telemetryStatus} />
518
- </div>
519
- )}
520
- </>
566
+ {!connected && (
567
+ <div className="flex items-center gap-2 px-1 py-2 text-xs text-amber-500">
568
+ <AlertCircle size={12} />
569
+ <span>数据上报需要 Redis;请先配置并测试 Redis 连接。</span>
570
+ </div>
521
571
  )}
522
572
  </div>
523
573
 
524
- {/* 数据上报 - 依赖 Redis,最下面 */}
574
+ {/* 分布式团队协作 - 最下面 */}
525
575
  <div>
526
576
  <SettingRow
527
- label="数据上报"
528
- description="将采集数据上报到 Redis,供团队看板使用"
577
+ label="分布式团队协作"
578
+ description="开启后 Hermit 平台识别 @团队 并创建跨团队协作任务;Agent 只读取团队列表,不直接派发"
529
579
  >
530
580
  <SettingsToggle
531
- enabled={telemetryEnabled && connected}
532
- onChange={(value) => void toggleTelemetry(value)}
581
+ enabled={collaborationEnabled}
582
+ onChange={(value) => void toggleCollaboration(value)}
533
583
  />
534
584
  </SettingRow>
535
585
 
536
- {!connected && (
586
+ {!connected && collaborationEnabled && (
537
587
  <div className="flex items-center gap-2 px-1 py-2 text-xs text-amber-500">
538
588
  <AlertCircle size={12} />
539
- <span>数据上报需要 Redis;请先配置并测试 Redis 连接。</span>
589
+ <span>跨团队协作需要 Redis 连接。</span>
540
590
  </div>
541
591
  )}
542
592
  </div>
@@ -0,0 +1,343 @@
1
+ import { useCallback, useEffect, useMemo, useState } from 'react';
2
+
3
+ import { Button } from '@renderer/components/ui/button';
4
+ import { cn } from '@renderer/lib/utils';
5
+ import { useStore } from '@renderer/store';
6
+ import { deriveTaskDisplayId } from '@shared/utils/taskIdentity';
7
+ import { Calendar, CheckCircle2, Circle, Columns3, Loader2, RefreshCw } from 'lucide-react';
8
+ import { useShallow } from 'zustand/react/shallow';
9
+
10
+ import { SchedulesView } from '../schedules/SchedulesView';
11
+
12
+ import type { GlobalTask, TeamTaskStatus } from '@shared/types';
13
+
14
+ type TasksSubTab = 'overview' | 'schedules';
15
+ type OverviewStatus = Extract<TeamTaskStatus, 'pending' | 'in_progress' | 'completed'>;
16
+
17
+ const SUB_TABS: { id: TasksSubTab; label: string; icon: React.ReactNode }[] = [
18
+ { id: 'overview', label: '任务总览', icon: <Columns3 size={14} /> },
19
+ { id: 'schedules', label: '定时任务', icon: <Calendar size={14} /> },
20
+ ];
21
+
22
+ const COLUMNS: {
23
+ id: OverviewStatus;
24
+ title: string;
25
+ icon: React.ReactNode;
26
+ headerBg: string;
27
+ bodyBg: string;
28
+ }[] = [
29
+ {
30
+ id: 'pending',
31
+ title: 'TODO',
32
+ icon: <Circle size={14} className="shrink-0 text-[var(--color-text-muted)]" />,
33
+ headerBg: 'rgba(59, 130, 246, 0.22)',
34
+ bodyBg: 'rgba(59, 130, 246, 0.05)',
35
+ },
36
+ {
37
+ id: 'in_progress',
38
+ title: 'IN PROGRESS',
39
+ icon: <Loader2 size={14} className="shrink-0 text-[var(--color-text-muted)]" />,
40
+ headerBg: 'rgba(234, 179, 8, 0.24)',
41
+ bodyBg: 'rgba(234, 179, 8, 0.06)',
42
+ },
43
+ {
44
+ id: 'completed',
45
+ title: 'DONE',
46
+ icon: <CheckCircle2 size={14} className="shrink-0 text-[var(--color-text-muted)]" />,
47
+ headerBg: 'rgba(34, 197, 94, 0.22)',
48
+ bodyBg: 'rgba(34, 197, 94, 0.05)',
49
+ },
50
+ ];
51
+
52
+ function isOverviewStatus(status: TeamTaskStatus): status is OverviewStatus {
53
+ return status === 'pending' || status === 'in_progress' || status === 'completed';
54
+ }
55
+
56
+ function getTaskUpdatedAt(task: GlobalTask): number {
57
+ const raw = task.updatedAt ?? task.createdAt;
58
+ const time = raw ? new Date(raw).getTime() : 0;
59
+ return Number.isFinite(time) ? time : 0;
60
+ }
61
+
62
+ function buildOptionLabel(value: string | null | undefined, fallback: string): string {
63
+ const trimmed = value?.trim();
64
+ return trimmed && trimmed.length > 0 ? trimmed : fallback;
65
+ }
66
+
67
+ export const TasksView = (): React.JSX.Element => {
68
+ const [activeTab, setActiveTab] = useState<TasksSubTab>('overview');
69
+
70
+ return (
71
+ <div className="flex h-full flex-col">
72
+ <div className="flex items-center border-b border-[var(--color-border)] px-4 pt-2">
73
+ {SUB_TABS.map((tab) => (
74
+ <button
75
+ key={tab.id}
76
+ onClick={() => setActiveTab(tab.id)}
77
+ className={cn(
78
+ 'flex items-center gap-1.5 border-b-2 px-4 pb-2 text-sm font-medium transition-colors',
79
+ activeTab === tab.id
80
+ ? 'border-[var(--color-primary)] text-[var(--color-text)]'
81
+ : 'border-transparent text-[var(--color-text-muted)] hover:text-[var(--color-text)]'
82
+ )}
83
+ >
84
+ {tab.icon}
85
+ {tab.label}
86
+ </button>
87
+ ))}
88
+ </div>
89
+ <div className="flex-1 overflow-auto">
90
+ {activeTab === 'overview' && <TaskOverviewPool />}
91
+ {activeTab === 'schedules' && <SchedulesView />}
92
+ </div>
93
+ </div>
94
+ );
95
+ };
96
+
97
+ const TaskOverviewPool = (): React.JSX.Element => {
98
+ const {
99
+ globalTasks,
100
+ globalTasksLoading,
101
+ globalTasksInitialized,
102
+ fetchAllTasks,
103
+ openGlobalTaskDetail,
104
+ } = useStore(
105
+ useShallow((s) => ({
106
+ globalTasks: s.globalTasks,
107
+ globalTasksLoading: s.globalTasksLoading,
108
+ globalTasksInitialized: s.globalTasksInitialized,
109
+ fetchAllTasks: s.fetchAllTasks,
110
+ openGlobalTaskDetail: s.openGlobalTaskDetail,
111
+ }))
112
+ );
113
+ const [teamFilter, setTeamFilter] = useState('all');
114
+ const [statusFilter, setStatusFilter] = useState<'all' | OverviewStatus>('all');
115
+ const [ownerFilter, setOwnerFilter] = useState('all');
116
+
117
+ useEffect(() => {
118
+ void fetchAllTasks();
119
+ }, [fetchAllTasks]);
120
+
121
+ const overviewTasks = useMemo(
122
+ () => globalTasks.filter((task) => isOverviewStatus(task.status) && !task.teamDeleted),
123
+ [globalTasks]
124
+ );
125
+
126
+ const teamOptions = useMemo(
127
+ () =>
128
+ Array.from(
129
+ new Map(overviewTasks.map((task) => [task.teamName, task.teamDisplayName])).entries()
130
+ ).sort((a, b) => a[1].localeCompare(b[1])),
131
+ [overviewTasks]
132
+ );
133
+
134
+ const ownerOptions = useMemo(() => {
135
+ const owners = new Set<string>();
136
+ for (const task of overviewTasks) {
137
+ if (task.owner?.trim()) owners.add(task.owner.trim());
138
+ }
139
+ return Array.from(owners).sort((a, b) => a.localeCompare(b));
140
+ }, [overviewTasks]);
141
+
142
+ const filteredTasks = useMemo(
143
+ () =>
144
+ overviewTasks
145
+ .filter((task) => teamFilter === 'all' || task.teamName === teamFilter)
146
+ .filter((task) => statusFilter === 'all' || task.status === statusFilter)
147
+ .filter((task) => ownerFilter === 'all' || task.owner === ownerFilter)
148
+ .sort((a, b) => getTaskUpdatedAt(b) - getTaskUpdatedAt(a)),
149
+ [overviewTasks, ownerFilter, statusFilter, teamFilter]
150
+ );
151
+
152
+ const grouped = useMemo(() => {
153
+ const map = new Map<OverviewStatus, GlobalTask[]>();
154
+ for (const column of COLUMNS) {
155
+ map.set(column.id, []);
156
+ }
157
+ for (const task of filteredTasks) {
158
+ if (isOverviewStatus(task.status)) {
159
+ map.get(task.status)?.push(task);
160
+ }
161
+ }
162
+ return map;
163
+ }, [filteredTasks]);
164
+
165
+ const clearFilters = useCallback(() => {
166
+ setTeamFilter('all');
167
+ setStatusFilter('all');
168
+ setOwnerFilter('all');
169
+ }, []);
170
+
171
+ if (globalTasksLoading && !globalTasksInitialized) {
172
+ return (
173
+ <div className="flex h-full items-center justify-center text-sm text-[var(--color-text-muted)]">
174
+ 加载团队任务…
175
+ </div>
176
+ );
177
+ }
178
+
179
+ return (
180
+ <div className="flex h-full min-w-0 flex-col gap-3 p-4">
181
+ <div className="flex flex-wrap items-end gap-2">
182
+ <div className="min-w-[180px]">
183
+ <label
184
+ htmlFor="tasks-overview-team-filter"
185
+ className="mb-1 block text-[11px] font-medium text-[var(--color-text-muted)]"
186
+ >
187
+ 团队
188
+ </label>
189
+ <select
190
+ id="tasks-overview-team-filter"
191
+ value={teamFilter}
192
+ onChange={(event) => setTeamFilter(event.target.value)}
193
+ className="h-8 w-full rounded-md border border-[var(--color-border)] bg-[var(--color-surface)] px-2 text-xs text-[var(--color-text)]"
194
+ >
195
+ <option value="all">全部团队</option>
196
+ {teamOptions.map(([teamName, displayName]) => (
197
+ <option key={teamName} value={teamName}>
198
+ {displayName}
199
+ </option>
200
+ ))}
201
+ </select>
202
+ </div>
203
+
204
+ <div className="min-w-[160px]">
205
+ <label
206
+ htmlFor="tasks-overview-status-filter"
207
+ className="mb-1 block text-[11px] font-medium text-[var(--color-text-muted)]"
208
+ >
209
+ 状态
210
+ </label>
211
+ <select
212
+ id="tasks-overview-status-filter"
213
+ value={statusFilter}
214
+ onChange={(event) => setStatusFilter(event.target.value as 'all' | OverviewStatus)}
215
+ className="h-8 w-full rounded-md border border-[var(--color-border)] bg-[var(--color-surface)] px-2 text-xs text-[var(--color-text)]"
216
+ >
217
+ <option value="all">全部状态</option>
218
+ <option value="pending">TODO</option>
219
+ <option value="in_progress">IN PROGRESS</option>
220
+ <option value="completed">DONE</option>
221
+ </select>
222
+ </div>
223
+
224
+ <div className="min-w-[160px]">
225
+ <label
226
+ htmlFor="tasks-overview-owner-filter"
227
+ className="mb-1 block text-[11px] font-medium text-[var(--color-text-muted)]"
228
+ >
229
+ 负责人
230
+ </label>
231
+ <select
232
+ id="tasks-overview-owner-filter"
233
+ value={ownerFilter}
234
+ onChange={(event) => setOwnerFilter(event.target.value)}
235
+ className="h-8 w-full rounded-md border border-[var(--color-border)] bg-[var(--color-surface)] px-2 text-xs text-[var(--color-text)]"
236
+ >
237
+ <option value="all">全部负责人</option>
238
+ {ownerOptions.map((owner) => (
239
+ <option key={owner} value={owner}>
240
+ {owner}
241
+ </option>
242
+ ))}
243
+ </select>
244
+ </div>
245
+
246
+ <Button variant="outline" size="sm" className="h-8 gap-1.5 text-xs" onClick={clearFilters}>
247
+ 清空筛选
248
+ </Button>
249
+ <Button
250
+ variant="ghost"
251
+ size="sm"
252
+ className="ml-auto h-8 gap-1.5 text-xs text-[var(--color-text-muted)]"
253
+ onClick={() => void fetchAllTasks()}
254
+ >
255
+ <RefreshCw size={12} />
256
+ 刷新
257
+ </Button>
258
+ </div>
259
+
260
+ <div className="w-full min-w-0 max-w-full overflow-x-auto overflow-y-hidden pb-6">
261
+ <div className="grid min-w-[900px] grid-cols-3 items-start gap-3">
262
+ {COLUMNS.map((column) => {
263
+ const tasks = grouped.get(column.id) ?? [];
264
+ return (
265
+ <section
266
+ key={column.id}
267
+ className="relative rounded-md"
268
+ style={{ backgroundColor: column.bodyBg }}
269
+ >
270
+ {tasks.length > 0 ? (
271
+ <span className="absolute -right-2 -top-2 z-10 min-w-5 rounded-full bg-[var(--color-surface-raised)] px-1.5 py-0 text-center text-[10px] font-medium leading-5 text-[var(--color-text-secondary)] ring-1 ring-[var(--color-border)]">
272
+ {tasks.length}
273
+ </span>
274
+ ) : null}
275
+ <header
276
+ className="rounded-t-md px-3 py-2"
277
+ style={{ backgroundColor: column.headerBg }}
278
+ >
279
+ <h4 className="flex items-center gap-2 text-xs font-semibold uppercase tracking-wide text-[var(--color-text)]">
280
+ {column.icon}
281
+ {column.title}
282
+ </h4>
283
+ </header>
284
+ <div className="flex flex-col gap-1.5 p-2">
285
+ {tasks.length === 0 ? (
286
+ <div className="rounded-md border border-dashed border-[var(--color-border)] p-3 text-xs text-[var(--color-text-muted)]">
287
+ No tasks
288
+ </div>
289
+ ) : (
290
+ tasks.map((task) => (
291
+ <GlobalOverviewTaskCard
292
+ key={`${task.teamName}:${task.id}`}
293
+ task={task}
294
+ onOpen={() => openGlobalTaskDetail(task.teamName, task.id)}
295
+ />
296
+ ))
297
+ )}
298
+ </div>
299
+ </section>
300
+ );
301
+ })}
302
+ </div>
303
+ </div>
304
+ </div>
305
+ );
306
+ };
307
+
308
+ const GlobalOverviewTaskCard = ({
309
+ task,
310
+ onOpen,
311
+ }: {
312
+ task: GlobalTask;
313
+ onOpen: () => void;
314
+ }): React.JSX.Element => {
315
+ const ownerLabel = buildOptionLabel(task.owner, '未分配');
316
+ const dispatchFrom = task.dispatchMeta?.originTeam;
317
+ const dispatchTo = task.dispatchMeta?.targetTeam;
318
+ return (
319
+ <button
320
+ type="button"
321
+ className="relative w-full rounded-md border border-[var(--color-border)] bg-[var(--color-surface-raised)] px-1.5 py-3 text-left text-xs transition-colors hover:border-[var(--color-border-emphasis)]"
322
+ onClick={onOpen}
323
+ >
324
+ <span className="absolute left-[3px] top-[2px] text-[9px] leading-none text-[var(--color-text-muted)]">
325
+ #{task.displayId ?? deriveTaskDisplayId(task.id)}
326
+ </span>
327
+ <div className="mb-2 pt-[11px]">
328
+ <h5 className="line-clamp-2 text-xs font-medium text-[var(--color-text)]">
329
+ {task.subject}
330
+ </h5>
331
+ {task.dispatchMeta ? (
332
+ <span className="mt-1 inline-flex items-center rounded-full bg-yellow-500/15 px-1.5 py-0.5 text-[10px] font-medium text-yellow-600 dark:text-yellow-400">
333
+ {dispatchFrom} 给 {dispatchTo} 派单
334
+ </span>
335
+ ) : null}
336
+ </div>
337
+ <div className="flex flex-wrap items-center gap-1.5 text-[10px] text-[var(--color-text-muted)]">
338
+ <span className="rounded bg-white/5 px-1.5 py-0.5">{task.teamDisplayName}</span>
339
+ <span className="rounded bg-white/5 px-1.5 py-0.5">{ownerLabel}</span>
340
+ </div>
341
+ </button>
342
+ );
343
+ };