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:
229
apps/admin-web/src/pages/AIRunLogs.tsx
Normal file
229
apps/admin-web/src/pages/AIRunLogs.tsx
Normal file
@@ -0,0 +1,229 @@
|
||||
/**
|
||||
* 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 = [
|
||||
"app1_chat", "app2_finance", "app3_clue", "app4_analysis",
|
||||
"app5_tactics", "app6_note_analysis", "app7_customer_analysis",
|
||||
"app8_clue_consolidated",
|
||||
].map((v) => ({ label: v, value: v }));
|
||||
|
||||
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;
|
||||
Reference in New Issue
Block a user