@yancyyu/openhermit 1.6.26 → 1.6.28

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 (74) hide show
  1. package/README.md +132 -80
  2. package/bin/hermit.mjs +2 -2
  3. package/dist-renderer/assets/{ProjectEditorOverlay-Byepdwo2.js → ProjectEditorOverlay-A4DZTvSy.js} +1 -1
  4. package/dist-renderer/assets/{TeamGraphOverlay-vvWu-2c9.js → TeamGraphOverlay-Ba5njic5.js} +1 -1
  5. package/dist-renderer/assets/{_basePickBy-DfsmMgXN.js → _basePickBy-BvnK-OC1.js} +1 -1
  6. package/dist-renderer/assets/{_baseUniq-Bve-IKz5.js → _baseUniq-DmFYXx9G.js} +1 -1
  7. package/dist-renderer/assets/{arc-4cbkhagw.js → arc-DX4ZQFY4.js} +1 -1
  8. package/dist-renderer/assets/{architectureDiagram-VXUJARFQ-CC9i0bMK.js → architectureDiagram-VXUJARFQ-DfYr3vEN.js} +1 -1
  9. package/dist-renderer/assets/{blockDiagram-VD42YOAC-BjFruJ65.js → blockDiagram-VD42YOAC-DuXdVeWn.js} +1 -1
  10. package/dist-renderer/assets/{c4Diagram-YG6GDRKO-CrYzsQC1.js → c4Diagram-YG6GDRKO-Bw2nixXe.js} +1 -1
  11. package/dist-renderer/assets/channel-Pre42N5O.js +1 -0
  12. package/dist-renderer/assets/{chunk-4BX2VUAB-Bb9MCt7J.js → chunk-4BX2VUAB-DLiNGQoE.js} +1 -1
  13. package/dist-renderer/assets/{chunk-55IACEB6-BpOVOXVa.js → chunk-55IACEB6-B1L_8VIF.js} +1 -1
  14. package/dist-renderer/assets/{chunk-B4BG7PRW-GtEiO-7n.js → chunk-B4BG7PRW-DaZMWKGk.js} +1 -1
  15. package/dist-renderer/assets/{chunk-DI55MBZ5-BRlzcOEj.js → chunk-DI55MBZ5-ku-dflJG.js} +1 -1
  16. package/dist-renderer/assets/{chunk-FMBD7UC4-DcvMVOZx.js → chunk-FMBD7UC4-DV-mF1dP.js} +1 -1
  17. package/dist-renderer/assets/{chunk-QN33PNHL-B9pkjVpd.js → chunk-QN33PNHL-ByGcDFQ0.js} +1 -1
  18. package/dist-renderer/assets/{chunk-QZHKN3VN-DzHPSm01.js → chunk-QZHKN3VN-7dv-Min8.js} +1 -1
  19. package/dist-renderer/assets/{chunk-TZMSLE5B-BU9c0Hcn.js → chunk-TZMSLE5B-WdXL5fTu.js} +1 -1
  20. package/dist-renderer/assets/classDiagram-2ON5EDUG-CdJsTJsj.js +1 -0
  21. package/dist-renderer/assets/classDiagram-v2-WZHVMYZB-CdJsTJsj.js +1 -0
  22. package/dist-renderer/assets/clone-BjQBiNfj.js +1 -0
  23. package/dist-renderer/assets/{cose-bilkent-S5V4N54A-BqOg0x3V.js → cose-bilkent-S5V4N54A-CNcsvqPl.js} +1 -1
  24. package/dist-renderer/assets/{dagre-6UL2VRFP-C9JTWefj.js → dagre-6UL2VRFP-DBNx4qqx.js} +1 -1
  25. package/dist-renderer/assets/{diagram-PSM6KHXK-ljleG6ui.js → diagram-PSM6KHXK-BfVlT6sT.js} +1 -1
  26. package/dist-renderer/assets/{diagram-QEK2KX5R-BbV-WSTr.js → diagram-QEK2KX5R-HvVjs0K6.js} +1 -1
  27. package/dist-renderer/assets/{diagram-S2PKOQOG-CKi3DFby.js → diagram-S2PKOQOG-DYb_KnWS.js} +1 -1
  28. package/dist-renderer/assets/{erDiagram-Q2GNP2WA-D3HE7b-j.js → erDiagram-Q2GNP2WA-Ba-IgI5G.js} +1 -1
  29. package/dist-renderer/assets/{flowDiagram-NV44I4VS-C2yLRmM0.js → flowDiagram-NV44I4VS-2iDN8Kpj.js} +1 -1
  30. package/dist-renderer/assets/{ganttDiagram-JELNMOA3-XEV4KtUf.js → ganttDiagram-JELNMOA3-Byjf8Fa3.js} +1 -1
  31. package/dist-renderer/assets/{gitGraphDiagram-V2S2FVAM-ufaCCg7c.js → gitGraphDiagram-V2S2FVAM-DbKvfZ_j.js} +1 -1
  32. package/dist-renderer/assets/{graph-BzPvdBp0.js → graph-Enirf-f8.js} +1 -1
  33. package/dist-renderer/assets/{index-BprOls_t.js → index-AjxP_rE_.js} +1 -1
  34. package/dist-renderer/assets/index-BIOJremZ.css +1 -0
  35. package/dist-renderer/assets/{index-DHq6dXy7.js → index-COZPUWJW.js} +1 -1
  36. package/dist-renderer/assets/{index-Cr91T9ef.js → index-ChR1D6ZF.js} +1 -1
  37. package/dist-renderer/assets/{index-DUIDxnaf.js → index-CtlzGepK.js} +1 -1
  38. package/dist-renderer/assets/{index-A5CMVuXA.js → index-DY1zqsb6.js} +538 -538
  39. package/dist-renderer/assets/{index-yNYjzR2R.js → index-DdhqolqE.js} +1 -1
  40. package/dist-renderer/assets/{infoDiagram-HS3SLOUP-DKP5zgHc.js → infoDiagram-HS3SLOUP-D6uicwz1.js} +1 -1
  41. package/dist-renderer/assets/{journeyDiagram-XKPGCS4Q-Omd7tmzE.js → journeyDiagram-XKPGCS4Q-DqwZsXlQ.js} +1 -1
  42. package/dist-renderer/assets/{kanban-definition-3W4ZIXB7-D7yw9yIY.js → kanban-definition-3W4ZIXB7-fCDVhVUm.js} +1 -1
  43. package/dist-renderer/assets/{layout-DZxAqFuM.js → layout-CPFgj98r.js} +1 -1
  44. package/dist-renderer/assets/{linear-BXWJygRB.js → linear-CYiQ7Y3M.js} +1 -1
  45. package/dist-renderer/assets/{mindmap-definition-VGOIOE7T-BfJ09SBb.js → mindmap-definition-VGOIOE7T-D31dS2KE.js} +1 -1
  46. package/dist-renderer/assets/{pieDiagram-ADFJNKIX-BYaLQhXj.js → pieDiagram-ADFJNKIX-BOsCJfds.js} +1 -1
  47. package/dist-renderer/assets/{quadrantDiagram-AYHSOK5B-DeA0B1fw.js → quadrantDiagram-AYHSOK5B-CYTVQCfr.js} +1 -1
  48. package/dist-renderer/assets/{requirementDiagram-UZGBJVZJ-DnFWn7-v.js → requirementDiagram-UZGBJVZJ-CODCFpkt.js} +1 -1
  49. package/dist-renderer/assets/{sankeyDiagram-TZEHDZUN-L9bek20k.js → sankeyDiagram-TZEHDZUN-Z4ce9ZtZ.js} +1 -1
  50. package/dist-renderer/assets/{sequenceDiagram-WL72ISMW-BBmcJUXb.js → sequenceDiagram-WL72ISMW-CmS9TxhW.js} +1 -1
  51. package/dist-renderer/assets/{stateDiagram-FKZM4ZOC-DrwPQvTq.js → stateDiagram-FKZM4ZOC-o9k-ns3q.js} +1 -1
  52. package/dist-renderer/assets/{stateDiagram-v2-4FDKWEC3-BOUQrTH6.js → stateDiagram-v2-4FDKWEC3-CxHMyEt1.js} +1 -1
  53. package/dist-renderer/assets/{timeline-definition-IT6M3QCI-Dldh9vsj.js → timeline-definition-IT6M3QCI-B6T3zrde.js} +1 -1
  54. package/dist-renderer/assets/{treemap-GDKQZRPO-BsGSs8-P.js → treemap-GDKQZRPO-CVd5GNDw.js} +1 -1
  55. package/dist-renderer/assets/{xychartDiagram-PRI3JC2R-BsR_bj-d.js → xychartDiagram-PRI3JC2R-CleBrdqc.js} +1 -1
  56. package/dist-renderer/index.html +2 -2
  57. package/package.json +1 -1
  58. package/src/main/server.ts +137 -8
  59. package/src/main/services/session-intelligence/SessionUsageParser.ts +446 -0
  60. package/src/main/services/session-intelligence/UsageTelemetryService.ts +237 -0
  61. package/src/renderer/api/httpClient.ts +1 -1
  62. package/src/renderer/components/dashboard/DashboardView.tsx +6 -105
  63. package/src/renderer/components/settings/SettingsTabs.tsx +2 -2
  64. package/src/renderer/components/settings/sections/TaskBusSection.tsx +463 -83
  65. package/src/renderer/components/team/TeamDetailView.tsx +40 -20
  66. package/src/renderer/components/team/TeamListView.tsx +35 -21
  67. package/src/renderer/components/team/dialogs/CreateTeamDialog.tsx +10 -7
  68. package/src/renderer/components/team/dialogs/EditTeamDialog.tsx +9 -6
  69. package/src/shared/types/team.ts +5 -0
  70. package/dist-renderer/assets/channel-BMMyVRy4.js +0 -1
  71. package/dist-renderer/assets/classDiagram-2ON5EDUG-Dz1VG1T3.js +0 -1
  72. package/dist-renderer/assets/classDiagram-v2-WZHVMYZB-Dz1VG1T3.js +0 -1
  73. package/dist-renderer/assets/clone-COsIIGZQ.js +0 -1
  74. package/dist-renderer/assets/index-CWpFqEvz.css +0 -1
@@ -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,11 @@ 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 [telemetryEnabled, setTelemetryEnabled] = useState(false);
206
+ const [telemetryPlatform, setTelemetryPlatform] = useState('claudecode');
207
+ const [scanning, setScanning] = useState(false);
208
+ const [telemetryStatus, setTelemetryStatus] = useState<TelemetryStatus | null>(null);
209
+
18
210
  useEffect(() => {
19
211
  fetch('/api/settings/task-bus')
20
212
  .then((r) => r.json())
@@ -25,18 +217,53 @@ export function TaskBusSection(): React.JSX.Element {
25
217
  setPort(data.redis.port ?? 6379);
26
218
  setPassword(data.redis.password ?? '');
27
219
  }
220
+ if (data.telemetry) {
221
+ setTelemetryEnabled(data.telemetry.enabled);
222
+ setTelemetryPlatform(data.telemetry.platform ?? 'claudecode');
223
+ }
28
224
  })
29
225
  .catch(() => {})
30
226
  .finally(() => setLoading(false));
31
- }, []);
32
227
 
33
- const save = async (connectRedis = false) => {
228
+ // Restore telemetry status + Redis connection state on mount
229
+ fetch('/api/telemetry/status')
230
+ .then((r) => r.json())
231
+ .then((s: TelemetryStatus) => {
232
+ if (s.connected) setConnected(true);
233
+ if ('sessions' in s && s.sessions > 0) setTelemetryStatus(s);
234
+ })
235
+ .catch(() => {});
236
+
237
+ const poll = setInterval(() => {
238
+ if (telemetryEnabled) {
239
+ fetch('/api/telemetry/status')
240
+ .then((r) => r.json())
241
+ .then((s: TelemetryStatus) => setTelemetryStatus(s))
242
+ .catch(() => {});
243
+ }
244
+ }, 30000);
245
+ return () => clearInterval(poll);
246
+ }, [telemetryEnabled]);
247
+
248
+ const buildConfig = (
249
+ overrides: Partial<{
250
+ enabled: boolean;
251
+ telemetryEnabled: boolean;
252
+ telemetryPlatform: string;
253
+ }> = {}
254
+ ): TaskBusConfig => ({
255
+ enabled: overrides.enabled ?? enabled,
256
+ redis: { host, port, password: password || undefined },
257
+ telemetry: {
258
+ enabled: overrides.telemetryEnabled ?? telemetryEnabled,
259
+ platform: (overrides.telemetryPlatform ?? telemetryPlatform) as 'claudecode',
260
+ },
261
+ });
262
+
263
+ const testRedisConnection = async (): Promise<boolean> => {
264
+ setConnecting(true);
34
265
  setMessage(null);
35
- if (connectRedis) setConnecting(true);
36
- const config: TaskBusConfig = {
37
- enabled,
38
- redis: { host, port, password: password || undefined },
39
- };
266
+ const config = buildConfig();
40
267
  try {
41
268
  const res = await fetch('/api/settings/task-bus', {
42
269
  method: 'PUT',
@@ -44,17 +271,13 @@ export function TaskBusSection(): React.JSX.Element {
44
271
  body: JSON.stringify(config),
45
272
  });
46
273
  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
- }
274
+ const ok = !!data.connected;
275
+ setConnected(ok);
276
+ setMessage(ok ? 'Redis 连接成功' : 'Redis 连接失败,请检查配置');
277
+ return ok;
56
278
  } catch (err) {
57
- setMessage(`操作失败: ${err}`);
279
+ setMessage(`连接失败: ${err}`);
280
+ return false;
58
281
  } finally {
59
282
  setConnecting(false);
60
283
  }
@@ -62,20 +285,83 @@ export function TaskBusSection(): React.JSX.Element {
62
285
 
63
286
  const toggle = (value: boolean) => {
64
287
  setEnabled(value);
65
- const config: TaskBusConfig = {
66
- enabled: value,
67
- redis: { host, port, password: password || undefined },
68
- };
288
+ const config = buildConfig({ enabled: value });
69
289
  fetch('/api/settings/task-bus', {
70
290
  method: 'PUT',
71
291
  headers: { 'Content-Type': 'application/json' },
72
292
  body: JSON.stringify(config),
73
293
  })
74
294
  .then((r) => r.json())
75
- .then(() => setMessage(value ? '已开启,指令已注入到团队工作目录' : '已关闭'))
295
+ .then(() => setMessage(value ? '团队总线已激活' : '已关闭'))
76
296
  .catch(() => setMessage('操作失败'));
77
297
  };
78
298
 
299
+ const toggleTelemetry = async (value: boolean) => {
300
+ if (!value) {
301
+ setTelemetryEnabled(false);
302
+ const config = buildConfig({ telemetryEnabled: false });
303
+ fetch('/api/settings/task-bus', {
304
+ method: 'PUT',
305
+ headers: { 'Content-Type': 'application/json' },
306
+ body: JSON.stringify(config),
307
+ }).catch(() => setMessage('操作失败'));
308
+ setTelemetryStatus(null);
309
+ return;
310
+ }
311
+
312
+ // Optimistic update: toggle on immediately
313
+ setTelemetryEnabled(true);
314
+
315
+ // Test Redis if not already connected
316
+ let redisReady = connected;
317
+ if (!redisReady) {
318
+ setMessage('正在测试 Redis 连接...');
319
+ redisReady = await testRedisConnection();
320
+ if (!redisReady) {
321
+ setTelemetryEnabled(false);
322
+ setMessage('Redis 连接失败,无法启用数据上报');
323
+ return;
324
+ }
325
+ }
326
+
327
+ setMessage(null);
328
+ const config = buildConfig({ telemetryEnabled: true });
329
+ try {
330
+ await fetch('/api/settings/task-bus', {
331
+ method: 'PUT',
332
+ headers: { 'Content-Type': 'application/json' },
333
+ body: JSON.stringify(config),
334
+ });
335
+ triggerScan();
336
+ } catch {
337
+ setTelemetryEnabled(false);
338
+ setMessage('操作失败');
339
+ }
340
+ };
341
+
342
+ const triggerScan = () => {
343
+ if (scanning) return;
344
+ setScanning(true);
345
+ fetch('/api/telemetry/scan', { method: 'POST' })
346
+ .then((r) => r.json())
347
+ .then((result: TelemetryStatus & { ok?: boolean }) => {
348
+ if ('sessions' in result) {
349
+ setTelemetryStatus(result);
350
+ }
351
+ })
352
+ .catch(() => setMessage('采集失败,请检查 Redis 连接'))
353
+ .finally(() => setScanning(false));
354
+ };
355
+
356
+ const saveTelemetryPlatform = (nextPlatform = telemetryPlatform) => {
357
+ const config = buildConfig({ telemetryPlatform: nextPlatform });
358
+ fetch('/api/settings/task-bus', {
359
+ method: 'PUT',
360
+ headers: { 'Content-Type': 'application/json' },
361
+ body: JSON.stringify(config),
362
+ }).catch(() => {});
363
+ };
364
+
79
365
  if (loading) {
80
366
  return (
81
367
  <div className="flex items-center justify-center py-12">
@@ -86,81 +372,175 @@ export function TaskBusSection(): React.JSX.Element {
86
372
 
87
373
  return (
88
374
  <div>
89
- <SettingsSectionHeader title="任务总线" icon={<Radio size={12} />} />
375
+ <SettingsSectionHeader title="团队总线" icon={<Radio size={12} />} />
90
376
 
91
377
  <SettingRow
92
- label="启用任务总线"
93
- description="开启后自动为所有团队注入跨团队任务派发指令到 CLAUDE.md"
378
+ label="启用团队总线"
379
+ description="开启后自动为所有团队注入跨团队协作指令到 CLAUDE.md"
94
380
  >
95
381
  <SettingsToggle enabled={enabled} onChange={toggle} />
96
382
  </SettingRow>
97
383
 
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
- 本地模式
110
- </span>
111
- ) : null}
112
- </div>
113
- </SettingRow>
114
-
115
384
  {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
- />
385
+ <>
386
+ {/* Redis 配置 - 必填 */}
387
+ <div className="border-b pb-4" style={{ borderColor: 'var(--color-border-subtle)' }}>
388
+ <div className="flex items-center gap-2 px-1 pb-2">
389
+ <span className="text-sm font-medium text-red-500">*</span>
390
+ <span className="text-sm font-medium">Redis</span>
391
+ <span className="text-xs text-[var(--color-text-muted)]">(数据上报必填)</span>
392
+ <div className="ml-auto flex items-center gap-2">
393
+ {connected ? (
394
+ <span className="flex items-center gap-1 text-xs text-emerald-500">
395
+ <Wifi size={12} />
396
+ 已连接
397
+ </span>
398
+ ) : (
399
+ <span className="flex items-center gap-1 text-xs text-red-500">
400
+ <WifiOff size={12} />
401
+ 未连接
402
+ </span>
403
+ )}
404
+ </div>
405
+ </div>
406
+
407
+ <div className="space-y-3 px-1 pt-2">
408
+ <div className="flex gap-3">
409
+ <div className="flex-1">
410
+ <label className="mb-1 block text-xs text-[var(--color-text-muted)]">主机</label>
411
+ <input
412
+ type="text"
413
+ value={host}
414
+ onChange={(e) => {
415
+ setHost(e.target.value);
416
+ setConnected(false);
417
+ }}
418
+ 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"
419
+ placeholder="127.0.0.1"
420
+ />
421
+ </div>
422
+ <div className="w-24">
423
+ <label className="mb-1 block text-xs text-[var(--color-text-muted)]">端口</label>
424
+ <input
425
+ type="number"
426
+ value={port}
427
+ onChange={(e) => {
428
+ setPort(Number(e.target.value));
429
+ setConnected(false);
430
+ }}
431
+ 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"
432
+ placeholder="6379"
433
+ />
434
+ </div>
128
435
  </div>
129
- <div className="w-24">
130
- <label className="mb-1 block text-xs text-[var(--color-text-muted)]">端口</label>
436
+ <div>
437
+ <label className="mb-1 block text-xs text-[var(--color-text-muted)]">密码</label>
131
438
  <input
132
- type="number"
133
- value={port}
134
- onChange={(e) => setPort(Number(e.target.value))}
439
+ type="password"
440
+ value={password}
441
+ onChange={(e) => {
442
+ setPassword(e.target.value);
443
+ setConnected(false);
444
+ }}
135
445
  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"
446
+ placeholder="可选"
137
447
  />
138
448
  </div>
449
+ <div className="flex items-center gap-3 pt-1">
450
+ <Button
451
+ size="sm"
452
+ onClick={testRedisConnection}
453
+ disabled={connecting}
454
+ className="gap-1.5"
455
+ >
456
+ {connecting ? <Loader2 size={12} className="animate-spin" /> : <Wifi size={12} />}
457
+ {connecting ? '连接中...' : '测试连接'}
458
+ </Button>
459
+ {message && (
460
+ <span className={`text-xs ${connected ? 'text-emerald-500' : 'text-red-500'}`}>
461
+ {message}
462
+ </span>
463
+ )}
464
+ </div>
139
465
  </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="可选"
466
+ </div>
467
+
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>
491
+ </SettingRow>
492
+
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
+ </>
521
+ )}
522
+ </div>
523
+
524
+ {/* 数据上报 - 依赖 Redis,最下面 */}
525
+ <div>
526
+ <SettingRow
527
+ label="数据上报"
528
+ description="将采集数据上报到 Redis,供团队看板使用"
529
+ >
530
+ <SettingsToggle
531
+ enabled={telemetryEnabled && connected}
532
+ onChange={(value) => void toggleTelemetry(value)}
148
533
  />
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>
534
+ </SettingRow>
535
+
536
+ {!connected && (
537
+ <div className="flex items-center gap-2 px-1 py-2 text-xs text-amber-500">
538
+ <AlertCircle size={12} />
539
+ <span>数据上报需要 Redis;请先配置并测试 Redis 连接。</span>
540
+ </div>
541
+ )}
162
542
  </div>
163
- </div>
543
+ </>
164
544
  )}
165
545
 
166
546
  {!enabled && message && (