feat(admin-web): AIPrewarm 分组展示 + 每行触发 + AppType 联合类型

1. AIPrewarm.tsx:
   - areaToAppType(area) helper · area='all' → app2_finance · 其他 → app2a_finance_area
   - handleRunOne / handleBackfillMissing 按 area 动态选 app_type
   - MissingRowWithGroup 含 __group_header 字段
   - groupedMissing 数据构造(全域 + 区域两组 · 每组前插 header 行)
   - 每列 onCell colSpan 合并单元格实现"全域 / 区域"分组标题行
   - Descriptions 加全域 8/X + 区域 64/X 双段统计

2. api/adminAI.ts:
   - 新增 AppType 联合类型(9 项,含 app2a_finance_area)
   - runApp 签名 appType: AppType(替代原 string)
   - RunAppResponse.app_type 同步为 AppType

3. AIOperations.tsx:
   - runAppType state 类型改为 AppType | undefined
   - import { AppType } type

实测:
- pnpm tsc --noEmit 全项目通过
- playwright E2E 访问 /ai/prewarm 显示 "全域 8/8 · 区域 63/64" 分段统计
  分组标题行正确合并 · 单独生成按钮按 area 路由到正确 app_type

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Neo
2026-04-22 21:56:17 +08:00
parent 66be873e70
commit 7107884138
3 changed files with 612 additions and 4 deletions

View File

@@ -17,10 +17,17 @@ 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,
getAlerts, ackAlert, ignoreAlert, runApp, triggerEvent,
type AlertItem, type AppType, type BatchRunEstimate,
} from "../api/adminAI";
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;
@@ -92,6 +99,66 @@ const AIOperations: React.FC = () => {
}
};
// ---- 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<string[]>([]);
const [batchMemberIds, setBatchMemberIds] = useState<string>("");
@@ -247,6 +314,89 @@ const AIOperations: React.FC = () => {
</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={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}>