import { useState } from "react"; import { useAppStore } from "../../store/useAppStore"; import { useAuditStore } from "../audit"; import { CardGrid, Notice, PageHeader, StatCard } from "../components"; import { Button, Surface, Tag } from "../../components/primitives"; import { Icon } from "../../components/Icon"; import { useAsyncData } from "../../lib/useAsyncData"; import { useAutoReload } from "../hooks"; import { adminObservabilityAudit, adminObservabilityMetrics, adminObservabilityTrace } from "../../lib/api"; import type { AdminTraceSpan, AuditRecord } from "../../lib/types"; type Tab = "logs" | "trace" | "metrics" | "audit"; const TABS: ReadonlyArray<{ id: Tab; label: string }> = [ { id: "logs", label: "实时日志" }, { id: "trace", label: "Tracing" }, { id: "metrics", label: "指标" }, { id: "audit", label: "审计" }, ]; const KIND_LABEL: Record = { llm_call: "LLM", tool_call: "工具", orchestration: "编排", state_transition: "状态", subagent: "子代理", memory: "记忆", guardrail: "护栏", }; export function ObservabilityModule(): React.ReactNode { const [tab, setTab] = useState("logs"); const deviceUpdates = useAppStore((s) => s.deviceUpdates); const audit = useAuditStore((s) => s.records); const clearAudit = useAuditStore((s) => s.clear); return (
{TABS.map((t) => ( ))}
{tab === "logs" ? : null} {tab === "trace" ? : null} {tab === "metrics" ? : null} {tab === "audit" ? : null}
); } function LogsTail({ lines }: { lines: string[] }): React.ReactNode { return (

实时日志流(WS 事件回显)

tail
{lines.length === 0 ? ( 暂无实时事件。接入 /ws/chat 后,此处回显 stream / tool / device_update / reminder 等事件。 ) : (
          {lines.join("\n")}
        
)} 完整后端日志分级 tail / 过滤 / 导出需后端 M10+ 提供。
); } function TraceView(): React.ReactNode { const trace = useAsyncData(() => adminObservabilityTrace(100), []); useAutoReload(trace.reload, 3000); const spans = trace.data ?? []; return (

Tracing 视图

实时 GET /api/v1/admin/observability/trace
{trace.loading ? ( 加载 span 数据… ) : trace.error ? ( trace 后端暂不可用:{trace.error} ) : spans.length === 0 ? ( 暂无 span。接入 /ws/chat 发起会话后,LLM / 工具 / 编排 / 子代理等 span 会实时记录到环形缓冲。 ) : ( )} 数据源:gateway trace 环形缓冲(经 TraceSink 对接 runtime 的 tracer)。可在 TraceSink 注入前展示占位,注入后即真实 span。
); } function SpanList({ spans }: { spans: AdminTraceSpan[] }): React.ReactNode { const [openId, setOpenId] = useState(null); return (
{spans.map((sp) => { const open = openId === sp.id; return ( ); })}
); } function MetricsView(): React.ReactNode { const metrics = useAsyncData(adminObservabilityMetrics, []); useAutoReload(metrics.reload, 3000); const m = metrics.data; const topPaths = Object.entries(m?.perPath ?? {}) .sort((a, b) => b[1] - a[1]) .slice(0, 8); return (
} label="请求 QPS" value={m ? m.qps.toFixed(1) : "—"} sub={m ? `总请求 ${m.totalRequests}` : "网关指标加载中"} tone="accent" /> } label="LLM 调用次数" value={m ? m.llmCalls : "—"} sub="从 trace span 累计" tone="neutral" /> } label="子代理事件" value={m ? m.subagentEvents : "—"} sub="从 trace span 累计" tone="ok" /> } label="工时/Token" value="—" sub="成本后端暂不统计" tone="warn" /> {metrics.error ? 指标后端暂不可用:{metrics.error} : null} {m && (topPaths.length > 0 || Object.keys(m.statusCounts).length > 0) ? (

请求分布(真实网关计数)

{topPaths.length > 0 ? (
{topPaths.map(([path, count]) => (
{path} {count}
))}
) : null} {Object.keys(m.statusCounts).length > 0 ? (
{Object.entries(m.statusCounts).map(([code, count]) => ( = 500 ? "err" : "warn"}> {code} × {count} ))} 平均 {m.avgLatencyMs.toFixed(1)}ms · 最大 {m.maxLatencyMs}ms
) : null} ) : null} LLM 调用次数 / 子代理事件由实际观测到的 trace span 累计;Token / 成本后端暂不统计(不伪造指标)。指标时序图 / 导出需后端 M10+ 提供。
); } interface AuditViewProps { records: AuditRecord[]; onClear: () => void; } function AuditView({ records: local, onClear }: AuditViewProps): React.ReactNode { const audit = useAsyncData(() => adminObservabilityAudit(100), []); useAutoReload(audit.reload, 5000); // Backend is the authoritative source; the local store is a fallback while the // endpoint is unavailable. const records = audit.data ?? local; const source = audit.data ? "后端" : "本地(后端未接入)"; return (

审计日志(who / what / when / result)

{source} 管理 + 敏感操作全程落审计
{audit.error && !audit.data ? 审计端点暂不可用,回退本地记录:{audit.error} : null} {records.length === 0 ? ( 暂无审计记录。本面板的管理/敏感操作(策略写入、主人档案编辑、API key 签发等)都会在此登记。 ) : ( {records.map((r) => ( ))}
who what when result
{r.who} {r.what} {new Date(r.when).toLocaleString()} {r.result}
)}
); } /** Formats a nanosecond duration into a human-readable label. */ function fmtDuration(ns: number): string { if (ns >= 1e9) return `${(ns / 1e9).toFixed(2)} s`; if (ns >= 1e6) return `${(ns / 1e6).toFixed(2)} ms`; if (ns >= 1e3) return `${(ns / 1e3).toFixed(1)} µs`; return `${ns} ns`; }