feat(compliance): content moderation on model input and output — local lexicon, Aliyun adapter, free completion, admin log
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N4f2nya58RoRu4yEmJgRGE
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
e742da268d
commit
4f3d8d7c62
@@ -121,6 +121,7 @@ export function AdminApp({ children }: { children: ReactNode }) {
|
||||
{ name: "feature-flags", list: "/admin/feature-flags", meta: { label: "功能开关", icon: <ControlOutlined /> } },
|
||||
{ name: "security", list: "/admin/security", meta: { label: "安全验证", icon: <SafetyCertificateOutlined /> } },
|
||||
{ name: "audit-logs", list: "/admin/audit-logs", meta: { label: "审计日志", icon: <AuditOutlined /> } },
|
||||
{ name: "moderation-events", list: "/admin/moderation-events", meta: { label: "内容审核", icon: <AuditOutlined /> } },
|
||||
]}
|
||||
options={{
|
||||
syncWithLocation: true,
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { Descriptions, Tag, type TableColumnsType } from "antd";
|
||||
|
||||
import { formatAdminDate, ResourceTable } from "@/components/admin/resource-table";
|
||||
|
||||
type ModerationRecord = {
|
||||
id: string;
|
||||
userId: string | null;
|
||||
side: "input" | "output";
|
||||
verdict: "block" | "review";
|
||||
route: string;
|
||||
categories: string[];
|
||||
ruleIds: string[];
|
||||
provider: string;
|
||||
requestId: string | null;
|
||||
createdAt: string;
|
||||
};
|
||||
|
||||
const CATEGORY_LABELS: Record<string, string> = {
|
||||
political: "政治",
|
||||
violent_extremism: "暴恐",
|
||||
sexual_minor: "涉未成年色情",
|
||||
self_harm: "自伤",
|
||||
illegal_trade: "违法交易",
|
||||
remote: "审核服务判定",
|
||||
};
|
||||
|
||||
const columns: TableColumnsType<ModerationRecord> = [
|
||||
{ title: "时间", dataIndex: "createdAt", sorter: true, render: formatAdminDate },
|
||||
{ title: "方向", dataIndex: "side", render: (side: string) => (side === "input" ? "用户输入" : "模型输出") },
|
||||
{ title: "处理", dataIndex: "verdict", render: (verdict: string) => <Tag color={verdict === "block" ? "red" : "gold"}>{verdict === "block" ? "拦截" : "待复核"}</Tag> },
|
||||
{ title: "入口", dataIndex: "route", sorter: true },
|
||||
{ title: "类别", dataIndex: "categories", render: (list: string[]) => list.map((c) => CATEGORY_LABELS[c] ?? c).join("、") },
|
||||
{ title: "命中规则", dataIndex: "ruleIds", render: (list: string[]) => list.join(", ") },
|
||||
{ title: "来源", dataIndex: "provider" },
|
||||
{ title: "用户 ID", dataIndex: "userId" },
|
||||
{ title: "Request ID", dataIndex: "requestId" },
|
||||
];
|
||||
|
||||
function Summary() {
|
||||
const [rows, setRows] = useState<{ day: string; category: string; hits: number }[] | null>(null);
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
fetch("/api/admin/moderation-events?summary=1", { credentials: "same-origin" })
|
||||
.then((response) => (response.ok ? response.json() : { data: [] }))
|
||||
.then((json: { data?: { day: string; category: string; hits: number }[] }) => { if (!cancelled) setRows(json.data ?? []); })
|
||||
.catch(() => { if (!cancelled) setRows([]); });
|
||||
return () => { cancelled = true; };
|
||||
}, []);
|
||||
const totals = new Map<string, number>();
|
||||
for (const row of rows ?? []) totals.set(row.category, (totals.get(row.category) ?? 0) + row.hits);
|
||||
return <Descriptions size="small" column={3} items={[
|
||||
{ key: "policy", label: "说明", children: "只记录类别与规则编号,不保存用户或模型原文" },
|
||||
...[...totals.entries()].map(([category, hits]) => ({ key: category, label: `近 14 天 · ${CATEGORY_LABELS[category] ?? category}`, children: String(hits) })),
|
||||
]} />;
|
||||
}
|
||||
|
||||
export default function ModerationEventsPage() {
|
||||
return <ResourceTable<ModerationRecord>
|
||||
resource="moderation-events"
|
||||
title="内容审核记录(只读)"
|
||||
columns={columns}
|
||||
statusOptions={[
|
||||
{ label: "用户输入", value: "input" },
|
||||
{ label: "模型输出", value: "output" },
|
||||
]}
|
||||
extra={<Summary />}
|
||||
/>;
|
||||
}
|
||||
Reference in New Issue
Block a user