import { useEffect, useMemo, useState } from "react"; import { guardrailEvents, type GuardrailEvent } from "../api"; // 输入护栏观测:真数据(来自 guardrail_event,middleware.Guardrail 命中即落库)。 // 命中事件流(blocked 硬拦 / suspect 灰区放行→Tier2 LLM 裁决) + 计数 + 原因分布。 // // 诚实边界:护栏规则(Tier1 正则/敏感词 + Tier2 LLM 分类器)目前是中间件里的代码常量, // 尚不支持运行时编辑(改规则需改代码重部署)。故本页只做观测,不摆"能改却不生效"的假配置面。 // 若要规则运行时可配,需另建配置存储 + 中间件读库(参考 prompt 控制面的热下发)。 const KIND_BADGE: Record = { blocked: "bg-rose-50 text-rose-600", suspect: "bg-amber-50 text-amber-600" }; const KIND_LABEL: Record = { blocked: "硬拦截", suspect: "灰区放行" }; // 解析 signals(JSON 数组字符串)为可读标签。 function signalLabels(raw: string): string[] { if (!raw) return []; try { const arr = JSON.parse(raw); return Array.isArray(arr) ? arr.map(String) : []; } catch { return [raw]; } } export function GuardrailsPage() { const [events, setEvents] = useState([]); const [filter, setFilter] = useState<"" | "blocked" | "suspect">(""); const [loading, setLoading] = useState(true); const [err, setErr] = useState(""); const load = () => { setLoading(true); guardrailEvents(100) .then((r) => { setEvents(r); setErr(""); }) .catch((e) => setErr((e as Error).message)) .finally(() => setLoading(false)); }; useEffect(load, []); const blocked = events.filter((e) => e.kind === "blocked").length; const suspect = events.filter((e) => e.kind === "suspect").length; // 原因/信号 Top(真实命中分布)。 const topReasons = useMemo(() => { const m = new Map(); for (const e of events) { const keys = e.kind === "blocked" ? [e.reason || "未标注"] : signalLabels(e.signals); for (const k of keys.length ? keys : ["未标注"]) m.set(k, (m.get(k) ?? 0) + 1); } return [...m.entries()].sort((a, b) => b[1] - a[1]).slice(0, 6); }, [events]); const shown = filter ? events.filter((e) => e.kind === filter) : events; if (loading) return
加载护栏事件中…
; if (err) return
护栏事件加载失败:{err}
; return (

输入护栏

命中事件流 · 硬拦截与灰区放行 · 原因分布(全平台真实命中)

{/* 计数 */}
{/* 规则说明(诚实:规则在代码里,非运行时可配) */}

护栏规则

Tier1(中间件,同步硬拦):注入正则 + 敏感词库命中即 blocked 拒绝请求。

Tier2(灰区升级):疑似输入打标 suspect 放行,由 Dispatcher 执行前调 LLM 分类器裁决。

规则当前定义在网关中间件代码中,尚不支持运行时编辑(改规则需改代码重部署)。运行时可配需另建配置存储 + 中间件读库,参考「提示词」控制面的热下发。

{/* 原因分布 */} {topReasons.length > 0 && (

命中原因 Top

{topReasons.map(([reason, n]) => (
{reason}
{n}
))}
)} {/* 事件流 */}

命中事件流

{([["", "全部"], ["blocked", "硬拦截"], ["suspect", "灰区"]] as const).map(([v, label]) => ( ))}
{shown.length === 0 ? (
暂无护栏命中 —— 没有可疑输入
) : (
{shown.map((e) => ( ))}
时间 类型 原因 / 命中信号 路径 来源
{new Date(e.at).toLocaleString("zh-CN")} {KIND_LABEL[e.kind] ?? e.kind} {e.kind === "blocked" ? ( {e.reason || "—"} ) : (
{signalLabels(e.signals).map((sig, i) => ( {sig} ))} {signalLabels(e.signals).length === 0 && }
)}
{e.method} {e.path} {e.actor || e.ip || "匿名"}
)}
); } const TONE: Record = { rose: "text-rose-500", amber: "text-amber-600", violet: "text-violet-600" }; function Stat({ label, value, sub, tone }: { label: string; value: string; sub?: string; tone: string }) { return (
{label}
{value}
{sub &&
{sub}
}
); }