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:
Jesse_Chen
2026-09-30 09:42:11 +08:00
co-authored by Claude Opus 5.5
parent e742da268d
commit 4f3d8d7c62
28 changed files with 1143 additions and 6 deletions
@@ -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 />}
/>;
}