@yancyyu/openhermit 1.6.27 → 1.6.29

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (101) hide show
  1. package/README.md +7 -1
  2. package/bin/hermit.mjs +2 -2
  3. package/dist-renderer/assets/ProjectEditorOverlay-CQm6jUR1.js +52 -0
  4. package/dist-renderer/assets/{TeamGraphOverlay-DVq8rt6_.js → TeamGraphOverlay-h0WDfifv.js} +1 -1
  5. package/dist-renderer/assets/{_basePickBy-ZbF0pKvS.js → _basePickBy-CgG_tjgX.js} +1 -1
  6. package/dist-renderer/assets/{_baseUniq-BBLBOeXc.js → _baseUniq-DwPTU9lP.js} +1 -1
  7. package/dist-renderer/assets/{arc-wGaEgkCf.js → arc-7nIrGRzY.js} +1 -1
  8. package/dist-renderer/assets/{architectureDiagram-VXUJARFQ-BpMkdC35.js → architectureDiagram-VXUJARFQ-BYhA6Ev2.js} +1 -1
  9. package/dist-renderer/assets/{blockDiagram-VD42YOAC-C8Z1xhG4.js → blockDiagram-VD42YOAC-BVpZUGDg.js} +1 -1
  10. package/dist-renderer/assets/{c4Diagram-YG6GDRKO-CJmlw9LA.js → c4Diagram-YG6GDRKO-DsdreMQ9.js} +1 -1
  11. package/dist-renderer/assets/channel-C0SqeFU7.js +1 -0
  12. package/dist-renderer/assets/{chunk-4BX2VUAB-CHPHiRPP.js → chunk-4BX2VUAB-CcoAs7Jd.js} +1 -1
  13. package/dist-renderer/assets/{chunk-55IACEB6-DyVohOQb.js → chunk-55IACEB6-CGGAOoXd.js} +1 -1
  14. package/dist-renderer/assets/{chunk-B4BG7PRW-p5bffh_R.js → chunk-B4BG7PRW-FhpTEPvD.js} +1 -1
  15. package/dist-renderer/assets/{chunk-DI55MBZ5-BnfGPSUu.js → chunk-DI55MBZ5-DoYySbm1.js} +1 -1
  16. package/dist-renderer/assets/{chunk-FMBD7UC4-B6SCKseX.js → chunk-FMBD7UC4-e9l2tGHG.js} +1 -1
  17. package/dist-renderer/assets/{chunk-QN33PNHL-L12RvLBR.js → chunk-QN33PNHL-DeiXVTCy.js} +1 -1
  18. package/dist-renderer/assets/{chunk-QZHKN3VN-DeH1Kxge.js → chunk-QZHKN3VN-DC2UJLJM.js} +1 -1
  19. package/dist-renderer/assets/{chunk-TZMSLE5B-BWnjzSlI.js → chunk-TZMSLE5B-BHFD9eZI.js} +1 -1
  20. package/dist-renderer/assets/classDiagram-2ON5EDUG-DWew1HpM.js +1 -0
  21. package/dist-renderer/assets/classDiagram-v2-WZHVMYZB-DWew1HpM.js +1 -0
  22. package/dist-renderer/assets/clone-Dm-k63Yr.js +1 -0
  23. package/dist-renderer/assets/{cose-bilkent-S5V4N54A-BtzoT5fu.js → cose-bilkent-S5V4N54A-BdybQraU.js} +1 -1
  24. package/dist-renderer/assets/{dagre-6UL2VRFP-CBBvuoUD.js → dagre-6UL2VRFP-DdF3pwM3.js} +1 -1
  25. package/dist-renderer/assets/{diagram-PSM6KHXK-Be9BAKws.js → diagram-PSM6KHXK-B9Ldd3nh.js} +1 -1
  26. package/dist-renderer/assets/{diagram-QEK2KX5R-BDS4PI_i.js → diagram-QEK2KX5R-XEqkrbpu.js} +1 -1
  27. package/dist-renderer/assets/{diagram-S2PKOQOG-2Rameaq7.js → diagram-S2PKOQOG-CipwtY59.js} +1 -1
  28. package/dist-renderer/assets/{erDiagram-Q2GNP2WA-CSIzCEZD.js → erDiagram-Q2GNP2WA-BB-2ISGo.js} +1 -1
  29. package/dist-renderer/assets/{flowDiagram-NV44I4VS-ForEIVM5.js → flowDiagram-NV44I4VS-B8XmJ0u2.js} +1 -1
  30. package/dist-renderer/assets/{ganttDiagram-JELNMOA3-BJrli_xr.js → ganttDiagram-JELNMOA3-D-8XglBb.js} +1 -1
  31. package/dist-renderer/assets/{gitGraphDiagram-V2S2FVAM-C_4GuLno.js → gitGraphDiagram-V2S2FVAM-DL4ChakD.js} +1 -1
  32. package/dist-renderer/assets/{graph-B1EAT_gw.js → graph-BiFNoBjP.js} +1 -1
  33. package/dist-renderer/assets/{index-eKRmS5kI.js → index-6m1ZAymG.js} +1 -1
  34. package/dist-renderer/assets/index-BhellmRb.css +1 -0
  35. package/dist-renderer/assets/{index-DYdseEwc.js → index-BowUl0Jb.js} +518 -514
  36. package/dist-renderer/assets/{index-DR602dwJ.js → index-Dp3kJTEe.js} +1 -1
  37. package/dist-renderer/assets/{index-Dwr5wu5x.js → index-TOpt_T7A.js} +1 -1
  38. package/dist-renderer/assets/{index-DOA_jbYb.js → index-qNBNjW4K.js} +1 -1
  39. package/dist-renderer/assets/{index-k4tnOFC5.js → index-vAykq1H1.js} +1 -1
  40. package/dist-renderer/assets/{infoDiagram-HS3SLOUP-DjI0uaMz.js → infoDiagram-HS3SLOUP-DRIBfHDi.js} +1 -1
  41. package/dist-renderer/assets/{journeyDiagram-XKPGCS4Q-jQ6Thae-.js → journeyDiagram-XKPGCS4Q-BOMiigU4.js} +1 -1
  42. package/dist-renderer/assets/{kanban-definition-3W4ZIXB7-CKw6InbL.js → kanban-definition-3W4ZIXB7-DDxeyjod.js} +1 -1
  43. package/dist-renderer/assets/{layout-Dad20y3V.js → layout-DNANbrI4.js} +1 -1
  44. package/dist-renderer/assets/{linear-vMgo_2Cv.js → linear-DxEJi1yT.js} +1 -1
  45. package/dist-renderer/assets/{mindmap-definition-VGOIOE7T-DYp6YoHL.js → mindmap-definition-VGOIOE7T-nBfGriW8.js} +1 -1
  46. package/dist-renderer/assets/{pieDiagram-ADFJNKIX-BytBecG9.js → pieDiagram-ADFJNKIX-Din5j6sV.js} +1 -1
  47. package/dist-renderer/assets/{quadrantDiagram-AYHSOK5B-RUaspLsc.js → quadrantDiagram-AYHSOK5B-DMVK2BEQ.js} +1 -1
  48. package/dist-renderer/assets/{requirementDiagram-UZGBJVZJ-rR2B1Use.js → requirementDiagram-UZGBJVZJ-6SC94Gg_.js} +1 -1
  49. package/dist-renderer/assets/{sankeyDiagram-TZEHDZUN-BJi5qYhq.js → sankeyDiagram-TZEHDZUN-CD2gghhu.js} +1 -1
  50. package/dist-renderer/assets/{sequenceDiagram-WL72ISMW-BM-wggUb.js → sequenceDiagram-WL72ISMW-BnhkN7nZ.js} +1 -1
  51. package/dist-renderer/assets/{stateDiagram-FKZM4ZOC-BqmcVjnj.js → stateDiagram-FKZM4ZOC-Bn8XdYX-.js} +1 -1
  52. package/dist-renderer/assets/{stateDiagram-v2-4FDKWEC3-By3JDVbB.js → stateDiagram-v2-4FDKWEC3-1b6sI1_g.js} +1 -1
  53. package/dist-renderer/assets/{timeline-definition-IT6M3QCI-szH0GUyk.js → timeline-definition-IT6M3QCI-CNs3RPoa.js} +1 -1
  54. package/dist-renderer/assets/treemap-GDKQZRPO-DU_yr827.js +162 -0
  55. package/dist-renderer/assets/{xychartDiagram-PRI3JC2R-dwDpvw0w.js → xychartDiagram-PRI3JC2R-B8o5J2f3.js} +1 -1
  56. package/dist-renderer/index.html +2 -2
  57. package/package.json +1 -1
  58. package/src/main/server.ts +800 -163
  59. package/src/main/services/session-intelligence/SessionUsageParser.ts +446 -0
  60. package/src/main/services/session-intelligence/UsageTelemetryService.ts +252 -0
  61. package/src/main/services/teams-mvp/CollaborationBoardService.ts +310 -0
  62. package/src/main/services/teams-mvp/TaskDispatchService.ts +880 -95
  63. package/src/main/services/teams-mvp/TeamProvisioningService.ts +58 -19
  64. package/src/main/services/teams-mvp/TeamWorkspaceService.ts +25 -2
  65. package/src/main/services/teams-mvp/index.ts +3 -0
  66. package/src/renderer/App.tsx +5 -0
  67. package/src/renderer/api/httpClient.ts +67 -0
  68. package/src/renderer/components/dashboard/DashboardView.tsx +6 -105
  69. package/src/renderer/components/layout/PaneContent.tsx +2 -0
  70. package/src/renderer/components/layout/SortableTab.tsx +1 -0
  71. package/src/renderer/components/layout/TabBarActions.tsx +12 -12
  72. package/src/renderer/components/schedules/SchedulesView.tsx +54 -22
  73. package/src/renderer/components/settings/SettingsTabs.tsx +2 -2
  74. package/src/renderer/components/settings/sections/AdvancedSection.tsx +1 -1
  75. package/src/renderer/components/settings/sections/TaskBusSection.tsx +511 -81
  76. package/src/renderer/components/tasks/TasksView.tsx +343 -0
  77. package/src/renderer/components/team/TeamDetailView.tsx +20 -98
  78. package/src/renderer/components/team/dialogs/LaunchTeamDialog.tsx +1 -1
  79. package/src/renderer/components/team/editor/EditorContextMenu.tsx +8 -23
  80. package/src/renderer/components/team/editor/EditorFileTree.tsx +0 -4
  81. package/src/renderer/components/team/editor/EditorSelectionMenu.tsx +1 -8
  82. package/src/renderer/components/team/editor/ProjectEditorOverlay.tsx +0 -10
  83. package/src/renderer/components/team/kanban/KanbanBoard.tsx +5 -1
  84. package/src/renderer/components/team/members/MemberDetailDialog.tsx +8 -33
  85. package/src/renderer/components/team/messages/MessageComposer.tsx +39 -3
  86. package/src/renderer/components/team/messages/MessagesPanel.tsx +72 -2
  87. package/src/renderer/components/team/messages/StatusBlock.tsx +2 -24
  88. package/src/renderer/components/team/schedule/ScheduleEmptyState.tsx +1 -1
  89. package/src/renderer/components/ui/MentionableTextarea.tsx +0 -1
  90. package/src/renderer/store/slices/scheduleSlice.ts +21 -0
  91. package/src/renderer/store/slices/teamSlice.ts +59 -23
  92. package/src/renderer/types/tabs.ts +1 -0
  93. package/src/shared/types/api.ts +29 -0
  94. package/src/shared/types/team.ts +109 -1
  95. package/dist-renderer/assets/ProjectEditorOverlay-BBwYdXPv.js +0 -57
  96. package/dist-renderer/assets/channel-DJUrwVrK.js +0 -1
  97. package/dist-renderer/assets/classDiagram-2ON5EDUG-blc3DrH7.js +0 -1
  98. package/dist-renderer/assets/classDiagram-v2-WZHVMYZB-blc3DrH7.js +0 -1
  99. package/dist-renderer/assets/clone-BftjWakJ.js +0 -1
  100. package/dist-renderer/assets/index-CWpFqEvz.css +0 -1
  101. package/dist-renderer/assets/treemap-GDKQZRPO-BCMlh-Ex.js +0 -162
@@ -3,7 +3,194 @@ import { useEffect, useState } from 'react';
3
3
  import { Button } from '@renderer/components/ui/button';
4
4
  import { SettingRow, SettingsSectionHeader, SettingsToggle } from '../components';
5
5
  import type { TaskBusConfig } from '@shared/types/team';
6
- import { Loader2, Radio, Wifi, WifiOff } from 'lucide-react';
6
+ import {
7
+ Loader2,
8
+ Radio,
9
+ Wifi,
10
+ WifiOff,
11
+ BarChart3,
12
+ Clock,
13
+ MessageSquare,
14
+ Zap,
15
+ Calendar,
16
+ AlertCircle,
17
+ } from 'lucide-react';
18
+
19
+ interface TelemetryStatus {
20
+ connected: boolean;
21
+ lastScan: string | null;
22
+ sessions: number;
23
+ messages: number;
24
+ tokensIn: number;
25
+ tokensOut: number;
26
+ cacheRead: number;
27
+ cacheCreation: number;
28
+ activeDays: number;
29
+ hourly: number[];
30
+ projects: Array<{
31
+ cwd: string;
32
+ sessions: number;
33
+ messages: number;
34
+ tokensIn: number;
35
+ tokensOut: number;
36
+ }>;
37
+ workSecondsByDay: Record<string, number>;
38
+ }
39
+
40
+ function formatNum(n: number | undefined): string {
41
+ if (n == null) return '采集中...';
42
+ if (n >= 1_000_000) return (n / 1_000_000).toFixed(1) + 'M';
43
+ if (n >= 1_000) return (n / 1_000).toFixed(1) + 'K';
44
+ return String(n);
45
+ }
46
+
47
+ function formatDuration(secs: number): string {
48
+ if (secs < 60) return `${secs}s`;
49
+ if (secs < 3600) return `${Math.round(secs / 60)}m`;
50
+ return `${(secs / 3600).toFixed(1)}h`;
51
+ }
52
+
53
+ function UsageDashboard({ status }: { status: TelemetryStatus }): React.JSX.Element {
54
+ const maxHourly = Math.max(...status.hourly, 1);
55
+ const recentDays = Object.keys(status.workSecondsByDay).sort().slice(-7);
56
+ const maxWorkSecs = Math.max(...Object.values(status.workSecondsByDay), 1);
57
+
58
+ return (
59
+ <div className="space-y-4 rounded-md border border-[var(--color-border)] bg-[var(--color-surface-raised)] p-4">
60
+ <div className="flex items-center justify-between">
61
+ <span className="text-xs font-medium text-[var(--color-text-muted)]">使用指标概览</span>
62
+ <span className="text-[10px] text-[var(--color-text-muted)]">累计数据(全部历史)</span>
63
+ </div>
64
+ <div className="grid grid-cols-2 gap-3 sm:grid-cols-4">
65
+ <StatCard
66
+ icon={<MessageSquare size={14} />}
67
+ label="会话"
68
+ value={formatNum(status.sessions)}
69
+ />
70
+ <StatCard
71
+ icon={<MessageSquare size={14} />}
72
+ label="消息"
73
+ value={formatNum(status.messages)}
74
+ />
75
+ <StatCard icon={<Zap size={14} />} label="Input" value={formatNum(status.tokensIn)} />
76
+ <StatCard icon={<Zap size={14} />} label="Output" value={formatNum(status.tokensOut)} />
77
+ <StatCard icon={<Calendar size={14} />} label="活跃天" value={String(status.activeDays)} />
78
+ <StatCard icon={<Zap size={14} />} label="Cache Read" value={formatNum(status.cacheRead)} />
79
+ <StatCard
80
+ icon={<Zap size={14} />}
81
+ label="Cache Create"
82
+ value={formatNum(status.cacheCreation)}
83
+ />
84
+ <StatCard
85
+ icon={<Clock size={14} />}
86
+ label="最近采集"
87
+ value={status.lastScan ? new Date(status.lastScan).toLocaleDateString('zh-CN') : '-'}
88
+ />
89
+ </div>
90
+
91
+ <div>
92
+ <div className="mb-2 text-xs font-medium text-[var(--color-text-muted)]">24小时分布</div>
93
+ <div className="flex h-16 items-end gap-0.5">
94
+ {status.hourly.map((count, i) => {
95
+ const pct = (count / maxHourly) * 100;
96
+ return (
97
+ <div
98
+ key={i}
99
+ className="flex-1 rounded-sm bg-indigo-500/60 transition-all hover:bg-indigo-500"
100
+ style={{ height: `${Math.max(pct, 2)}%` }}
101
+ title={`${i}:00 - ${count} messages`}
102
+ />
103
+ );
104
+ })}
105
+ </div>
106
+ <div className="mt-1 flex justify-between text-[10px] text-[var(--color-text-muted)]">
107
+ <span>0h</span>
108
+ <span>6h</span>
109
+ <span>12h</span>
110
+ <span>18h</span>
111
+ <span>24h</span>
112
+ </div>
113
+ </div>
114
+
115
+ {recentDays.length > 0 && (
116
+ <div>
117
+ <div className="mb-2 text-xs font-medium text-[var(--color-text-muted)]">
118
+ 日工作时长(近 {recentDays.length} 天)
119
+ </div>
120
+ <div className="flex h-16 items-end gap-1">
121
+ {recentDays.map((day) => {
122
+ const secs = status.workSecondsByDay[day] ?? 0;
123
+ const pct = (secs / maxWorkSecs) * 100;
124
+ const label = day.slice(5);
125
+ return (
126
+ <div key={day} className="flex flex-1 flex-col items-center gap-1">
127
+ <span className="text-[10px] text-[var(--color-text-muted)]">
128
+ {formatDuration(secs)}
129
+ </span>
130
+ <div
131
+ className="w-full rounded-sm bg-emerald-500/60 transition-all hover:bg-emerald-500"
132
+ style={{ height: `${Math.max(pct, 2)}%` }}
133
+ title={`${day} - ${formatDuration(secs)}`}
134
+ />
135
+ <span className="text-[10px] text-[var(--color-text-muted)]">{label}</span>
136
+ </div>
137
+ );
138
+ })}
139
+ </div>
140
+ </div>
141
+ )}
142
+
143
+ {status.projects.length > 0 && (
144
+ <div>
145
+ <div className="mb-2 text-xs font-medium text-[var(--color-text-muted)]">
146
+ 项目排行(累计)
147
+ </div>
148
+ {/* Header row */}
149
+ <div className="grid grid-cols-[1fr_64px_64px] items-center gap-2 pb-1 text-[10px] text-[var(--color-text-muted)]">
150
+ <span>项目</span>
151
+ <span className="text-right">消息</span>
152
+ <span className="text-right">Token</span>
153
+ </div>
154
+ <div className="max-h-40 space-y-1 overflow-y-auto">
155
+ {status.projects.slice(0, 10).map((proj, i) => (
156
+ <div key={i} className="grid grid-cols-[1fr_64px_64px] items-center gap-2 text-xs">
157
+ <span className="truncate text-[var(--color-text-secondary)]" title={proj.cwd}>
158
+ {proj.cwd.split('/').pop() || proj.cwd}
159
+ </span>
160
+ <span className="text-right text-[var(--color-text-muted)]">
161
+ {formatNum(proj.messages)}
162
+ </span>
163
+ <span className="text-right text-[var(--color-text-muted)]">
164
+ {formatNum(proj.tokensIn + proj.tokensOut)}
165
+ </span>
166
+ </div>
167
+ ))}
168
+ </div>
169
+ </div>
170
+ )}
171
+ </div>
172
+ );
173
+ }
174
+
175
+ function StatCard({
176
+ icon,
177
+ label,
178
+ value,
179
+ }: {
180
+ icon: React.ReactNode;
181
+ label: string;
182
+ value: string;
183
+ }): React.JSX.Element {
184
+ return (
185
+ <div className="flex flex-col gap-1 rounded bg-[var(--color-bg)] p-2">
186
+ <div className="flex items-center gap-1 text-[var(--color-text-muted)]">
187
+ {icon}
188
+ <span className="text-[10px]">{label}</span>
189
+ </div>
190
+ <div className="text-sm font-medium">{value}</div>
191
+ </div>
192
+ );
193
+ }
7
194
 
8
195
  export function TaskBusSection(): React.JSX.Element {
9
196
  const [enabled, setEnabled] = useState(false);
@@ -15,6 +202,13 @@ export function TaskBusSection(): React.JSX.Element {
15
202
  const [connected, setConnected] = useState(false);
16
203
  const [message, setMessage] = useState<string | null>(null);
17
204
 
205
+ const [collectionEnabled, setCollectionEnabled] = useState(false);
206
+ const [uploadEnabled, setUploadEnabled] = useState(false);
207
+ const [collaborationEnabled, setCollaborationEnabled] = useState(false);
208
+ const [telemetryPlatform, setTelemetryPlatform] = useState('claudecode');
209
+ const [scanning, setScanning] = useState(false);
210
+ const [telemetryStatus, setTelemetryStatus] = useState<TelemetryStatus | null>(null);
211
+
18
212
  useEffect(() => {
19
213
  fetch('/api/settings/task-bus')
20
214
  .then((r) => r.json())
@@ -25,18 +219,59 @@ export function TaskBusSection(): React.JSX.Element {
25
219
  setPort(data.redis.port ?? 6379);
26
220
  setPassword(data.redis.password ?? '');
27
221
  }
222
+ if (data.telemetry) {
223
+ setCollectionEnabled(data.telemetry.enabled);
224
+ setUploadEnabled(data.telemetry.uploadEnabled ?? false);
225
+ setTelemetryPlatform(data.telemetry.platform ?? 'claudecode');
226
+ }
227
+ setCollaborationEnabled(data.collaboration ?? false);
28
228
  })
29
229
  .catch(() => {})
30
230
  .finally(() => setLoading(false));
31
- }, []);
32
231
 
33
- const save = async (connectRedis = false) => {
232
+ // Restore telemetry status + Redis connection state on mount
233
+ fetch('/api/telemetry/status')
234
+ .then((r) => r.json())
235
+ .then((s: TelemetryStatus) => {
236
+ if (s.connected) setConnected(true);
237
+ if ('sessions' in s && s.sessions > 0) setTelemetryStatus(s);
238
+ })
239
+ .catch(() => {});
240
+
241
+ const poll = setInterval(() => {
242
+ if (collectionEnabled) {
243
+ fetch('/api/telemetry/status')
244
+ .then((r) => r.json())
245
+ .then((s: TelemetryStatus) => setTelemetryStatus(s))
246
+ .catch(() => {});
247
+ }
248
+ }, 30000);
249
+ return () => clearInterval(poll);
250
+ }, [collectionEnabled]);
251
+
252
+ const buildConfig = (
253
+ overrides: Partial<{
254
+ enabled: boolean;
255
+ collectionEnabled: boolean;
256
+ uploadEnabled: boolean;
257
+ collaborationEnabled: boolean;
258
+ telemetryPlatform: string;
259
+ }> = {}
260
+ ): TaskBusConfig => ({
261
+ enabled: overrides.enabled ?? enabled,
262
+ redis: { host, port, password: password || undefined },
263
+ collaboration: overrides.collaborationEnabled ?? collaborationEnabled,
264
+ telemetry: {
265
+ enabled: overrides.collectionEnabled ?? collectionEnabled,
266
+ uploadEnabled: overrides.uploadEnabled ?? uploadEnabled,
267
+ platform: (overrides.telemetryPlatform ?? telemetryPlatform) as 'claudecode',
268
+ },
269
+ });
270
+
271
+ const testRedisConnection = async (): Promise<boolean> => {
272
+ setConnecting(true);
34
273
  setMessage(null);
35
- if (connectRedis) setConnecting(true);
36
- const config: TaskBusConfig = {
37
- enabled,
38
- redis: { host, port, password: password || undefined },
39
- };
274
+ const config = buildConfig();
40
275
  try {
41
276
  const res = await fetch('/api/settings/task-bus', {
42
277
  method: 'PUT',
@@ -44,17 +279,13 @@ export function TaskBusSection(): React.JSX.Element {
44
279
  body: JSON.stringify(config),
45
280
  });
46
281
  const data = await res.json();
47
- if (connectRedis) {
48
- setConnected(!!data.connected);
49
- setMessage(
50
- data.connected ? 'Redis 连接成功,分布式派发已启用' : 'Redis 连接失败,仅本地派发'
51
- );
52
- } else {
53
- setConnected(false);
54
- setMessage(enabled ? '已开启,指令已注入到团队工作目录' : '已关闭');
55
- }
282
+ const ok = !!data.connected;
283
+ setConnected(ok);
284
+ setMessage(ok ? 'Redis 连接成功' : 'Redis 连接失败,请检查配置');
285
+ return ok;
56
286
  } catch (err) {
57
- setMessage(`操作失败: ${err}`);
287
+ setMessage(`连接失败: ${err}`);
288
+ return false;
58
289
  } finally {
59
290
  setConnecting(false);
60
291
  }
@@ -62,17 +293,105 @@ export function TaskBusSection(): React.JSX.Element {
62
293
 
63
294
  const toggle = (value: boolean) => {
64
295
  setEnabled(value);
65
- const config: TaskBusConfig = {
66
- enabled: value,
67
- redis: { host, port, password: password || undefined },
68
- };
296
+ const config = buildConfig({ enabled: value });
69
297
  fetch('/api/settings/task-bus', {
70
298
  method: 'PUT',
71
299
  headers: { 'Content-Type': 'application/json' },
72
300
  body: JSON.stringify(config),
73
301
  })
74
302
  .then((r) => r.json())
75
- .then(() => setMessage(value ? '已开启,指令已注入到团队工作目录' : '已关闭'))
303
+ .then(() => setMessage(value ? '团队总线已激活' : '已关闭'))
304
+ .catch(() => setMessage('操作失败'));
305
+ };
306
+
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) => {
325
+ if (!value) {
326
+ setUploadEnabled(false);
327
+ const config = buildConfig({ uploadEnabled: false });
328
+ fetch('/api/settings/task-bus', {
329
+ method: 'PUT',
330
+ headers: { 'Content-Type': 'application/json' },
331
+ body: JSON.stringify(config),
332
+ }).catch(() => setMessage('操作失败'));
333
+ return;
334
+ }
335
+
336
+ // Upload requires Redis
337
+ let redisReady = connected;
338
+ if (!redisReady) {
339
+ setMessage('正在测试 Redis 连接...');
340
+ redisReady = await testRedisConnection();
341
+ if (!redisReady) {
342
+ setUploadEnabled(false);
343
+ setMessage('Redis 连接失败,无法启用数据上报');
344
+ return;
345
+ }
346
+ }
347
+
348
+ setUploadEnabled(true);
349
+ setMessage(null);
350
+ const config = buildConfig({ uploadEnabled: true });
351
+ try {
352
+ await fetch('/api/settings/task-bus', {
353
+ method: 'PUT',
354
+ headers: { 'Content-Type': 'application/json' },
355
+ body: JSON.stringify(config),
356
+ });
357
+ } catch {
358
+ setUploadEnabled(false);
359
+ setMessage('操作失败');
360
+ }
361
+ };
362
+
363
+ const triggerScan = () => {
364
+ if (scanning) return;
365
+ setScanning(true);
366
+ fetch('/api/telemetry/scan', { method: 'POST' })
367
+ .then((r) => r.json())
368
+ .then((result: TelemetryStatus & { ok?: boolean }) => {
369
+ if ('sessions' in result) {
370
+ setTelemetryStatus(result);
371
+ }
372
+ })
373
+ .catch(() => setMessage('采集失败,请检查本地 Claude Code 会话目录'))
374
+ .finally(() => setScanning(false));
375
+ };
376
+
377
+ const saveTelemetryPlatform = (nextPlatform = telemetryPlatform) => {
378
+ const config = buildConfig({ telemetryPlatform: nextPlatform });
379
+ fetch('/api/settings/task-bus', {
380
+ method: 'PUT',
381
+ headers: { 'Content-Type': 'application/json' },
382
+ body: JSON.stringify(config),
383
+ }).catch(() => {});
384
+ };
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 ? '已开启分布式团队协作' : '已关闭分布式团队协作'))
76
395
  .catch(() => setMessage('操作失败'));
77
396
  };
78
397
 
@@ -86,81 +405,192 @@ export function TaskBusSection(): React.JSX.Element {
86
405
 
87
406
  return (
88
407
  <div>
89
- <SettingsSectionHeader title="任务总线" icon={<Radio size={12} />} />
408
+ <SettingsSectionHeader title="本地数据采集" icon={<BarChart3 size={12} />} />
90
409
 
91
410
  <SettingRow
92
- label="启用任务总线"
93
- description="开启后自动为所有团队注入跨团队任务派发指令到 CLAUDE.md"
411
+ label="数据采集"
412
+ description="扫描本机 ~/.claude/projects 会话文件,采集使用指标;不需要 Redis,也不会上传对话内容"
94
413
  >
95
- <SettingsToggle enabled={enabled} onChange={toggle} />
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>
96
431
  </SettingRow>
97
432
 
98
- {/* Redis */}
99
- <SettingRow label="Redis" description="可选,配置后启用跨主机分布式派发">
100
- <div className="flex items-center gap-2">
101
- {connected ? (
102
- <span className="flex items-center gap-1 text-xs text-emerald-500">
103
- <Wifi size={12} />
104
- 已连接
105
- </span>
106
- ) : enabled ? (
107
- <span className="flex items-center gap-1 text-xs text-[var(--color-text-muted)]">
108
- <WifiOff size={12} />
109
- 本地模式
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。
110
451
  </span>
111
- ) : null}
112
- </div>
452
+ </div>
453
+
454
+ {telemetryStatus && (
455
+ <div className="py-3">
456
+ <UsageDashboard status={telemetryStatus} />
457
+ </div>
458
+ )}
459
+ </>
460
+ )}
461
+
462
+ <SettingsSectionHeader title="团队总线" icon={<Radio size={12} />} />
463
+
464
+ <SettingRow
465
+ label="启用团队总线"
466
+ description="用于 Redis 连接、数据上报和跨团队协作;本地数据采集不依赖它"
467
+ >
468
+ <SettingsToggle enabled={enabled} onChange={toggle} />
113
469
  </SettingRow>
114
470
 
115
471
  {enabled && (
116
- <div className="border-b pb-4" style={{ borderColor: 'var(--color-border-subtle)' }}>
117
- <div className="space-y-3 px-1 pt-2">
118
- <div className="flex gap-3">
119
- <div className="flex-1">
120
- <label className="mb-1 block text-xs text-[var(--color-text-muted)]">主机</label>
121
- <input
122
- type="text"
123
- value={host}
124
- onChange={(e) => setHost(e.target.value)}
125
- className="w-full rounded-md border border-[var(--color-border)] bg-[var(--color-bg)] px-2.5 py-1.5 text-sm outline-none focus:border-indigo-500/50 focus:ring-1 focus:ring-indigo-500/30"
126
- placeholder="127.0.0.1"
127
- />
472
+ <>
473
+ {/* Redis 配置 - 必填 */}
474
+ <div className="border-b pb-4" style={{ borderColor: 'var(--color-border-subtle)' }}>
475
+ <div className="flex items-center gap-2 px-1 pb-2">
476
+ <span className="text-sm font-medium text-red-500">*</span>
477
+ <span className="text-sm font-medium">Redis</span>
478
+ <span className="text-xs text-[var(--color-text-muted)]">
479
+ (上报和跨团队协作必填)
480
+ </span>
481
+ <div className="ml-auto flex items-center gap-2">
482
+ {connected ? (
483
+ <span className="flex items-center gap-1 text-xs text-emerald-500">
484
+ <Wifi size={12} />
485
+ 已连接
486
+ </span>
487
+ ) : (
488
+ <span className="flex items-center gap-1 text-xs text-red-500">
489
+ <WifiOff size={12} />
490
+ 未连接
491
+ </span>
492
+ )}
128
493
  </div>
129
- <div className="w-24">
130
- <label className="mb-1 block text-xs text-[var(--color-text-muted)]">端口</label>
494
+ </div>
495
+
496
+ <div className="space-y-3 px-1 pt-2">
497
+ <div className="flex gap-3">
498
+ <div className="flex-1">
499
+ <label className="mb-1 block text-xs text-[var(--color-text-muted)]">主机</label>
500
+ <input
501
+ type="text"
502
+ value={host}
503
+ onChange={(e) => {
504
+ setHost(e.target.value);
505
+ setConnected(false);
506
+ }}
507
+ className="w-full rounded-md border border-[var(--color-border)] bg-[var(--color-bg)] px-2.5 py-1.5 text-sm outline-none focus:border-indigo-500/50 focus:ring-1 focus:ring-indigo-500/30"
508
+ placeholder="127.0.0.1"
509
+ />
510
+ </div>
511
+ <div className="w-24">
512
+ <label className="mb-1 block text-xs text-[var(--color-text-muted)]">端口</label>
513
+ <input
514
+ type="number"
515
+ value={port}
516
+ onChange={(e) => {
517
+ setPort(Number(e.target.value));
518
+ setConnected(false);
519
+ }}
520
+ className="w-full rounded-md border border-[var(--color-border)] bg-[var(--color-bg)] px-2.5 py-1.5 text-sm outline-none focus:border-indigo-500/50 focus:ring-1 focus:ring-indigo-500/30"
521
+ placeholder="6379"
522
+ />
523
+ </div>
524
+ </div>
525
+ <div>
526
+ <label className="mb-1 block text-xs text-[var(--color-text-muted)]">密码</label>
131
527
  <input
132
- type="number"
133
- value={port}
134
- onChange={(e) => setPort(Number(e.target.value))}
528
+ type="password"
529
+ value={password}
530
+ onChange={(e) => {
531
+ setPassword(e.target.value);
532
+ setConnected(false);
533
+ }}
135
534
  className="w-full rounded-md border border-[var(--color-border)] bg-[var(--color-bg)] px-2.5 py-1.5 text-sm outline-none focus:border-indigo-500/50 focus:ring-1 focus:ring-indigo-500/30"
136
- placeholder="6379"
535
+ placeholder="可选"
137
536
  />
138
537
  </div>
538
+ <div className="flex items-center gap-3 pt-1">
539
+ <Button
540
+ size="sm"
541
+ onClick={testRedisConnection}
542
+ disabled={connecting}
543
+ className="gap-1.5"
544
+ >
545
+ {connecting ? <Loader2 size={12} className="animate-spin" /> : <Wifi size={12} />}
546
+ {connecting ? '连接中...' : '测试连接'}
547
+ </Button>
548
+ {message && (
549
+ <span className={`text-xs ${connected ? 'text-emerald-500' : 'text-red-500'}`}>
550
+ {message}
551
+ </span>
552
+ )}
553
+ </div>
139
554
  </div>
140
- <div>
141
- <label className="mb-1 block text-xs text-[var(--color-text-muted)]">密码</label>
142
- <input
143
- type="password"
144
- value={password}
145
- onChange={(e) => setPassword(e.target.value)}
146
- className="w-full rounded-md border border-[var(--color-border)] bg-[var(--color-bg)] px-2.5 py-1.5 text-sm outline-none focus:border-indigo-500/50 focus:ring-1 focus:ring-indigo-500/30"
147
- placeholder="可选"
555
+ </div>
556
+
557
+ {/* 数据上报 - 依赖 Redis,最下面 */}
558
+ <div>
559
+ <SettingRow label="数据上报" description="将采集数据上报到 Redis,供团队看板使用">
560
+ <SettingsToggle
561
+ enabled={uploadEnabled}
562
+ onChange={(value) => void toggleUpload(value)}
148
563
  />
149
- </div>
150
- <div className="flex items-center gap-3 pt-1">
151
- <Button
152
- size="sm"
153
- onClick={() => save(true)}
154
- disabled={connecting}
155
- className="gap-1.5"
156
- >
157
- {connecting && <Loader2 size={12} className="animate-spin" />}
158
- {connecting ? '连接中...' : '测试连接'}
159
- </Button>
160
- {message && <span className="text-xs text-[var(--color-text-muted)]">{message}</span>}
161
- </div>
564
+ </SettingRow>
565
+
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>
571
+ )}
162
572
  </div>
163
- </div>
573
+
574
+ {/* 分布式团队协作 - 最下面 */}
575
+ <div>
576
+ <SettingRow
577
+ label="分布式团队协作"
578
+ description="开启后 Hermit 平台识别 @团队 并创建跨团队协作任务;Agent 只读取团队列表,不直接派发"
579
+ >
580
+ <SettingsToggle
581
+ enabled={collaborationEnabled}
582
+ onChange={(value) => void toggleCollaboration(value)}
583
+ />
584
+ </SettingRow>
585
+
586
+ {!connected && collaborationEnabled && (
587
+ <div className="flex items-center gap-2 px-1 py-2 text-xs text-amber-500">
588
+ <AlertCircle size={12} />
589
+ <span>跨团队协作需要 Redis 连接。</span>
590
+ </div>
591
+ )}
592
+ </div>
593
+ </>
164
594
  )}
165
595
 
166
596
  {!enabled && message && (