feat: 累积功能变更 — 聊天集成、租户管理、小程序更新、ETL 增强、迁移脚本
包含多个会话的累积代码变更: - backend: AI 聊天服务、触发器调度、认证增强、WebSocket、调度器最小间隔 - admin-web: ETL 状态页、任务管理、调度配置、登录优化 - miniprogram: 看板页面、聊天集成、UI 组件、导航更新 - etl: DWS 新任务(finance_area_daily/board_cache)、连接器增强 - tenant-admin: 项目初始化 - db: 19 个迁移脚本(etl_feiqiu 11 + zqyy_app 8) - packages/shared: 枚举和工具函数更新 - tools: 数据库工具、报表生成、健康检查 - docs: PRD/架构/部署/合约文档更新 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
329
apps/admin-web/src/pages/AIOperations.tsx
Normal file
329
apps/admin-web/src/pages/AIOperations.tsx
Normal file
@@ -0,0 +1,329 @@
|
||||
/**
|
||||
* 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 {
|
||||
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,
|
||||
type AlertItem, type BatchRunEstimate,
|
||||
} from "../api/adminAI";
|
||||
|
||||
const { TextArea } = Input;
|
||||
const { Title } = Typography;
|
||||
|
||||
const APP_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" },
|
||||
];
|
||||
|
||||
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 3: 批量执行 ----
|
||||
const [batchAppTypes, setBatchAppTypes] = useState<string[]>([]);
|
||||
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);
|
||||
}
|
||||
};
|
||||
|
||||
// ---- 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>
|
||||
|
||||
<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={APP_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 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={APP_TYPE_OPTIONS}
|
||||
value={batchAppTypes}
|
||||
onChange={(v) => setBatchAppTypes(v as string[])}
|
||||
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;
|
||||
Reference in New Issue
Block a user