Files
Neo-ZQYY/apps/admin-web/src/pages/AIRunLogs.tsx
Neo caf179a5da feat: 2026-04-15~05-02 累积变更基线 — AI 重构 + Runtime Context + DWS 修复
涵盖(每条对应已存的审计记录):
- 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 逐一处理
2026-05-04 02:30:19 +08:00

232 lines
8.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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 }));