import { useCallback, useEffect, useMemo, useState, type ReactNode } from "react"; import { Alert, Avatar, Badge, Breadcrumb, Button, Card, Dropdown, Input, Layout, Menu, Select, Space, Spin, Tag, Tooltip, type MenuProps } from "antd"; import { AlertOutlined, ApiOutlined, BarChartOutlined, BellOutlined, CloudServerOutlined, DashboardOutlined, DollarOutlined, DatabaseOutlined, MenuFoldOutlined, MenuUnfoldOutlined, ReloadOutlined, RobotOutlined, SearchOutlined, SettingOutlined, ToolOutlined, UserOutlined } from "@ant-design/icons"; import { useAuth } from "../auth/useAuth"; import { platformApi, type ContentQualitySummary } from "../api/client"; import { StatusCard } from "../components/StatusCard"; import { OpsAlertWorkspace } from "./OpsAlertWorkspace"; import { OpsBillingWorkspace } from "./OpsBillingWorkspace"; import { OpsConfigWorkspace } from "./OpsConfigWorkspace"; import { OpsDiagnosticsWorkspace } from "./OpsDiagnosticsWorkspace"; import { OpsDeviceWorkspace } from "./OpsDeviceWorkspace"; import { OpsContentModelWorkspace } from "./OpsContentModelWorkspace"; type MaintenanceArea = "overview" | "devices" | "config" | "models" | "quality" | "billing" | "alerts" | "diagnostics"; type HealthState = { loading: boolean; error?: string; live?: string; service?: string; ready?: string; checks?: Record; version?: string; }; const areaMeta: Record = { overview: { title: "维护总览", description: "集中查看服务健康、构建版本和当前维护权限。", icon: }, devices: { title: "设备管理", description: "登记设备、维护家庭与长者归属、吊销设备并安全轮换密钥。", icon: }, config: { title: "运行配置", description: "管理配置版本、资源引用、离线评测、审批和灰度发布。", icon: }, models: { title: "大模型管理", description: "按客户组织配置、测试并激活聊天总结、关键词和故事候选生成模型。", icon: }, quality: { title: "内容质量", description: "按模型和提示词版本检查故事、事实与记忆的人工审核反馈。", icon: }, billing: { title: "用量、费用与额度", description: "查看整体通话时长并按家庭、设备筛选,同时维护核价与硬额度策略。", icon: }, alerts: { title: "告警处置", description: "维护告警路由、通知测试、静默窗口和处置时间线。", icon: }, diagnostics: { title: "联调与诊断", description: "处理依赖异常、失败作业、敏感访问、验收快照和恢复操作。", icon: } }; export function MaintenancePortal() { const auth = useAuth(); const [collapsed, setCollapsed] = useState(() => window.innerWidth <= 720); const [area, setArea] = useState(() => areaFromURL()); const [searchValue, setSearchValue] = useState(""); const [health, setHealth] = useState({ loading: true }); const refreshHealth = useCallback(async (signal?: AbortSignal) => { setHealth((current) => ({ ...current, loading: true, error: undefined })); try { const [live, ready, version] = await Promise.all([ platformApi.liveness(signal), platformApi.readiness(signal), platformApi.version(signal) ]); setHealth({ loading: false, live: live.data.status, service: live.data.service, ready: ready.data.status, checks: ready.data.checks, version: version.data.version }); } catch (error) { if (error instanceof DOMException && error.name === "AbortError") return; setHealth({ loading: false, error: error instanceof Error ? error.message : "未知错误" }); } }, []); useEffect(() => { const controller = new AbortController(); void refreshHealth(controller.signal); return () => controller.abort(); }, [refreshHealth]); useEffect(() => { const onPopState = () => setArea(areaFromURL()); window.addEventListener("popstate", onPopState); return () => window.removeEventListener("popstate", onPopState); }, []); const operationsAccounts = useMemo( () => auth.currentUser?.accounts.filter((account) => account.account_kind === "operator" && account.platform_role !== "customer") ?? [], [auth.currentUser] ); const hasOperationsAccess = operationsAccounts.length > 0; const meta = areaMeta[area]; function navigate(next: MaintenanceArea) { setArea(next); if (window.innerWidth <= 720) setCollapsed(true); const url = new URL(window.location.href); if (next === "overview") url.searchParams.delete("area"); else url.searchParams.set("area", next); window.history.pushState({ area: next }, "", `${url.pathname}${url.search}${url.hash}`); } function search() { const value = searchValue.trim().toLowerCase(); if (!value) return; const matches: Array<[string[], MaintenanceArea]> = [ [["总览", "健康", "版本", "health"], "overview"], [["设备管理", "资产", "密钥", "device management"], "devices"], [["配置", "灰度", "模型路由", "config"], "config"], [["大模型", "模型", "豆包", "ollama", "model"], "models"], [["质量", "审核", "反馈", "quality"], "quality"], [["费用", "额度", "账单", "用量", "billing", "cost"], "billing"], [["告警", "静默", "路由", "alert"], "alerts"] ]; const target = matches.find(([keywords]) => keywords.some((keyword) => value.includes(keyword)))?.[1] ?? "diagnostics"; navigate(target); } const menuItems: MenuProps["items"] = [ { type: "group", label: "平台维护", children: [ { key: "overview", icon: , label: "维护总览" }, { key: "devices", icon: , label: "设备管理" }, { key: "config", icon: , label: "运行配置" }, { key: "models", icon: , label: "大模型管理" }, { key: "quality", icon: , label: "内容质量" }, { key: "billing", icon: , label: "用量与费用" }, { key: "alerts", icon: , label: "告警处置" }, { key: "diagnostics", icon: , label: "联调与诊断" } ] } ]; const accountMenu: MenuProps["items"] = [ { key: "logout", icon: , label: "退出登录", onClick: () => void auth.logout() } ]; return
setSearchValue(event.target.value)} onSearch={search} prefix={} placeholder="搜索设备、通话、作业" allowClear className="maintenance-search" /> : }
{collapsed ? null : 平台运维控制台}
navigate(key as MaintenanceArea)} />
{collapsed ? : <>当前构建{health.version ?? "dev"}}
{meta.icon}

{meta.title}

{meta.description}

{auth.loading ? 正在校验维护身份… : null} {!auth.loading && auth.error ? } /> : null} {!auth.loading && !auth.currentUser && !auth.error ? void auth.login()} /> : null} {auth.currentUser && !hasOperationsAccess ? window.location.assign("/")}>返回家庭端} /> : null} {health.error ? : null} {auth.currentUser && auth.oidcUser && hasOperationsAccess ?
{area === "overview" ? : null} {area === "devices" ? : null} {area === "config" ? : null} {area === "models" ? : null} {area === "quality" ? : null} {area === "billing" ? : null} {area === "alerts" ? : null} {area === "diagnostics" ? : null}
: null}
; } function ContentQualityWorkspace({ accessToken, accounts }: { accessToken: string; accounts: NonNullable["currentUser"]>["accounts"]; }) { const [tenantID, setTenantID] = useState(accounts[0]?.tenant_id ?? ""); const [items, setItems] = useState([]); const [loading, setLoading] = useState(false); const [error, setError] = useState(); const [revision, setRevision] = useState(0); useEffect(() => { if (!accounts.some((account) => account.tenant_id === tenantID)) setTenantID(accounts[0]?.tenant_id ?? ""); }, [accounts, tenantID]); useEffect(() => { if (!tenantID) { setItems([]); return; } const controller = new AbortController(); setLoading(true); setError(undefined); void platformApi.opsContentQuality(tenantID, accessToken, controller.signal) .then((response) => setItems(response.data.items)) .catch((cause) => { if (!(cause instanceof DOMException && cause.name === "AbortError")) setError(cause instanceof Error ? cause.message : "内容质量读取失败"); }) .finally(() => { if (!controller.signal.aborted) setLoading(false); }); return () => controller.abort(); }, [accessToken, revision, tenantID]); const totals = items.reduce((result, item) => ({ candidates: result.candidates + item.story_candidates + item.fact_candidates, confirmed: result.confirmed + item.confirmed, edited: result.edited + item.edited, rejected: result.rejected + item.rejected, issues: result.issues + Object.values(item.error_types).reduce((sum, count) => sum + count, 0) }), { candidates: 0, confirmed: 0, edited: 0, rejected: 0, issues: 0 }); const reviewed = totals.confirmed + totals.edited + totals.rejected; return
质量反馈范围只展示当前维护账号有权访问的客户组织;统计不包含故事、事实或记忆正文。