From 504387c65757a07917f002a16145e7e4a2e46f60 Mon Sep 17 00:00:00 2001 From: npc0-hue Date: Mon, 24 Aug 2026 14:06:14 +0800 Subject: [PATCH] Fix server file workspace null handling --- platform/dto/resources.go | 5 +- platform_web/api/client.test.ts | 19 +++ platform_web/api/client.ts | 14 +- platform_web/pages/ServerDetailPage.tsx | 9 +- .../examples/scum-server-plugin/manifest.json | 145 ++++++++++++++++++ plugins/tests/manifest-validation.test.ts | 5 +- 6 files changed, 190 insertions(+), 7 deletions(-) diff --git a/platform/dto/resources.go b/platform/dto/resources.go index f4d4f77..3db3a3e 100644 --- a/platform/dto/resources.go +++ b/platform/dto/resources.go @@ -1164,7 +1164,7 @@ func fileWorkspaceToDomain(body PluginFileWorkspaceBody) domain.PluginFileWorksp } func fileWorkspaceFromDomain(workspace domain.PluginFileWorkspace) PluginFileWorkspaceBody { workspace = domain.CopyPluginFileWorkspace(workspace) - body := PluginFileWorkspaceBody{DefaultDirectoryKey: workspace.DefaultDirectoryKey} + body := PluginFileWorkspaceBody{DefaultDirectoryKey: workspace.DefaultDirectoryKey, Directories: []PluginLogicalDirectoryBody{}, Files: []PluginLogicalFileBody{}, ConfigFields: []PluginConfigFieldBody{}} for _, item := range workspace.Directories { body.Directories = append(body.Directories, PluginLogicalDirectoryBody{Key: item.Key, Label: item.Label, Scope: item.Scope}) } @@ -1956,6 +1956,9 @@ func ServerFileWorkspaceFromDomain(view domain.ServerFileWorkspaceView) ServerFi func ServerFileTransferPolicyFromDomain(policy domain.ServerFileTransferPolicy) ServerFileTransferPolicyResponse { policy = domain.CopyServerFileTransferPolicy(policy) + if policy.Notes == nil { + policy.Notes = []string{} + } return ServerFileTransferPolicyResponse{Channel: policy.Channel, UploadChunkSizeBytes: policy.UploadChunkSizeBytes, DownloadChunkSizeBytes: policy.DownloadChunkSizeBytes, MaxInlineEditBytes: policy.MaxInlineEditBytes, MaxBrowserUploadBytes: policy.MaxBrowserUploadBytes, Notes: policy.Notes} } diff --git a/platform_web/api/client.test.ts b/platform_web/api/client.test.ts index eaf85f3..7414fb7 100644 --- a/platform_web/api/client.test.ts +++ b/platform_web/api/client.test.ts @@ -657,6 +657,25 @@ describe("PlatformApiClient AI providers", () => { expect(fetchMock).toHaveBeenCalledTimes(44); }); + it("normalizes server file workspace null arrays from older platform responses", async () => { + const fetchMock = vi.fn(async (input: RequestInfo | URL) => { + const url = String(input); + if (url.endsWith("/api/v1/server-instances/server-1/files/workspace")) { + return jsonResponse({ ...serverFileWorkspace, defaultDirectoryKey: "", directories: null, files: null, configFields: null, transfer: { ...serverFileWorkspace.transfer, notes: null } }); + } + if (url.endsWith("/api/v1/server-instances/server-1/files/list?directoryKey=scum-config")) { + return jsonResponse({ serverInstanceId: server.id, pluginId: plugin.id, directoryKey: "scum-config", state: "declared", entries: null }); + } + throw new Error(`unexpected request: ${url}`); + }); + vi.stubGlobal("fetch", fetchMock); + + const client = new PlatformApiClient(); + + await expect(client.getServerFileWorkspace(server.id)).resolves.toMatchObject({ directories: [], files: [], configFields: [], transfer: { notes: [] } }); + await expect(client.listServerFiles(server.id, { directoryKey: "scum-config" })).resolves.toMatchObject({ entries: [] }); + }); + it("calls plugin marketplace endpoints with filter and state contracts", async () => { const fetchMock = vi.fn(async (input: RequestInfo | URL, init?: RequestInit) => { const url = String(input); diff --git a/platform_web/api/client.ts b/platform_web/api/client.ts index 13b100c..026bf66 100644 --- a/platform_web/api/client.ts +++ b/platform_web/api/client.ts @@ -585,16 +585,16 @@ export class PlatformApiClient { } async getServerFileWorkspace(serverInstanceId: string): Promise { - return this.request(`/server-instances/${encodeURIComponent(serverInstanceId)}/files/workspace`); + return normalizeServerFileWorkspace(await this.request(`/server-instances/${encodeURIComponent(serverInstanceId)}/files/workspace`)); } async listServerFiles(serverInstanceId: string, request: Partial = {}): Promise { const params = serverFileListQuery(request); - return this.request(`/server-instances/${encodeURIComponent(serverInstanceId)}/files/list${params}`); + return normalizeServerFileList(await this.request(`/server-instances/${encodeURIComponent(serverInstanceId)}/files/list${params}`)); } async refreshServerFiles(serverInstanceId: string, request: ServerFileListRequest): Promise { - return this.request(`/server-instances/${encodeURIComponent(serverInstanceId)}/files/refresh`, { method: "POST", body: request }); + return normalizeServerFileList(await this.request(`/server-instances/${encodeURIComponent(serverInstanceId)}/files/refresh`, { method: "POST", body: request })); } async readServerFile(serverInstanceId: string, request: ServerFileReadRequest): Promise { @@ -964,4 +964,12 @@ function serverFileListQuery(request: Partial): string { return query ? `?${query}` : ""; } +function normalizeServerFileWorkspace(response: ServerFileWorkspaceResponse): ServerFileWorkspaceResponse { + return { ...response, directories: response.directories ?? [], files: response.files ?? [], configFields: response.configFields ?? [], transfer: { ...response.transfer, notes: response.transfer.notes ?? [] } }; +} + +function normalizeServerFileList(response: ServerFileListResponse): ServerFileListResponse { + return { ...response, entries: response.entries ?? [] }; +} + export const platformApiClient = new PlatformApiClient(readWebRuntimeEnv().platformApiBaseUrl); diff --git a/platform_web/pages/ServerDetailPage.tsx b/platform_web/pages/ServerDetailPage.tsx index 976c093..aba20fe 100644 --- a/platform_web/pages/ServerDetailPage.tsx +++ b/platform_web/pages/ServerDetailPage.tsx @@ -596,7 +596,7 @@ function ServerFilesSection({ instance, session, operations }: ServerFilesSectio const [editor, setEditor] = useState({ entry: null, key: "", draft: "", loading: false, saving: false }); const activeDirectory = workspace.status === "ready" ? workspace.data.directories.find((item) => item.key === directoryKey) : undefined; - const canUpload = workspace.status === "ready" && activeDirectory?.scope !== "logs" && !uploadBusy; + const canUpload = workspace.status === "ready" && Boolean(activeDirectory) && activeDirectory?.scope !== "logs" && !uploadBusy; const entries = list.status === "ready" ? list.data.entries : []; const loadWorkspace = useCallback(async () => { @@ -604,7 +604,11 @@ function ServerFilesSection({ instance, session, operations }: ServerFilesSectio try { const response = await platformApiClient.getServerFileWorkspace(instance.id); setWorkspace({ status: "ready", data: response }); - setDirectoryKey((current) => current || response.defaultDirectoryKey || response.directories[0]?.key || ""); + 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: "插件尚未声明文件工作区;需要在插件 manifest 中添加 fileWorkspace。" } }); + } } catch (error) { setWorkspace({ status: "error", reason: error instanceof Error ? error.message : "文件工作区加载失败" }); setList({ status: "error", reason: "文件工作区不可用" }); @@ -773,6 +777,7 @@ function ServerFilesSection({ instance, session, operations }: ServerFilesSectio
+ {workspace.data.directories.length === 0 && 插件尚未声明可浏览目录} {workspace.data.directories.map((directory) => (