import { configurationCatalog } from "./schemas.js"; import type { SCUMConfigField, SCUMFeatureWorkspace, SCUMLogicalDirectory, SCUMLogicalFile } from "./contracts.js"; type StateSetter = (next: T | ((previous: T) => T)) => void; export type ReactLike = { createElement: (...args: any[]) => any; useEffect?: (effect: () => void | (() => void), deps: readonly unknown[]) => void; useState?: (initialState: T | (() => T)) => [T, StateSetter]; }; export type SCUMFileReadSnapshot = { serverInstanceId: string; pluginId: string; key: string; state: "ready" | "pending" | "not-read" | "unavailable" | string; content?: string; version?: number; checksum?: string; sizeBytes?: number; jobId?: string; readAt?: string; reason?: string; }; export type SCUMPageContext = { serverInstanceId?: string; permissions: string[]; availability: { available: boolean; reason?: string }; featureAvailability?: Array<{ key: string; available: boolean; reason?: string }>; workspace?: SCUMFeatureWorkspace; workspaceActions?: { requestFile?: (fileKey: string) => Promise<{ status: string; message: string; jobId?: string }>; getFileSnapshot?: (fileKey: string) => Promise; writeFile?: (fileKey: string, content: string, options?: { expectedChecksum?: string }) => Promise<{ status: string; message: string; jobId?: string }>; }; }; type NormalizedWorkspace = { defaultDirectoryKey: string; directories: readonly SCUMLogicalDirectory[]; files: readonly SCUMLogicalFile[]; configFields: readonly SCUMConfigField[] }; type ConfigMode = "fields" | "source"; type RawEncoding = "utf-8" | "utf-16le"; type FileRequestState = { fileKey: string; status: string; message: string; jobId?: string } | null; type PreviewState = { fileKey: string; mode: ConfigMode; summary: string; proposedContent: string; lines: readonly DiffLine[] } | null; type DiffLine = { kind: "same" | "added" | "removed"; text: string }; const fallbackDirectories: readonly SCUMLogicalDirectory[] = [ { key: "scum-config", label: "服务器配置", scope: "config" }, { key: "scum-logs", label: "日志文件", scope: "logs" } ]; const fallbackFiles: readonly SCUMLogicalFile[] = [ { key: "scum-server-settings", directoryKey: "scum-config", label: "ServerSettings.ini", kind: "config", editable: true }, { key: "scum-game-config", directoryKey: "scum-config", label: "Game.ini", kind: "config" }, { key: "scum-engine-config", directoryKey: "scum-config", label: "Engine.ini", kind: "config" }, { key: "scum-game-user-settings", directoryKey: "scum-config", label: "GameUserSettings.ini", kind: "config" }, { key: "scum-admin-log", directoryKey: "scum-logs", label: "Admin.log", kind: "log", streamKey: "scum.admin" }, { key: "scum-chat-log", directoryKey: "scum-logs", label: "Chat.log", kind: "log", streamKey: "scum.chat" }, { key: "scum-kill-log", directoryKey: "scum-logs", label: "Kill.log", kind: "log", streamKey: "scum.kill" }, { key: "scum-login-log", directoryKey: "scum-logs", label: "Login.log", kind: "log", streamKey: "scum.login" }, { key: "scum-server-log", directoryKey: "scum-logs", label: "Server.log", kind: "log", streamKey: "scum.server" } ]; const directoryRelativePaths: Record = { "scum-config": "/SCUM/Saved/Config/WindowsServer", "scum-logs": "/SCUM/Saved/SaveFiles/Logs" }; export function renderSCUMFeaturePage(react: ReactLike, input: SCUMPageContext) { const e = react.createElement; const workspace = normalizeWorkspace(input.workspace); const [selectedDirectoryKey, setSelectedDirectoryKey] = usePluginState(react, workspace.defaultDirectoryKey); const effectiveDirectoryKey = workspace.directories.some((directory) => directory.key === selectedDirectoryKey) ? selectedDirectoryKey : workspace.defaultDirectoryKey; const directoryFiles = workspace.files.filter((file) => file.directoryKey === effectiveDirectoryKey); const defaultFileKey = initialFileKey(workspace, effectiveDirectoryKey); const [selectedFileKey, setSelectedFileKey] = usePluginState(react, defaultFileKey); const selectedFile = directoryFiles.find((file) => file.key === selectedFileKey) ?? directoryFiles[0] ?? workspace.files[0]; const selectedFields = selectedFile ? workspace.configFields.filter((field) => field.fileKey === selectedFile.key) : []; const [configMode, setConfigMode] = usePluginState(react, "fields"); const [fieldDraft, setFieldDraft] = usePluginState>(react, {}); const [rawDraft, setRawDraft] = usePluginState>(react, {}); const [rawEncoding, setRawEncoding] = usePluginState(react, "utf-8"); const [snapshot, setSnapshot] = usePluginState(react, null); const [requestState, setRequestState] = usePluginState(react, null); const [writeState, setWriteState] = usePluginState(react, null); const [preview, setPreview] = usePluginState(react, null); const scoped = Boolean(input.serverInstanceId); const canFilesRead = scoped && input.permissions.includes("server.files.read"); const canFilesWrite = scoped && input.permissions.includes("server.files.write"); if (react.useEffect) { react.useEffect(() => { let active = true; setPreview(null); setWriteState(null); if (!selectedFile || !input.workspaceActions?.getFileSnapshot) { setSnapshot(null); return () => { active = false; }; } void input.workspaceActions.getFileSnapshot(selectedFile.key).then((next) => { if (active) setSnapshot(next); }).catch((error) => { if (active) setSnapshot({ serverInstanceId: input.serverInstanceId ?? "", pluginId: "game.scum", key: selectedFile.key, state: "unavailable", reason: error instanceof Error ? error.message : "无法读取文件快照。" }); }); return () => { active = false; }; }, [input.serverInstanceId, input.workspaceActions?.getFileSnapshot, selectedFile?.key]); } function selectDirectory(directoryKey: string) { const nextFiles = workspace.files.filter((file) => file.directoryKey === directoryKey); const nextFile = nextFiles.find((file) => file.kind === "config") ?? nextFiles[0]; setSelectedDirectoryKey(directoryKey); if (nextFile) selectFile(nextFile, directoryKey); } function selectFile(file: SCUMLogicalFile, directoryKey = file.directoryKey) { setSelectedDirectoryKey(directoryKey); setSelectedFileKey(file.key); setConfigMode(file.kind === "config" && workspace.configFields.some((field) => field.fileKey === file.key) ? "fields" : "source"); setRequestState(null); setWriteState(null); setPreview(null); setRawEncoding("utf-8"); } function requestSelectedFile() { if (!selectedFile || !canFilesRead || !input.workspaceActions?.requestFile) return; setRequestState({ fileKey: selectedFile.key, status: "pending", message: "正在提交文件读取请求..." }); setPreview(null); void input.workspaceActions.requestFile(selectedFile.key).then((result) => { setRequestState({ fileKey: selectedFile.key, status: result.status, message: result.message, jobId: result.jobId }); if (result.status === "queued" || result.status === "ok") { setSnapshot({ serverInstanceId: input.serverInstanceId ?? "", pluginId: "game.scum", key: selectedFile.key, state: "pending", jobId: result.jobId, reason: "等待运行端完成文件读取。" }); } }).catch((error) => setRequestState({ fileKey: selectedFile.key, status: "error", message: error instanceof Error ? error.message : "文件读取请求失败。" })); } function refreshSelectedSnapshot() { if (!selectedFile || !input.workspaceActions?.getFileSnapshot) return; setPreview(null); void input.workspaceActions.getFileSnapshot(selectedFile.key).then(setSnapshot).catch((error) => { setSnapshot({ serverInstanceId: input.serverInstanceId ?? "", pluginId: "game.scum", key: selectedFile.key, state: "unavailable", reason: error instanceof Error ? error.message : "无法读取文件快照。" }); }); } function previewModeChange(mode: ConfigMode, content: string, current: string) { if (!selectedFile) return; const diff = buildSimpleDiff(current, content); setPreview({ fileKey: selectedFile.key, mode, proposedContent: content, summary: diff.summary, lines: diff.lines }); } function writePreviewedContent() { if (!selectedFile || !preview || preview.fileKey !== selectedFile.key || !input.workspaceActions?.writeFile) return; setWriteState({ fileKey: selectedFile.key, status: "pending", message: "正在提交声明文件写入..." }); void input.workspaceActions.writeFile(selectedFile.key, preview.proposedContent, { expectedChecksum: snapshot?.checksum }).then((result) => { setWriteState({ fileKey: selectedFile.key, status: result.status, message: result.message, jobId: result.jobId }); setPreview(null); }).catch((error) => setWriteState({ fileKey: selectedFile.key, status: "error", message: error instanceof Error ? error.message : "文件写入请求失败。" })); } return e("section", { className: "console-panel", "aria-label": "SCUM 文件管理" }, e("div", { className: "panel-header" }, e("div", null, e("h2", null, "文件管理"), e("p", { className: "provider-id" }, "第一级选择目录,第二级选择目录内文件;配置默认表单,日志只读原文。")), e("span", { className: "page-status" }, scoped ? "声明文件工作区" : "插件页面未绑定服务器") ), e("div", { className: "file-workbench" }, navigationPane(e, workspace, effectiveDirectoryKey, directoryFiles, selectedFile?.key, selectDirectory, selectFile), selectedFile ? fileDetail(e, { file: selectedFile, fields: selectedFields, mode: selectedFile.kind === "log" ? "source" : configMode, setMode: setConfigMode, fieldDraft, setFieldDraft, rawDraft, setRawDraft, rawEncoding, setRawEncoding, snapshot: snapshot?.key === selectedFile.key ? snapshot : null, requestState: requestState?.fileKey === selectedFile.key ? requestState : null, writeState: writeState?.fileKey === selectedFile.key ? writeState : null, preview: preview?.fileKey === selectedFile.key ? preview : null, canFilesRead, canFilesWrite, canRequestFile: Boolean(input.workspaceActions?.requestFile), canRefreshSnapshot: Boolean(input.workspaceActions?.getFileSnapshot), canWriteFile: Boolean(input.workspaceActions?.writeFile), onRequestFile: requestSelectedFile, onRefreshSnapshot: refreshSelectedSnapshot, onPreview: previewModeChange, onWrite: writePreviewedContent }) : e("div", { className: "file-workbench-detail" }, e("p", { className: "page-status" }, "当前插件没有可展示的声明文件。")) ) ); } function navigationPane(e: ReactLike["createElement"], workspace: NormalizedWorkspace, directoryKey: string, directoryFiles: readonly SCUMLogicalFile[], selectedFileKey: string | undefined, onDirectoryChange: (directoryKey: string) => void, onFileSelect: (file: SCUMLogicalFile) => void) { const activeDirectory = workspace.directories.find((directory) => directory.key === directoryKey); const selectedFile = directoryFiles.find((file) => file.key === selectedFileKey) ?? directoryFiles[0]; return e("aside", { className: "file-workbench-nav", "aria-label": "SCUM 文件两级菜单" }, e("label", { className: "file-workbench-picker" }, e("span", null, "目录"), e("select", { value: directoryKey, onChange: (event: { target: { value: string } }) => onDirectoryChange(event.target.value), "aria-label": "选择目录" }, workspace.directories.map((directory) => e("option", { key: directory.key, value: directory.key }, directoryRelativePath(directory.key) ?? directory.label)) ) ), e("label", { className: "file-workbench-picker" }, e("span", null, "文件"), e("select", { value: selectedFile?.key ?? "", onChange: (event: { target: { value: string } }) => { const file = directoryFiles.find((candidate) => candidate.key === event.target.value); if (file) onFileSelect(file); }, "aria-label": "选择文件" }, directoryFiles.length ? directoryFiles.map((file) => e("option", { key: file.key, value: file.key }, file.label)) : e("option", { value: "" }, "没有已声明文件") ) ), e("div", { className: "file-workbench-directory-heading" }, e("strong", null, activeDirectory?.label ?? "插件声明目录"), e("span", null, directoryRelativePath(directoryKey) ?? "插件逻辑目录"), e("small", null, `${directoryFiles.length} 个声明文件`) ) ); } function fileDetail(e: ReactLike["createElement"], props: { file: SCUMLogicalFile; fields: readonly SCUMConfigField[]; mode: ConfigMode; setMode: StateSetter; fieldDraft: Record; setFieldDraft: StateSetter>; rawDraft: Record; setRawDraft: StateSetter>; rawEncoding: RawEncoding; setRawEncoding: StateSetter; snapshot: SCUMFileReadSnapshot | null; requestState: FileRequestState; writeState: FileRequestState; preview: PreviewState; canFilesRead: boolean; canFilesWrite: boolean; canRequestFile: boolean; canRefreshSnapshot: boolean; canWriteFile: boolean; onRequestFile: () => void; onRefreshSnapshot: () => void; onPreview: (mode: ConfigMode, content: string, current: string) => void; onWrite: () => void; }) { const canShowFields = props.file.kind === "config" && props.fields.length > 0; const currentContent = props.snapshot?.state === "ready" ? decodeRawContent(props.snapshot.content ?? "", props.rawEncoding) : ""; const snapshotValues = props.snapshot?.state === "ready" ? parseIniAssignments(currentContent) : {}; return e("article", { className: "file-workbench-detail", "aria-label": `文件 ${props.file.label}` }, e("div", { className: "panel-header" }, e("div", null, e("h2", null, props.file.label), e("p", { className: "provider-id" }, `${directoryRelativePath(props.file.directoryKey) ?? "插件声明目录"} · ${props.file.kind === "config" ? "配置文件" : "日志文件"}`)), e("div", { className: "console-row-actions" }, e("button", { type: "button", className: "icon-command", disabled: !props.canFilesRead || !props.canRequestFile, onClick: props.onRequestFile }, "读取文件"), e("button", { type: "button", className: "icon-command", disabled: !props.canRefreshSnapshot, onClick: props.onRefreshSnapshot }, "刷新结果") ) ), props.requestState ? e("p", { className: "page-status", "data-state": props.requestState.status }, props.requestState.message) : null, props.writeState ? e("p", { className: "page-status", "data-state": props.writeState.status }, props.writeState.message) : null, props.file.kind === "log" || props.snapshot?.state === "ready" ? encodingSwitcher(e, props.rawEncoding, props.setRawEncoding) : null, canShowFields ? e("div", { className: "file-workbench-mode", role: "tablist", "aria-label": "表单配置与原文模式" }, e("span", null, "编辑视图"), e("button", { type: "button", role: "tab", "aria-selected": props.mode === "fields", className: props.mode === "fields" ? "file-workbench-mode-active" : undefined, onClick: () => props.setMode("fields") }, "配置表单"), e("button", { type: "button", role: "tab", "aria-selected": props.mode === "source", className: props.mode === "source" ? "file-workbench-mode-active" : undefined, onClick: () => props.setMode("source") }, "原文模式") ) : null, canShowFields && props.mode === "fields" ? modeledConfigurationFields(e, props.file, props.fields, props.fieldDraft, props.setFieldDraft, snapshotValues, currentContent, props.snapshot?.state === "ready", props.canFilesWrite, props.canWriteFile, props.preview, props.onPreview, props.onWrite) : rawFileView(e, props.file, currentContent, props.rawDraft, props.setRawDraft, props.snapshot, props.canFilesWrite, props.canWriteFile, props.preview, props.onPreview, props.onWrite) ); } function encodingSwitcher(e: ReactLike["createElement"], encoding: RawEncoding, setEncoding: StateSetter) { return e("div", { className: "file-workbench-mode file-workbench-encoding", role: "tablist", "aria-label": "原文编码" }, e("span", null, "文本编码"), e("button", { type: "button", role: "tab", "aria-selected": encoding === "utf-8", className: encoding === "utf-8" ? "file-workbench-mode-active" : undefined, onClick: () => setEncoding("utf-8") }, "UTF-8"), e("button", { type: "button", role: "tab", "aria-selected": encoding === "utf-16le", className: encoding === "utf-16le" ? "file-workbench-mode-active" : undefined, onClick: () => setEncoding("utf-16le") }, "UTF-16 LE") ); } function modeledConfigurationFields(e: ReactLike["createElement"], file: SCUMLogicalFile, fields: readonly SCUMConfigField[], draft: Record, setDraft: StateSetter>, values: Record, currentContent: string, hasReadSnapshot: boolean, canFilesWrite: boolean, canWriteFile: boolean, preview: PreviewState, onPreview: (mode: ConfigMode, content: string, current: string) => void, onWrite: () => void) { const editable = hasReadSnapshot && canFilesWrite && file.editable !== false; const fieldKeys = new Set(fields.map((field) => field.key)); const selectedDraft = Object.fromEntries(Object.entries(draft).filter(([key]) => fieldKeys.has(key))); const hasChanges = Object.keys(selectedDraft).length > 0; const unknown = unknownIniAssignments(currentContent, fields); return e("div", { className: "file-workbench-fields" }, fields.map((field) => { const value = draft[field.key] ?? values[field.configKey] ?? field.defaultValue; return e("label", { className: "file-workbench-field", key: field.key }, e("span", null, e("strong", null, field.label), e("small", null, `${field.configKey} · ${field.description}`)), fieldControl(e, field, value, editable, (next) => setDraft((current) => ({ ...current, [field.key]: next }))), e("small", null, `${controlLabel(field)} · ${field.restartImpact === "restart-required" ? "修改后需要受控重启" : "可在安全窗口内生效"}`) ); }), unknown.length ? e("section", { className: "file-workbench-unknown", "aria-label": "未建模配置项" }, e("h3", null, "未建模配置项"), unknown.map((item) => e("div", { className: "console-row", key: `${item.key}:${item.index}` }, e("span", null, item.key), e("strong", null, item.value || "空值")))) : null, e("div", { className: "console-row-actions file-workbench-actions" }, e("button", { type: "button", className: "icon-command", disabled: !editable || !hasChanges, onClick: () => onPreview("fields", composeIniContent(currentContent, fields, selectedDraft), currentContent) }, "预览改动"), e("button", { type: "button", className: "primary-command", disabled: !editable || !canWriteFile || !preview || preview.mode !== "fields", onClick: onWrite }, "提交写入") ), preview && preview.mode === "fields" ? diffPreview(e, preview) : null, e("p", { className: "page-status" }, hasReadSnapshot ? editable ? "配置表单会保留原文中的未知行;提交前先预览差异。" : "当前账号仅有读取权限,配置项为只读。" : "先读取文件后才显示服务器当前值;未读取时不会把默认值伪装成原文。") ); } function fieldControl(e: ReactLike["createElement"], field: SCUMConfigField, value: string, editable: boolean, onChange: (next: string) => void) { if (field.control === "boolean") { return e("select", { value: normalizeBoolean(value) ? "true" : "false", disabled: !editable, "aria-label": field.label, onChange: (event: { target: { value: string } }) => onChange(event.target.value) }, e("option", { value: "true" }, "是"), e("option", { value: "false" }, "否") ); } if (field.control === "number" || field.control === "port") { return e("div", { className: "file-workbench-number-control" }, e("input", { type: "range", value, min: field.minimum, max: field.maximum, disabled: !editable, "aria-label": `${field.label}滑动输入`, onChange: (event: { target: { value: string } }) => onChange(event.target.value) }), e("input", { type: "number", value, min: field.minimum, max: field.maximum, readOnly: !editable, "aria-label": field.label, onChange: (event: { target: { value: string } }) => onChange(event.target.value) }) ); } return e("input", { type: "text", value, readOnly: !editable, "aria-label": field.label, onChange: (event: { target: { value: string } }) => onChange(event.target.value) }); } function rawFileView(e: ReactLike["createElement"], file: SCUMLogicalFile, currentContent: string, rawDraft: Record, setRawDraft: StateSetter>, snapshot: SCUMFileReadSnapshot | null, canFilesWrite: boolean, canWriteFile: boolean, preview: PreviewState, onPreview: (mode: ConfigMode, content: string, current: string) => void, onWrite: () => void) { if (snapshot?.state !== "ready") { const message = snapshot?.reason ?? (file.kind === "config" ? "尚未读取此配置文件的受控原文。" : "尚未读取此日志文件的受控内容。"); return e("div", { className: "file-workbench-raw" }, e("p", { className: "page-status" }, message), snapshot?.jobId ? e("small", null, `读取任务 ${snapshot.jobId}`) : null); } const rawValue = rawDraft[file.key] ?? currentContent; const editable = file.kind === "config" && canFilesWrite && file.editable !== false; if (file.kind === "log") { return e("div", { className: "file-workbench-raw" }, e("pre", { className: "runtime-task-log" }, rawValue), e("p", { className: "page-status" }, `日志原文只读 · ${contentLineCount(rawValue)} 行 · ${snapshot.sizeBytes ?? 0} B`)); } return e("div", { className: "file-workbench-raw" }, e("textarea", { className: "file-workbench-raw-editor", value: rawValue, readOnly: !editable, spellCheck: false, "aria-label": `${file.label} 原文模式编辑`, onChange: (event: { target: { value: string } }) => setRawDraft((current) => ({ ...current, [file.key]: event.target.value })) }), e("div", { className: "console-row-actions file-workbench-actions" }, e("button", { type: "button", className: "icon-command", disabled: !editable || rawValue === currentContent, onClick: () => onPreview("source", rawValue, currentContent) }, "预览改动"), e("button", { type: "button", className: "primary-command", disabled: !editable || !canWriteFile || !preview || preview.mode !== "source", onClick: onWrite }, "提交写入") ), preview && preview.mode === "source" ? diffPreview(e, preview) : null, e("p", { className: "page-status" }, editable ? "原文模式会整文件写入声明 file key;提交前请先预览差异。" : "原文配置当前只读。") ); } function diffPreview(e: ReactLike["createElement"], preview: Exclude) { return e("section", { className: "file-workbench-diff", "aria-label": "文件改动预览" }, e("div", { className: "console-row" }, e("span", null, "差异预览"), e("strong", null, preview.summary)), e("pre", { className: "runtime-task-log" }, preview.lines.map((line) => `${line.kind === "added" ? "+" : line.kind === "removed" ? "-" : " "} ${line.text}`).join("\n")) ); } function usePluginState(react: ReactLike, initialState: T): [T, StateSetter] { return react.useState ? react.useState(initialState) : [initialState, () => undefined]; } function normalizeWorkspace(workspace?: SCUMFeatureWorkspace): NormalizedWorkspace { const directories = workspace?.directories?.length ? workspace.directories : fallbackDirectories; const files = workspace?.files?.length ? workspace.files : fallbackFiles; const configFields = workspace?.configFields?.length ? workspace.configFields : configurationCatalog; return { defaultDirectoryKey: workspace?.defaultDirectoryKey && directories.some((directory) => directory.key === workspace.defaultDirectoryKey) ? workspace.defaultDirectoryKey : directories[0]?.key ?? "scum-config", directories, files, configFields }; } function initialFileKey(workspace: NormalizedWorkspace, directoryKey: string): string { return workspace.files.find((file) => file.directoryKey === directoryKey && file.kind === "config")?.key ?? workspace.files.find((file) => file.directoryKey === directoryKey)?.key ?? workspace.files[0]?.key ?? ""; } function directoryRelativePath(key: string): string | undefined { return directoryRelativePaths[key]; } function controlLabel(field: SCUMConfigField): string { const range = field.minimum !== undefined || field.maximum !== undefined ? `范围 ${field.minimum ?? "不限"}-${field.maximum ?? "不限"}` : ""; const label = field.control === "boolean" ? "是/否选择" : field.control === "number" || field.control === "port" ? "滑动输入" : "文本填空"; return [label, range].filter(Boolean).join(" · "); } function normalizeBoolean(value: string): boolean { return /^(true|1|yes|on)$/i.test(String(value).trim()); } function parseIniAssignments(content: string): Record { const values: Record = {}; for (const line of content.split("\n")) { const parsed = parseIniAssignment(line); if (parsed) values[parsed.key] = parsed.value; } return values; } function unknownIniAssignments(content: string, fields: readonly SCUMConfigField[]): Array<{ key: string; value: string; index: number }> { const known = new Set(fields.map((field) => field.configKey)); const unknown: Array<{ key: string; value: string; index: number }> = []; content.split("\n").forEach((line, index) => { const parsed = parseIniAssignment(line); if (parsed && !known.has(parsed.key)) unknown.push({ ...parsed, index }); }); return unknown; } function parseIniAssignment(line: string): { key: string; value: string } | null { const trimmed = line.trim(); if (!trimmed || trimmed.startsWith("#") || trimmed.startsWith(";") || trimmed.startsWith("[")) return null; const separator = line.indexOf("="); if (separator < 1) return null; return { key: line.slice(0, separator).trim(), value: line.slice(separator + 1).trim() }; } function composeIniContent(content: string, fields: readonly SCUMConfigField[], draft: Record): string { const byField = new Map(fields.map((field) => [field.key, field])); const changes = new Map(); for (const [fieldKey, value] of Object.entries(draft)) { const field = byField.get(fieldKey); if (field) changes.set(field.configKey, value); } if (changes.size === 0) return content; const applied = new Set(); const lines = content.split("\n").map((line) => { const parsed = parseIniAssignment(line); if (!parsed || !changes.has(parsed.key)) return line; applied.add(parsed.key); return `${line.slice(0, line.indexOf("=") + 1)}${changes.get(parsed.key) ?? ""}`; }); for (const [key, value] of changes) { if (!applied.has(key)) lines.push(`${key}=${value}`); } return content.endsWith("\n") ? lines.join("\n") : lines.join("\n").replace(/\n$/, ""); } function decodeRawContent(content: string, encoding: RawEncoding): string { if (encoding === "utf-8") return content; const bytes = Uint8Array.from(Array.from(content), (char) => char.charCodeAt(0) & 0xff); if (typeof TextDecoder !== "undefined") return new TextDecoder("utf-16le").decode(bytes); let decoded = ""; for (let index = 0; index < bytes.length; index += 2) decoded += String.fromCharCode(bytes[index] | ((bytes[index + 1] ?? 0) << 8)); return decoded; } function buildSimpleDiff(current: string, proposed: string): { summary: string; lines: readonly DiffLine[] } { const currentLines = current.split("\n"); const proposedLines = proposed.split("\n"); const max = Math.max(currentLines.length, proposedLines.length); const lines: DiffLine[] = []; let added = 0; let removed = 0; for (let index = 0; index < max; index += 1) { const before = currentLines[index]; const after = proposedLines[index]; if (before === after) { if (before !== undefined) lines.push({ kind: "same", text: before }); continue; } if (before !== undefined) { removed += 1; lines.push({ kind: "removed", text: before }); } if (after !== undefined) { added += 1; lines.push({ kind: "added", text: after }); } } return { summary: `+${added} / -${removed} 行变更`, lines }; } function contentLineCount(content: string): number { return content ? content.split("\n").length : 0; }