涵盖(每条对应已存的审计记录): - AI 模块拆分:apps/backend/app/ai/apps -> prompts/(8 个 APP + app2a 派生) audit: 2026-04-20__ai-module-complete.md - admin-web AI 管理套件:AIDashboard / AIOperations / AIRunLogs / AITriggers / TriggerManager audit: 2026-04-21__admin-web-ai-management-suite.md - App2 财务洞察 prompt v3 -> v5.1 + 小程序 AI 接入(chat / board-finance) audit: 2026-04-22__app2_prompt_v5_1_and_miniprogram_ai_insight.md - App2 prewarm 全过滤器 + AI 触发器 cron reschedule audit: 2026-04-21__app2-finance-prewarm-all-filters.md migration: 20260420_ai_trigger_jobs_and_app2_prewarm.sql / 20260421_app2_prewarm_cron_reschedule.sql - AppType 联合类型对齐 + adminAiAppTypes.test.ts audit: 2026-04-30__admin_web_ai_app_type_alignment.md - DashScope tokens_used 提取修复 audit: 2026-04-30__backend_dashscope_tokens_used_extraction.md - App3 线索完整详情 prompt audit: 2026-05-01__backend_app3_full_detail_prompt.md - Runtime Context 沙箱(5-1~5-2 主线): - 后端 schema/service + admin_runtime_context / xcx_runtime_clock 两个 router - admin-web RuntimeContext.tsx + miniprogram runtime-clock.ts - migration: 20260501__runtime_context_sandbox.sql - tools/db/verify_admin_web_sandbox.py + verify_sandbox_end_to_end.py - database/changes: 7 份 sandbox_* 验证报告 - 飞球 DWS 修复:finance_area_daily 区域汇总 + task_engine 调整 + RLS 视图业务日上界(migration 20260502 + scripts/ops/gen_rls_business_date_migration.py) 合规: - .gitignore 启用 tmp/ 排除 - 不入仓:apps/etl/connectors/feiqiu/.env(API_TOKEN secret,本地修改保留) 待验证清单: - docs/audit/changes/2026-05-04__cumulative_baseline_pending_verification.md 每个主题的功能完整性 / 上线验证几乎都未收口,按优先级 P0~P3 逐一处理
232 lines
8.9 KiB
TypeScript
232 lines
8.9 KiB
TypeScript
/**
|
||
* AI 调用明细页面。
|
||
*
|
||
* - 顶部筛选器:app_type / status / trigger_type / site_id / 日期范围
|
||
* - 主体:分页表格(app_type、trigger_type、member_id、tokens、延迟、状态)
|
||
* - 点击行:Drawer 展示完整 prompt / response / error_message
|
||
*/
|
||
|
||
import React, { useEffect, useState, useCallback } from "react";
|
||
import {
|
||
Card, Table, Tag, Select, Button, DatePicker, Row, Space,
|
||
Drawer, Descriptions, message, Typography,
|
||
} from "antd";
|
||
import { ReloadOutlined } from "@ant-design/icons";
|
||
import type { ColumnsType } from "antd/es/table";
|
||
import {
|
||
getRunLogs, getRunLogDetail,
|
||
type RunLogItem, type RunLogDetailResponse, type RunLogQuery,
|
||
} from "../api/adminAI";
|
||
|
||
const { RangePicker } = DatePicker;
|
||
const { Title } = Typography;
|
||
|
||
const STATUS_COLOR: Record<string, string> = {
|
||
success: "green", failed: "red", timeout: "orange",
|
||
circuit_open: "volcano", pending: "default", running: "processing",
|
||
};
|
||
|
||
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 AIRunLogs: React.FC = () => {
|
||
const [items, setItems] = useState<RunLogItem[]>([]);
|
||
const [total, setTotal] = useState(0);
|
||
const [loading, setLoading] = useState(false);
|
||
const [page, setPage] = useState(1);
|
||
const [pageSize] = useState(20);
|
||
|
||
// 筛选状态
|
||
const [appType, setAppType] = useState<string | undefined>();
|
||
const [status, setStatus] = useState<string | undefined>();
|
||
const [triggerType, setTriggerType] = useState<string | undefined>();
|
||
const [siteId, setSiteId] = useState<number | undefined>();
|
||
const [dateRange, setDateRange] = useState<[string, string] | null>(null);
|
||
|
||
// Drawer 详情
|
||
const [drawerVisible, setDrawerVisible] = useState(false);
|
||
const [detail, setDetail] = useState<RunLogDetailResponse | null>(null);
|
||
const [detailLoading, setDetailLoading] = useState(false);
|
||
|
||
const load = useCallback(async () => {
|
||
setLoading(true);
|
||
try {
|
||
const params: RunLogQuery = {
|
||
page, page_size: pageSize,
|
||
app_type: appType, status, trigger_type: triggerType,
|
||
site_id: siteId,
|
||
date_from: dateRange?.[0], date_to: dateRange?.[1],
|
||
};
|
||
const res = await getRunLogs(params);
|
||
setItems(res.items);
|
||
setTotal(res.total);
|
||
} catch {
|
||
message.error("加载调用记录失败");
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
}, [page, pageSize, appType, status, triggerType, siteId, dateRange]);
|
||
|
||
useEffect(() => { load(); }, [load]);
|
||
|
||
const handleRowClick = async (id: number) => {
|
||
setDetailLoading(true);
|
||
setDrawerVisible(true);
|
||
try {
|
||
const res = await getRunLogDetail(id);
|
||
setDetail(res);
|
||
} catch {
|
||
message.error("加载详情失败");
|
||
} finally {
|
||
setDetailLoading(false);
|
||
}
|
||
};
|
||
|
||
const APP_TYPE_OPTIONS = RUN_LOG_APP_TYPE_OPTIONS;
|
||
|
||
const columns: ColumnsType<RunLogItem> = [
|
||
{ title: "ID", dataIndex: "id", key: "id", width: 70 },
|
||
{ title: "App 类型", dataIndex: "app_type", key: "app_type", width: 160 },
|
||
{ title: "触发方式", dataIndex: "trigger_type", key: "trigger_type", width: 110 },
|
||
{ title: "会员 ID", dataIndex: "member_id", key: "member_id", width: 100, render: (v) => v ?? "—" },
|
||
{ title: "Tokens", dataIndex: "tokens_used", key: "tokens_used", width: 90, align: "right" },
|
||
{
|
||
title: "延迟", dataIndex: "latency_ms", key: "latency_ms", width: 90, align: "right",
|
||
render: (v: number | null) => v != null ? `${v}ms` : "—",
|
||
},
|
||
{
|
||
title: "状态", dataIndex: "status", key: "status", width: 110,
|
||
render: (v: string) => <Tag color={STATUS_COLOR[v] ?? "default"}>{v}</Tag>,
|
||
},
|
||
{ title: "时间", dataIndex: "created_at", key: "created_at", width: 170, render: fmtTime },
|
||
];
|
||
|
||
return (
|
||
<div>
|
||
<Row justify="space-between" align="middle" style={{ marginBottom: 16 }}>
|
||
<Title level={4} style={{ margin: 0 }}>AI 调用明细</Title>
|
||
<Button icon={<ReloadOutlined />} onClick={load} loading={loading}>刷新</Button>
|
||
</Row>
|
||
|
||
{/* 筛选器行 */}
|
||
<Card size="small" style={{ marginBottom: 16 }}>
|
||
<Space wrap>
|
||
<Select
|
||
allowClear placeholder="App 类型" style={{ width: 180 }}
|
||
value={appType} onChange={(v) => { setAppType(v); setPage(1); }}
|
||
options={APP_TYPE_OPTIONS}
|
||
/>
|
||
<Select
|
||
allowClear placeholder="状态" style={{ width: 130 }}
|
||
value={status} onChange={(v) => { setStatus(v); setPage(1); }}
|
||
options={[
|
||
{ label: "success", value: "success" },
|
||
{ label: "failed", value: "failed" },
|
||
{ label: "timeout", value: "timeout" },
|
||
{ label: "circuit_open", value: "circuit_open" },
|
||
]}
|
||
/>
|
||
<Select
|
||
allowClear placeholder="触发方式" style={{ width: 130 }}
|
||
value={triggerType} onChange={(v) => { setTriggerType(v); setPage(1); }}
|
||
options={[
|
||
{ label: "event", value: "event" },
|
||
{ label: "scheduled", value: "scheduled" },
|
||
{ label: "manual", value: "manual" },
|
||
{ label: "backfill", value: "backfill" },
|
||
]}
|
||
/>
|
||
<Select
|
||
allowClear placeholder="门店" style={{ width: 180 }}
|
||
value={siteId} onChange={(v) => { setSiteId(v); setPage(1); }}
|
||
options={[{ label: "默认门店", value: 2790685415443269 }]}
|
||
/>
|
||
<RangePicker
|
||
onChange={(_, dateStrings) => {
|
||
setDateRange(dateStrings[0] ? [dateStrings[0], dateStrings[1]] : null);
|
||
setPage(1);
|
||
}}
|
||
/>
|
||
</Space>
|
||
</Card>
|
||
|
||
{/* 主体表格 */}
|
||
<Table<RunLogItem>
|
||
columns={columns}
|
||
dataSource={items}
|
||
rowKey="id"
|
||
loading={loading}
|
||
scroll={{ x: 1000 }}
|
||
onRow={(record) => ({
|
||
onClick: () => handleRowClick(record.id),
|
||
style: { cursor: "pointer" },
|
||
})}
|
||
pagination={{
|
||
current: page, pageSize, total,
|
||
onChange: (p) => setPage(p),
|
||
showTotal: (t) => `共 ${t} 条`,
|
||
}}
|
||
/>
|
||
|
||
{/* 详情 Drawer */}
|
||
<Drawer
|
||
title={`调用记录详情 #${detail?.id ?? ""}`}
|
||
open={drawerVisible}
|
||
onClose={() => { setDrawerVisible(false); setDetail(null); }}
|
||
width={640}
|
||
loading={detailLoading}
|
||
>
|
||
{detail && (
|
||
<>
|
||
<Descriptions column={2} bordered size="small" style={{ marginBottom: 16 }}>
|
||
<Descriptions.Item label="App 类型">{detail.app_type}</Descriptions.Item>
|
||
<Descriptions.Item label="触发方式">{detail.trigger_type}</Descriptions.Item>
|
||
<Descriptions.Item label="会员 ID">{detail.member_id ?? "—"}</Descriptions.Item>
|
||
<Descriptions.Item label="门店 ID">{detail.site_id}</Descriptions.Item>
|
||
<Descriptions.Item label="Tokens">{detail.tokens_used}</Descriptions.Item>
|
||
<Descriptions.Item label="延迟">{detail.latency_ms != null ? `${detail.latency_ms}ms` : "—"}</Descriptions.Item>
|
||
<Descriptions.Item label="状态">
|
||
<Tag color={STATUS_COLOR[detail.status] ?? "default"}>{detail.status}</Tag>
|
||
</Descriptions.Item>
|
||
<Descriptions.Item label="Session ID">{detail.session_id ?? "—"}</Descriptions.Item>
|
||
<Descriptions.Item label="创建时间" span={2}>{fmtTime(detail.created_at)}</Descriptions.Item>
|
||
<Descriptions.Item label="完成时间" span={2}>{fmtTime(detail.finished_at)}</Descriptions.Item>
|
||
</Descriptions>
|
||
|
||
{detail.error_message && (
|
||
<Card title="错误信息" size="small" style={{ marginBottom: 16 }}>
|
||
<pre style={{ margin: 0, whiteSpace: "pre-wrap", color: "#cf1322" }}>
|
||
{detail.error_message}
|
||
</pre>
|
||
</Card>
|
||
)}
|
||
|
||
<Card title="Request Prompt" size="small" style={{ marginBottom: 16 }}>
|
||
<pre style={{ margin: 0, whiteSpace: "pre-wrap", maxHeight: 300, overflow: "auto", background: "#f5f5f5", padding: 8, borderRadius: 4 }}>
|
||
{detail.request_prompt ?? "(无)"}
|
||
</pre>
|
||
</Card>
|
||
|
||
<Card title="Response" size="small">
|
||
<pre style={{ margin: 0, whiteSpace: "pre-wrap", maxHeight: 300, overflow: "auto", background: "#f5f5f5", padding: 8, borderRadius: 4 }}>
|
||
{detail.response_text ?? "(无)"}
|
||
</pre>
|
||
</Card>
|
||
</>
|
||
)}
|
||
</Drawer>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
export default AIRunLogs;
|
||
|
||
export const RUN_LOG_APP_TYPE_OPTIONS = [
|
||
"app1_chat", "app2_finance", "app2a_finance_area", "app3_clue",
|
||
"app4_analysis", "app5_tactics", "app6_note", "app7_customer",
|
||
"app8_consolidate", "app8_consolidation",
|
||
].map((v) => ({ label: v, value: v }));
|