import { ChevronRight, Download, Eye, FileText, Folder, MoonStar, PackageOpen, Pencil, RefreshCw, RotateCcw, Save, Search, Settings2, ShieldCheck, Sparkles, Square, Terminal, Upload, UserRoundMinus, UserRoundPlus, WandSparkles, X } from "lucide-react"; import { type ChangeEvent, type FormEvent, useCallback, useEffect, useMemo, useRef, useState } from "react"; import { platformApiClient } from "../api/client"; import type { ConfigDiffLineResponse, DeclaredFileReadSnapshotResponse, GamePluginResponse, JobResponse, ServerInstanceResponse, ServerMemberResponse, ServerDeploymentResponse, ServerConfigDiffPreviewResponse, RunEndpointResponse, ServerFileEntryResponse, ServerFileListResponse, ServerFileWorkspaceResponse } from "../api/types"; import { ConfirmDialog } from "../components/OperationControls"; import { ServerManagementTerminalDrawer } from "../components/ServerManagementTerminalDrawer"; import { ServerConfigEditor } from "../components/ServerConfigEditor"; import { ServerGameUpdatePanel } from "../components/ServerGameUpdatePanel"; import { EmptyState, ErrorState, LoadingState, ResultBadge } from "../components/StateViews"; import type { PageComponentProps } from "../contracts/page"; import { canRestartServer, canStartServer, canStopServer, runtimeObservationFreshness, serverMetadataFormFromInstance, type ServerMetadataFormState } from "../contracts/serverManagement"; import { serverDetailSections, type ConfigDiffView, type LlmSuggestionView, type ServerDetailSection } from "../contracts/workspace"; import { serverLifecycleCommandRequest, serverMetadataUpdateRequestFromForm } from "../schemas/serverManagement"; import { cx } from "../utils/classes"; import { downloadServerFileResult } from "../utils/serverFileTransfer"; import { stateLabel, statusClass } from "./ServersPage"; import { PluginPageHostPage } from "./PluginPageHostPage"; type LoadState = { status: "loading" } | { status: "error"; reason: string } | { status: "ready"; data: T }; const serverDetailRefreshMs = 15000; export function ServerDetailPage(props: PageComponentProps) { const { session, params, operations, onNavigate } = props; const serverId = params.serverId ?? ""; const [section, setSection] = useState("manage"); const [instance, setInstance] = useState>({ status: "loading" }); const [plugins, setPlugins] = useState([]); const [runEndpoint, setRunEndpoint] = useState(); const [deployment, setDeployment] = useState>({ status: "loading" }); const [confirm, setConfirm] = useState Promise }>(null); const [confirmBusy, setConfirmBusy] = useState(false); const [terminalOpen, setTerminalOpen] = useState(false); const [terminalJobId, setTerminalJobId] = useState(null); const [configEditorOpen, setConfigEditorOpen] = useState(false); const defaultSectionResolvedRef = useRef(false); const refresh = useCallback(async () => { if (!serverId) { setInstance({ status: "error", reason: "缺少服务器 ID" }); return; } setInstance({ status: "loading" }); try { const [detail, pluginResponse, deploymentResponse, endpointResponse] = await Promise.all([ platformApiClient.getServerInstance(serverId), platformApiClient.listGamePlugins(), platformApiClient .getServerDeployment(serverId) .then((data): LoadState => ({ status: "ready", data })) .catch((error): LoadState => ({ status: "error", reason: error instanceof Error ? error.message : "部署定义加载失败" })), platformApiClient.listRunEndpoints({ status: "online" }).catch(() => ({ items: [], count: 0 })) ]); setInstance({ status: "ready", data: detail }); setPlugins(pluginResponse.items); setDeployment(deploymentResponse); setRunEndpoint(endpointResponse.items.find((endpoint) => endpoint.id === detail.runEndpointId)); } catch (error) { setInstance({ status: "error", reason: error instanceof Error ? error.message : "加载失败" }); setRunEndpoint(undefined); setDeployment({ status: "error", reason: "部署定义加载失败" }); } }, [serverId]); useEffect(() => { void refresh(); }, [refresh]); const refreshOperationalState = useCallback(async () => { if (!serverId) return; try { const [detail, endpointResponse] = await Promise.all([ platformApiClient.getServerInstance(serverId), platformApiClient.listRunEndpoints({ status: "online" }) ]); setInstance({ status: "ready", data: detail }); setRunEndpoint(endpointResponse.items.find((endpoint) => endpoint.id === detail.runEndpointId)); } catch { setRunEndpoint(undefined); } }, [serverId]); useEffect(() => { const timer = window.setInterval(() => void refreshOperationalState(), serverDetailRefreshMs); return () => window.clearInterval(timer); }, [refreshOperationalState]); const canManageServers = session.capabilities.includes("servers.manage"); const readyPlugin = instance.status === "ready" ? plugins.find((plugin) => plugin.id === instance.data.pluginId) : undefined; const detailFreshness = instance.status === "ready" ? runtimeObservationFreshness(instance.data, runEndpoint) : "unverified"; const detailStateText = instance.status === "ready" && detailFreshness === "fresh" ? stateLabel(instance.data.state) : instance.status === "ready" ? `最后观测:${stateLabel(instance.data.state)}(Run 未验证)` : "未验证"; const detailSections = useMemo(() => serverDetailSectionEntries(readyPlugin), [readyPlugin]); const defaultPluginPage = readyPlugin?.pages.find((page) => page.key === "players") ?? readyPlugin?.pages[0]; useEffect(() => { if (!params.routeKey || !readyPlugin?.pages.some((page) => page.key === params.routeKey)) return; setSection(`plugin:${params.routeKey}`); }, [params.routeKey, readyPlugin]); useEffect(() => { if (detailSections.some((entry) => entry.id === section)) return; setSection(detailSections[0]?.id ?? "manage"); }, [detailSections, section]); useEffect(() => { if (params.routeKey || !defaultPluginPage || defaultSectionResolvedRef.current) return; defaultSectionResolvedRef.current = true; if (section !== "manage") return; setSection(`plugin:${defaultPluginPage.key}`); }, [defaultPluginPage, params.routeKey, section]); function requestLifecycle(current: ServerInstanceResponse, action: "start" | "stop" | "restart") { const intent = action === "start" ? "启动服务器" : action === "stop" ? "停止服务器" : "重启服务器"; setConfirm({ title: action === "start" ? "启动服务器" : action === "stop" ? "停止服务器" : "重启服务器", description: action === "start" ? `确认启动服务器 ${current.name}(${current.id})?` : action === "stop" ? `停止服务器 ${current.name}(${current.id})会断开所有在线玩家,确认继续?` : `重启服务器 ${current.name}(${current.id})会先按插件声明优雅关闭 SCUM 服务,停止成功后自动重新启动;在线玩家会被断开。`, danger: action === "stop", run: async () => { const operationId = operations.begin({ intent, targetKind: "server", targetId: current.id, requester: session.displayName }); try { const request = serverLifecycleCommandRequest(current, action); const result = action === "start" ? await platformApiClient.startServerInstance(current.id, request) : action === "stop" ? await platformApiClient.stopServerInstance(current.id, request) : await platformApiClient.restartServerInstance(current.id, request); operations.succeed(operationId, `任务 ${result.job.id}(${result.job.capability})已派发`, result.job); await refresh(); } catch (error) { operations.fail(operationId, error instanceof Error ? error.message : "操作失败", operationId); } } }); } if (!serverId) { return ( onNavigate("servers")} /> ); } return (
{instance.status !== "ready" && (

服务器详情

)} {instance.status === "loading" && } {instance.status === "error" && ( void refresh()} /> )} {instance.status !== "ready" && ( )} {instance.status === "ready" && ( <>

{instance.data.name}

{detailStateText}
{pluginPageKeyFromSection(section) && readyPlugin && } {section === "manage" && } {section === "manage" && ( setInstance({ status: "ready", data: next })} /> )} {section === "manage" && setInstance({ status: "ready", data: next })} />} {section === "manage" && ( void refreshOperationalState()} /> )} {section === "files" && } {section === "llm" && { setTerminalJobId(jobId ?? null); setTerminalOpen(true); }} />} { setTerminalOpen(false); setTerminalJobId(null); }} /> {configEditorOpen && setConfigEditorOpen(false)} />} )} setConfirm(null)} onConfirm={() => { if (!confirm) { return; } setConfirmBusy(true); void confirm.run().finally(() => { setConfirmBusy(false); setConfirm(null); }); }} />
); } function serverDetailSectionEntries(plugin?: GamePluginResponse): Array<{ id: ServerDetailSection; label: string }> { const pluginPages = (plugin?.pages ?? []).map((page) => ({ id: `plugin:${page.key}` as ServerDetailSection, label: page.title })); return [...pluginPages, ...serverDetailSections]; } function ServerDetailSectionNav({ entries, section, onChange }: { entries: Array<{ id: ServerDetailSection; label: string }>; section: ServerDetailSection; onChange: (section: ServerDetailSection) => void }) { const pluginEntries = entries.filter((entry) => entry.id.startsWith("plugin:")); const platformEntries = entries.filter((entry) => !entry.id.startsWith("plugin:")); return ( ); } function pluginPageKeyFromSection(section: ServerDetailSection): string | null { return section.startsWith("plugin:") ? section.slice("plugin:".length) : null; } interface PluginPageSectionProps { pageProps: PageComponentProps; serverId: string; plugin: GamePluginResponse; routeKey: string; } function PluginPageSection({ pageProps, serverId, plugin, routeKey }: PluginPageSectionProps) { const params = useMemo(() => ({ ...pageProps.params, pluginId: plugin.id, routeKey, serverId }), [pageProps.params.pluginId, pageProps.params.routeKey, pageProps.params.serverId, plugin.id, routeKey, serverId]); return ; } interface ServerMetadataSectionProps { instance: ServerInstanceResponse; session: PageComponentProps["session"]; operations: PageComponentProps["operations"]; onChanged: (instance: ServerInstanceResponse) => void; } function ServerMetadataSection({ instance, session, operations, onChanged }: ServerMetadataSectionProps) { const [draft, setDraft] = useState(() => serverMetadataFormFromInstance(instance)); const [result, setResult] = useState<{ status: "succeeded" | "failed" | "pending"; label: string } | null>(null); useEffect(() => { setDraft(serverMetadataFormFromInstance(instance)); }, [instance.id, instance.name]); async function saveMetadata(event: FormEvent) { event.preventDefault(); const operationId = operations.begin({ intent: "更新服务器信息", targetKind: "server", targetId: instance.id, requester: session.displayName }); setResult({ status: "pending", label: "正在保存服务器信息" }); try { const updated = await platformApiClient.updateServerInstance(instance.id, serverMetadataUpdateRequestFromForm(draft)); onChanged(updated); operations.succeed(operationId, `服务器信息已更新:${updated.id}`); setResult({ status: "succeeded", label: `已更新 ${updated.name}` }); } catch (error) { operations.fail(operationId, error instanceof Error ? error.message : "服务器信息更新失败"); setResult({ status: "failed", label: error instanceof Error ? error.message : "服务器信息更新失败" }); } } return (

基本信息

{result && }
void saveMetadata(event)}>
); } interface ServerDeploymentSectionProps { instance: ServerInstanceResponse; deployment: LoadState; } function ServerDeploymentSection({ instance, deployment }: ServerDeploymentSectionProps) { if (deployment.status === "loading") return ; if (deployment.status === "error") return ; const view = deployment.data; const projection = view.projection; const hasProjection = Boolean(projection?.templateKey || projection?.templateVersion || projection?.preflightState || projection?.discoveryState || projection?.mappingState || projection?.verificationState || projection?.failureCode); return

部署定义

{view.mode || "未配置"} · 修订 {view.revision}

服务器目录是主目录;执行目录只用于高级自定义启动,留空时继承服务器目录。路径和命令均为受保护输入,平台普通响应不会回显。

服务器目录{view.serverRootConfigured ? "已配置" : "未配置"}
高级执行目录{view.workingDirectoryConfigured ? "已配置" : "使用服务器目录"}
启动设置{view.startCommandConfigured ? "已配置" : view.mode === "custom-command" ? "未配置" : "插件引导"}
{view.latestDispatch &&
最近 Run 调度{view.latestDispatch.deploymentDefinitionIncluded ? `部署定义已随任务发送 · r${view.latestDispatch.deploymentRevision} · ${view.latestDispatch.jobState}` : "未携带部署定义"}
}{view.latestDispatch?.runConfirmed &&
Run 执行确认已按 r{view.latestDispatch.deploymentRevision} 确认执行
}
{hasProjection &&
部署验证模板{projection?.templateVersion ? `${projection.templateKey ?? "已选择"} · v${projection.templateVersion}` : projection?.templateKey ?? "等待 Run 预检"}
预检 / 扫描{deploymentProjectionLabel(projection?.preflightState)} / {deploymentProjectionLabel(projection?.discoveryState)}
配置映射 / 健康验证{deploymentProjectionLabel(projection?.mappingState)} / {deploymentProjectionLabel(projection?.verificationState)}
{projection?.failureCode &&
失败原因{projection.failureCode}
}
}
; } function deploymentProjectionLabel(value?: string): string { switch (value) { case "queued": return "排队中"; case "running": return "执行中"; case "passed": return "已通过"; case "applied": return "已写入"; case "unchanged": return "未变化"; case "failed": return "失败"; case "skipped": return "已跳过"; default: return "待返回"; } } function deploymentProgressLabel(progress: JobResponse["progress"]): string { switch (progress.phase) { case "queued": return "任务已排队,等待 Run 领取"; case "claimed": return "Run 已领取任务"; case "preflight": return "正在执行本机预检"; case "install": return "正在安装服务器"; case "configure": return "正在写入游戏配置"; case "start": return "正在启动服务器"; case "health": return "正在进行健康检查"; default: return "部署任务已提交"; } } interface ServerAdministratorsSectionProps { instance: ServerInstanceResponse; session: PageComponentProps["session"]; onChanged: (instance: ServerInstanceResponse) => void; } function ServerAdministratorsSection({ instance, session, onChanged }: ServerAdministratorsSectionProps) { const [candidates, setCandidates] = useState>({ status: "loading" }); const [selectedUserId, setSelectedUserId] = useState(""); const [busyUserId, setBusyUserId] = useState(null); const [result, setResult] = useState<{ status: "succeeded" | "failed"; label: string } | null>(null); const isOwner = instance.ownerUserId === session.id; const refreshCandidates = useCallback(async () => { if (!isOwner) { setCandidates({ status: "ready", data: [] }); return; } setCandidates({ status: "loading" }); try { const response = await platformApiClient.listServerAdministratorCandidates(instance.id); setCandidates({ status: "ready", data: response.items }); setSelectedUserId((current) => (response.items.some((user) => user.id === current) ? current : response.items[0]?.id ?? "")); } catch (error) { setCandidates({ status: "error", reason: error instanceof Error ? error.message : "加载候选管理员失败" }); } }, [instance.id, isOwner]); useEffect(() => { void refreshCandidates(); }, [refreshCandidates]); async function addAdministrator(event: FormEvent) { event.preventDefault(); if (!selectedUserId) { return; } setBusyUserId(selectedUserId); setResult(null); try { const next = await platformApiClient.addServerAdministrator(instance.id, { userId: selectedUserId }); onChanged(next); setResult({ status: "succeeded", label: "已添加服务器管理员" }); await refreshCandidates(); } catch (error) { setResult({ status: "failed", label: error instanceof Error ? error.message : "添加管理员失败" }); } finally { setBusyUserId(null); } } async function removeAdministrator(userId: string) { setBusyUserId(userId); setResult(null); try { const next = await platformApiClient.removeServerAdministrator(instance.id, userId); onChanged(next); setResult({ status: "succeeded", label: "已移除服务器管理员" }); await refreshCandidates(); } catch (error) { setResult({ status: "failed", label: error instanceof Error ? error.message : "移除管理员失败" }); } finally { setBusyUserId(null); } } return (

管理成员

{isOwner ? "服主可邀请/移除管理员" : "仅服主可调整成员"}
{result && (
)}
服主:{instance.ownerUserId || "未绑定"} 服务器管理员:{instance.adminUserIds.length > 0 ? instance.adminUserIds.join(" / ") : "暂无"}
{isOwner && ( <>
void addAdministrator(event)} aria-label="邀请服务器管理员">
{candidates.status === "loading" && } {candidates.status === "error" && ( void refreshCandidates()} compact /> )} {instance.adminUserIds.length > 0 && (
{instance.adminUserIds.map((userId) => (
{userId} 服务器管理员
))}
)} )}
); } interface ServerFilesSectionProps { instance: ServerInstanceResponse; session: PageComponentProps["session"]; operations: PageComponentProps["operations"]; } interface ServerFileEditorState { entry: ServerFileEntryResponse | null; key: string; draft: string; snapshot?: DeclaredFileReadSnapshotResponse; loading: boolean; saving: boolean; message?: string; error?: string; } function ServerFilesSection({ instance, session, operations }: ServerFilesSectionProps) { const [workspace, setWorkspace] = useState>({ status: "loading" }); const [list, setList] = useState>({ status: "loading" }); const [directoryKey, setDirectoryKey] = useState(""); const [relativePath, setRelativePath] = useState(""); const [searchDraft, setSearchDraft] = useState(""); const [searchQuery, setSearchQuery] = useState(""); const [recursive, setRecursive] = useState(false); const [panelResult, setPanelResult] = useState<{ status: "pending" | "succeeded" | "failed"; label: string } | null>(null); const [uploadBusy, setUploadBusy] = useState(false); const [editor, setEditor] = useState({ entry: null, key: "", draft: "", loading: false, saving: false }); const browseRequestRef = useRef<{ key: string; idempotencyKey: string } | null>(null); const activeDirectory = workspace.status === "ready" ? workspace.data.directories.find((item) => item.key === directoryKey) : undefined; const canUpload = workspace.status === "ready" && Boolean(activeDirectory) && !uploadBusy; const entries = list.status === "ready" ? list.data.entries : []; const loadWorkspace = useCallback(async () => { browseRequestRef.current = null; setWorkspace({ status: "loading" }); try { const response = await platformApiClient.getServerFileWorkspace(instance.id); setWorkspace({ status: "ready", data: response }); const nextDirectoryKey = response.defaultDirectoryKey || response.directories[0]?.key || ""; setDirectoryKey((current) => current || nextDirectoryKey); if (!nextDirectoryKey) setList({ status: "ready", data: { serverInstanceId: response.serverInstanceId, pluginId: response.pluginId, directoryKey: "", state: "declared", entries: [], reason: "尚未获得服务器文件入口。" } }); } catch (error) { setWorkspace({ status: "error", reason: error instanceof Error ? error.message : "文件工作区加载失败" }); setList({ status: "error", reason: "文件工作区不可用" }); } }, [instance.id]); const browseList = useCallback(async (options: { silent?: boolean; forceNew?: boolean; manual?: boolean } = {}): Promise => { if (!directoryKey) return undefined; if (!options.silent) setList({ status: "loading" }); const browseKey = `${directoryKey}:${relativePath}:${searchQuery}:${recursive}`; if (options.forceNew || browseRequestRef.current?.key !== browseKey) { browseRequestRef.current = { key: browseKey, idempotencyKey: serverFileIdempotency("browse", instance.id, `${browseKey}:${Date.now()}`) }; } const operationId = options.manual ? operations.begin({ intent: "读取文件目录", targetKind: "server", targetId: instance.id, requester: session.displayName }) : ""; if (!options.silent) setPanelResult({ status: "pending", label: "正在读取目录…" }); try { const response = await platformApiClient.browseServerFiles(instance.id, { directoryKey, path: relativePath || undefined, query: searchQuery || undefined, recursive, idempotencyKey: browseRequestRef.current?.idempotencyKey }); setList({ status: "ready", data: response }); if (operationId) operations.succeed(operationId, response.state === "ready" ? "目录读取完成" : "正在读取目录", response.job); if (response.state === "ready") setPanelResult(null); else if (response.state === "failed") setPanelResult({ status: "failed", label: response.reason ?? "目录读取失败" }); else setPanelResult({ status: "pending", label: serverFileListPendingLabel(response) }); return response; } catch (error) { setList({ status: "error", reason: error instanceof Error ? error.message : "文件列表加载失败" }); if (operationId) operations.fail(operationId, error instanceof Error ? error.message : "目录读取失败", operationId); setPanelResult({ status: "failed", label: error instanceof Error ? error.message : "目录读取失败" }); return undefined; } }, [directoryKey, instance.id, operations, recursive, relativePath, searchQuery, session.displayName]); useEffect(() => { void loadWorkspace(); }, [loadWorkspace]); useEffect(() => { if (workspace.status !== "ready" || !directoryKey) return; void browseList({ forceNew: true }); }, [browseList, directoryKey, relativePath, recursive, searchQuery, workspace.status]); useEffect(() => { if (workspace.status !== "ready" || !directoryKey || list.status !== "ready" || list.data.state !== "pending") return; const timer = window.setTimeout(() => void browseList({ silent: true }), 500); return () => window.clearTimeout(timer); }, [browseList, directoryKey, list, workspace.status]); async function openEntry(entry: ServerFileEntryResponse) { if (entry.kind === "directory") { setDirectoryKey(entry.directoryKey || directoryKey); setRelativePath(entry.relativePath ?? ""); setEditor({ entry: null, key: "", draft: "", loading: false, saving: false }); return; } await openEditor(entry); } async function openEditor(entry: ServerFileEntryResponse) { const key = serverFileEntryKey(entry); if (!key) { setPanelResult({ status: "failed", label: "该文件缺少路径 key,不能读取。" }); return; } setEditor({ entry, key, draft: "", loading: true, saving: false, message: "正在读取最近快照…" }); try { const snapshot = await platformApiClient.getServerFileReadSnapshot(instance.id, key); if (snapshot.state === "ready") { setEditor({ entry, key, draft: snapshot.content ?? "", snapshot, loading: false, saving: false, message: snapshot.content === undefined ? snapshot.reason ?? "文件快照已就绪,但内容不适合内联编辑。" : "已加载最近读取快照。" }); return; } const operationId = operations.begin({ intent: "读取文件", targetKind: "server", targetId: instance.id, requester: session.displayName }); const dispatch = await platformApiClient.readServerFile(instance.id, { key, idempotencyKey: serverFileIdempotency("read", instance.id, key) }); operations.succeed(operationId, `读取任务 ${dispatch.job.id} 已派发`, dispatch.job); setEditor({ entry, key, draft: "", snapshot, loading: false, saving: false, message: snapshot.reason ?? "读取任务已派发;Run 返回后再次打开即可编辑。" }); setPanelResult({ status: "pending", label: "正在读取文件…" }); } catch (error) { const reason = error instanceof Error ? error.message : "文件读取失败"; setEditor({ entry, key, draft: "", loading: false, saving: false, error: reason }); setPanelResult({ status: "failed", label: reason }); } } async function saveEditor() { if (!editor.entry || !editor.key || editor.saving) return; const operationId = operations.begin({ intent: "保存文件", targetKind: "server", targetId: instance.id, requester: session.displayName }); setEditor((current) => ({ ...current, saving: true, error: undefined, message: "正在派发写入任务…" })); try { const dispatch = await platformApiClient.writeServerFile(instance.id, { key: editor.key, content: editor.draft, expectedVersion: editor.snapshot?.version, expectedChecksum: editor.snapshot?.checksum, idempotencyKey: serverFileIdempotency("write", instance.id, editor.key) }); operations.succeed(operationId, `写入任务 ${dispatch.job.id} 已派发`, dispatch.job); setEditor((current) => ({ ...current, saving: false, message: "保存任务已派发;Run 会在工作区内原子写入。" })); setPanelResult({ status: "pending", label: "正在保存文件…" }); await browseList({ forceNew: true }); } catch (error) { const reason = error instanceof Error ? error.message : "文件保存失败"; operations.fail(operationId, reason, operationId); setEditor((current) => ({ ...current, saving: false, error: reason })); setPanelResult({ status: "failed", label: reason }); } } async function downloadEntry(entry: ServerFileEntryResponse) { const key = serverFileEntryKey(entry); if (!key) return; const operationId = operations.begin({ intent: "下载文件", targetKind: "server", targetId: instance.id, requester: session.displayName }); const idempotencyKey = serverFileIdempotency("download", instance.id, key); setPanelResult({ status: "pending", label: "正在准备文件下载…" }); try { let result = await platformApiClient.prepareServerFileDownload(instance.id, { key, idempotencyKey }); for (let attempt = 0; result.status === "pending" && attempt < 240; attempt += 1) { setPanelResult({ status: "pending", label: `正在准备下载… ${Math.min(99, Math.max(1, attempt))}%` }); await new Promise((resolve) => window.setTimeout(resolve, 500)); result = await platformApiClient.prepareServerFileDownload(instance.id, { key, idempotencyKey }); } if (result.status !== "ready") throw new Error(result.reason ?? "文件下载准备超时"); const message = await downloadServerFileResult(platformApiClient, result); operations.succeed(operationId, message, result.job); setPanelResult({ status: "succeeded", label: message }); } catch (error) { const reason = error instanceof Error ? error.message : "文件下载失败"; operations.fail(operationId, reason, operationId); setPanelResult({ status: "failed", label: reason }); } } async function uploadFile(event: ChangeEvent) { const file = event.target.files?.[0]; event.target.value = ""; if (!file || !directoryKey || workspace.status !== "ready") return; if (file.size > workspace.data.transfer.maxBrowserUploadBytes) { setPanelResult({ status: "failed", label: `文件超过浏览器上传上限:${formatBytes(workspace.data.transfer.maxBrowserUploadBytes)}` }); return; } const operationId = operations.begin({ intent: "上传文件", targetKind: "server", targetId: instance.id, requester: session.displayName }); setUploadBusy(true); setPanelResult({ status: "pending", label: `正在暂存上传:${file.name}` }); try { const response = await platformApiClient.uploadServerFile(instance.id, { directoryKey, relativePath: relativePath || undefined, file, idempotencyKey: serverFileIdempotency("upload", instance.id, file.name) }); operations.succeed(operationId, `上传已暂存,写入任务 ${response.job.id} 已派发`, response.job); setPanelResult({ status: "pending", label: `上传已提交:${response.relativePath}` }); await browseList({ forceNew: true }); } catch (error) { const reason = error instanceof Error ? error.message : "文件上传失败"; operations.fail(operationId, reason, operationId); setPanelResult({ status: "failed", label: reason }); } finally { setUploadBusy(false); } } function submitSearch(event: FormEvent) { event.preventDefault(); setSearchQuery(searchDraft.trim()); } function goUp() { if (relativePath) { setRelativePath(relativePath.split("/").filter(Boolean).slice(0, -1).join("/")); return; } if (workspace.status === "ready" && directoryKey !== workspace.data.defaultDirectoryKey) { setDirectoryKey(workspace.data.defaultDirectoryKey || workspace.data.directories[0]?.key || directoryKey); } } function closeEditor() { setEditor({ entry: null, key: "", draft: "", loading: false, saving: false }); } if (workspace.status === "loading") return ; if (workspace.status === "error") return void loadWorkspace()} compact />; return (

服务器文件

{activeDirectory?.label ?? (directoryKey || "服务器根目录")} {relativePath.split("/").filter(Boolean).map((part) => {part})}
{workspace.data.directories.length > 1 &&
{workspace.data.directories.map((directory) => ( ))}
}
setSearchDraft(event.target.value)} />
{panelResult && } {list.status === "loading" && } {list.status === "error" && void browseList({ forceNew: true })} compact />} {list.status === "ready" && (
{entries.length === 0 && } {entries.map((entry) => ( ))}
文件名称大小修改时间备注操作
{serverFileListEmptyLabel(list.data)}
{entry.kind === "directory" ? "--" : formatBytes(entry.sizeBytes)} {formatDateTime(entry.modifiedAt)} {entry.remark || entry.scope || "--"}
{entry.kind === "directory" ? : } {entry.kind === "file" && }
)} {editor.entry && (

文件编辑

{editor.entry.name}
{editor.entry && editor.loading && } {editor.entry && editor.error && } {editor.entry && editor.message && !editor.error && {editor.message}} {editor.entry && editor.snapshot?.state === "ready" && editor.snapshot.content !== undefined && (