import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { LayoutDashboard, Workflow, Database, FileText, Activity, Bookmark, Boxes, Settings } from "lucide-react"; import { TopBar } from "./shell/TopBar"; import { LeftNav, type ViewKey } from "./shell/LeftNav"; import { BottomDrawer } from "./shell/BottomDrawer"; import { StudioView } from "./studio/StudioView"; import { MemoryView } from "./views/MemoryView"; import { KbView } from "./views/KbView"; import { ReportView } from "./views/ReportView"; import { RunsView } from "./views/RunsView"; import { Home } from "./views/Home"; import { Placeholder } from "./views/Placeholder"; import { CommandPalette, type Command } from "./components/CommandPalette"; import { UpdateBanner } from "./components/UpdateBanner"; import { Login } from "./views/Login"; import { submitTask, streamTokens, streamExec, taskStatus, authMe, logout, type Identity, type AuthUser } from "./lib/api"; import type { TaskDsl } from "./lib/dsl"; import { emptyRun, type RunState } from "./lib/run"; import { ToastProvider } from "./ui"; // 会话标识:本地持久化生成一次(多轮历史用,与鉴权身份分离)。 function getSessionId(): string { let s = localStorage.getItem("sdx_sess"); if (!s) { s = "sess-" + Math.random().toString(36).slice(2, 10); localStorage.setItem("sdx_sess", s); } return s; } const PLACEHOLDERS: Partial> = { home: { title: "工作台", desc: "概览:知识库 / 文档 / 近期运行 / 待办报告 / 配额计费 + 快捷入口。" }, kb: { title: "知识库 (RAG)", desc: "入库流水线监控 · 检索调试台(带来源徽标) · 文档/块浏览 · 知识图谱 · 检索评测。依赖 embedding + 入库 worker + 真实混合检索。" }, report: { title: "报告生成", desc: "模板库 · 大纲编辑 · 章节并行生成进度 · 实时预览(含引用) · 导出 docx/pdf。依赖 RAG 核心链 + UniOffice。" }, runs: { title: "运行 · 观测", desc: "实时执行 · 节点轨迹 · 工具调用 · 运行历史复盘。当前运行结果见底部抽屉。" }, market: { title: "市场 · Packs", desc: "垂直包(法律/医疗/金融) · Agent 模板 · 开通向导(建租户→入库→注册模板→应用配置)。依赖多租户 + Pack 格式。" }, admin: { title: "管理", desc: "租户/工作区 · 用户计费 · 护栏 · 模型与连接 · 设置。" }, }; export default function App() { const [view, setView] = useState("home"); const [user, setUser] = useState(null); const [authLoading, setAuthLoading] = useState(true); const identity = useMemo(() => ({ userId: user?.id ?? "", sessionId: getSessionId() }), [user]); const [run, setRun] = useState(emptyRun); const [cmdOpen, setCmdOpen] = useState(false); const closeRef = useRef<(() => void) | null>(null); const execCloseRef = useRef<(() => void) | null>(null); const pollRef = useRef(null); const stopPoll = () => { if (pollRef.current != null) { window.clearInterval(pollRef.current); pollRef.current = null; } }; // 全局 ⌘K / Ctrl+K 唤起命令面板(键盘优先工作站入口)。 useEffect(() => { const onKey = (e: KeyboardEvent) => { if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === "k") { e.preventDefault(); setCmdOpen((o) => !o); } }; window.addEventListener("keydown", onKey); return () => window.removeEventListener("keydown", onKey); }, []); // 启动校验现有令牌;并监听 401 登出事件(令牌失效 → 回登录页)。 useEffect(() => { authMe() .then(setUser) .catch(() => setUser(null)) .finally(() => setAuthLoading(false)); const onLogout = () => setUser(null); window.addEventListener("sdx:logout", onLogout); return () => window.removeEventListener("sdx:logout", onLogout); }, []); const onLogout = useCallback(() => { logout(); setUser(null); }, []); const commands = useMemo(() => { const go = (key: ViewKey) => () => setView(key); return [ { id: "home", label: "前往 · 工作台", icon: LayoutDashboard, group: "页面", keywords: "home dashboard 概览", run: go("home") }, { id: "studio", label: "前往 · 编排", icon: Workflow, group: "页面", keywords: "studio agent graph 编排 图", run: go("studio") }, { id: "kb", label: "前往 · 知识库", icon: Database, group: "页面", keywords: "kb rag 检索 图谱 入库", run: go("kb") }, { id: "report", label: "前往 · 报告生成", icon: FileText, group: "页面", keywords: "report 报告 word docx", run: go("report") }, { id: "runs", label: "前往 · 运行观测", icon: Activity, group: "页面", keywords: "runs trace 轨迹 观测", run: go("runs") }, { id: "memory", label: "前往 · 记忆", icon: Bookmark, group: "页面", keywords: "memory 画像 偏好", run: go("memory") }, { id: "market", label: "前往 · 市场 Packs", icon: Boxes, group: "页面", keywords: "market pack 垂直", run: go("market") }, { id: "admin", label: "前往 · 管理", icon: Settings, group: "页面", keywords: "admin 租户 模型 设置", run: go("admin") }, { id: "act-report", label: "生成报告", icon: FileText, group: "动作", keywords: "新建 report 撰写", run: go("report") }, { id: "act-ingest", label: "入库知识", icon: Database, group: "动作", keywords: "上传 ingest 文件", run: go("kb") }, { id: "act-studio", label: "新建 Agent 编排", icon: Workflow, group: "动作", keywords: "new flow", run: go("studio") }, ]; }, []); const onRun = useCallback( async (dsl: TaskDsl) => { closeRef.current?.(); execCloseRef.current?.(); stopPoll(); const t0 = Date.now(); setRun({ phase: "submitting", output: "", events: [{ t: 0, label: "提交任务" }], exec: [] }); try { const taskId = await submitTask(dsl, identity); let first = true; setRun((r) => ({ ...r, phase: "streaming", taskId, events: [...r.events, { t: Date.now() - t0, label: `已发布 ${taskId}` }], })); // 轮询后端任务状态:可靠捕获 waiting(审批中断)—— exec 实时事件会抢跑,状态落 PG 不会丢。 const terminal = new Set(["done", "failed", "timeout", "rejected"]); pollRef.current = window.setInterval(async () => { try { const s = await taskStatus(taskId); setRun((r) => (r.taskId === taskId ? { ...r, lifecycle: s.status, detail: s.detail } : r)); if (terminal.has(s.status)) stopPoll(); } catch { /* 忽略瞬时失败,下个 tick 再试 */ } }, 1500); // 执行轨迹(运行·观测):与 token 流并行订阅,逐节点点亮。 execCloseRef.current = streamExec( taskId, (ev) => setRun((r) => ({ ...r, exec: [...r.exec, ev] })), () => {}, () => {}, ); closeRef.current = streamTokens( taskId, (tok) => setRun((r) => { const ev = first ? [...r.events, { t: Date.now() - t0, label: "首 token" }] : r.events; first = false; return { ...r, output: r.output + tok, events: ev }; }), () => setRun((r) => ({ ...r, phase: "done", events: [...r.events, { t: Date.now() - t0, label: "完成" }], })), () => setRun((r) => ({ ...r, phase: "error", error: "连接中断" })), ); } catch (e) { setRun((r) => ({ ...r, phase: "error", error: (e as Error).message })); } }, [identity], ); // 鉴权门:校验中显示占位;未登录显示登录页;登录后进入主应用。 if (authLoading) { return
加载中…
; } if (!user) { return ( ); } return (
{/* 顶部柔光,增加纵深 */}
setCmdOpen(true)} />
{view === "home" ? ( ) : view === "studio" ? ( ) : view === "kb" ? ( ) : view === "report" ? ( ) : view === "runs" ? ( ) : view === "memory" ? ( ) : ( )}
setCmdOpen(false)} commands={commands} />
); }