Compare commits

..
Author SHA1 Message Date
ji ruo bf2ef120da fix: make resource metrics platform aware 2026-10-30 01:39:39 +08:00
7 changed files with 68 additions and 109 deletions

No files matched your search

@@ -6,9 +6,11 @@ import { usePathname } from "next/navigation";
import { Separator } from "@/components/ui/separator";
import { SidebarTrigger } from "@/components/ui/sidebar";
import { CpuIcon, HardDriveIcon, MemoryStickIcon } from "lucide-react";
import { useCurrentUser } from "@/hooks/use-current-user";
import { api } from "@/lib/api";
import { cn } from "@/lib/utils";
import type { SystemMetrics } from "@/lib/types";
import { EgressIpNavbar, EgressIpProvider } from "./egress-ip-banner";
import { LiveClock } from "./live-clock";
@@ -76,6 +78,7 @@ function MainContentInner({ children }: { children: ReactNode }) {
</div>
<div className="min-w-0 flex-1">
<EgressIpNavbar />
<SystemResourceNavbar />
</div>
<div className="flex shrink-0 items-center gap-2">
<LiveClock />
@@ -95,3 +98,42 @@ function MainContentInner({ children }: { children: ReactNode }) {
</>
);
}
function SystemResourceNavbar() {
const [metrics, setMetrics] = useState<SystemMetrics | null>(null);
useEffect(() => {
let alive = true;
const load = async () => {
try {
const next = await api.systemMetrics();
if (alive) setMetrics(next);
} catch {
/* Keep the status bar quiet while the API is unavailable. */
}
};
void load();
const timer = setInterval(load, 5000);
return () => { alive = false; clearInterval(timer); };
}, []);
if (!metrics) return null;
if (!metrics.supported) {
return <div className="flex items-center justify-center text-xs text-muted-foreground" role="status">资源状态:暂不支持</div>;
}
const items = [
[CpuIcon, "CPU", metrics.cpu.percent],
[MemoryStickIcon, "内存", metrics.memory.percent],
[HardDriveIcon, "磁盘", metrics.data_disk.percent],
] as const;
return (
<div className="flex items-center justify-center gap-3 text-[10px] text-muted-foreground" role="status" aria-live="polite">
<span>容器资源</span>
{items.map(([Icon, label, value]) => (
<span key={label} className="inline-flex items-center gap-1 tabular-nums" title={`${label} ${value.toFixed(1)}%`}>
<Icon className="size-3" aria-hidden="true" /> {label} {value.toFixed(1)}%
</span>
))}
</div>
);
}
-52
View File
@@ -9,9 +9,6 @@ import {
ArrowUpRightIcon,
BugIcon,
ClockIcon,
CpuIcon,
HardDriveIcon,
MemoryStickIcon,
NetworkIcon,
ShieldCheckIcon,
TargetIcon,
@@ -41,7 +38,6 @@ import type {
Tool,
TrafficExchange,
UsageStats,
SystemMetrics,
} from "@/lib/types";
import { cn } from "@/lib/utils";
@@ -105,35 +101,6 @@ function statusBg(code: number): string {
return "bg-red-400";
}
function SystemResourceStatus({ metrics }: { metrics: SystemMetrics | null }) {
if (!metrics) return null;
if (!metrics.supported) {
return <div className="ml-auto text-xs text-muted-foreground" role="status">资源状态:暂不支持</div>;
}
const items = [
[CpuIcon, "CPU", metrics.cpu.percent],
[MemoryStickIcon, "内存", metrics.memory.percent],
[HardDriveIcon, "磁盘", metrics.data_disk.percent],
] as const;
return (
<div className="ml-auto flex min-w-0 items-center justify-end gap-3 text-xs text-muted-foreground" role="status" aria-live="polite">
<span className="shrink-0 font-medium">容器资源</span>
{items.map(([Icon, label, value]) => (
<div key={label} className="w-24 min-w-0" title={`${label} ${value.toFixed(1)}%`}>
<div className="mb-1 flex items-center justify-between gap-1 text-[10px]">
<span className="inline-flex items-center gap-1"><Icon className="size-3" aria-hidden="true" />{label}</span>
<span className="tabular-nums">{value.toFixed(1)}%</span>
</div>
<Progress
value={value}
className="h-2 bg-muted [&_[data-slot=progress-indicator]]:bg-slate-500 dark:[&_[data-slot=progress-indicator]]:bg-slate-400"
/>
</div>
))}
</div>
);
}
// ── sub-components ────────────────────────────────────────────────────────────
function LiveDot({ className }: { className?: string }) {
@@ -180,22 +147,6 @@ export default function DashboardPage() {
const [skills, setSkills] = React.useState<SkillItem[]>([]);
const [tools, setTools] = React.useState<Tool[]>([]);
const [llmProfiles, setLLMProfiles] = React.useState<LLMProfile[]>([]);
const [systemMetrics, setSystemMetrics] = React.useState<SystemMetrics | null>(null);
React.useEffect(() => {
let alive = true;
const load = async () => {
try {
const next = await api.systemMetrics();
if (alive) setSystemMetrics(next);
} catch {
/* retry on the next interval */
}
};
void load();
const timer = setInterval(load, 5000);
return () => { alive = false; clearInterval(timer); };
}, []);
// The task list is the most expensive dashboard source. Poll it independently
// so a large history cannot hold back every other dashboard panel.
@@ -544,13 +495,10 @@ export default function DashboardPage() {
<div className="flex flex-1 flex-col gap-4 pb-6">
{/* ── Header ── */}
<div>
<div className="flex items-center gap-6">
<div>
<h1 className="text-lg font-semibold tracking-tight">总览</h1>
<p className="text-xs text-muted-foreground">系统全局状态 · 实时刷新</p>
</div>
<SystemResourceStatus metrics={systemMetrics} />
</div>
</div>
{/* ── Row 1: 5 stat cards ── */}
@@ -923,7 +923,7 @@ export default function FindingsPage() {
})}
</div>
<div className="scrollbar-none flex min-w-0 flex-nowrap items-center gap-2 overflow-x-auto pb-1">
<div className="flex min-w-0 flex-nowrap items-center gap-2 overflow-x-auto pb-1">
<InputGroup className="w-72 shrink-0">
<InputGroupInput
type="search"
+3 -25
View File
@@ -65,28 +65,6 @@ function originLabel(row: InterceptApprovalRow) {
return "—";
}
// Background task runs carry a pool label such as "work#2 · #2770", while
// conversation-backed runs carry the agent key (for example "reporter").
// Present a stable, human-readable agent label in both cases.
function agentLabel(row: InterceptApprovalRow) {
const key = (row.conv_agent_key || "").trim().toLowerCase();
const labels: Record<string, string> = {
auto: "自动 Agent",
mainagent: "主 Agent",
planner: "规划 Agent",
worker: "执行 Worker",
reporter: "报告编写",
retester: "复测 Agent",
pentest: "渗透测试 Agent",
};
if (key && labels[key]) return labels[key];
const raw = (row.agent_name || "").trim();
if (/^work#\d+(?:\s|$)/i.test(raw)) return "执行 Worker";
const rawKey = raw.toLowerCase();
if (labels[rawKey]) return labels[rawKey];
return raw || "未知 Agent";
}
function ApprovalOrigin({ row, detail = false }: { row: InterceptApprovalRow; detail?: boolean }) {
const [locating, setLocating] = React.useState(false);
const label = detail && row.task_id ? `任务 ${row.task_id}` : originLabel(row);
@@ -476,7 +454,7 @@ export function ApprovalDetail({
<SideQuestionButton side={side} />
</div>
<CodeBlock
label={`${agentLabel(current)} · 工具请求`}
label={`${current.agent_name || current.conv_agent_key || "Agent"} · 工具请求`}
text={JSON.stringify(row.tool_input ?? {}, null, 2)}
/>
{command ? (
@@ -748,8 +726,8 @@ function ApprovalTable({
<span className="truncate" title={originLabel(row)}>
<ApprovalOrigin row={row} />
</span>
<span className="truncate text-muted-foreground text-xs" title={agentLabel(row)}>
Agent:{agentLabel(row)}
<span className="truncate text-muted-foreground text-xs">
{row.agent_name || row.conv_agent_key}
</span>
</div>
</TableCell>
+20 -29
View File
@@ -2,8 +2,14 @@
import * as React from "react";
import { EditorState } from "@codemirror/state";
import { drawSelection, EditorView, highlightActiveLine, keymap, lineNumbers } from "@codemirror/view";
import { defaultKeymap, history, historyKeymap, indentWithTab } from "@codemirror/commands";
import {
drawSelection,
EditorView,
highlightActiveLine,
keymap,
lineNumbers,
} from "@codemirror/view";
import { defaultKeymap, indentWithTab } from "@codemirror/commands";
import { defaultHighlightStyle, HighlightStyle, StreamLanguage, syntaxHighlighting } from "@codemirror/language";
import { tags } from "@lezer/highlight";
@@ -31,31 +37,17 @@ const httpLanguage = StreamLanguage.define({
},
});
const packetTheme = EditorView.theme(
{
"&": { height: "100%", backgroundColor: "transparent", color: "var(--foreground)" },
".cm-scroller": {
overflow: "auto",
fontFamily: "var(--font-mono)",
fontSize: "0.75rem",
fontWeight: "500",
lineHeight: "1.65rem",
},
".cm-content": { minHeight: "100%", padding: "0.5rem 0.5rem" },
".cm-gutters": {
backgroundColor: "transparent",
color: "var(--muted-foreground)",
border: "0",
borderRight: "1px solid var(--border)",
},
".cm-lineNumbers .cm-gutterElement": { minWidth: "2rem", padding: "0 0.5rem" },
".cm-activeLine": { backgroundColor: "transparent" },
".cm-activeLineGutter": { backgroundColor: "transparent" },
".cm-selectionBackground, ::selection": { backgroundColor: "#c9d2e3" },
".cm-cursor": { borderLeftColor: "#222" },
},
{ dark: false },
);
const packetTheme = EditorView.theme({
"&": { height: "100%", backgroundColor: "transparent", color: "var(--foreground)" },
".cm-scroller": { overflow: "auto", fontFamily: "var(--font-mono)", fontSize: "0.75rem", fontWeight: "500", lineHeight: "1.65rem" },
".cm-content": { minHeight: "100%", padding: "0.5rem 0.5rem" },
".cm-gutters": { backgroundColor: "transparent", color: "var(--muted-foreground)", border: "0", borderRight: "1px solid var(--border)" },
".cm-lineNumbers .cm-gutterElement": { minWidth: "2rem", padding: "0 0.5rem" },
".cm-activeLine": { backgroundColor: "transparent" },
".cm-activeLineGutter": { backgroundColor: "transparent" },
".cm-selectionBackground, ::selection": { backgroundColor: "#c9d2e3" },
".cm-cursor": { borderLeftColor: "#222" },
}, { dark: false });
export function PacketCodeEditor({
value,
@@ -84,8 +76,7 @@ export function PacketCodeEditor({
lineNumbers(),
drawSelection(),
highlightActiveLine(),
history(),
keymap.of([...defaultKeymap, ...historyKeymap, indentWithTab]),
keymap.of([...defaultKeymap, indentWithTab]),
httpLanguage,
syntaxHighlighting(defaultHighlightStyle),
syntaxHighlighting(packetHighlight),
@@ -112,7 +112,7 @@ export function RepeaterWorkspace() {
</div>
</div>
<div className="flex min-h-10 min-w-0 items-center rounded-md border bg-muted/20 p-1">
<div className="scrollbar-none flex min-w-0 flex-1 items-center gap-1 overflow-x-auto">
<div className="flex min-w-0 flex-1 items-center gap-1 overflow-x-auto">
{workspace.tabs.map((tab) => {
const selected = tab.id === workspace.activeTabId;
return (
+1 -1
View File
@@ -14,7 +14,7 @@ function InputGroup({ className, ...props }: React.ComponentProps<"div">) {
data-slot="input-group"
role="group"
className={cn(
"group/input-group relative flex h-8 w-full min-w-0 items-center rounded-lg border border-input transition-colors outline-none in-data-[slot=combobox-content]:focus-within:border-inherit in-data-[slot=combobox-content]:focus-within:ring-0 has-disabled:bg-input/50 has-disabled:opacity-50 has-[[data-slot=input-group-control]:focus-visible]:border-ring has-[[data-slot=input-group-control]:focus-visible]:ring-1 has-[[data-slot=input-group-control]:focus-visible]:ring-ring/30 has-[[data-slot][aria-invalid=true]]:border-destructive has-[[data-slot][aria-invalid=true]]:ring-3 has-[[data-slot][aria-invalid=true]]:ring-destructive/20 has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-start]]:h-auto has-[>[data-align=block-start]]:flex-col has-[>textarea]:h-auto dark:bg-input/30 dark:has-disabled:bg-input/80 dark:has-[[data-slot][aria-invalid=true]]:ring-destructive/40 has-[>[data-align=block-end]]:[&>input]:pt-3 has-[>[data-align=block-start]]:[&>input]:pb-3 has-[>[data-align=inline-end]]:[&>input]:pr-1.5 has-[>[data-align=inline-start]]:[&>input]:pl-1.5",
"group/input-group relative flex h-8 w-full min-w-0 items-center rounded-lg border border-input transition-colors outline-none in-data-[slot=combobox-content]:focus-within:border-inherit in-data-[slot=combobox-content]:focus-within:ring-0 has-disabled:bg-input/50 has-disabled:opacity-50 has-[[data-slot=input-group-control]:focus-visible]:border-ring has-[[data-slot=input-group-control]:focus-visible]:ring-3 has-[[data-slot=input-group-control]:focus-visible]:ring-ring/50 has-[[data-slot][aria-invalid=true]]:border-destructive has-[[data-slot][aria-invalid=true]]:ring-3 has-[[data-slot][aria-invalid=true]]:ring-destructive/20 has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-start]]:h-auto has-[>[data-align=block-start]]:flex-col has-[>textarea]:h-auto dark:bg-input/30 dark:has-disabled:bg-input/80 dark:has-[[data-slot][aria-invalid=true]]:ring-destructive/40 has-[>[data-align=block-end]]:[&>input]:pt-3 has-[>[data-align=block-start]]:[&>input]:pb-3 has-[>[data-align=inline-end]]:[&>input]:pr-1.5 has-[>[data-align=inline-start]]:[&>input]:pl-1.5",
className
)}
{...props}