import { Code2, FileCog, Save, X } from "lucide-react"; import { useCallback, useEffect, useMemo, useState } from "react"; import { platformApiClient } from "../api/client"; import type { DeclaredFileReadSnapshotResponse, PluginConfigFieldResponse, PluginLogicalFileResponse, ServerFileWorkspaceResponse, ServerInstanceResponse } from "../api/types"; import type { OperationTracker } from "../stores/operations"; import { iniValue, updateIniValues } from "../utils/iniConfig"; import { ErrorState, LoadingState, ResultBadge } from "./StateViews"; interface ServerConfigEditorProps { instance: ServerInstanceResponse; operations: OperationTracker; requester: string; onClose: () => void; } type EditorMode = "friendly" | "source"; export function ServerConfigEditor({ instance, operations, requester, onClose }: ServerConfigEditorProps) { const [workspace, setWorkspace] = useState(null); const [selectedKey, setSelectedKey] = useState(""); const [mode, setMode] = useState("friendly"); const [snapshot, setSnapshot] = useState(null); const [raw, setRaw] = useState(""); const [values, setValues] = useState>({}); const [loading, setLoading] = useState(true); const [saving, setSaving] = useState(false); const [message, setMessage] = useState(""); const [error, setError] = useState(""); const configFiles = useMemo(() => (workspace?.files ?? []).filter((file) => file.kind === "config" && file.editable !== false), [workspace]); const selectedFile = configFiles.find((file) => file.key === selectedKey) ?? configFiles[0]; const fields = useMemo(() => (workspace?.configFields ?? []).filter((field) => field.fileKey === selectedFile?.key), [selectedFile?.key, workspace?.configFields]); const readSelectedFile = useCallback(async (file: PluginLogicalFileResponse) => { setLoading(true); setError(""); setMessage("正在通过 Run 读取配置文件…"); try { let next = await platformApiClient.getServerFileReadSnapshot(instance.id, file.key); if (next.state !== "ready") { const operationId = operations.begin({ intent: "读取服务器配置", targetKind: "config", targetId: instance.id, requester }); const dispatch = await platformApiClient.readServerFile(instance.id, { key: file.key, idempotencyKey: configOperationKey("read", instance.id, file.key) }); operations.succeed(operationId, `配置读取任务 ${dispatch.job.id} 已派发`, dispatch.job); for (let attempt = 0; attempt < 60 && next.state !== "ready"; attempt += 1) { await new Promise((resolve) => window.setTimeout(resolve, 500)); next = await platformApiClient.getServerFileReadSnapshot(instance.id, file.key); } } if (next.state !== "ready" || next.content === undefined) throw new Error(next.reason ?? "Run 尚未返回配置文件内容。"); setSnapshot(next); setRaw(next.content); setValues(Object.fromEntries(fields.map((field) => [field.key, iniValue(next.content ?? "", field.configKey)]))); setMessage(`已读取 ${file.label}${next.readAt ? ` · ${formatReadTime(next.readAt)}` : ""}`); } catch (readError) { setSnapshot(null); setRaw(""); setError(readError instanceof Error ? readError.message : "配置文件读取失败"); setMessage(""); } finally { setLoading(false); } }, [fields, instance.id, operations, requester]); useEffect(() => { if (!workspace) { void platformApiClient.getServerFileWorkspace(instance.id).then((next) => { setWorkspace(next); setSelectedKey(next.files.find((file) => file.kind === "config" && file.editable !== false)?.key ?? ""); }).catch((loadError) => setError(loadError instanceof Error ? loadError.message : "配置工作区加载失败")).finally(() => setLoading(false)); } }, [instance.id, workspace]); useEffect(() => { if (selectedFile) void readSelectedFile(selectedFile); }, [readSelectedFile, selectedFile?.key]); async function save() { if (!selectedFile || !snapshot || saving) return; const content = mode === "source" ? raw : updateIniValues(raw, Object.fromEntries(fields.map((field) => [field.configKey, values[field.key] ?? ""]))); const operationId = operations.begin({ intent: "保存服务器配置", targetKind: "config", targetId: instance.id, requester }); setSaving(true); setError(""); setMessage("正在向 Run 派发配置写入任务…"); try { const dispatch = await platformApiClient.writeServerFile(instance.id, { key: selectedFile.key, content, expectedVersion: snapshot.version, expectedChecksum: snapshot.checksum, idempotencyKey: configOperationKey("write", instance.id, selectedFile.key) }); operations.succeed(operationId, `配置写入任务 ${dispatch.job.id} 已派发`, dispatch.job); setRaw(content); setSnapshot({ ...snapshot, content }); setMessage(`已派发 ${selectedFile.label} 写入任务;重启影响:${fields.some((field) => field.restartImpact === "restart-required") ? "需要重启服务器" : "无需重启"}。`); } catch (saveError) { operations.fail(operationId, saveError instanceof Error ? saveError.message : "配置写入失败", operationId); setError(saveError instanceof Error ? saveError.message : "配置写入失败"); } finally { setSaving(false); } } return (

修改配置

{instance.name}
{loading && !workspace && } {error && !workspace && } {workspace && configFiles.length === 0 && } {workspace && configFiles.length > 0 && (

{selectedFile?.label}

配置内容由 Run 返回
{mode === "friendly" ? "人类可读模式:只修改插件声明的配置项" : "源码模式:编辑完整原始文件"}
{message && } {error && workspace && } {loading && } {!loading && mode === "friendly" &&
{fields.length === 0 && 该配置文件没有声明可视化字段,请切换到源码模式。}{fields.map((field) => setValues((current) => ({ ...current, [field.key]: value }))} />)}
} {!loading && mode === "source" &&