Fix server file workspace null handling
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -585,16 +585,16 @@ export class PlatformApiClient {
|
||||
}
|
||||
|
||||
async getServerFileWorkspace(serverInstanceId: string): Promise<ServerFileWorkspaceResponse> {
|
||||
return this.request<ServerFileWorkspaceResponse>(`/server-instances/${encodeURIComponent(serverInstanceId)}/files/workspace`);
|
||||
return normalizeServerFileWorkspace(await this.request<ServerFileWorkspaceResponse>(`/server-instances/${encodeURIComponent(serverInstanceId)}/files/workspace`));
|
||||
}
|
||||
|
||||
async listServerFiles(serverInstanceId: string, request: Partial<ServerFileListRequest> = {}): Promise<ServerFileListResponse> {
|
||||
const params = serverFileListQuery(request);
|
||||
return this.request<ServerFileListResponse>(`/server-instances/${encodeURIComponent(serverInstanceId)}/files/list${params}`);
|
||||
return normalizeServerFileList(await this.request<ServerFileListResponse>(`/server-instances/${encodeURIComponent(serverInstanceId)}/files/list${params}`));
|
||||
}
|
||||
|
||||
async refreshServerFiles(serverInstanceId: string, request: ServerFileListRequest): Promise<ServerFileListResponse> {
|
||||
return this.request<ServerFileListResponse>(`/server-instances/${encodeURIComponent(serverInstanceId)}/files/refresh`, { method: "POST", body: request });
|
||||
return normalizeServerFileList(await this.request<ServerFileListResponse>(`/server-instances/${encodeURIComponent(serverInstanceId)}/files/refresh`, { method: "POST", body: request }));
|
||||
}
|
||||
|
||||
async readServerFile(serverInstanceId: string, request: ServerFileReadRequest): Promise<FileOperationDispatchResponse> {
|
||||
@@ -964,4 +964,12 @@ function serverFileListQuery(request: Partial<ServerFileListRequest>): 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);
|
||||
|
||||
@@ -596,7 +596,7 @@ function ServerFilesSection({ instance, session, operations }: ServerFilesSectio
|
||||
const [editor, setEditor] = useState<ServerFileEditorState>({ 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
|
||||
</div>
|
||||
<div className="server-file-toolbar">
|
||||
<div className="server-file-directory-tabs" role="tablist" aria-label="文件目录">
|
||||
{workspace.data.directories.length === 0 && <span className="provider-id">插件尚未声明可浏览目录</span>}
|
||||
{workspace.data.directories.map((directory) => (
|
||||
<button key={directory.key} type="button" className={cx("segmented-button", directory.key === directoryKey && "segmented-button-active")} onClick={() => { setDirectoryKey(directory.key); setRelativePath(""); }}>
|
||||
{directory.label}
|
||||
|
||||
Reference in New Issue
Block a user