import { type ChangeEvent, type FormEvent, type MouseEvent as ReactMouseEvent, useEffect, useMemo, useRef, useState, } from "react"; import { useLocation, useNavigate } from "react-router"; import { type ActiveEditSession, type ScriptItem, type ScriptType, type StableVersion, type Visibility, type WorkspaceDirectory, } from "../../services/api"; import { useApi, useAuth } from "~/context/AuthContext"; import Icon from "../../components/Icon"; import SchedulePage from "../schedules/SchedulePage"; import { DashboardPage, SystemAdminPage } from "../admin/AdminPages"; import "../../styles/platform.css"; import { WorkspaceTreeGroup, scriptIcon } from "./WorkspaceTree"; import { ScriptWorkspace } from "./ScriptWorkspace"; type NewScriptForm = { name: string; scriptType: ScriptType; visibility: Visibility; parentPath: string; }; type ContextMenuState = { x: number; y: number; kind: "root" | "directory" | "file"; path: string; script?: ScriptItem; }; type ToastState = { tone: "success" | "error" | "info"; message: string; }; const navigation = [ { label: "工作台", icon: "home" as const, page: "home" as const }, { label: "构建脚本", icon: "script" as const, page: "scripts" as const }, { label: "调度配置", icon: "schedule" as const, page: "schedules" as const }, { label: "系统管理", icon: "settings" as const, page: "system" as const }, ]; type ActivePage = "home" | "scripts" | "schedules" | "system"; function pageFromPath(pathname: string): ActivePage { const page = pathname.replace(/^\/+|\/+$/g, ""); return ["scripts", "schedules", "system"].includes(page) ? page as ActivePage : "home"; } function pathForPage(page: ActivePage): string { return page === "home" ? "/workbench" : `/${page}`; } const initialForm: NewScriptForm = { name: "", scriptType: "notebook", visibility: "workspace", parentPath: "", }; function ownedScriptPath(item: ScriptItem) { return item.relative_path.replaceAll("\\", "/").split("/").slice(2).join("/"); } function parentOf(path: string) { const parts = path.split("/"); parts.pop(); return parts.join("/"); } function inferredDirectories(items: ScriptItem[]): WorkspaceDirectory[] { const result = new Map(); for (const item of items) { const parts = parentOf(ownedScriptPath(item)).split("/").filter(Boolean); let parentPath = ""; for (const name of parts) { const path = parentPath ? `${parentPath}/${name}` : name; result.set(path, { path, name, parent_path: parentPath }); parentPath = path; } } return [...result.values()]; } function mergeDirectories( left: WorkspaceDirectory[], right: WorkspaceDirectory[], ): WorkspaceDirectory[] { return [...new Map( [...left, ...right].map((item) => [item.path, item]), ).values()]; } export default function ModelPlatformApp() { const { currentWorkspace } = useAuth(); if (!currentWorkspace) { return (
加载中…
); } return ; } function AuthenticatedModelPlatformApp() { const location = useLocation(); const navigate = useNavigate(); const activePage = pageFromPath(location.pathname); const auth = useAuth(); const { user, workspaces, setCurrentWorkspace, logout } = auth; const currentWorkspace = auth.currentWorkspace!; const api = useApi(); const [scripts, setScripts] = useState([]); const [directories, setDirectories] = useState([]); const [selectedId, setSelectedId] = useState(null); const [keyword, setKeyword] = useState(""); const [loading, setLoading] = useState(true); const [refreshing, setRefreshing] = useState(false); const [apiOnline, setApiOnline] = useState(false); const [createOpen, setCreateOpen] = useState(false); const [creating, setCreating] = useState(false); const [form, setForm] = useState(initialForm); const [folderDialog, setFolderDialog] = useState<{ open: boolean; parentPath: string; name: string; busy: boolean; }>({ open: false, parentPath: "", name: "", busy: false }); const [contextMenu, setContextMenu] = useState(null); const [workspaceMenuOpen, setWorkspaceMenuOpen] = useState(false); const [uploadParentPath, setUploadParentPath] = useState(""); const [uploading, setUploading] = useState(false); const uploadInputRef = useRef(null); const [toast, setToast] = useState(null); const [editSession, setEditSession] = useState(null); const editSessionRef = useRef(null); const selectedIdRef = useRef(null); const editorOpenRequestRef = useRef(0); const editorOpeningRef = useRef(false); const [embeddedJupyterUrl, setEmbeddedJupyterUrl] = useState(null); const [editBusy, setEditBusy] = useState(false); const [editorOpenError, setEditorOpenError] = useState<{ scriptId: string; message: string; } | null>(null); const [versions, setVersions] = useState([]); const [versionsLoading, setVersionsLoading] = useState(false); const [publishTarget, setPublishTarget] = useState(null); const [releaseNote, setReleaseNote] = useState(""); const [publishVisibility, setPublishVisibility] = useState("workspace"); const [publishing, setPublishing] = useState(false); const [publishedVersion, setPublishedVersion] = useState(null); const load = async (silent = false) => { if (!silent) setLoading(true); setRefreshing(silent); try { const [items, folderItems] = await Promise.all([ api.listScripts(), api.listWorkspaceDirectories(), ]); setScripts(items); setDirectories(folderItems); setApiOnline(true); setSelectedId((current) => { if (current && items.some((item) => item.script_id === current)) { selectedIdRef.current = current; return current; } const nextSelectedId = ( items.find((item) => item.script_type === "notebook")?.script_id ?? items[0]?.script_id ?? null ); selectedIdRef.current = nextSelectedId; return nextSelectedId; }); } catch (error) { setApiOnline(false); setToast({ tone: "error", message: error instanceof Error ? error.message : "脚本列表加载失败", }); } finally { setLoading(false); setRefreshing(false); } }; useEffect(() => { void load(); }, []); useEffect(() => { if (!toast) return; const timer = window.setTimeout(() => setToast(null), 3200); return () => window.clearTimeout(timer); }, [toast]); useEffect(() => { if (!contextMenu) return; const close = () => setContextMenu(null); const closeOnEscape = (event: KeyboardEvent) => { if (event.key === "Escape") close(); }; window.addEventListener("pointerdown", close); window.addEventListener("blur", close); window.addEventListener("resize", close); window.addEventListener("scroll", close, true); window.addEventListener("keydown", closeOnEscape); return () => { window.removeEventListener("pointerdown", close); window.removeEventListener("blur", close); window.removeEventListener("resize", close); window.removeEventListener("scroll", close, true); window.removeEventListener("keydown", closeOnEscape); }; }, [contextMenu]); useEffect(() => { editSessionRef.current = editSession; }, [editSession]); useEffect(() => { if (!selectedId) { setVersions([]); return; } let ignore = false; setVersionsLoading(true); void api.listScriptVersions(selectedId) .then((items) => { if (!ignore) setVersions(items); }) .catch((error) => { if (!ignore) { setToast({ tone: "error", message: error instanceof Error ? error.message : "版本列表加载失败", }); } }) .finally(() => { if (!ignore) setVersionsLoading(false); }); return () => { ignore = true; }; }, [selectedId]); useEffect(() => { if (!editSession) return; const intervalSeconds = Math.max( 5, editSession.heartbeat_interval_seconds || 15, ); let heartbeatRunning = false; const timer = window.setInterval(() => { if (heartbeatRunning) return; const current = editSessionRef.current; if (!current || current.edit_session_id !== editSession.edit_session_id) { return; } heartbeatRunning = true; void api.heartbeatFileLock(current) .then((updated) => { setEditSession((active) => active && active.edit_session_id === updated.edit_session_id ? { ...active, session_status: updated.session_status, expires_at: updated.expires_at, } : active); }) .catch((error) => { setEditSession(null); setEmbeddedJupyterUrl(null); setToast({ tone: "error", message: `编辑锁心跳已中断:${ error instanceof Error ? error.message : "请重新打开文件" }`, }); }) .finally(() => { heartbeatRunning = false; }); }, intervalSeconds * 1000); return () => window.clearInterval(timer); }, [editSession?.edit_session_id, editSession?.heartbeat_interval_seconds]); useEffect(() => { if (!editSession?.ticket_expires_at) return; const expiresAt = new Date(editSession.ticket_expires_at).getTime(); const renewAfter = Math.max(15_000, expiresAt - Date.now() - 60_000); const timer = window.setTimeout(() => { const current = editSessionRef.current; if (!current || current.edit_session_id !== editSession.edit_session_id) { return; } void api.createJupyterAccessTicket(current) .then((ticket) => { setEditSession((active) => active && active.edit_session_id === ticket.edit_session_id ? { ...active, ticket_expires_at: ticket.expires_at } : active); }) .catch((error) => { setToast({ tone: "error", message: `Jupyter 访问票据续签失败:${ error instanceof Error ? error.message : "请重新打开文件" }`, }); }); }, renewAfter); return () => window.clearTimeout(timer); }, [editSession?.edit_session_id, editSession?.ticket_expires_at]); useEffect(() => { if (!editSession) return; const handleUnload = () => { const current = editSessionRef.current; if (current) api.releaseFileLockOnUnload(current); }; window.addEventListener("beforeunload", handleUnload); return () => window.removeEventListener("beforeunload", handleUnload); }, [editSession?.edit_session_id]); const filteredScripts = useMemo(() => { const normalized = keyword.trim().toLocaleLowerCase(); if (!normalized) return scripts; return scripts.filter((item) => item.script_name.toLocaleLowerCase().includes(normalized), ); }, [keyword, scripts]); const memberScriptGroups = (() => { const currentUserScripts = filteredScripts.filter( (item) => item.owner_user_id === user?.user_id, ); const inferred = inferredDirectories(currentUserScripts); return [{ user: user, scripts: currentUserScripts, directories: mergeDirectories(directories, inferred), }]; })(); const selected = scripts.find((item) => item.script_id === selectedId) ?? null; const selectScript = (scriptId: string | null) => { if (selectedIdRef.current !== scriptId) { editorOpenRequestRef.current += 1; setEditorOpenError(null); } selectedIdRef.current = scriptId; setSelectedId(scriptId); }; const openScriptEditor = async ( script: ScriptItem, showToast = true, ) => { if (editorOpeningRef.current) return; editorOpeningRef.current = true; const requestId = editorOpenRequestRef.current + 1; editorOpenRequestRef.current = requestId; const requestIsCurrent = () => editorOpenRequestRef.current === requestId && selectedIdRef.current === script.script_id; const clearSessionIfActive = (session: ActiveEditSession) => { if ( editSessionRef.current?.edit_session_id === session.edit_session_id ) { setEmbeddedJupyterUrl(null); setEditSession(null); editSessionRef.current = null; } }; setEditBusy(true); setEditorOpenError((current) => current?.scriptId === script.script_id ? null : current); let active = editSessionRef.current; let newlyAcquired = false; try { if (active && active.script_id !== script.script_id) { await api.releaseFileLock(active); setEmbeddedJupyterUrl(null); setEditSession(null); editSessionRef.current = null; active = null; } if (!requestIsCurrent()) return; if (!active) { active = await api.acquireFileLock(script); newlyAcquired = true; } if (!requestIsCurrent()) { if (active) { await api.releaseFileLock(active); clearSessionIfActive(active); } return; } const ticket = await api.createJupyterAccessTicket(active); if (!requestIsCurrent()) { await api.releaseFileLock(active); clearSessionIfActive(active); return; } const readySession = { ...active, ticket_expires_at: ticket.expires_at, }; setEditSession(readySession); editSessionRef.current = readySession; setEmbeddedJupyterUrl(ticket.jupyter_url); if (showToast) { setToast({ tone: "success", message: `${script.script_name} 已打开,Jupyter 已嵌入当前工作区`, }); } } catch (error) { if (newlyAcquired && active) { try { await api.releaseFileLock(active); } catch { // The database lease is the final safety net if compensation cannot reach Runtime. } setEditSession(null); editSessionRef.current = null; } setEmbeddedJupyterUrl(null); if (requestIsCurrent()) { const message = error instanceof Error ? error.message : "打开编辑器失败"; setEditorOpenError({ scriptId: script.script_id, message }); if (showToast) { setToast({ tone: "error", message }); } } } finally { editorOpeningRef.current = false; setEditBusy(false); } }; useEffect(() => { if ( activePage !== "scripts" || !selected || selected.script_type !== "notebook" || editBusy || editorOpenError?.scriptId === selected.script_id || ( editSession?.script_id === selected.script_id && embeddedJupyterUrl ) ) { return; } void openScriptEditor(selected, false); }, [ activePage, editBusy, editSession?.script_id, editorOpenError?.scriptId, embeddedJupyterUrl, selected?.script_id, selected?.script_type, ]); const endEditing = async (closeTab = false, showToast = true) => { editorOpenRequestRef.current += 1; setEditorOpenError(null); const active = editSessionRef.current; if (!active) { if (closeTab) selectScript(null); return; } setEditBusy(true); try { await api.releaseFileLock(active); setEmbeddedJupyterUrl(null); setEditSession(null); editSessionRef.current = null; if (closeTab) selectScript(null); if (showToast) { setToast({ tone: "success", message: `${active.script_name} 的编辑锁已释放`, }); } } catch (error) { setToast({ tone: "error", message: error instanceof Error ? error.message : "释放编辑锁失败", }); } finally { setEditBusy(false); } }; useEffect(() => { const active = editSessionRef.current; if ( !active || !selected || selected.script_type === "notebook" || selected.script_id === active.script_id || editBusy ) { return; } void endEditing(false, false); }, [editBusy, selected?.script_id, selected?.script_type]); useEffect(() => { if ( activePage === "scripts" || editBusy || (!editSessionRef.current && !editorOpeningRef.current) ) { return; } void endEditing(true, false); }, [activePage, editBusy]); const openPublishDialog = (script: ScriptItem) => { setPublishTarget(script); setReleaseNote(""); setPublishVisibility( script.visibility === "private" ? "private" : "workspace", ); }; const submitPublish = async (event: FormEvent) => { event.preventDefault(); if (!publishTarget) return; setPublishing(true); try { const version = await api.publishScriptVersion({ script: publishTarget, releaseNote, visibility: publishVisibility, }); setVersions((items) => [ version, ...items.filter((item) => item.versions_id !== version.versions_id), ]); setPublishTarget(null); setPublishedVersion(version); setToast({ tone: "success", message: `${version.version_label} 稳定版本发布成功`, }); } catch (error) { setToast({ tone: "error", message: error instanceof Error ? error.message : "稳定版本发布失败", }); } finally { setPublishing(false); } }; const submitCreate = async (event: FormEvent) => { event.preventDefault(); if (!form.name.trim()) return; setCreating(true); try { const created = await api.createScript(form); setScripts((items) => [created, ...items]); selectScript(created.script_id); setCreateOpen(false); setForm(initialForm); setToast({ tone: "success", message: `${created.script_name} 已创建`, }); } catch (error) { setToast({ tone: "error", message: error instanceof Error ? error.message : "创建失败", }); } finally { setCreating(false); } }; const openCreateDialog = ( parentPath = "", scriptType: ScriptType = "notebook", ) => { setContextMenu(null); setForm({ ...initialForm, parentPath, scriptType, }); setCreateOpen(true); }; const openFolderDialog = (parentPath = "") => { setContextMenu(null); setFolderDialog({ open: true, parentPath, name: "", busy: false, }); }; const chooseUpload = (parentPath = "") => { setContextMenu(null); setUploadParentPath(parentPath); uploadInputRef.current?.click(); }; const handleUpload = async (event: ChangeEvent) => { const files = Array.from(event.target.files ?? []); event.target.value = ""; if (files.length === 0) return; setUploading(true); let lastCreated: ScriptItem | null = null; try { for (const file of files) { lastCreated = await api.uploadScript(file, uploadParentPath); } await load(true); if (lastCreated) selectScript(lastCreated.script_id); setToast({ tone: "success", message: `${files.length} 个文件已上传到${ uploadParentPath ? ` ${uploadParentPath}` : "当前目录" }`, }); } catch (error) { await load(true); setToast({ tone: "error", message: error instanceof Error ? error.message : "文件上传失败", }); } finally { setUploading(false); } }; const submitFolder = async (event: FormEvent) => { event.preventDefault(); if (!folderDialog.name.trim()) return; setFolderDialog((current) => ({ ...current, busy: true })); try { await api.createWorkspaceDirectory( folderDialog.name.trim(), folderDialog.parentPath, ); await load(true); setFolderDialog({ open: false, parentPath: "", name: "", busy: false, }); setToast({ tone: "success", message: `${folderDialog.name.trim()} 文件夹已创建`, }); } catch (error) { setFolderDialog((current) => ({ ...current, busy: false })); setToast({ tone: "error", message: error instanceof Error ? error.message : "文件夹创建失败", }); } }; const removeScript = async (script: ScriptItem) => { setContextMenu(null); if (!window.confirm(`确定删除文件“${script.script_name}”吗?稳定版本会保留。`)) { return; } if (editSessionRef.current?.script_id === script.script_id) { await endEditing(false, false); if (editSessionRef.current?.script_id === script.script_id) return; } try { await api.deleteScript(script.script_id); if (selectedIdRef.current === script.script_id) selectScript(null); await load(true); setToast({ tone: "success", message: `${script.script_name} 已删除`, }); } catch (error) { setToast({ tone: "error", message: error instanceof Error ? error.message : "文件删除失败", }); } }; const removeDirectory = async (path: string) => { setContextMenu(null); if (!window.confirm(`确定递归删除文件夹“${path}”及其内容吗?稳定版本会保留。`)) { return; } const activeScript = scripts.find( (item) => item.script_id === editSessionRef.current?.script_id, ); if ( activeScript && ( ownedScriptPath(activeScript) === path || ownedScriptPath(activeScript).startsWith(`${path}/`) ) ) { await endEditing(false, false); if (editSessionRef.current?.script_id === activeScript.script_id) return; } try { const result = await api.deleteWorkspaceDirectory(path); const selectedScript = scripts.find( (item) => item.script_id === selectedIdRef.current, ); if ( selectedScript && ownedScriptPath(selectedScript).startsWith(`${path}/`) ) { selectScript(null); } await load(true); setToast({ tone: "success", message: `${path} 已删除(含 ${result.deleted_scripts} 个脚本)`, }); } catch (error) { setToast({ tone: "error", message: error instanceof Error ? error.message : "文件夹删除失败", }); } }; const showContextMenu = ( event: ReactMouseEvent, target: Omit, ) => { event.preventDefault(); event.stopPropagation(); const width = 188; const height = target.kind === "file" ? 92 : 190; setContextMenu({ ...target, x: Math.min(event.clientX, window.innerWidth - width - 8), y: Math.min(event.clientY, window.innerHeight - height - 8), }); }; return (
开发工作区

{{ home: "工作台", scripts: "构建脚本", schedules: "调度配置", system: "系统管理", }[activePage]}

{apiOnline ? "服务已连接" : "服务未连接"}
{workspaceMenuOpen && (
{workspaces.map((workspace) => ( ))}
)}
{activePage === "scripts" ? (
{selected ? ( void openScriptEditor(selected)} onEndEditing={() => void endEditing()} onClose={() => { if ( editSessionRef.current?.script_id === selected.script_id ) { void endEditing(true); } else { selectScript(null); } }} onPublish={() => openPublishDialog(selected)} onInfo={setToast} /> ) : (
构建脚本工作台

创建你的第一个模型脚本

通过 Notebook 完成数据探索,或使用 Python 脚本构建可调度的处理任务。

)}
) : activePage === "schedules" ? ( ) : activePage === "system" ? ( ) : ( { navigate(pathForPage(page)); }} /> )}
{createOpen && (
工作副本

新建构建脚本

保存到:{form.parentPath || "个人根目录"}
脚本类型
)} {folderDialog.open && (
WORKSPACE

新建文件夹

创建到:{folderDialog.parentPath || "个人根目录"}
)} {contextMenu && (
event.stopPropagation()} > {contextMenu.kind === "file" && contextMenu.script ? ( <> ) : ( <> {contextMenu.kind === "directory" && ( <> )} )}
)} {publishTarget && (
不可变制品

发布稳定版本

{publishTarget.script_name} 当前 Workspace 工作副本