包含多个会话的累积代码变更: - 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>
164 lines
6.3 KiB
TypeScript
164 lines
6.3 KiB
TypeScript
/**
|
||
* [ARCHIVED] 日志查看器页面。
|
||
*
|
||
* 已废弃:功能已合并到 ETLTasks 页面的"任务管理"Tab。
|
||
* 归档日期:2026-03-25
|
||
* 归档原因:admin-web-restructure spec,需求 8(LogViewer 废弃)
|
||
*
|
||
* - 输入执行 ID,通过 WebSocket 实时接收日志
|
||
* - 支持加载历史日志
|
||
* - 关键词过滤(大小写不敏感)
|
||
*/
|
||
|
||
import React, { useState, useRef, useCallback, useEffect } from "react";
|
||
import { Input, Button, Space, message, Card, Typography, Tag, Badge, Segmented } from "antd";
|
||
import {
|
||
LinkOutlined, DisconnectOutlined, HistoryOutlined,
|
||
FileTextOutlined, SearchOutlined, ClearOutlined,
|
||
AppstoreOutlined, UnorderedListOutlined,
|
||
} from "@ant-design/icons";
|
||
import { apiClient } from "../../api/client";
|
||
import LogStream from "../../components/LogStream";
|
||
import TaskLogViewer from "../../components/TaskLogViewer";
|
||
|
||
const { Title, Text } = Typography;
|
||
|
||
/* ------------------------------------------------------------------ */
|
||
/* 纯函数:日志过滤 */
|
||
/* ------------------------------------------------------------------ */
|
||
|
||
export function filterLogLines(lines: string[], keyword: string): string[] {
|
||
if (!keyword.trim()) return lines;
|
||
const lower = keyword.toLowerCase();
|
||
return lines.filter((line) => line.toLowerCase().includes(lower));
|
||
}
|
||
|
||
/* ------------------------------------------------------------------ */
|
||
/* 页面组件 */
|
||
/* ------------------------------------------------------------------ */
|
||
|
||
const LogViewer: React.FC = () => {
|
||
const [executionId, setExecutionId] = useState("");
|
||
const [lines, setLines] = useState<string[]>([]);
|
||
const [filterKeyword, setFilterKeyword] = useState("");
|
||
const [connected, setConnected] = useState(false);
|
||
/** 展示模式:raw = 原始流,grouped = 按任务分组 */
|
||
const [viewMode, setViewMode] = useState<"raw" | "grouped">("grouped");
|
||
const wsRef = useRef<WebSocket | null>(null);
|
||
|
||
useEffect(() => {
|
||
return () => { wsRef.current?.close(); };
|
||
}, []);
|
||
|
||
const handleConnect = useCallback(() => {
|
||
const id = executionId.trim();
|
||
if (!id) { message.warning("请输入执行 ID"); return; }
|
||
wsRef.current?.close();
|
||
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
|
||
const host = window.location.host;
|
||
const ws = new WebSocket(`${protocol}//${host}/ws/logs/${id}`);
|
||
wsRef.current = ws;
|
||
ws.onopen = () => { setConnected(true); message.success("WebSocket 已连接"); };
|
||
ws.onmessage = (event) => { setLines((prev) => [...prev, event.data]); };
|
||
ws.onclose = () => { setConnected(false); };
|
||
ws.onerror = () => { message.error("WebSocket 连接失败"); setConnected(false); };
|
||
}, [executionId]);
|
||
|
||
const handleDisconnect = useCallback(() => {
|
||
wsRef.current?.close();
|
||
wsRef.current = null;
|
||
setConnected(false);
|
||
}, []);
|
||
|
||
const handleLoadHistory = useCallback(async () => {
|
||
const id = executionId.trim();
|
||
if (!id) { message.warning("请输入执行 ID"); return; }
|
||
try {
|
||
const { data } = await apiClient.get<{ execution_id: string; output_log: string | null; error_log: string | null }>(
|
||
`/execution/${id}/logs`
|
||
);
|
||
const parts: string[] = [];
|
||
if (data.output_log) parts.push(data.output_log);
|
||
if (data.error_log) parts.push(data.error_log);
|
||
const historyLines = parts.join("\n").split("\n");
|
||
setLines(historyLines);
|
||
message.success("历史日志加载完成");
|
||
} catch { message.error("加载历史日志失败"); }
|
||
}, [executionId]);
|
||
|
||
const handleClear = useCallback(() => { setLines([]); }, []);
|
||
|
||
const filteredLines = filterLogLines(lines, filterKeyword);
|
||
|
||
return (
|
||
<div style={{ display: "flex", flexDirection: "column", height: "100%" }}>
|
||
<div style={{ marginBottom: 12, display: "flex", justifyContent: "space-between", alignItems: "center" }}>
|
||
<Title level={4} style={{ margin: 0 }}>
|
||
<FileTextOutlined style={{ marginRight: 8 }} />
|
||
日志查看器
|
||
</Title>
|
||
<Space>
|
||
{connected && <Badge status="processing" text={<Text type="success">已连接</Text>} />}
|
||
<Tag>{lines.length} 行</Tag>
|
||
{filterKeyword && <Tag color="blue">{filteredLines.length} 条匹配</Tag>}
|
||
</Space>
|
||
</div>
|
||
|
||
{/* 操作栏 */}
|
||
<Card size="small" style={{ marginBottom: 12 }}>
|
||
<Space wrap style={{ width: "100%", justifyContent: "space-between" }}>
|
||
<Space>
|
||
<Input
|
||
placeholder="执行 ID"
|
||
value={executionId}
|
||
onChange={(e) => setExecutionId(e.target.value)}
|
||
style={{ width: 280, fontFamily: "monospace" }}
|
||
onPressEnter={handleConnect}
|
||
allowClear
|
||
/>
|
||
{connected ? (
|
||
<Button icon={<DisconnectOutlined />} danger onClick={handleDisconnect}>断开</Button>
|
||
) : (
|
||
<Button type="primary" icon={<LinkOutlined />} onClick={handleConnect}>连接</Button>
|
||
)}
|
||
<Button icon={<HistoryOutlined />} onClick={handleLoadHistory}>加载历史</Button>
|
||
<Button icon={<ClearOutlined />} onClick={handleClear} disabled={lines.length === 0}>清空</Button>
|
||
</Space>
|
||
<Space>
|
||
<Segmented
|
||
value={viewMode}
|
||
onChange={(v) => setViewMode(v as "raw" | "grouped")}
|
||
options={[
|
||
{ value: "grouped", icon: <AppstoreOutlined />, label: "按任务" },
|
||
{ value: "raw", icon: <UnorderedListOutlined />, label: "原始" },
|
||
]}
|
||
size="small"
|
||
/>
|
||
{viewMode === "raw" && (
|
||
<Input
|
||
prefix={<SearchOutlined />}
|
||
placeholder="过滤关键词..."
|
||
value={filterKeyword}
|
||
onChange={(e) => setFilterKeyword(e.target.value)}
|
||
allowClear
|
||
style={{ width: 220 }}
|
||
/>
|
||
)}
|
||
</Space>
|
||
</Space>
|
||
</Card>
|
||
|
||
{/* 日志展示区域 */}
|
||
<div style={{ flex: 1, minHeight: 0, overflow: "auto" }}>
|
||
{viewMode === "grouped" ? (
|
||
<TaskLogViewer lines={lines} />
|
||
) : (
|
||
<LogStream executionId={executionId} lines={filteredLines} />
|
||
)}
|
||
</div>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
export default LogViewer;
|