diff --git a/frontend/app/features/platform/ModelPlatformApp copy.tsx b/frontend/app/features/platform/ModelPlatformApp copy.tsx deleted file mode 100644 index ce6500a..0000000 --- a/frontend/app/features/platform/ModelPlatformApp copy.tsx +++ /dev/null @@ -1,1513 +0,0 @@ -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 LatestVersion, - type ScriptItem, - type ScriptType, - type StableVersion, - type Visibility, - type WorkspaceDirectory, -} from "../../services/api"; -import { useApi, useAuth } from "../../context/AuthContext"; -import Icon from "../../components/common/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 [openTabIds, setOpenTabIds] = useState([]); - 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 [latestVersion, setLatestVersion] = useState( - null, - ); - const [latestVersionLoading, setLatestVersionLoading] = 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 [sidebarCollapsed, setSidebarCollapsed] = useState(false); - - 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) { - setLatestVersion(null); - return; - } - let ignore = false; - setLatestVersionLoading(true); - void api.getLatestScriptVersion(selectedId) - .then((item) => { - if (!ignore) setLatestVersion(item); - }) - .catch((error) => { - if (!ignore) { - setToast({ - tone: "error", - message: - error instanceof Error - ? error.message - : "最新版本加载失败", - }); - } - }) - .finally(() => { - if (!ignore) setLatestVersionLoading(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) => { - setSelectedId(scriptId); - }; - - const openTab = (scriptId: string) => { - if (selectedIdRef.current !== scriptId) { - editorOpenRequestRef.current += 1; - setEditorOpenError(null); - } - selectedIdRef.current = scriptId; - setSelectedId(scriptId); - setOpenTabIds((current) => { - if (current.includes(scriptId)) { - return current; - } - return [...current, scriptId]; - }); - }; - - const closeTab = async (scriptId: string, event?: ReactMouseEvent) => { - event?.stopPropagation(); - - // 如果关闭的是正在编辑的脚本,先释放编辑锁 - if (editSessionRef.current?.script_id === scriptId) { - await endEditing(false, false); - } - - setOpenTabIds((current) => { - const index = current.indexOf(scriptId); - if (index === -1) return current; - const newTabs = current.filter((id) => id !== scriptId); - if (selectedId === scriptId) { - const nextId = newTabs[index] ?? newTabs[index - 1] ?? null; - setSelectedId(nextId); - selectedIdRef.current = nextId; - } - return newTabs; - }); - }; - - const switchTab = (scriptId: string) => { - if (selectedIdRef.current !== scriptId) { - editorOpenRequestRef.current += 1; - setEditorOpenError(null); - } - setSelectedId(scriptId); - selectedIdRef.current = 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 (closeTabFlag = true, showToast = true) => { - editorOpenRequestRef.current += 1; - setEditorOpenError(null); - const active = editSessionRef.current; - const scriptId = active?.script_id; - if (!active) { - if (closeTabFlag && scriptId) { - void closeTab(scriptId); - } - return; - } - setEditBusy(true); - try { - await api.releaseFileLock(active); - setEmbeddedJupyterUrl(null); - setEditSession(null); - editSessionRef.current = null; - if (closeTabFlag && scriptId) { - void closeTab(scriptId); - } - 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, - }); - 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; - const suffix = form.scriptType === "notebook" ? ".ipynb" : ".py"; - const requestedName = form.name.trim(); - const normalizedName = requestedName.toLocaleLowerCase().endsWith(suffix) - ? requestedName - : `${requestedName}${suffix}`; - const duplicate = scripts.some((script) => - script.script_type === form.scriptType - && script.script_name.toLocaleLowerCase() === normalizedName.toLocaleLowerCase() - ); - if (duplicate) { - setToast({ tone: "error", message: `${normalizedName} 已存在,请更换名称` }); - return; - } - setCreating(true); - try { - const created = await api.createScript(form); - setScripts((items) => [created, ...items]); - openTab(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 ? ( - { - const s = scripts.find((item) => item.script_id === id); - return { - scriptId: id, - scriptName: s?.script_name ?? "未知", - scriptType: s?.script_type ?? "notebook", - }; - })} - onOpenEditor={() => void openScriptEditor(selected)} - onEndEditing={() => void endEditing()} - onClose={(scriptId, event) => void closeTab(scriptId, event)} - onSwitchTab={switchTab} - onNewTab={() => openCreateDialog("")} - 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 工作副本 - -
-