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:
Neo
2026-04-06 00:03:48 +08:00
parent 70324d8542
commit 6f8f12314f
515 changed files with 76604 additions and 7456 deletions

View File

@@ -0,0 +1,163 @@
/**
* [ARCHIVED] 日志查看器页面。
*
* 已废弃:功能已合并到 ETLTasks 页面的"任务管理"Tab。
* 归档日期2026-03-25
* 归档原因admin-web-restructure spec需求 8LogViewer 废弃)
*
* - 输入执行 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;