Fix server file browse flow

This commit is contained in:
npc0-hue
2026-08-26 22:00:23 +08:00
parent 861ee752ed
commit 6369a8099a
22 changed files with 627 additions and 132 deletions
+6 -1
View File
@@ -281,6 +281,10 @@ describe("PlatformApiClient AI providers", () => {
expect(JSON.parse(String(init.body))).toEqual({ directoryKey: "configs", query: "server", recursive: true, idempotencyKey: "idem-file-list" });
return jsonResponse({ serverInstanceId: server.id, pluginId: plugin.id, directoryKey: "configs", state: "pending", entries: [], job: { ...job, id: "job-file-list", capability: "files.list", targetKey: "configs" }, reason: "queued" });
}
if (url.endsWith("/api/v1/server-instances/server-1/files/browse") && init?.method === "POST") {
expect(JSON.parse(String(init.body))).toEqual({ directoryKey: "configs", query: "server", recursive: true, idempotencyKey: "idem-file-browse" });
return jsonResponse({ serverInstanceId: server.id, pluginId: plugin.id, directoryKey: "configs", state: "ready", entries: [{ name: "server.properties", kind: "file", directoryKey: "configs", relativePath: "config/server.properties", logicalKey: "config/server.properties", scope: "config", sizeBytes: 42, editable: true, downloadable: true, remark: "配置文件" }], job: { ...job, id: "job-file-browse", capability: "files.list", targetKey: "configs" }, reason: "目录读取完成。" });
}
if (url.endsWith("/api/v1/server-instances/server-1/files/read-snapshot?key=config%2Fserver.properties")) {
return jsonResponse({ serverInstanceId: server.id, pluginId: plugin.id, key: "config/server.properties", state: "ready", content: "server.name=Example\n", version: 3, checksum: "sha256:filechecksum", sizeBytes: 20, readAt: "2026-07-03T00:00:00Z" });
}
@@ -595,6 +599,7 @@ describe("PlatformApiClient AI providers", () => {
await expect(client.getServerFileWorkspace(server.id)).resolves.toMatchObject({ defaultDirectoryKey: "configs", transfer: { channel: "run-file-transfer" } });
await expect(client.listServerFiles(server.id, { directoryKey: "configs", query: "server", recursive: true })).resolves.toMatchObject({ state: "declared", entries: [{ logicalKey: "config/server.properties" }] });
await expect(client.refreshServerFiles(server.id, { directoryKey: "configs", query: "server", recursive: true, idempotencyKey: "idem-file-list" })).resolves.toMatchObject({ state: "pending", job: { capability: "files.list" } });
await expect(client.browseServerFiles(server.id, { directoryKey: "configs", query: "server", recursive: true, idempotencyKey: "idem-file-browse" })).resolves.toMatchObject({ state: "ready", entries: [{ logicalKey: "config/server.properties" }] });
await expect(client.getServerFileReadSnapshot(server.id, "config/server.properties")).resolves.toMatchObject({ state: "ready", content: "server.name=Example\n" });
await expect(client.readServerFile(server.id, { key: "config/server.properties", idempotencyKey: "idem-file-read" })).resolves.toMatchObject({ operation: "read", job: { capability: "files.read" } });
await expect(client.writeServerFile(server.id, { key: "config/server.properties", content: "server.name=Example\n", expectedVersion: 3, expectedChecksum: "sha256:filechecksum", idempotencyKey: "idem-file-write" })).resolves.toMatchObject({ operation: "write", job: { capability: "files.write" } });
@@ -654,7 +659,7 @@ describe("PlatformApiClient AI providers", () => {
client.invokeAI({ requestId: "ai-1", serverInstanceId: server.id, purpose: "config.suggest", prompt: "Tune PVP safely", currentConfig: "server.name=Example Survival #1\n" })
).resolves.toMatchObject({ status: "ok", usage: { mocked: true }, configRecommendation: { diffSummary: "review required" } });
expect(fetchMock).toHaveBeenCalledTimes(44);
expect(fetchMock).toHaveBeenCalledTimes(45);
});
it("normalizes server file workspace null arrays from older platform responses", async () => {
+4
View File
@@ -596,6 +596,10 @@ export class PlatformApiClient {
return normalizeServerFileList(await this.request<ServerFileListResponse>(`/server-instances/${encodeURIComponent(serverInstanceId)}/files/refresh`, { method: "POST", body: request }));
}
async browseServerFiles(serverInstanceId: string, request: ServerFileListRequest): Promise<ServerFileListResponse> {
return normalizeServerFileList(await this.request<ServerFileListResponse>(`/server-instances/${encodeURIComponent(serverInstanceId)}/files/browse`, { method: "POST", body: request }));
}
async readServerFile(serverInstanceId: string, request: ServerFileReadRequest): Promise<FileOperationDispatchResponse> {
return this.request<FileOperationDispatchResponse>(`/server-instances/${encodeURIComponent(serverInstanceId)}/files/read`, { method: "POST", body: request });
}
+1 -1
View File
@@ -28,7 +28,7 @@ Normal browser login uses the platform's HttpOnly SameSite cookie and `credentia
- `listServerAdministratorCandidates`, `addServerAdministrator`, and `removeServerAdministrator` call server membership endpoints so server owners can invite or remove active non-platform-admin server administrators.
- Game-specific pages use the scoped `plugin-data` collection API and declared plugin bridge machine actions; Platform does not expose game-specific projection or workflow clients.
- `dispatchFileOperation` posts `FileOperationDispatchRequest` to `/file-operations/dispatch` using logical file keys and scoped refs rather than raw host paths; it remains the low-level compatibility dispatch for file work.
- `getServerFileWorkspace`, `listServerFiles`, `refreshServerFiles`, `readServerFile`, `getServerFileReadSnapshot`, `writeServerFile`, `uploadServerFile`, and `prepareServerFileDownload` power the first-party server-detail file manager. The page renders a generic server-root entry, requests live listings through `files.list`, reads snapshots through `files.read`, saves through `files.write`, and stages browser uploads as server-instance artifacts before Run pulls input chunks on the dedicated file-transfer channel. Server detail may call only these server-file APIs plus the encapsulated download helper; it must not call raw artifact-transfer methods directly.
- `getServerFileWorkspace`, `browseServerFiles`, `listServerFiles`, `refreshServerFiles`, `readServerFile`, `getServerFileReadSnapshot`, `writeServerFile`, `uploadServerFile`, and `prepareServerFileDownload` power the first-party server-detail file manager. The page renders a generic server-root entry and uses `browseServerFiles` as the live directory path for open, refresh, and search actions; compatibility `list`/`refresh` clients remain available for older flows. File reads use `files.read`, saves use `files.write`, and browser uploads stage server-instance artifacts before Run pulls input chunks on the dedicated file-transfer channel. Server detail may call only these server-file APIs plus the encapsulated download helper; it must not call raw artifact-transfer methods directly.
- `listArtifacts`, `openArtifactDownload`, and `readArtifactContent` use platform artifact routes for available job/server artifacts. Browser reads are chunked through `/artifacts/{id}/content` and must render only safe filenames, checksums, progress, and platform storage behavior.
- `authorizePluginBridge` posts `PluginBridgeAuthorizeRequest` to `/plugin-bridge/authorize` for preflight decisions.
- `executePluginBridge` posts `PluginBridgeExecuteRequest` to `/plugin-bridge/execute` from host-owned bridge dispatch utilities only. Plugin pages receive typed `PluginBridgeExecuteResponse` envelopes and never receive the platform API client, bearer token, raw provider key, run socket, host path, or storage credential.