@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.
- package/README.md +7 -1
- package/bin/hermit.mjs +2 -2
- package/dist-renderer/assets/ProjectEditorOverlay-CQm6jUR1.js +52 -0
- package/dist-renderer/assets/{TeamGraphOverlay-DVq8rt6_.js → TeamGraphOverlay-h0WDfifv.js} +1 -1
- package/dist-renderer/assets/{_basePickBy-ZbF0pKvS.js → _basePickBy-CgG_tjgX.js} +1 -1
- package/dist-renderer/assets/{_baseUniq-BBLBOeXc.js → _baseUniq-DwPTU9lP.js} +1 -1
- package/dist-renderer/assets/{arc-wGaEgkCf.js → arc-7nIrGRzY.js} +1 -1
- package/dist-renderer/assets/{architectureDiagram-VXUJARFQ-BpMkdC35.js → architectureDiagram-VXUJARFQ-BYhA6Ev2.js} +1 -1
- package/dist-renderer/assets/{blockDiagram-VD42YOAC-C8Z1xhG4.js → blockDiagram-VD42YOAC-BVpZUGDg.js} +1 -1
- package/dist-renderer/assets/{c4Diagram-YG6GDRKO-CJmlw9LA.js → c4Diagram-YG6GDRKO-DsdreMQ9.js} +1 -1
- package/dist-renderer/assets/channel-C0SqeFU7.js +1 -0
- package/dist-renderer/assets/{chunk-4BX2VUAB-CHPHiRPP.js → chunk-4BX2VUAB-CcoAs7Jd.js} +1 -1
- package/dist-renderer/assets/{chunk-55IACEB6-DyVohOQb.js → chunk-55IACEB6-CGGAOoXd.js} +1 -1
- package/dist-renderer/assets/{chunk-B4BG7PRW-p5bffh_R.js → chunk-B4BG7PRW-FhpTEPvD.js} +1 -1
- package/dist-renderer/assets/{chunk-DI55MBZ5-BnfGPSUu.js → chunk-DI55MBZ5-DoYySbm1.js} +1 -1
- package/dist-renderer/assets/{chunk-FMBD7UC4-B6SCKseX.js → chunk-FMBD7UC4-e9l2tGHG.js} +1 -1
- package/dist-renderer/assets/{chunk-QN33PNHL-L12RvLBR.js → chunk-QN33PNHL-DeiXVTCy.js} +1 -1
- package/dist-renderer/assets/{chunk-QZHKN3VN-DeH1Kxge.js → chunk-QZHKN3VN-DC2UJLJM.js} +1 -1
- package/dist-renderer/assets/{chunk-TZMSLE5B-BWnjzSlI.js → chunk-TZMSLE5B-BHFD9eZI.js} +1 -1
- package/dist-renderer/assets/classDiagram-2ON5EDUG-DWew1HpM.js +1 -0
- package/dist-renderer/assets/classDiagram-v2-WZHVMYZB-DWew1HpM.js +1 -0
- package/dist-renderer/assets/clone-Dm-k63Yr.js +1 -0
- package/dist-renderer/assets/{cose-bilkent-S5V4N54A-BtzoT5fu.js → cose-bilkent-S5V4N54A-BdybQraU.js} +1 -1
- package/dist-renderer/assets/{dagre-6UL2VRFP-CBBvuoUD.js → dagre-6UL2VRFP-DdF3pwM3.js} +1 -1
- package/dist-renderer/assets/{diagram-PSM6KHXK-Be9BAKws.js → diagram-PSM6KHXK-B9Ldd3nh.js} +1 -1
- package/dist-renderer/assets/{diagram-QEK2KX5R-BDS4PI_i.js → diagram-QEK2KX5R-XEqkrbpu.js} +1 -1
- package/dist-renderer/assets/{diagram-S2PKOQOG-2Rameaq7.js → diagram-S2PKOQOG-CipwtY59.js} +1 -1
- package/dist-renderer/assets/{erDiagram-Q2GNP2WA-CSIzCEZD.js → erDiagram-Q2GNP2WA-BB-2ISGo.js} +1 -1
- package/dist-renderer/assets/{flowDiagram-NV44I4VS-ForEIVM5.js → flowDiagram-NV44I4VS-B8XmJ0u2.js} +1 -1
- package/dist-renderer/assets/{ganttDiagram-JELNMOA3-BJrli_xr.js → ganttDiagram-JELNMOA3-D-8XglBb.js} +1 -1
- package/dist-renderer/assets/{gitGraphDiagram-V2S2FVAM-C_4GuLno.js → gitGraphDiagram-V2S2FVAM-DL4ChakD.js} +1 -1
- package/dist-renderer/assets/{graph-B1EAT_gw.js → graph-BiFNoBjP.js} +1 -1
- package/dist-renderer/assets/{index-eKRmS5kI.js → index-6m1ZAymG.js} +1 -1
- package/dist-renderer/assets/index-BhellmRb.css +1 -0
- package/dist-renderer/assets/{index-DYdseEwc.js → index-BowUl0Jb.js} +518 -514
- package/dist-renderer/assets/{index-DR602dwJ.js → index-Dp3kJTEe.js} +1 -1
- package/dist-renderer/assets/{index-Dwr5wu5x.js → index-TOpt_T7A.js} +1 -1
- package/dist-renderer/assets/{index-DOA_jbYb.js → index-qNBNjW4K.js} +1 -1
- package/dist-renderer/assets/{index-k4tnOFC5.js → index-vAykq1H1.js} +1 -1
- package/dist-renderer/assets/{infoDiagram-HS3SLOUP-DjI0uaMz.js → infoDiagram-HS3SLOUP-DRIBfHDi.js} +1 -1
- package/dist-renderer/assets/{journeyDiagram-XKPGCS4Q-jQ6Thae-.js → journeyDiagram-XKPGCS4Q-BOMiigU4.js} +1 -1
- package/dist-renderer/assets/{kanban-definition-3W4ZIXB7-CKw6InbL.js → kanban-definition-3W4ZIXB7-DDxeyjod.js} +1 -1
- package/dist-renderer/assets/{layout-Dad20y3V.js → layout-DNANbrI4.js} +1 -1
- package/dist-renderer/assets/{linear-vMgo_2Cv.js → linear-DxEJi1yT.js} +1 -1
- package/dist-renderer/assets/{mindmap-definition-VGOIOE7T-DYp6YoHL.js → mindmap-definition-VGOIOE7T-nBfGriW8.js} +1 -1
- package/dist-renderer/assets/{pieDiagram-ADFJNKIX-BytBecG9.js → pieDiagram-ADFJNKIX-Din5j6sV.js} +1 -1
- package/dist-renderer/assets/{quadrantDiagram-AYHSOK5B-RUaspLsc.js → quadrantDiagram-AYHSOK5B-DMVK2BEQ.js} +1 -1
- package/dist-renderer/assets/{requirementDiagram-UZGBJVZJ-rR2B1Use.js → requirementDiagram-UZGBJVZJ-6SC94Gg_.js} +1 -1
- package/dist-renderer/assets/{sankeyDiagram-TZEHDZUN-BJi5qYhq.js → sankeyDiagram-TZEHDZUN-CD2gghhu.js} +1 -1
- package/dist-renderer/assets/{sequenceDiagram-WL72ISMW-BM-wggUb.js → sequenceDiagram-WL72ISMW-BnhkN7nZ.js} +1 -1
- package/dist-renderer/assets/{stateDiagram-FKZM4ZOC-BqmcVjnj.js → stateDiagram-FKZM4ZOC-Bn8XdYX-.js} +1 -1
- package/dist-renderer/assets/{stateDiagram-v2-4FDKWEC3-By3JDVbB.js → stateDiagram-v2-4FDKWEC3-1b6sI1_g.js} +1 -1
- package/dist-renderer/assets/{timeline-definition-IT6M3QCI-szH0GUyk.js → timeline-definition-IT6M3QCI-CNs3RPoa.js} +1 -1
- package/dist-renderer/assets/treemap-GDKQZRPO-DU_yr827.js +162 -0
- package/dist-renderer/assets/{xychartDiagram-PRI3JC2R-dwDpvw0w.js → xychartDiagram-PRI3JC2R-B8o5J2f3.js} +1 -1
- package/dist-renderer/index.html +2 -2
- package/package.json +1 -1
- package/src/main/server.ts +800 -163
- package/src/main/services/session-intelligence/SessionUsageParser.ts +446 -0
- package/src/main/services/session-intelligence/UsageTelemetryService.ts +252 -0
- package/src/main/services/teams-mvp/CollaborationBoardService.ts +310 -0
- package/src/main/services/teams-mvp/TaskDispatchService.ts +880 -95
- package/src/main/services/teams-mvp/TeamProvisioningService.ts +58 -19
- package/src/main/services/teams-mvp/TeamWorkspaceService.ts +25 -2
- package/src/main/services/teams-mvp/index.ts +3 -0
- package/src/renderer/App.tsx +5 -0
- package/src/renderer/api/httpClient.ts +67 -0
- package/src/renderer/components/dashboard/DashboardView.tsx +6 -105
- package/src/renderer/components/layout/PaneContent.tsx +2 -0
- package/src/renderer/components/layout/SortableTab.tsx +1 -0
- package/src/renderer/components/layout/TabBarActions.tsx +12 -12
- package/src/renderer/components/schedules/SchedulesView.tsx +54 -22
- package/src/renderer/components/settings/SettingsTabs.tsx +2 -2
- package/src/renderer/components/settings/sections/AdvancedSection.tsx +1 -1
- package/src/renderer/components/settings/sections/TaskBusSection.tsx +511 -81
- package/src/renderer/components/tasks/TasksView.tsx +343 -0
- package/src/renderer/components/team/TeamDetailView.tsx +20 -98
- package/src/renderer/components/team/dialogs/LaunchTeamDialog.tsx +1 -1
- package/src/renderer/components/team/editor/EditorContextMenu.tsx +8 -23
- package/src/renderer/components/team/editor/EditorFileTree.tsx +0 -4
- package/src/renderer/components/team/editor/EditorSelectionMenu.tsx +1 -8
- package/src/renderer/components/team/editor/ProjectEditorOverlay.tsx +0 -10
- package/src/renderer/components/team/kanban/KanbanBoard.tsx +5 -1
- package/src/renderer/components/team/members/MemberDetailDialog.tsx +8 -33
- package/src/renderer/components/team/messages/MessageComposer.tsx +39 -3
- package/src/renderer/components/team/messages/MessagesPanel.tsx +72 -2
- package/src/renderer/components/team/messages/StatusBlock.tsx +2 -24
- package/src/renderer/components/team/schedule/ScheduleEmptyState.tsx +1 -1
- package/src/renderer/components/ui/MentionableTextarea.tsx +0 -1
- package/src/renderer/store/slices/scheduleSlice.ts +21 -0
- package/src/renderer/store/slices/teamSlice.ts +59 -23
- package/src/renderer/types/tabs.ts +1 -0
- package/src/shared/types/api.ts +29 -0
- package/src/shared/types/team.ts +109 -1
- package/dist-renderer/assets/ProjectEditorOverlay-BBwYdXPv.js +0 -57
- 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
- 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 {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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(
|
|
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
|
|
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="
|
|
408
|
+
<SettingsSectionHeader title="本地数据采集" icon={<BarChart3 size={12} />} />
|
|
90
409
|
|
|
91
410
|
<SettingRow
|
|
92
|
-
label="
|
|
93
|
-
description="
|
|
411
|
+
label="数据采集"
|
|
412
|
+
description="扫描本机 ~/.claude/projects 会话文件,采集使用指标;不需要 Redis,也不会上传对话内容"
|
|
94
413
|
>
|
|
95
|
-
<
|
|
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
|
-
{
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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
|
-
|
|
112
|
-
|
|
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
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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
|
-
|
|
130
|
-
|
|
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="
|
|
133
|
-
value={
|
|
134
|
-
onChange={(e) =>
|
|
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="
|
|
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
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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
|
-
</
|
|
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>
|
|
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
|
-
|
|
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 && (
|