/** * AI 手动操作页面。 * * 4 个 Card 区域: * - Card 1:手动重跑(App + member_id + site_id → 执行) * - Card 2:缓存失效(app_type + member_id + site_id → 失效) * - Card 3:批量执行(app_types + member_ids + site_id → 预估 → 确认) * - Card 4:告警管理(告警列表 + 确认/忽略) */ import React, { useEffect, useState, useCallback } from "react"; import { Alert, Card, Row, Col, Select, Input, Button, Table, Tag, Space, Checkbox, Modal, Statistic, message, Typography, } from "antd"; import { ReloadOutlined } from "@ant-design/icons"; import type { ColumnsType } from "antd/es/table"; import { retryTriggerJob, invalidateCache, createBatchRun, confirmBatchRun, getAlerts, ackAlert, ignoreAlert, runApp, triggerEvent, type AlertItem, type AppType, type BatchRunEstimate, } from "../api/adminAI"; // F1-5a: sandbox 模式提示条数据源 import { fetchRuntimeContext, type RuntimeContext } from "../api/runtimeContext"; const EVENT_TYPE_OPTIONS = [ { label: "消费事件(App3→App8→App7 [+ App4→App5])", value: "consumption" }, { label: "备注事件(App6→App8)", value: "note_created" }, { label: "任务分配(App4→App5)", value: "task_assigned" }, { label: "DWS 完成(App2 × 72 组合预热)", value: "dws_completed" }, ]; const { TextArea } = Input; const { Title } = Typography; export const CACHE_TYPE_OPTIONS = [ { label: "App3 维客线索", value: "app3_clue" }, { label: "App4 关系分析", value: "app4_analysis" }, { label: "App5 话术参考", value: "app5_tactics" }, { label: "App6 备注分析", value: "app6_note_analysis" }, { label: "App7 客户分析", value: "app7_customer_analysis" }, { label: "App8 线索整理", value: "app8_clue_consolidated" }, ]; export const RUN_APP_TYPE_OPTIONS: { label: string; value: AppType }[] = [ { label: "App3 维客线索", value: "app3_clue" }, { label: "App4 关系分析", value: "app4_analysis" }, { label: "App5 话术参考", value: "app5_tactics" }, { label: "App6 备注分析", value: "app6_note" }, { label: "App7 客户分析", value: "app7_customer" }, { label: "App8 线索整理", value: "app8_consolidation" }, ]; const ALERT_STATUS_COLOR: Record = { failed: "red", timeout: "orange", circuit_open: "volcano", }; const ALERT_MGMT_COLOR: Record = { pending: "warning", acknowledged: "success", ignored: "default", }; function fmtTime(raw: string | null): string { if (!raw) return "—"; const d = new Date(raw); return Number.isNaN(d.getTime()) ? raw : d.toLocaleString("zh-CN"); } const AIOperations: React.FC = () => { // ---- Card 1: 手动重跑 ---- const [retryJobId, setRetryJobId] = useState(""); const [retryLoading, setRetryLoading] = useState(false); const handleRetry = async () => { const id = Number(retryJobId); if (!id || Number.isNaN(id)) { message.warning("请输入有效的任务 ID"); return; } setRetryLoading(true); try { const res = await retryTriggerJob(id); message.success(`已创建重跑任务 #${res.trigger_job_id}`); setRetryJobId(""); } catch { message.error("重跑失败"); } finally { setRetryLoading(false); } }; // ---- Card 2: 缓存失效 ---- const [cacheAppType, setCacheAppType] = useState(); const [cacheMemberId, setCacheMemberId] = useState(""); const [cacheSiteId, setCacheSiteId] = useState(2790685415443269); const [cacheLoading, setCacheLoading] = useState(false); const [cacheResult, setCacheResult] = useState(null); const handleInvalidate = async () => { setCacheLoading(true); setCacheResult(null); try { const res = await invalidateCache({ site_id: cacheSiteId, app_type: cacheAppType, member_id: cacheMemberId ? Number(cacheMemberId) : undefined, }); setCacheResult(res.affected_count); message.success(`已失效 ${res.affected_count} 条缓存`); } catch { message.error("缓存失效操作失败"); } finally { setCacheLoading(false); } }; // ---- Card 2.5: 按需重新生成 ---- const [runAppType, setRunAppType] = useState(); const [runMemberId, setRunMemberId] = useState(""); const [runSiteId, setRunSiteId] = useState(2790685415443269); const [runLoading, setRunLoading] = useState(false); const [runResult, setRunResult] = useState<{ success: boolean; text: string } | null>(null); const handleRunApp = async () => { if (!runAppType) { message.warning("请选择 App 类型"); return; } setRunLoading(true); setRunResult(null); try { const res = await runApp(runAppType, { site_id: runSiteId, member_id: runMemberId ? Number(runMemberId) : undefined, }); if (res.success) { setRunResult({ success: true, text: "执行成功,缓存已更新" }); message.success("执行成功"); } else { setRunResult({ success: false, text: res.error ?? "执行失败" }); message.error(res.error ?? "执行失败"); } } catch { message.error("请求失败"); } finally { setRunLoading(false); } }; // ---- Card 2.6: 手动触发事件链(越过去重)---- const [evtType, setEvtType] = useState("consumption"); const [evtSiteId, setEvtSiteId] = useState(2790685415443269); const [evtMemberId, setEvtMemberId] = useState(""); const [evtAssistantId, setEvtAssistantId] = useState(""); const [evtForced, setEvtForced] = useState(true); const [evtLoading, setEvtLoading] = useState(false); const [evtResult, setEvtResult] = useState(null); const handleTriggerEvent = async () => { if (!evtType) { message.warning("请选择事件类型"); return; } setEvtLoading(true); setEvtResult(null); try { const res = await triggerEvent({ event_type: evtType, site_id: evtSiteId, member_id: evtMemberId ? Number(evtMemberId) : undefined, assistant_id: evtAssistantId ? Number(evtAssistantId) : undefined, is_forced: evtForced, }); setEvtResult(res.trigger_job_id); message.success(`事件已触发,job_id=${res.trigger_job_id}(后台异步执行)`); } catch { message.error("触发失败"); } finally { setEvtLoading(false); } }; // ---- Card 3: 批量执行 ---- const [batchAppTypes, setBatchAppTypes] = useState([]); const [batchMemberIds, setBatchMemberIds] = useState(""); const [batchSiteId, setBatchSiteId] = useState(2790685415443269); const [batchLoading, setBatchLoading] = useState(false); const [batchEstimate, setBatchEstimate] = useState(null); const [confirmVisible, setConfirmVisible] = useState(false); const [confirmLoading, setConfirmLoading] = useState(false); const parseMemberIds = (text: string): number[] => text.split(/[\n,;\s]+/).map(Number).filter((n) => !Number.isNaN(n) && n > 0); const handleBatchEstimate = async () => { const memberIds = parseMemberIds(batchMemberIds); if (batchAppTypes.length === 0) { message.warning("请选择至少一个 App"); return; } if (memberIds.length === 0) { message.warning("请输入有效的会员 ID"); return; } setBatchLoading(true); try { const res = await createBatchRun({ app_types: batchAppTypes, member_ids: memberIds, site_id: batchSiteId }); setBatchEstimate(res); setConfirmVisible(true); } catch { message.error("预估失败"); } finally { setBatchLoading(false); } }; const handleBatchConfirm = async () => { if (!batchEstimate) return; setConfirmLoading(true); try { await confirmBatchRun(batchEstimate.batch_id); message.success("批量执行已启动"); setConfirmVisible(false); setBatchEstimate(null); setBatchAppTypes([]); setBatchMemberIds(""); } catch { message.error("确认执行失败"); } finally { setConfirmLoading(false); } }; // ---- F1-5a: Sandbox 模式提示条 ---- // 沙箱机制 P0-7 主线:让运维进入 AI 操作页前能看到当前 sandbox 状态, // 避免"以为 live 模式"误触发批量执行实际跑在 sandbox 数据集上的混淆。 const [runtimeCtx, setRuntimeCtx] = useState(null); useEffect(() => { // 复用 cacheSiteId 作为当前关注 site(默认 2790685415443269,与 cacheSiteId / runSiteId 一致) let cancelled = false; (async () => { try { const ctx = await fetchRuntimeContext(cacheSiteId); if (!cancelled) setRuntimeCtx(ctx); } catch { // 失败不阻断页面渲染(get_runtime_context 表不存在时后端降级 live) } })(); return () => { cancelled = true; }; }, [cacheSiteId]); // ---- Card 4: 告警管理 ---- const [alerts, setAlerts] = useState([]); const [alertTotal, setAlertTotal] = useState(0); const [alertLoading, setAlertLoading] = useState(false); const [alertPage, setAlertPage] = useState(1); const loadAlerts = useCallback(async () => { setAlertLoading(true); try { const res = await getAlerts({ page: alertPage, page_size: 10 }); setAlerts(res.items); setAlertTotal(res.total); } catch { message.error("加载告警列表失败"); } finally { setAlertLoading(false); } }, [alertPage]); useEffect(() => { loadAlerts(); }, [loadAlerts]); const handleAck = async (id: number) => { try { await ackAlert(id); message.success("已确认告警"); loadAlerts(); } catch { message.error("确认失败"); } }; const handleIgnore = async (id: number) => { try { await ignoreAlert(id); message.success("已忽略告警"); loadAlerts(); } catch { message.error("忽略失败"); } }; const alertColumns: ColumnsType = [ { title: "ID", dataIndex: "id", key: "id", width: 70 }, { title: "App", dataIndex: "app_type", key: "app_type", width: 150 }, { title: "状态", dataIndex: "status", key: "status", width: 100, render: (v: string) => {v}, }, { title: "告警状态", dataIndex: "alert_status", key: "alert_status", width: 100, render: (v: string | null) => v ? {v} : "—", }, { title: "错误信息", dataIndex: "error_message", key: "error_message", ellipsis: true, render: (v) => v ?? "—" }, { title: "时间", dataIndex: "created_at", key: "created_at", width: 160, render: fmtTime }, { title: "操作", key: "action", width: 140, render: (_: unknown, r: AlertItem) => ( ), }, ]; return (
AI 手动操作 {runtimeCtx && runtimeCtx.is_sandbox && ( 沙箱模式 · 业务日 {runtimeCtx.sandbox_date ?? "—"} · 实例 {runtimeCtx.sandbox_instance_id ?? "—"} } description={ 当前 site_id={cacheSiteId} 处于沙箱模式。本页所有 AI 触发(手动重跑 / 缓存失效 / 按需执行 / 批量执行)将使用 沙箱业务日 ({runtimeCtx.sandbox_date}) 而非真实今日;ETL 视图自动按业务日上界裁剪,助教/会员消费数据仅可见 沙箱日及之前。结果写入 ai_run_logs 时 runtime_mode=sandbox + sandbox_instance_id 隔离, 不污染 live 数据。如需切回 live,前往 运行上下文 页。 } /> )} {/* Card 1: 手动重跑 */} setRetryJobId(e.target.value)} /> {/* Card 2: 缓存失效 */} setCacheMemberId(e.target.value)} /> setRunMemberId(e.target.value)} />
门店
setEvtMemberId(e.target.value)} />
assistant_id(可选)
setEvtAssistantId(e.target.value)} />
跳过去重 + 操作
setEvtForced(e.target.checked)}>强制 {evtResult != null && job #{evtResult}}
{/* Card 3: 批量执行 */}
选择 App
setBatchAppTypes(v as AppType[])} style={{ display: "flex", flexDirection: "column", gap: 4 }} />
会员 ID(每行一个或逗号分隔)