Neo F1-5 反馈: "让沙箱起到其真正的作用. 真正的模拟日期, 仅能看到沙箱设定日期 及之前日期的数据, 并运行 AI 的各个业务." 调研发现 (4 个并行子代理): batch-run 端点 _run_batch 是空壳 stub (只 logger.info, 实际不跑 AI), GUC apply_runtime_session_vars 0 处调用 (dead code), 7 张业务表 6 张有 runtime 复合索引唯独 ai_run_logs 漏建, App2/2a 3 行 _calc_date_range 漏传 ref_date. 本 commit (F1-5a 阶段 A 主体, F1-5b 后续完整 zqyy_app RLS 视图层): 后端核心: - admin_service.py: _run_batch 真实化 (Semaphore(5)+asyncio.gather+ return_exceptions=True+ctx_snapshot 防漂移); estimate 入口抓 RuntimeContext 快照, confirm 取出传给 worker - admin_ai.py: confirm_batch_run lazy 注入 dispatcher - admin_service.retry_trigger_job: INSERT 落 runtime_mode + sandbox_instance_id 列 (用 runtime_insert_columns helper) - runtime_context.py: get_runtime_context 加 bind_to_session 参数, 激活 GUC app.current_business_date / app.current_runtime_mode - run_log_service.create_log: 启用 bind_to_session=True 试点 App2/2a 3 行 ref_date 修复: - app2_finance_prompt.py:817 储值卡余额变化板块 - app2_finance_prompt.py:841 日粒度 series + 异常检测窗口 - app2a_finance_area_prompt.py:466 区域日粒度 series DB: - migrations/20260505__ai_run_logs_runtime_index.sql: 补 (site_id, runtime_mode, sandbox_instance_id, created_at DESC) 复合索引 前端: - AIOperations.tsx: 顶部加 sandbox 模式提示条 (Alert 显示 sandbox_date + sandbox_instance_id + 影响范围 + 切回 live 入口) 未做 (留 F1-5b 完整 zqyy_app RLS 视图层一并): - B1 admin_service 6 处 CURRENT_DATE -> business_date - B2 fdw_queries 异常分支兜底 - GUC 完整传递 (fdw_queries / page_context 等) - 测试 3 套 (.gitignore:71 排除, F2-2 入仓时 commit) - P20 SPEC \xa76/\xa710/\xa711/\xa715 (F1-5b 完整收口后同步更准确) Neo 决策: docs/_overview/wave1-findings/F1-5-impl-decisions.md 详见 docs/audit/changes/2026-05-05__wave1_f1_5a_sandbox_batch_run.md
531 lines
20 KiB
TypeScript
531 lines
20 KiB
TypeScript
/**
|
||
* 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<string, string> = {
|
||
failed: "red", timeout: "orange", circuit_open: "volcano",
|
||
};
|
||
|
||
const ALERT_MGMT_COLOR: Record<string, string> = {
|
||
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<string>("");
|
||
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<string | undefined>();
|
||
const [cacheMemberId, setCacheMemberId] = useState<string>("");
|
||
const [cacheSiteId, setCacheSiteId] = useState<number>(2790685415443269);
|
||
const [cacheLoading, setCacheLoading] = useState(false);
|
||
const [cacheResult, setCacheResult] = useState<number | null>(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<AppType | undefined>();
|
||
const [runMemberId, setRunMemberId] = useState<string>("");
|
||
const [runSiteId, setRunSiteId] = useState<number>(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<string>("consumption");
|
||
const [evtSiteId, setEvtSiteId] = useState<number>(2790685415443269);
|
||
const [evtMemberId, setEvtMemberId] = useState<string>("");
|
||
const [evtAssistantId, setEvtAssistantId] = useState<string>("");
|
||
const [evtForced, setEvtForced] = useState<boolean>(true);
|
||
const [evtLoading, setEvtLoading] = useState(false);
|
||
const [evtResult, setEvtResult] = useState<number | null>(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<AppType[]>([]);
|
||
const [batchMemberIds, setBatchMemberIds] = useState<string>("");
|
||
const [batchSiteId, setBatchSiteId] = useState<number>(2790685415443269);
|
||
const [batchLoading, setBatchLoading] = useState(false);
|
||
const [batchEstimate, setBatchEstimate] = useState<BatchRunEstimate | null>(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<RuntimeContext | null>(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<AlertItem[]>([]);
|
||
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<AlertItem> = [
|
||
{ 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) => <Tag color={ALERT_STATUS_COLOR[v] ?? "default"}>{v}</Tag>,
|
||
},
|
||
{
|
||
title: "告警状态", dataIndex: "alert_status", key: "alert_status", width: 100,
|
||
render: (v: string | null) => v ? <Tag color={ALERT_MGMT_COLOR[v] ?? "default"}>{v}</Tag> : "—",
|
||
},
|
||
{ 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) => (
|
||
<Space>
|
||
<Button size="small" onClick={() => handleAck(r.id)} disabled={r.alert_status === "acknowledged"}>确认</Button>
|
||
<Button size="small" onClick={() => handleIgnore(r.id)} disabled={r.alert_status === "ignored"}>忽略</Button>
|
||
</Space>
|
||
),
|
||
},
|
||
];
|
||
|
||
return (
|
||
<div>
|
||
<Title level={4} style={{ marginBottom: 16 }}>AI 手动操作</Title>
|
||
|
||
{runtimeCtx && runtimeCtx.is_sandbox && (
|
||
<Alert
|
||
type="warning"
|
||
showIcon
|
||
style={{ marginBottom: 16 }}
|
||
message={
|
||
<span>
|
||
<strong>沙箱模式</strong> · 业务日 <strong>{runtimeCtx.sandbox_date ?? "—"}</strong> ·
|
||
实例 <code>{runtimeCtx.sandbox_instance_id ?? "—"}</code>
|
||
</span>
|
||
}
|
||
description={
|
||
<span>
|
||
当前 site_id={cacheSiteId} 处于沙箱模式。本页所有 AI 触发(手动重跑 / 缓存失效 / 按需执行 / 批量执行)将使用
|
||
沙箱业务日 ({runtimeCtx.sandbox_date}) 而非真实今日;ETL 视图自动按业务日上界裁剪,助教/会员消费数据仅可见
|
||
沙箱日及之前。结果写入 ai_run_logs 时 runtime_mode=sandbox + sandbox_instance_id 隔离,
|
||
不污染 live 数据。如需切回 live,前往 <a href="/settings/runtime-context">运行上下文</a> 页。
|
||
</span>
|
||
}
|
||
/>
|
||
)}
|
||
|
||
<Row gutter={16} style={{ marginBottom: 16 }}>
|
||
{/* Card 1: 手动重跑 */}
|
||
<Col span={12}>
|
||
<Card title="手动重跑" size="small">
|
||
<Space direction="vertical" style={{ width: "100%" }}>
|
||
<Input
|
||
placeholder="调度任务 ID" value={retryJobId}
|
||
onChange={(e) => setRetryJobId(e.target.value)}
|
||
/>
|
||
<Button type="primary" onClick={handleRetry} loading={retryLoading}>执行重跑</Button>
|
||
</Space>
|
||
</Card>
|
||
</Col>
|
||
|
||
{/* Card 2: 缓存失效 */}
|
||
<Col span={12}>
|
||
<Card title="缓存失效" size="small">
|
||
<Space direction="vertical" style={{ width: "100%" }}>
|
||
<Select
|
||
allowClear placeholder="App 类型(可选)" style={{ width: "100%" }}
|
||
value={cacheAppType} onChange={setCacheAppType}
|
||
options={CACHE_TYPE_OPTIONS}
|
||
/>
|
||
<Input
|
||
placeholder="会员 ID(可选)" value={cacheMemberId}
|
||
onChange={(e) => setCacheMemberId(e.target.value)}
|
||
/>
|
||
<Select
|
||
placeholder="门店" style={{ width: "100%" }}
|
||
value={cacheSiteId} onChange={setCacheSiteId}
|
||
options={[{ label: "默认门店", value: 2790685415443269 }]}
|
||
/>
|
||
<Space>
|
||
<Button type="primary" danger onClick={handleInvalidate} loading={cacheLoading}>执行失效</Button>
|
||
{cacheResult != null && <Statistic title="受影响记录" value={cacheResult} />}
|
||
</Space>
|
||
</Space>
|
||
</Card>
|
||
</Col>
|
||
</Row>
|
||
|
||
{/* Card 2.5: 按需重新生成 */}
|
||
<Card title="按需重新生成" size="small" style={{ marginBottom: 16 }}>
|
||
<Row gutter={16}>
|
||
<Col span={6}>
|
||
<Select
|
||
allowClear placeholder="App 类型" style={{ width: "100%" }}
|
||
value={runAppType} onChange={setRunAppType}
|
||
options={RUN_APP_TYPE_OPTIONS}
|
||
/>
|
||
</Col>
|
||
<Col span={6}>
|
||
<Input
|
||
placeholder="会员 ID(部分 App 必填)" value={runMemberId}
|
||
onChange={(e) => setRunMemberId(e.target.value)}
|
||
/>
|
||
</Col>
|
||
<Col span={6}>
|
||
<Select
|
||
placeholder="门店" style={{ width: "100%" }}
|
||
value={runSiteId} onChange={setRunSiteId}
|
||
options={[{ label: "默认门店", value: 2790685415443269 }]}
|
||
/>
|
||
</Col>
|
||
<Col span={6}>
|
||
<Space>
|
||
<Button type="primary" onClick={handleRunApp} loading={runLoading}>立即执行</Button>
|
||
{runResult && (
|
||
<Tag color={runResult.success ? "success" : "error"}>{runResult.text}</Tag>
|
||
)}
|
||
</Space>
|
||
</Col>
|
||
</Row>
|
||
</Card>
|
||
|
||
{/* Card 2.6: 手动触发事件链(越过去重,调试利器)*/}
|
||
<Card
|
||
title="手动触发事件链(调试用)" size="small" style={{ marginBottom: 16 }}
|
||
extra={<Tag color="orange">默认跳过去重</Tag>}
|
||
>
|
||
<Row gutter={12}>
|
||
<Col span={6}>
|
||
<div style={{ marginBottom: 4, fontSize: 12, color: "#888" }}>事件类型</div>
|
||
<Select
|
||
value={evtType} onChange={setEvtType}
|
||
options={EVENT_TYPE_OPTIONS}
|
||
style={{ width: "100%" }}
|
||
/>
|
||
</Col>
|
||
<Col span={5}>
|
||
<div style={{ marginBottom: 4, fontSize: 12, color: "#888" }}>门店</div>
|
||
<Select
|
||
value={evtSiteId} onChange={setEvtSiteId}
|
||
options={[{ label: "默认门店", value: 2790685415443269 }]}
|
||
style={{ width: "100%" }}
|
||
/>
|
||
</Col>
|
||
<Col span={4}>
|
||
<div style={{ marginBottom: 4, fontSize: 12, color: "#888" }}>member_id(可选)</div>
|
||
<Input
|
||
placeholder="consumption/note/task 事件需填"
|
||
value={evtMemberId}
|
||
onChange={(e) => setEvtMemberId(e.target.value)}
|
||
/>
|
||
</Col>
|
||
<Col span={4}>
|
||
<div style={{ marginBottom: 4, fontSize: 12, color: "#888" }}>assistant_id(可选)</div>
|
||
<Input
|
||
placeholder="task_assigned 事件需填"
|
||
value={evtAssistantId}
|
||
onChange={(e) => setEvtAssistantId(e.target.value)}
|
||
/>
|
||
</Col>
|
||
<Col span={5}>
|
||
<div style={{ marginBottom: 4, fontSize: 12, color: "#888" }}>跳过去重 + 操作</div>
|
||
<Space>
|
||
<Checkbox checked={evtForced} onChange={(e) => setEvtForced(e.target.checked)}>强制</Checkbox>
|
||
<Button type="primary" danger onClick={handleTriggerEvent} loading={evtLoading}>触发</Button>
|
||
{evtResult != null && <Tag color="processing">job #{evtResult}</Tag>}
|
||
</Space>
|
||
</Col>
|
||
</Row>
|
||
</Card>
|
||
|
||
{/* Card 3: 批量执行 */}
|
||
<Card title="批量执行" size="small" style={{ marginBottom: 16 }}>
|
||
<Row gutter={16}>
|
||
<Col span={8}>
|
||
<div style={{ marginBottom: 8, fontWeight: 500 }}>选择 App</div>
|
||
<Checkbox.Group
|
||
options={RUN_APP_TYPE_OPTIONS}
|
||
value={batchAppTypes}
|
||
onChange={(v) => setBatchAppTypes(v as AppType[])}
|
||
style={{ display: "flex", flexDirection: "column", gap: 4 }}
|
||
/>
|
||
</Col>
|
||
<Col span={8}>
|
||
<div style={{ marginBottom: 8, fontWeight: 500 }}>会员 ID(每行一个或逗号分隔)</div>
|
||
<TextArea
|
||
rows={6} value={batchMemberIds}
|
||
onChange={(e) => setBatchMemberIds(e.target.value)}
|
||
placeholder="例如: 12345 67890"
|
||
/>
|
||
</Col>
|
||
<Col span={8}>
|
||
<div style={{ marginBottom: 8, fontWeight: 500 }}>门店</div>
|
||
<Select
|
||
style={{ width: "100%", marginBottom: 16 }}
|
||
value={batchSiteId} onChange={setBatchSiteId}
|
||
options={[{ label: "默认门店", value: 2790685415443269 }]}
|
||
/>
|
||
<Button type="primary" onClick={handleBatchEstimate} loading={batchLoading}>
|
||
预估并执行
|
||
</Button>
|
||
</Col>
|
||
</Row>
|
||
</Card>
|
||
|
||
{/* 批量执行确认弹窗 */}
|
||
<Modal
|
||
title="确认批量执行"
|
||
open={confirmVisible}
|
||
onCancel={() => { setConfirmVisible(false); setBatchEstimate(null); }}
|
||
onOk={handleBatchConfirm}
|
||
confirmLoading={confirmLoading}
|
||
okText="确认执行" cancelText="取消"
|
||
>
|
||
{batchEstimate && (
|
||
<Row gutter={16}>
|
||
<Col span={12}>
|
||
<Statistic title="预估调用次数" value={batchEstimate.estimated_calls} suffix="次" />
|
||
</Col>
|
||
<Col span={12}>
|
||
<Statistic title="预估 Token 消耗" value={batchEstimate.estimated_tokens} />
|
||
</Col>
|
||
</Row>
|
||
)}
|
||
<p style={{ marginTop: 16, color: "#faad14" }}>
|
||
确认后将在后台异步执行,请确保预算充足。
|
||
</p>
|
||
</Modal>
|
||
|
||
{/* Card 4: 告警管理 */}
|
||
<Card
|
||
title="告警管理" size="small"
|
||
extra={<Button icon={<ReloadOutlined />} size="small" onClick={loadAlerts}>刷新</Button>}
|
||
>
|
||
<Table<AlertItem>
|
||
columns={alertColumns}
|
||
dataSource={alerts}
|
||
rowKey="id" size="small"
|
||
loading={alertLoading}
|
||
pagination={{
|
||
current: alertPage, pageSize: 10, total: alertTotal,
|
||
onChange: (p) => setAlertPage(p),
|
||
showTotal: (t) => `共 ${t} 条`,
|
||
}}
|
||
/>
|
||
</Card>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
export default AIOperations;
|