"use client"; import { Activity, Database, History, KeyRound, PlayCircle, ShieldCheck, Zap } from "lucide-react"; import { useCallback, useEffect, useState } from "react"; import { Badge, Button, Card, EmptyState, ErrorState, LoadingBlock, Select, TableWrap, Td, Th, TextInput, cn, formatTime, statusTone, useToast, } from "@/components/admin/ui"; import { nl2sqlApi, type MaintenanceTask, type QueryHistoryItem } from "@/lib/nl2sql-api"; type Tab = "query" | "history" | "permission" | "ops"; const TABS: { key: Tab; label: string; icon: typeof Activity }[] = [ { key: "query", label: "查询与健康", icon: Activity }, { key: "history", label: "查询历史", icon: History }, { key: "permission", label: "权限总览", icon: KeyRound }, { key: "ops", label: "运维面板", icon: ShieldCheck }, ]; const JOB_TASKS: { value: MaintenanceTask; label: string }[] = [ { value: "metadata_sync", label: "元数据同步" }, { value: "vector_cleanup", label: "向量清理" }, { value: "consistency_check", label: "一致性校验" }, { value: "history_cleanup", label: "历史清理" }, ]; /** 通用 JSON 折叠查看:管理接口字段不固定,按原样展示避免前端臆造字段。 */ function JsonBlock({ data, empty = "暂无数据" }: { data: unknown; empty?: string }) { if (data === null || data === undefined) return ; if (Array.isArray(data) && data.length === 0) return ; return (
      {JSON.stringify(data, null, 2)}
    
); } export function Nl2SqlConsole() { const toast = useToast(); const [tab, setTab] = useState("query"); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [question, setQuestion] = useState(""); const [queryResult, setQueryResult] = useState | null>(null); const [health, setHealth] = useState | null>(null); const [sql, setSql] = useState(""); const [explain, setExplain] = useState[] | null>(null); const [history, setHistory] = useState([]); const [running, setRunning] = useState[]>([]); const [diagnostics, setDiagnostics] = useState | null>(null); const [roles, setRoles] = useState[]>([]); const [tables, setTables] = useState[]>([]); const [columns, setColumns] = useState[]>([]); const [sensitive, setSensitive] = useState[]>([]); const [selectedRole, setSelectedRole] = useState(""); const [metrics, setMetrics] = useState | null>(null); const [runtimeConfig, setRuntimeConfig] = useState | null>(null); const [semantics, setSemantics] = useState | null>(null); const [jobHistory, setJobHistory] = useState[]>([]); const [busy, setBusy] = useState(null); const loadBase = useCallback(async () => { setLoading(true); setError(null); try { const [healthData, runningData] = await Promise.all([ nl2sqlApi.health(), nl2sqlApi.running(), ]); setHealth(healthData); setRunning(runningData ?? []); } catch (err) { setError(err instanceof Error ? err.message : "NL2SQL 状态加载失败"); } finally { setLoading(false); } }, []); useEffect(() => { void loadBase(); }, [loadBase]); const loadHistory = useCallback(async () => { setLoading(true); try { const page = await nl2sqlApi.history({ page: 1, page_size: 20 }); setHistory(page.items ?? []); setError(null); } catch (err) { setError(err instanceof Error ? err.message : "查询历史加载失败"); } finally { setLoading(false); } }, []); const loadPermission = useCallback(async () => { setLoading(true); try { const roleList = await nl2sqlApi.roles(); setRoles(roleList ?? []); setSensitive((await nl2sqlApi.sensitiveFields()) ?? []); if (!selectedRole && roleList?.length) { const firstId = String((roleList[0] as { id?: number }).id ?? ""); setSelectedRole(firstId); } setError(null); } catch (err) { setError(err instanceof Error ? err.message : "权限数据加载失败"); } finally { setLoading(false); } }, [selectedRole]); const loadOps = useCallback(async () => { setLoading(true); try { const [metricsData, configData, semanticsData, jobs] = await Promise.all([ nl2sqlApi.metrics(), nl2sqlApi.runtimeConfig(), nl2sqlApi.semantics(), nl2sqlApi.jobHistory({ page: 1, page_size: 10 }), ]); setMetrics(metricsData); setRuntimeConfig(configData); setSemantics(semanticsData); setJobHistory(jobs.items ?? []); setError(null); } catch (err) { setError(err instanceof Error ? err.message : "运维数据加载失败"); } finally { setLoading(false); } }, []); useEffect(() => { if (tab === "history") void loadHistory(); if (tab === "permission") void loadPermission(); if (tab === "ops") void loadOps(); }, [tab, loadHistory, loadPermission, loadOps]); useEffect(() => { if (!selectedRole) return; void (async () => { try { const [tableList, columnList] = await Promise.all([ nl2sqlApi.roleTables(Number(selectedRole)), nl2sqlApi.roleColumns(Number(selectedRole)), ]); setTables(tableList ?? []); setColumns(columnList ?? []); } catch { setTables([]); setColumns([]); } })(); }, [selectedRole]); const runQuery = useCallback(async () => { if (!question.trim()) return; setBusy("query"); try { const result = await nl2sqlApi.query({ question, include_sql: true, page_size: 50 }); setQueryResult(result as unknown as Record); toast("查询完成", "success"); } catch (err) { toast(err instanceof Error ? err.message : "查询失败", "error"); } finally { setBusy(null); } }, [question, toast]); const runExplain = useCallback(async () => { if (!sql.trim()) return; setBusy("explain"); try { setExplain(await nl2sqlApi.explain(sql)); } catch (err) { toast(err instanceof Error ? err.message : "执行计划解析失败", "error"); } finally { setBusy(null); } }, [sql, toast]); const trigger = useCallback( async (key: string, action: () => Promise, okText: string) => { setBusy(key); try { await action(); toast(okText, "success"); } catch (err) { toast(err instanceof Error ? err.message : "操作失败", "error"); } finally { setBusy(null); } }, [toast] ); return (
{TABS.map((item) => ( ))}
} > {error && void loadBase()} />} {loading && } {!loading && !error && tab === "query" && (
setQuestion(event.target.value)} placeholder="例如:查询近一个月申购金额最高的前 10 个客户" />
{queryResult && (
{typeof queryResult.summary === "string" && (

{queryResult.summary}

)} {typeof queryResult.generated_sql === "string" && (
                    {queryResult.generated_sql}
                  
)}
)}
setSql(event.target.value)} placeholder="SELECT * FROM ..." />
{explain &&
{}
}
)} {!loading && !error && tab === "history" && (
{history.length === 0 ? ( ) : ( 问题 状态 行数 耗时 时间 操作 {history.map((item) => ( {item.question} {item.status ?? "--"} {item.row_count ?? "--"} {item.elapsed_ms != null ? `${item.elapsed_ms} ms` : "--"} {formatTime(item.create_time)} ))} )}
)} {!loading && !error && tab === "permission" && (
{roles.length === 0 ? ( ) : ( 角色 最大行数 状态 操作 {roles.map((role, index) => { const id = String((role as { id?: number }).id ?? index); return ( {String(role.employee_role ?? "--")} {String(role.max_rows ?? "--")} {role.active === false ? "停用" : "启用"} ); })} )}
)} {!loading && !error && tab === "ops" && (
{JOB_TASKS.map((job) => ( ))}
} >
void trigger( "semantics", async () => setSemantics(await nl2sqlApi.refreshSemantics()), "语义目录已刷新" ) } > 刷新 } >
void trigger( "diagnostics", async () => setDiagnostics(await nl2sqlApi.diagnostics()), "诊断信息已刷新" ) } > 重新诊断 } >
{ if (event.key !== "Enter") return; const names = event.currentTarget.value .split(",") .map((item) => item.trim()) .filter(Boolean); if (!names.length) return; void trigger("cache", () => nl2sqlApi.invalidateCache(names), "缓存已失效"); }} />

回车提交缓存失效;中止运行中查询请使用上方「运行中查询」返回的 query_id 调用 kill 接口。

)}

说明:本分区按约定只做只读展示与运维触发,权限的增删改表单不在本轮范围内。

); }