@yancyyu/openhermit 1.6.27 → 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.
- package/README.md +7 -1
- package/bin/hermit.mjs +2 -2
- package/dist-renderer/assets/{ProjectEditorOverlay-BBwYdXPv.js → ProjectEditorOverlay-A4DZTvSy.js} +1 -1
- package/dist-renderer/assets/{TeamGraphOverlay-DVq8rt6_.js → TeamGraphOverlay-Ba5njic5.js} +1 -1
- package/dist-renderer/assets/{_basePickBy-ZbF0pKvS.js → _basePickBy-BvnK-OC1.js} +1 -1
- package/dist-renderer/assets/{_baseUniq-BBLBOeXc.js → _baseUniq-DmFYXx9G.js} +1 -1
- package/dist-renderer/assets/{arc-wGaEgkCf.js → arc-DX4ZQFY4.js} +1 -1
- package/dist-renderer/assets/{architectureDiagram-VXUJARFQ-BpMkdC35.js → architectureDiagram-VXUJARFQ-DfYr3vEN.js} +1 -1
- package/dist-renderer/assets/{blockDiagram-VD42YOAC-C8Z1xhG4.js → blockDiagram-VD42YOAC-DuXdVeWn.js} +1 -1
- package/dist-renderer/assets/{c4Diagram-YG6GDRKO-CJmlw9LA.js → c4Diagram-YG6GDRKO-Bw2nixXe.js} +1 -1
- package/dist-renderer/assets/channel-Pre42N5O.js +1 -0
- package/dist-renderer/assets/{chunk-4BX2VUAB-CHPHiRPP.js → chunk-4BX2VUAB-DLiNGQoE.js} +1 -1
- package/dist-renderer/assets/{chunk-55IACEB6-DyVohOQb.js → chunk-55IACEB6-B1L_8VIF.js} +1 -1
- package/dist-renderer/assets/{chunk-B4BG7PRW-p5bffh_R.js → chunk-B4BG7PRW-DaZMWKGk.js} +1 -1
- package/dist-renderer/assets/{chunk-DI55MBZ5-BnfGPSUu.js → chunk-DI55MBZ5-ku-dflJG.js} +1 -1
- package/dist-renderer/assets/{chunk-FMBD7UC4-B6SCKseX.js → chunk-FMBD7UC4-DV-mF1dP.js} +1 -1
- package/dist-renderer/assets/{chunk-QN33PNHL-L12RvLBR.js → chunk-QN33PNHL-ByGcDFQ0.js} +1 -1
- package/dist-renderer/assets/{chunk-QZHKN3VN-DeH1Kxge.js → chunk-QZHKN3VN-7dv-Min8.js} +1 -1
- package/dist-renderer/assets/{chunk-TZMSLE5B-BWnjzSlI.js → chunk-TZMSLE5B-WdXL5fTu.js} +1 -1
- package/dist-renderer/assets/classDiagram-2ON5EDUG-CdJsTJsj.js +1 -0
- package/dist-renderer/assets/classDiagram-v2-WZHVMYZB-CdJsTJsj.js +1 -0
- package/dist-renderer/assets/clone-BjQBiNfj.js +1 -0
- package/dist-renderer/assets/{cose-bilkent-S5V4N54A-BtzoT5fu.js → cose-bilkent-S5V4N54A-CNcsvqPl.js} +1 -1
- package/dist-renderer/assets/{dagre-6UL2VRFP-CBBvuoUD.js → dagre-6UL2VRFP-DBNx4qqx.js} +1 -1
- package/dist-renderer/assets/{diagram-PSM6KHXK-Be9BAKws.js → diagram-PSM6KHXK-BfVlT6sT.js} +1 -1
- package/dist-renderer/assets/{diagram-QEK2KX5R-BDS4PI_i.js → diagram-QEK2KX5R-HvVjs0K6.js} +1 -1
- package/dist-renderer/assets/{diagram-S2PKOQOG-2Rameaq7.js → diagram-S2PKOQOG-DYb_KnWS.js} +1 -1
- package/dist-renderer/assets/{erDiagram-Q2GNP2WA-CSIzCEZD.js → erDiagram-Q2GNP2WA-Ba-IgI5G.js} +1 -1
- package/dist-renderer/assets/{flowDiagram-NV44I4VS-ForEIVM5.js → flowDiagram-NV44I4VS-2iDN8Kpj.js} +1 -1
- package/dist-renderer/assets/{ganttDiagram-JELNMOA3-BJrli_xr.js → ganttDiagram-JELNMOA3-Byjf8Fa3.js} +1 -1
- package/dist-renderer/assets/{gitGraphDiagram-V2S2FVAM-C_4GuLno.js → gitGraphDiagram-V2S2FVAM-DbKvfZ_j.js} +1 -1
- package/dist-renderer/assets/{graph-B1EAT_gw.js → graph-Enirf-f8.js} +1 -1
- package/dist-renderer/assets/{index-eKRmS5kI.js → index-AjxP_rE_.js} +1 -1
- package/dist-renderer/assets/index-BIOJremZ.css +1 -0
- package/dist-renderer/assets/{index-Dwr5wu5x.js → index-COZPUWJW.js} +1 -1
- package/dist-renderer/assets/{index-k4tnOFC5.js → index-ChR1D6ZF.js} +1 -1
- package/dist-renderer/assets/{index-DR602dwJ.js → index-CtlzGepK.js} +1 -1
- package/dist-renderer/assets/{index-DYdseEwc.js → index-DY1zqsb6.js} +511 -511
- package/dist-renderer/assets/{index-DOA_jbYb.js → index-DdhqolqE.js} +1 -1
- package/dist-renderer/assets/{infoDiagram-HS3SLOUP-DjI0uaMz.js → infoDiagram-HS3SLOUP-D6uicwz1.js} +1 -1
- package/dist-renderer/assets/{journeyDiagram-XKPGCS4Q-jQ6Thae-.js → journeyDiagram-XKPGCS4Q-DqwZsXlQ.js} +1 -1
- package/dist-renderer/assets/{kanban-definition-3W4ZIXB7-CKw6InbL.js → kanban-definition-3W4ZIXB7-fCDVhVUm.js} +1 -1
- package/dist-renderer/assets/{layout-Dad20y3V.js → layout-CPFgj98r.js} +1 -1
- package/dist-renderer/assets/{linear-vMgo_2Cv.js → linear-CYiQ7Y3M.js} +1 -1
- package/dist-renderer/assets/{mindmap-definition-VGOIOE7T-DYp6YoHL.js → mindmap-definition-VGOIOE7T-D31dS2KE.js} +1 -1
- package/dist-renderer/assets/{pieDiagram-ADFJNKIX-BytBecG9.js → pieDiagram-ADFJNKIX-BOsCJfds.js} +1 -1
- package/dist-renderer/assets/{quadrantDiagram-AYHSOK5B-RUaspLsc.js → quadrantDiagram-AYHSOK5B-CYTVQCfr.js} +1 -1
- package/dist-renderer/assets/{requirementDiagram-UZGBJVZJ-rR2B1Use.js → requirementDiagram-UZGBJVZJ-CODCFpkt.js} +1 -1
- package/dist-renderer/assets/{sankeyDiagram-TZEHDZUN-BJi5qYhq.js → sankeyDiagram-TZEHDZUN-Z4ce9ZtZ.js} +1 -1
- package/dist-renderer/assets/{sequenceDiagram-WL72ISMW-BM-wggUb.js → sequenceDiagram-WL72ISMW-CmS9TxhW.js} +1 -1
- package/dist-renderer/assets/{stateDiagram-FKZM4ZOC-BqmcVjnj.js → stateDiagram-FKZM4ZOC-o9k-ns3q.js} +1 -1
- package/dist-renderer/assets/{stateDiagram-v2-4FDKWEC3-By3JDVbB.js → stateDiagram-v2-4FDKWEC3-CxHMyEt1.js} +1 -1
- package/dist-renderer/assets/{timeline-definition-IT6M3QCI-szH0GUyk.js → timeline-definition-IT6M3QCI-B6T3zrde.js} +1 -1
- package/dist-renderer/assets/{treemap-GDKQZRPO-BCMlh-Ex.js → treemap-GDKQZRPO-CVd5GNDw.js} +1 -1
- package/dist-renderer/assets/{xychartDiagram-PRI3JC2R-dwDpvw0w.js → xychartDiagram-PRI3JC2R-CleBrdqc.js} +1 -1
- package/dist-renderer/index.html +2 -2
- package/package.json +1 -1
- package/src/main/server.ts +120 -3
- package/src/main/services/session-intelligence/SessionUsageParser.ts +446 -0
- package/src/main/services/session-intelligence/UsageTelemetryService.ts +237 -0
- package/src/renderer/components/dashboard/DashboardView.tsx +6 -105
- package/src/renderer/components/settings/SettingsTabs.tsx +2 -2
- package/src/renderer/components/settings/sections/TaskBusSection.tsx +463 -83
- package/src/shared/types/team.ts +5 -0
- package/dist-renderer/assets/channel-DJUrwVrK.js +0 -1
- package/dist-renderer/assets/classDiagram-2ON5EDUG-blc3DrH7.js +0 -1
- package/dist-renderer/assets/classDiagram-v2-WZHVMYZB-blc3DrH7.js +0 -1
- package/dist-renderer/assets/clone-BftjWakJ.js +0 -1
- 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 {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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(
|
|
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
|
|
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="
|
|
375
|
+
<SettingsSectionHeader title="团队总线" icon={<Radio size={12} />} />
|
|
90
376
|
|
|
91
377
|
<SettingRow
|
|
92
|
-
label="
|
|
93
|
-
description="
|
|
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
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
className="
|
|
126
|
-
|
|
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
|
|
130
|
-
<label className="mb-1 block text-xs text-[var(--color-text-muted)]"
|
|
436
|
+
<div>
|
|
437
|
+
<label className="mb-1 block text-xs text-[var(--color-text-muted)]">密码</label>
|
|
131
438
|
<input
|
|
132
|
-
type="
|
|
133
|
-
value={
|
|
134
|
-
onChange={(e) =>
|
|
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="
|
|
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
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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
|
-
</
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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
|
-
|
|
543
|
+
</>
|
|
164
544
|
)}
|
|
165
545
|
|
|
166
546
|
{!enabled && message && (
|
package/src/shared/types/team.ts
CHANGED
|
@@ -166,6 +166,11 @@ export interface TaskBusConfig {
|
|
|
166
166
|
password?: string;
|
|
167
167
|
db?: number;
|
|
168
168
|
};
|
|
169
|
+
telemetry?: {
|
|
170
|
+
enabled: boolean;
|
|
171
|
+
/** Data source platform. Currently only 'claudecode'. */
|
|
172
|
+
platform: 'claudecode';
|
|
173
|
+
};
|
|
169
174
|
}
|
|
170
175
|
|
|
171
176
|
export interface TaskDispatchPayload {
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{a6 as o,a7 as n}from"./index-DYdseEwc.js";const t=(a,r)=>o.lang.round(n.parse(a)[r]);export{t as c};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{s as a,c as s,a as e,C as t}from"./chunk-B4BG7PRW-p5bffh_R.js";import{_ as i}from"./index-DYdseEwc.js";import"./chunk-FMBD7UC4-B6SCKseX.js";import"./chunk-55IACEB6-DyVohOQb.js";import"./chunk-QN33PNHL-L12RvLBR.js";import"./splashScene-C8lWNnm4.js";var u={parser:e,get db(){return new t},renderer:s,styles:a,init:i(r=>{r.class||(r.class={}),r.class.arrowMarkerAbsolute=r.arrowMarkerAbsolute},"init")};export{u as diagram};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{s as a,c as s,a as e,C as t}from"./chunk-B4BG7PRW-p5bffh_R.js";import{_ as i}from"./index-DYdseEwc.js";import"./chunk-FMBD7UC4-B6SCKseX.js";import"./chunk-55IACEB6-DyVohOQb.js";import"./chunk-QN33PNHL-L12RvLBR.js";import"./splashScene-C8lWNnm4.js";var u={parser:e,get db(){return new t},renderer:s,styles:a,init:i(r=>{r.class||(r.class={}),r.class.arrowMarkerAbsolute=r.arrowMarkerAbsolute},"init")};export{u as diagram};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{b as r}from"./_baseUniq-BBLBOeXc.js";var e=4;function a(o){return r(o,e)}export{a as c};
|