import { App, Button, Input, Skeleton } from "antd"; import { Switch } from "@/pages/admin/ui/controls"; import { AlertTriangle, ArrowDown, ArrowUp, BadgeCheck, Eye, ListOrdered, Plus, RefreshCw, RotateCcw, Save, Search, ShieldAlert, ShieldCheck, Trash2 } from "lucide-react"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useBlocker } from "react-router"; import { cn } from "@/lib/utils"; import { getAdminResponseInterceptionSetting, updateAdminResponseInterceptionSetting, type ResponseInterceptionRule, type ResponseInterceptionSetting } from "@/services/api/response-interception"; import { AdminPageFrame } from "../components/admin-shell"; import { AdminStatusBadge, SettingsSectionCard } from "../components/admin-ui"; const MAX_RULES = 100; const MAX_MATCH_RUNES = 200; const MAX_REPLACE_RUNES = 500; const emptyRule = (): ResponseInterceptionRule => ({ contains: "", replace: "" }); export default function ResponseInterceptionSettingsPage() { const { message, modal } = App.useApp(); const [setting, setSetting] = useState(null); const [enabled, setEnabled] = useState(false); const [rules, setRules] = useState([]); const [previewInput, setPreviewInput] = useState(""); const [loading, setLoading] = useState(true); const [refreshing, setRefreshing] = useState(false); const [saving, setSaving] = useState(false); const [loadError, setLoadError] = useState(""); const [saveError, setSaveError] = useState(""); const requestVersionRef = useRef(0); const navigationConfirmOpenRef = useRef(false); const navigationTriggerRef = useRef(null); const draft = useMemo(() => normalizeResponseInterceptionSetting({ enabled, rules }), [enabled, rules]); const dirty = useMemo(() => Boolean(setting && !responseInterceptionSettingsEqual(draft, setting)), [draft, setting]); const enabledDirty = Boolean(setting && enabled !== setting.enabled); const duplicateRuleIndexes = useMemo(() => findDuplicateRuleIndexes(draft.rules), [draft.rules]); const preview = useMemo(() => previewResponseInterception(previewInput, draft), [draft, previewInput]); const load = useCallback( async (initial = false, announce = false) => { const requestVersion = ++requestVersionRef.current; if (initial) setLoading(true); else setRefreshing(true); setLoadError(""); try { const result = await getAdminResponseInterceptionSetting(); if (requestVersion !== requestVersionRef.current) return; if (!isResponseInterceptionSetting(result.setting)) throw new Error("服务端返回的模型响应拦截配置格式无效"); setSetting(normalizeResponseInterceptionSetting(result.setting)); setEnabled(result.setting.enabled); setRules(result.setting.rules.map((rule) => ({ ...rule }))); setSaveError(""); if (announce) message.success("已重新读取模型响应拦截配置"); } catch (error) { if (requestVersion !== requestVersionRef.current) return; const errorMessage = error instanceof Error ? error.message : "读取模型响应拦截配置失败"; setLoadError(errorMessage); if (!initial) message.error(errorMessage); } finally { if (requestVersion === requestVersionRef.current) { setLoading(false); setRefreshing(false); } } }, [message], ); useEffect(() => { void load(true); return () => { requestVersionRef.current += 1; }; }, [load]); const blocker = useBlocker(dirty && !saving); useEffect(() => { const beforeUnload = (event: BeforeUnloadEvent) => { if (!dirty || saving) return; event.preventDefault(); }; window.addEventListener("beforeunload", beforeUnload); return () => window.removeEventListener("beforeunload", beforeUnload); }, [dirty, saving]); useEffect(() => { if (blocker.state !== "blocked" || navigationConfirmOpenRef.current) return; navigationConfirmOpenRef.current = true; navigationTriggerRef.current = document.activeElement instanceof HTMLElement && document.activeElement !== document.body ? document.activeElement : null; modal.confirm({ title: "放弃响应拦截调整?", content: "当前页面有尚未保存的开关、规则内容或优先级调整,离开后这些草稿会丢失。服务端正在使用的拦截配置不会改变。", okText: "放弃并离开", cancelText: "继续编辑", okButtonProps: { danger: true }, onOk: () => { navigationConfirmOpenRef.current = false; navigationTriggerRef.current = null; blocker.proceed(); }, onCancel: () => { navigationConfirmOpenRef.current = false; blocker.reset(); window.requestAnimationFrame(() => { const fallback = document.querySelector(".admin-intercept-command-actions button"); const target = navigationTriggerRef.current?.isConnected ? navigationTriggerRef.current : fallback; target?.focus(); navigationTriggerRef.current = null; }); }, }); }, [blocker, modal]); const resetDraft = () => { if (!setting || saving) return; setEnabled(setting.enabled); setRules(setting.rules.map((rule) => ({ ...rule }))); setSaveError(""); message.info("已撤销模型响应拦截的未保存调整"); }; const requestRefresh = () => { if (!dirty) { void load(false, true); return; } modal.confirm({ title: "放弃调整并重新读取?", content: "重新读取会丢弃当前开关、规则内容和优先级草稿,并以服务端配置为准。", okText: "放弃并刷新", cancelText: "继续编辑", okButtonProps: { danger: true }, onOk: () => load(false, true), }); }; const updateRule = (index: number, field: keyof ResponseInterceptionRule, value: string) => { setRules((current) => current.map((rule, ruleIndex) => (ruleIndex === index ? { ...rule, [field]: value } : rule))); setSaveError(""); }; const moveRule = (index: number, offset: -1 | 1) => { setRules((current) => { const target = index + offset; if (target < 0 || target >= current.length) return current; const next = current.map((rule) => ({ ...rule })); [next[index], next[target]] = [next[target], next[index]]; return next; }); setSaveError(""); }; const removeRule = (index: number) => { setRules((current) => current.filter((_, ruleIndex) => ruleIndex !== index)); setSaveError(""); }; const addRule = () => { if (rules.length >= MAX_RULES) { message.warning(`模型响应拦截规则不能超过 ${MAX_RULES} 条`); return; } setRules((current) => [...current, emptyRule()]); setSaveError(""); window.requestAnimationFrame(() => document.querySelector(`[aria-label="第 ${rules.length + 1} 条匹配文案"]`)?.focus()); }; const save = async (next?: ResponseInterceptionSetting) => { if (!setting) return; const expected = normalizeResponseInterceptionSetting(next || { enabled, rules }); const validationError = validateResponseInterceptionSetting(expected); if (validationError) { message.error(validationError); return; } setSaving(true); setSaveError(""); try { const result = await updateAdminResponseInterceptionSetting(expected); if (!isResponseInterceptionSetting(result.setting)) throw new Error("服务端返回的模型响应拦截配置格式无效"); const normalizedResult = normalizeResponseInterceptionSetting(result.setting); if (!responseInterceptionSettingsEqual(normalizedResult, expected)) throw new Error("服务端返回的模型响应拦截配置与本次保存内容不一致,请重新读取后核对"); setSetting(normalizedResult); setEnabled(normalizedResult.enabled); setRules(normalizedResult.rules.map((rule) => ({ ...rule }))); message.success("模型响应拦截配置已保存"); } catch (error) { const errorMessage = error instanceof Error ? error.message : "保存模型响应拦截配置失败"; setSaveError(`${errorMessage}。未自动重试,请重新读取当前配置后再决定是否保存。`); message.error(errorMessage); throw error; } finally { setSaving(false); } }; const submitSave = async () => { if (!setting) return; const expected = normalizeResponseInterceptionSetting({ enabled, rules }); const validationError = validateResponseInterceptionSetting(expected); if (validationError) { message.error(validationError); return; } try { await save(expected); } catch { // 保存错误已在 save 中就地提示。 } }; const changeEnabled = (nextEnabled: boolean) => { if (!setting || saving || nextEnabled === enabled) return; setEnabled(nextEnabled); setSaveError(""); }; if (loading && !setting) { return (
); } if (!setting) { return (

无法读取模型响应拦截配置

{loadError || "当前没有可显示的配置,请稍后重试。"}

); } return (
{dirty ? "响应拦截有调整待保存" : "响应拦截配置已同步"}

{dirty ? "当前开关、文案和优先级只在本页暂存;本地预览不会调用后端。" : "仅投影用户最终看到的错误文案,原始响应与请求明细保持不变。"}

{dirty ? ( ) : null}
{loadError || saveError ? (
) : null}
{enabled ? ( <>
) : null}
); } function normalizeResponseInterceptionSetting(value: ResponseInterceptionSetting): ResponseInterceptionSetting { return { enabled: Boolean(value.enabled), rules: Array.isArray(value.rules) ? value.rules.map((rule) => ({ contains: rule.contains.trim(), replace: rule.replace.trim() })) : [], }; } function validateResponseInterceptionSetting(value: ResponseInterceptionSetting) { if (value.rules.length > MAX_RULES) return `模型响应拦截规则不能超过 ${MAX_RULES} 条`; for (let index = 0; index < value.rules.length; index += 1) { const rule = value.rules[index]; if (!rule.contains) return `第 ${index + 1} 条拦截规则的匹配文案不能为空`; if (Array.from(rule.contains).length > MAX_MATCH_RUNES) return `第 ${index + 1} 条拦截规则的匹配文案不能超过 ${MAX_MATCH_RUNES} 个字符`; if (!rule.replace) return `第 ${index + 1} 条拦截规则的替换文案不能为空`; if (Array.from(rule.replace).length > MAX_REPLACE_RUNES) return `第 ${index + 1} 条拦截规则的替换文案不能超过 ${MAX_REPLACE_RUNES} 个字符`; } return ""; } function responseInterceptionSettingsEqual(left: ResponseInterceptionSetting, right: ResponseInterceptionSetting) { const normalizedLeft = normalizeResponseInterceptionSetting(left); const normalizedRight = normalizeResponseInterceptionSetting(right); if (normalizedLeft.enabled !== normalizedRight.enabled || normalizedLeft.rules.length !== normalizedRight.rules.length) return false; return normalizedLeft.rules.every((rule, index) => rule.contains === normalizedRight.rules[index].contains && rule.replace === normalizedRight.rules[index].replace); } function findDuplicateRuleIndexes(rules: ResponseInterceptionRule[]) { const seen = new Map(); const duplicates = new Set(); rules.forEach((rule, index) => { const key = rule.contains.trim().toLocaleLowerCase(); if (!key) return; const first = seen.get(key); if (first === undefined) seen.set(key, index); else { duplicates.add(first); duplicates.add(index); } }); return duplicates; } function previewResponseInterception(input: string, setting: ResponseInterceptionSetting) { const raw = input.trim(); if (!raw) return { raw: "", output: "", matchIndex: -1 }; const lower = raw.toLocaleLowerCase(); const matchIndex = setting.rules.findIndex((rule) => rule.contains && lower.includes(rule.contains.toLocaleLowerCase())); return { raw, matchIndex, output: setting.enabled && matchIndex >= 0 ? setting.rules[matchIndex].replace : raw, }; } function isResponseInterceptionSetting(value: unknown): value is ResponseInterceptionSetting { if (!value || typeof value !== "object") return false; const setting = value as Partial; return typeof setting.enabled === "boolean" && Array.isArray(setting.rules) && setting.rules.every((rule) => Boolean(rule && typeof rule === "object" && typeof rule.contains === "string" && typeof rule.replace === "string")); }