first commit
This commit is contained in:
@@ -0,0 +1,27 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
describe("platformApiClient runtime environment", () => {
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
vi.unstubAllEnvs();
|
||||
vi.resetModules();
|
||||
});
|
||||
|
||||
it("uses VITE_PLATFORM_API_BASE_URL for the shared client", async () => {
|
||||
vi.stubEnv("VITE_PLATFORM_API_BASE_URL", "http://127.0.0.1:18080/api/v1");
|
||||
vi.resetModules();
|
||||
|
||||
const fetchMock = vi.fn(async () =>
|
||||
new Response(JSON.stringify({ items: [], count: 0 }), {
|
||||
status: 200,
|
||||
headers: { "Content-Type": "application/json" }
|
||||
})
|
||||
);
|
||||
vi.stubGlobal("fetch", fetchMock);
|
||||
|
||||
const { platformApiClient } = await import("./client");
|
||||
await expect(platformApiClient.listUsers()).resolves.toMatchObject({ count: 0 });
|
||||
|
||||
expect(fetchMock).toHaveBeenCalledWith("http://127.0.0.1:18080/api/v1/users", expect.any(Object));
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,506 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { PlatformApiClient, setPlatformApiSessionToken } from "./client";
|
||||
import type { AiProviderResponse, GamePluginResponse, JobResponse, MarketplacePluginResponse, RunEndpointResponse, ServerInstanceResponse } from "./types";
|
||||
|
||||
const provider: AiProviderResponse = {
|
||||
id: "ai.openai",
|
||||
name: "OpenAI",
|
||||
kind: "openai",
|
||||
baseUrl: "https://api.openai.com/v1",
|
||||
apiKeyRef: "secret://providers/openai",
|
||||
models: ["gpt-4.1"],
|
||||
defaultModel: "gpt-4.1",
|
||||
relayMode: "direct",
|
||||
timeoutMs: 30000,
|
||||
status: "active",
|
||||
redactionPolicy: "default"
|
||||
};
|
||||
|
||||
const plugin: GamePluginResponse = {
|
||||
id: "game.example",
|
||||
name: "Example Server",
|
||||
version: "0.1.0",
|
||||
serverType: "example",
|
||||
serverDisplayName: "Example Server",
|
||||
manifestRef: "artifact://manifests/game.example/0.1.0",
|
||||
createFormSchemaRef: "schemas/create-form.schema.json",
|
||||
requiredRunCapabilities: ["process.start", "logs.read"],
|
||||
declaredPermissions: ["server.read", "server.logs.read", "ai.invoke"],
|
||||
permissions: { ai: true, logs: true, files: false, jobs: false, artifacts: false },
|
||||
lifecycleActions: { start: "actions/start.json" },
|
||||
bridgeActions: ["server.instances.read", "logs.query", "ai.invoke"],
|
||||
pages: [{ key: "logs", title: "Logs", path: "/logs", permissions: ["server.logs.read"], bridgeActions: ["logs.query"] }],
|
||||
tags: ["example"],
|
||||
aiPurposes: ["logs.diagnose"],
|
||||
status: "installed"
|
||||
};
|
||||
|
||||
const marketplacePlugin: MarketplacePluginResponse = {
|
||||
id: "game.example",
|
||||
name: "Example Server",
|
||||
description: "Development plugin",
|
||||
version: "0.1.0",
|
||||
serverType: "example",
|
||||
serverDisplayName: "Example Server",
|
||||
supportedOs: ["linux", "darwin"],
|
||||
manifestRef: "artifact://manifests/game.example/0.1.0",
|
||||
createFormSchemaRef: "schemas/create-form.schema.json",
|
||||
capabilities: ["process.install", "process.start", "logs.read"],
|
||||
declaredPermissions: ["server.read", "server.logs.read", "ai.invoke"],
|
||||
permissions: { ai: true, logs: true, files: false, jobs: false, artifacts: false },
|
||||
lifecycleActions: { install: "actions/install.json", start: "actions/start.json", stop: "actions/stop.json" },
|
||||
bridgeActions: ["server.instances.read", "logs.query", "ai.invoke"],
|
||||
pages: [{ key: "logs", title: "Logs", path: "/logs", permissions: ["server.logs.read"], bridgeActions: ["logs.query"] }],
|
||||
tags: ["example"],
|
||||
aiPurposes: ["logs.diagnose"],
|
||||
status: "installed",
|
||||
source: "platform-registry"
|
||||
};
|
||||
|
||||
const server: ServerInstanceResponse = {
|
||||
id: "server-1",
|
||||
pluginId: "game.example",
|
||||
pluginVersion: "0.1.0",
|
||||
runEndpointId: "run-local",
|
||||
name: "Example Survival #1",
|
||||
ownerUserId: "user-owner",
|
||||
adminUserIds: ["user-admin-1"],
|
||||
state: "running",
|
||||
configVersion: 1,
|
||||
createdAt: "2026-07-03T00:00:00Z",
|
||||
updatedAt: "2026-07-03T00:00:00Z"
|
||||
};
|
||||
|
||||
const endpoint: RunEndpointResponse = {
|
||||
id: "run-local",
|
||||
displayName: "Local Run",
|
||||
version: "0.1.0",
|
||||
status: "online",
|
||||
capabilities: ["process.install", "process.start", "process.stop"],
|
||||
capacity: { maxJobs: 4, runningJobs: 0, queuedJobs: 1 },
|
||||
lastHeartbeatAt: "2026-07-03T00:00:00Z"
|
||||
};
|
||||
|
||||
const job: JobResponse = {
|
||||
id: "job-1",
|
||||
serverInstanceId: server.id,
|
||||
runEndpointId: endpoint.id,
|
||||
capability: "process.start",
|
||||
idempotencyKey: "idem-start",
|
||||
state: "queued",
|
||||
progress: { percent: 0, message: "queued" },
|
||||
createdAt: "2026-07-03T00:00:00Z",
|
||||
updatedAt: "2026-07-03T00:00:00Z"
|
||||
};
|
||||
|
||||
const artifact = {
|
||||
id: "artifact-1",
|
||||
ownerKind: "job",
|
||||
ownerId: job.id,
|
||||
sizeBytes: 18,
|
||||
checksum: "sha256:artifactchecksum",
|
||||
state: "available",
|
||||
createdAt: "2026-07-03T00:00:00Z",
|
||||
updatedAt: "2026-07-03T00:00:00Z"
|
||||
};
|
||||
|
||||
describe("PlatformApiClient AI providers", () => {
|
||||
afterEach(() => {
|
||||
setPlatformApiSessionToken(null);
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it("calls AI provider management endpoints with named contracts", async () => {
|
||||
const fetchMock = vi.fn(async (input: RequestInfo | URL, init?: RequestInit) => {
|
||||
const url = String(input);
|
||||
if (url.endsWith("/api/v1/ai-providers") && (!init?.method || init.method === "GET")) {
|
||||
return jsonResponse({ items: [provider], count: 1 });
|
||||
}
|
||||
if (url.endsWith("/api/v1/ai-providers") && init?.method === "POST") {
|
||||
return jsonResponse(provider);
|
||||
}
|
||||
if (url.endsWith("/api/v1/ai-providers/ai.openai") && init?.method === "PUT") {
|
||||
return jsonResponse({ ...provider, name: "OpenAI Relay" });
|
||||
}
|
||||
if (url.endsWith("/api/v1/ai-providers/ai.openai/status") && init?.method === "POST") {
|
||||
return jsonResponse({ ...provider, status: "disabled" });
|
||||
}
|
||||
if (url.endsWith("/api/v1/ai-providers/ai.openai/test") && init?.method === "POST") {
|
||||
return jsonResponse({ providerId: provider.id, mode: "metadata", success: true, message: "metadata validation passed" });
|
||||
}
|
||||
if (url.endsWith("/api/v1/ai-providers/ai.openai/models")) {
|
||||
return jsonResponse({ providerId: provider.id, defaultModel: provider.defaultModel, models: provider.models });
|
||||
}
|
||||
throw new Error(`unexpected request: ${url}`);
|
||||
});
|
||||
vi.stubGlobal("fetch", fetchMock);
|
||||
|
||||
const client = new PlatformApiClient();
|
||||
|
||||
await expect(client.listAiProviders()).resolves.toMatchObject({ count: 1 });
|
||||
await expect(client.createAiProvider(provider)).resolves.toMatchObject({ id: provider.id });
|
||||
await expect(client.updateAiProvider(provider.id, { ...provider, name: "OpenAI Relay" })).resolves.toMatchObject({ name: "OpenAI Relay" });
|
||||
await expect(client.setAiProviderStatus(provider.id, { status: "disabled" })).resolves.toMatchObject({ status: "disabled" });
|
||||
await expect(client.testAiProvider(provider.id)).resolves.toMatchObject({ success: true, mode: "metadata" });
|
||||
await expect(client.listAiProviderModels(provider.id)).resolves.toMatchObject({ models: ["gpt-4.1"] });
|
||||
|
||||
expect(fetchMock).toHaveBeenCalledTimes(6);
|
||||
});
|
||||
|
||||
it("calls console shell resource endpoints with named contracts", async () => {
|
||||
const fetchMock = vi.fn(async (input: RequestInfo | URL, init?: RequestInit) => {
|
||||
const url = String(input);
|
||||
if (url.endsWith("/healthz")) {
|
||||
return jsonResponse({ service: "platform", status: "ok", version: "0.1.0", time: "2026-07-03T00:00:00Z" });
|
||||
}
|
||||
if (url.endsWith("/api/v1/game-plugins")) {
|
||||
return jsonResponse({ items: [plugin], count: 1 });
|
||||
}
|
||||
if (url.endsWith("/api/v1/server-instances") && (!init?.method || init.method === "GET")) {
|
||||
return jsonResponse({ items: [server], count: 1 });
|
||||
}
|
||||
if (url.endsWith("/api/v1/metrics/platform")) {
|
||||
return jsonResponse({ cpuPercent: 28, memoryPercent: 42, diskPercent: 19, source: "platform-derived", collectedAt: "2026-07-03T00:00:00Z" });
|
||||
}
|
||||
if (url.endsWith("/api/v1/metrics/server-instances")) {
|
||||
return jsonResponse({
|
||||
items: [
|
||||
{
|
||||
serverInstanceId: server.id,
|
||||
online: true,
|
||||
playerCount: 5,
|
||||
maxPlayers: 20,
|
||||
tps: 19.8,
|
||||
latencyMs: 42,
|
||||
cpuPercent: 31,
|
||||
memoryPercent: 44,
|
||||
diskPercent: 22,
|
||||
source: "platform-derived",
|
||||
collectedAt: "2026-07-03T00:00:00Z"
|
||||
}
|
||||
],
|
||||
count: 1
|
||||
});
|
||||
}
|
||||
if (url.endsWith("/api/v1/server-instances/server-1/config")) {
|
||||
return jsonResponse({
|
||||
serverInstanceId: server.id,
|
||||
configVersion: 1,
|
||||
format: "properties",
|
||||
key: "server.properties",
|
||||
content: "server.name=Example Survival #1\n",
|
||||
source: "platform-derived",
|
||||
updatedAt: "2026-07-03T00:00:00Z"
|
||||
});
|
||||
}
|
||||
if (url.endsWith("/api/v1/server-instances/server-1/config/diff") && init?.method === "POST") {
|
||||
expect(JSON.parse(String(init.body))).toEqual({
|
||||
expectedConfigVersion: 1,
|
||||
key: "server.properties",
|
||||
proposedContent: "server.name=Example Survival #2\n"
|
||||
});
|
||||
return jsonResponse({
|
||||
serverInstanceId: server.id,
|
||||
configVersion: 1,
|
||||
key: "server.properties",
|
||||
currentContent: "server.name=Example Survival #1\n",
|
||||
proposedContent: "server.name=Example Survival #2\n",
|
||||
diff: [
|
||||
{ kind: "removed", oldNumber: 1, content: "server.name=Example Survival #1" },
|
||||
{ kind: "added", newNumber: 1, content: "server.name=Example Survival #2" }
|
||||
],
|
||||
hasChanges: true,
|
||||
source: "platform-review",
|
||||
reviewedAt: "2026-07-03T00:00:00Z"
|
||||
});
|
||||
}
|
||||
if (url.endsWith("/api/v1/server-instances/server-1/config/approve") && init?.method === "POST") {
|
||||
expect(JSON.parse(String(init.body))).toEqual({
|
||||
expectedConfigVersion: 1,
|
||||
key: "server.properties",
|
||||
proposedContent: "server.name=Example Survival #2\n",
|
||||
idempotencyKey: "idem-config"
|
||||
});
|
||||
return jsonResponse({
|
||||
status: "queued",
|
||||
preview: {
|
||||
serverInstanceId: server.id,
|
||||
configVersion: 1,
|
||||
key: "server.properties",
|
||||
currentContent: "server.name=Example Survival #1\n",
|
||||
proposedContent: "server.name=Example Survival #2\n",
|
||||
diff: [{ kind: "added", newNumber: 1, content: "server.name=Example Survival #2" }],
|
||||
hasChanges: true,
|
||||
source: "platform-review",
|
||||
reviewedAt: "2026-07-03T00:00:00Z"
|
||||
},
|
||||
job: { ...job, id: "job-config-write", capability: "config.write", targetKey: "server.properties", inputRef: "input://server-config/server-1/server.properties/v1" }
|
||||
});
|
||||
}
|
||||
if (url.endsWith("/api/v1/file-operations/dispatch") && init?.method === "POST") {
|
||||
expect(JSON.parse(String(init.body))).toEqual({
|
||||
serverInstanceId: server.id,
|
||||
operation: "read",
|
||||
key: "logs/latest.log",
|
||||
idempotencyKey: "idem-file"
|
||||
});
|
||||
return jsonResponse({
|
||||
status: "queued",
|
||||
serverInstanceId: server.id,
|
||||
operation: "read",
|
||||
key: "logs/latest.log",
|
||||
job: { ...job, id: "job-file-read", capability: "files.read", targetKey: "logs/latest.log" }
|
||||
});
|
||||
}
|
||||
if (url.endsWith("/api/v1/run/endpoints")) {
|
||||
return jsonResponse({ items: [endpoint], count: 1 });
|
||||
}
|
||||
if (url.endsWith("/api/v1/jobs")) {
|
||||
return jsonResponse({ items: [job], count: 1 });
|
||||
}
|
||||
if (url.endsWith("/api/v1/jobs?serverInstanceId=server-1")) {
|
||||
return jsonResponse({ items: [job], count: 1 });
|
||||
}
|
||||
if (url.endsWith("/api/v1/artifacts?ownerKind=job&ownerId=job-1&state=available")) {
|
||||
return jsonResponse({ items: [artifact], count: 1 });
|
||||
}
|
||||
if (url.endsWith("/api/v1/artifacts/artifact-1/download") && init?.method === "POST") {
|
||||
return jsonResponse({
|
||||
artifactId: artifact.id,
|
||||
ownerKind: artifact.ownerKind,
|
||||
ownerId: artifact.ownerId,
|
||||
filename: "artifact-1.bin",
|
||||
contentType: "application/octet-stream",
|
||||
sizeBytes: artifact.sizeBytes,
|
||||
checksum: artifact.checksum,
|
||||
state: artifact.state,
|
||||
downloadUrl: "/api/v1/artifacts/artifact-1/content",
|
||||
expiresAt: "2026-07-03T00:15:00Z",
|
||||
rangeSupported: true,
|
||||
chunkSizeBytes: 1048576,
|
||||
storageBehavior: "platform-memory-transfer-session"
|
||||
});
|
||||
}
|
||||
if (url.endsWith("/api/v1/artifacts/artifact-1/content?offset=0&limit=8")) {
|
||||
return new Response(new TextEncoder().encode("artifact").buffer, {
|
||||
status: 206,
|
||||
headers: {
|
||||
"Content-Type": "application/octet-stream",
|
||||
"Content-Length": "8",
|
||||
"Content-Range": "bytes 0-7/18",
|
||||
"X-Artifact-Id": artifact.id,
|
||||
"X-Artifact-Checksum": artifact.checksum,
|
||||
"X-Artifact-Content-Checksum": "sha256:chunkchecksum",
|
||||
"X-Artifact-Storage": "platform-memory-transfer-session"
|
||||
}
|
||||
});
|
||||
}
|
||||
if (url.endsWith("/api/v1/server-instances/workflows/create") && init?.method === "POST") {
|
||||
return jsonResponse({ accepted: true, action: "create", instance: { ...server, state: "installing" }, job: { ...job, capability: "process.install" } });
|
||||
}
|
||||
if (url.endsWith("/api/v1/server-instances/server-1/start") && init?.method === "POST") {
|
||||
return jsonResponse({ accepted: true, action: "start", instance: server, job });
|
||||
}
|
||||
if (url.endsWith("/api/v1/server-instances/server-1/stop") && init?.method === "POST") {
|
||||
return jsonResponse({ accepted: true, action: "stop", instance: server, job: { ...job, capability: "process.stop" } });
|
||||
}
|
||||
if (url.endsWith("/api/v1/server-instances/server-1/administrators/candidates") && (!init?.method || init.method === "GET")) {
|
||||
return jsonResponse({ items: [{ id: "user-2", displayName: "Helper", status: "active", roles: ["server-admin"] }], count: 1 });
|
||||
}
|
||||
if (url.endsWith("/api/v1/server-instances/server-1/administrators") && init?.method === "POST") {
|
||||
return jsonResponse({ ...server, adminUserIds: [...server.adminUserIds, "user-2"] });
|
||||
}
|
||||
if (url.endsWith("/api/v1/server-instances/server-1/administrators/user-2") && init?.method === "DELETE") {
|
||||
return jsonResponse({ ...server, adminUserIds: [] });
|
||||
}
|
||||
if (url.endsWith("/api/v1/plugin-bridge/authorize") && init?.method === "POST") {
|
||||
return jsonResponse({
|
||||
pluginId: plugin.id,
|
||||
routeKey: "logs",
|
||||
action: "logs.query",
|
||||
allowed: true,
|
||||
requiredPermissions: ["server.logs.read"],
|
||||
effectivePermissions: ["server.logs.read"]
|
||||
});
|
||||
}
|
||||
if (url.endsWith("/api/v1/plugin-bridge/execute") && init?.method === "POST") {
|
||||
expect(JSON.parse(String(init.body))).toEqual({
|
||||
requestId: "req-bridge",
|
||||
pluginId: plugin.id,
|
||||
routeKey: "logs",
|
||||
serverInstanceId: server.id,
|
||||
action: "logs.query",
|
||||
payload: { logStreamId: "log-1" }
|
||||
});
|
||||
return jsonResponse({
|
||||
requestId: "req-bridge",
|
||||
pluginId: plugin.id,
|
||||
routeKey: "logs",
|
||||
serverInstanceId: server.id,
|
||||
action: "logs.query",
|
||||
status: "ok",
|
||||
result: { entryCount: "0" }
|
||||
});
|
||||
}
|
||||
if (url.endsWith("/api/v1/ai/invocations") && init?.method === "POST") {
|
||||
expect(JSON.parse(String(init.body))).toEqual({
|
||||
requestId: "ai-1",
|
||||
serverInstanceId: server.id,
|
||||
purpose: "config.suggest",
|
||||
prompt: "Tune PVP safely",
|
||||
currentConfig: "server.name=Example Survival #1\n"
|
||||
});
|
||||
return jsonResponse({
|
||||
requestId: "ai-1",
|
||||
purpose: "config.suggest",
|
||||
providerId: "ai.openai",
|
||||
model: "gpt-4.1",
|
||||
status: "ok",
|
||||
recommendation: "Review before applying.",
|
||||
configRecommendation: { key: "server.properties", suggestedConfig: "server.name=Example Survival #1\npvp=false\n", diffSummary: "review required" },
|
||||
usage: { providerId: "ai.openai", model: "gpt-4.1", inputTokens: 20, outputTokens: 12, mocked: true }
|
||||
});
|
||||
}
|
||||
throw new Error(`unexpected request: ${url}`);
|
||||
});
|
||||
vi.stubGlobal("fetch", fetchMock);
|
||||
|
||||
const client = new PlatformApiClient();
|
||||
|
||||
await expect(client.health()).resolves.toMatchObject({ status: "ok" });
|
||||
await expect(client.listGamePlugins()).resolves.toMatchObject({ count: 1 });
|
||||
await expect(client.listServerInstances()).resolves.toMatchObject({ count: 1 });
|
||||
await expect(client.getPlatformResourceUsage()).resolves.toMatchObject({ source: "platform-derived", cpuPercent: 28 });
|
||||
await expect(client.listServerMetrics()).resolves.toMatchObject({ count: 1, items: [{ serverInstanceId: server.id, online: true }] });
|
||||
await expect(client.getServerConfig(server.id)).resolves.toMatchObject({ content: "server.name=Example Survival #1\n" });
|
||||
await expect(
|
||||
client.previewServerConfigDiff(server.id, { expectedConfigVersion: 1, key: "server.properties", proposedContent: "server.name=Example Survival #2\n" })
|
||||
).resolves.toMatchObject({ hasChanges: true, source: "platform-review" });
|
||||
await expect(
|
||||
client.approveServerConfigWrite(server.id, { expectedConfigVersion: 1, key: "server.properties", proposedContent: "server.name=Example Survival #2\n", idempotencyKey: "idem-config" })
|
||||
).resolves.toMatchObject({ status: "queued", job: { capability: "config.write", targetKey: "server.properties" } });
|
||||
await expect(client.dispatchFileOperation({ serverInstanceId: server.id, operation: "read", key: "logs/latest.log", idempotencyKey: "idem-file" })).resolves.toMatchObject({
|
||||
status: "queued",
|
||||
job: { capability: "files.read", targetKey: "logs/latest.log" }
|
||||
});
|
||||
await expect(client.listRunEndpoints()).resolves.toMatchObject({ count: 1 });
|
||||
await expect(client.listJobs()).resolves.toMatchObject({ count: 1 });
|
||||
await expect(client.listJobs(server.id)).resolves.toMatchObject({ count: 1 });
|
||||
await expect(client.listArtifacts({ ownerKind: "job", ownerId: job.id, state: "available" })).resolves.toMatchObject({ count: 1, items: [{ id: artifact.id }] });
|
||||
await expect(client.openArtifactDownload(artifact.id)).resolves.toMatchObject({ downloadUrl: "/api/v1/artifacts/artifact-1/content", rangeSupported: true });
|
||||
await expect(client.readArtifactContent(artifact.id, 0, 8)).resolves.toMatchObject({ contentLength: 8, contentRange: "bytes 0-7/18", checksum: artifact.checksum });
|
||||
await expect(client.createServerWorkflow({ id: "server-2", pluginId: plugin.id, runEndpointId: endpoint.id, name: "Server 2", idempotencyKey: "idem-create" })).resolves.toMatchObject({
|
||||
action: "create"
|
||||
});
|
||||
await expect(client.startServerInstance(server.id, { expectedConfigVersion: 1, idempotencyKey: "idem-start" })).resolves.toMatchObject({ action: "start" });
|
||||
await expect(client.stopServerInstance(server.id, { expectedConfigVersion: 1, idempotencyKey: "idem-stop" })).resolves.toMatchObject({ action: "stop" });
|
||||
await expect(client.listServerAdministratorCandidates(server.id)).resolves.toMatchObject({ count: 1 });
|
||||
await expect(client.addServerAdministrator(server.id, { userId: "user-2" })).resolves.toMatchObject({ adminUserIds: ["user-admin-1", "user-2"] });
|
||||
await expect(client.removeServerAdministrator(server.id, "user-2")).resolves.toMatchObject({ adminUserIds: [] });
|
||||
await expect(client.authorizePluginBridge({ pluginId: plugin.id, routeKey: "logs", action: "logs.query" })).resolves.toMatchObject({
|
||||
allowed: true
|
||||
});
|
||||
await expect(
|
||||
client.executePluginBridge({ requestId: "req-bridge", pluginId: plugin.id, routeKey: "logs", serverInstanceId: server.id, action: "logs.query", payload: { logStreamId: "log-1" } })
|
||||
).resolves.toMatchObject({ status: "ok", result: { entryCount: "0" } });
|
||||
await expect(
|
||||
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(24);
|
||||
});
|
||||
|
||||
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);
|
||||
if (url.endsWith("/api/v1/plugin-marketplace/plugins?status=installed&serverType=example&capability=logs.read&keyword=example")) {
|
||||
return jsonResponse({ items: [marketplacePlugin], count: 1 });
|
||||
}
|
||||
if (url.endsWith("/api/v1/plugin-marketplace/plugins/game.example") && (!init?.method || init.method === "GET")) {
|
||||
return jsonResponse(marketplacePlugin);
|
||||
}
|
||||
if (url.endsWith("/api/v1/plugin-marketplace/plugins/game.example/state") && init?.method === "POST") {
|
||||
expect(JSON.parse(String(init.body))).toEqual({ action: "disable" });
|
||||
return jsonResponse({ ...marketplacePlugin, status: "disabled" });
|
||||
}
|
||||
throw new Error(`unexpected request: ${url}`);
|
||||
});
|
||||
vi.stubGlobal("fetch", fetchMock);
|
||||
|
||||
const client = new PlatformApiClient();
|
||||
|
||||
await expect(client.listMarketplacePlugins({ status: "installed", serverType: "example", capability: "logs.read", keyword: "example" })).resolves.toMatchObject({ count: 1 });
|
||||
await expect(client.getMarketplacePlugin(marketplacePlugin.id)).resolves.toMatchObject({ source: "platform-registry" });
|
||||
await expect(client.setMarketplacePluginState(marketplacePlugin.id, { action: "disable" })).resolves.toMatchObject({ status: "disabled" });
|
||||
|
||||
expect(fetchMock).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
|
||||
it("surfaces config diff preview failures from the platform", async () => {
|
||||
const fetchMock = vi.fn(async (input: RequestInfo | URL, init?: RequestInit) => {
|
||||
const url = String(input);
|
||||
if (url.endsWith("/api/v1/server-instances/server-1/config/diff") && init?.method === "POST") {
|
||||
return new Response(JSON.stringify({ code: "validation", message: "expectedConfigVersion must match server instance" }), {
|
||||
status: 400,
|
||||
headers: { "Content-Type": "application/json" }
|
||||
});
|
||||
}
|
||||
throw new Error(`unexpected request: ${url}`);
|
||||
});
|
||||
vi.stubGlobal("fetch", fetchMock);
|
||||
|
||||
const client = new PlatformApiClient();
|
||||
|
||||
await expect(client.previewServerConfigDiff(server.id, { expectedConfigVersion: 0, key: "server.properties", proposedContent: "changed=true\n" })).rejects.toThrow(
|
||||
"expectedConfigVersion must match server instance"
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps raw key fields out of provider responses", () => {
|
||||
expect("apiKey" in provider).toBe(false);
|
||||
expect("rawApiKey" in provider).toBe(false);
|
||||
expect(provider.apiKeyRef).toBe("secret://providers/openai");
|
||||
});
|
||||
|
||||
it("calls auth endpoints and attaches bearer sessions", async () => {
|
||||
const fetchMock = vi.fn(async (input: RequestInfo | URL, init?: RequestInit) => {
|
||||
const url = String(input);
|
||||
if (url.endsWith("/api/v1/auth/login") && init?.method === "POST") {
|
||||
return jsonResponse({
|
||||
user: { id: "user-admin", displayName: "Operator", status: "active", roles: ["platform-admin"] },
|
||||
sessionId: "session-token",
|
||||
status: "authenticated",
|
||||
message: "登录成功"
|
||||
});
|
||||
}
|
||||
if (url.endsWith("/api/v1/users/current")) {
|
||||
expect(new Headers(init?.headers).get("Authorization")).toBe("Bearer session-token");
|
||||
return jsonResponse({ id: "user-admin", displayName: "Operator", status: "active", roles: ["platform-admin"] });
|
||||
}
|
||||
if (url.endsWith("/api/v1/auth/logout") && init?.method === "POST") {
|
||||
expect(new Headers(init?.headers).get("Authorization")).toBe("Bearer session-token");
|
||||
return new Response(null, { status: 204 });
|
||||
}
|
||||
throw new Error(`unexpected request: ${url}`);
|
||||
});
|
||||
vi.stubGlobal("fetch", fetchMock);
|
||||
|
||||
const client = new PlatformApiClient();
|
||||
const login = await client.login({ account: "operator.local@example.test", password: "operator-local" });
|
||||
expect(login.sessionId).toBe("session-token");
|
||||
|
||||
setPlatformApiSessionToken(login.sessionId ?? null);
|
||||
await expect(client.getCurrentUser()).resolves.toMatchObject({ id: "user-admin" });
|
||||
await expect(client.logout()).resolves.toBeUndefined();
|
||||
|
||||
expect(fetchMock).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
});
|
||||
|
||||
function jsonResponse(body: unknown): Response {
|
||||
return new Response(JSON.stringify(body), {
|
||||
status: 200,
|
||||
headers: { "Content-Type": "application/json" }
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,414 @@
|
||||
import type {
|
||||
AiProviderListResponse,
|
||||
AiProviderModelsResponse,
|
||||
AiProviderRequest,
|
||||
AiProviderResponse,
|
||||
AiProviderStatusRequest,
|
||||
AiProviderTestResponse,
|
||||
AiProviderUpdateRequest,
|
||||
AIInvocationRequest,
|
||||
AIInvocationResponse,
|
||||
ApiErrorResponse,
|
||||
ArtifactContentChunk,
|
||||
ArtifactDownloadReferenceResponse,
|
||||
ArtifactFilterRequest,
|
||||
ArtifactListResponse,
|
||||
AuthSessionResponse,
|
||||
AuditEventListResponse,
|
||||
CurrentUserResponse,
|
||||
FileOperationDispatchRequest,
|
||||
FileOperationDispatchResponse,
|
||||
GamePluginListResponse,
|
||||
HealthResponse,
|
||||
JobCreateRequest,
|
||||
JobListResponse,
|
||||
JobResponse,
|
||||
LlmConfigSuggestionRequest,
|
||||
LlmConfigSuggestionResponse,
|
||||
LogStreamCursorRequest,
|
||||
LogStreamCursorResponse,
|
||||
LogStreamListResponse,
|
||||
LoginRequest,
|
||||
MarketplacePluginFilterRequest,
|
||||
MarketplacePluginListResponse,
|
||||
MarketplacePluginResponse,
|
||||
MarketplacePluginStateRequest,
|
||||
PlatformResourceUsageResponse,
|
||||
PluginBridgeAuthorizeRequest,
|
||||
PluginBridgeAuthorizeResponse,
|
||||
PluginBridgeExecuteRequest,
|
||||
PluginBridgeExecuteResponse,
|
||||
RegisterRequest,
|
||||
RunEndpointListResponse,
|
||||
ServerConfigResponse,
|
||||
ServerConfigDiffPreviewRequest,
|
||||
ServerConfigDiffPreviewResponse,
|
||||
ServerLifecycleCommandRequest,
|
||||
ServerLifecycleCreateRequest,
|
||||
ServerLifecycleResponse,
|
||||
ServerConfigWriteApprovalRequest,
|
||||
ServerConfigWriteDispatchResponse,
|
||||
ServerInstanceListResponse,
|
||||
ServerInstanceResponse,
|
||||
ServerMemberListResponse,
|
||||
ServerMemberRequest,
|
||||
ServerMetricsListResponse,
|
||||
UserCreateRequest,
|
||||
UserListResponse,
|
||||
UserProfileUpdateRequest,
|
||||
UserResponse,
|
||||
UserThemePreferenceRequest,
|
||||
UserThemePreferenceResponse,
|
||||
UserUpdateRequest
|
||||
} from "./types";
|
||||
import { readWebRuntimeEnv } from "../schemas/env";
|
||||
|
||||
let platformApiSessionToken: string | null = null;
|
||||
|
||||
export function setPlatformApiSessionToken(token: string | null) {
|
||||
platformApiSessionToken = token;
|
||||
}
|
||||
|
||||
export class PlatformApiClient {
|
||||
constructor(private readonly baseUrl = "/api/v1", private readonly sessionTokenProvider: () => string | null = () => platformApiSessionToken) {}
|
||||
|
||||
async health(): Promise<HealthResponse> {
|
||||
return this.request<HealthResponse>("/healthz", { absolute: true });
|
||||
}
|
||||
|
||||
async listGamePlugins(): Promise<GamePluginListResponse> {
|
||||
return this.request<GamePluginListResponse>("/game-plugins");
|
||||
}
|
||||
|
||||
async listMarketplacePlugins(filter: MarketplacePluginFilterRequest = {}): Promise<MarketplacePluginListResponse> {
|
||||
return this.request<MarketplacePluginListResponse>(`/plugin-marketplace/plugins${marketplaceQuery(filter)}`);
|
||||
}
|
||||
|
||||
async getMarketplacePlugin(id: string): Promise<MarketplacePluginResponse> {
|
||||
return this.request<MarketplacePluginResponse>(`/plugin-marketplace/plugins/${encodeURIComponent(id)}`);
|
||||
}
|
||||
|
||||
async setMarketplacePluginState(id: string, request: MarketplacePluginStateRequest): Promise<MarketplacePluginResponse> {
|
||||
return this.request<MarketplacePluginResponse>(`/plugin-marketplace/plugins/${encodeURIComponent(id)}/state`, {
|
||||
method: "POST",
|
||||
body: request
|
||||
});
|
||||
}
|
||||
|
||||
async listServerInstances(): Promise<ServerInstanceListResponse> {
|
||||
return this.request<ServerInstanceListResponse>("/server-instances");
|
||||
}
|
||||
|
||||
async createServerWorkflow(request: ServerLifecycleCreateRequest): Promise<ServerLifecycleResponse> {
|
||||
return this.request<ServerLifecycleResponse>("/server-instances/workflows/create", {
|
||||
method: "POST",
|
||||
body: request
|
||||
});
|
||||
}
|
||||
|
||||
async startServerInstance(id: string, request: ServerLifecycleCommandRequest): Promise<ServerLifecycleResponse> {
|
||||
return this.request<ServerLifecycleResponse>(`/server-instances/${encodeURIComponent(id)}/start`, {
|
||||
method: "POST",
|
||||
body: request
|
||||
});
|
||||
}
|
||||
|
||||
async stopServerInstance(id: string, request: ServerLifecycleCommandRequest): Promise<ServerLifecycleResponse> {
|
||||
return this.request<ServerLifecycleResponse>(`/server-instances/${encodeURIComponent(id)}/stop`, {
|
||||
method: "POST",
|
||||
body: request
|
||||
});
|
||||
}
|
||||
|
||||
async listServerAdministratorCandidates(id: string): Promise<ServerMemberListResponse> {
|
||||
return this.request<ServerMemberListResponse>(`/server-instances/${encodeURIComponent(id)}/administrators/candidates`);
|
||||
}
|
||||
|
||||
async addServerAdministrator(id: string, request: ServerMemberRequest): Promise<ServerInstanceResponse> {
|
||||
return this.request<ServerInstanceResponse>(`/server-instances/${encodeURIComponent(id)}/administrators`, {
|
||||
method: "POST",
|
||||
body: request
|
||||
});
|
||||
}
|
||||
|
||||
async removeServerAdministrator(id: string, userId: string): Promise<ServerInstanceResponse> {
|
||||
return this.request<ServerInstanceResponse>(`/server-instances/${encodeURIComponent(id)}/administrators/${encodeURIComponent(userId)}`, {
|
||||
method: "DELETE"
|
||||
});
|
||||
}
|
||||
|
||||
async listRunEndpoints(): Promise<RunEndpointListResponse> {
|
||||
return this.request<RunEndpointListResponse>("/run/endpoints");
|
||||
}
|
||||
|
||||
async listJobs(serverInstanceId?: string): Promise<JobListResponse> {
|
||||
const params = serverInstanceId ? `?serverInstanceId=${encodeURIComponent(serverInstanceId)}` : "";
|
||||
return this.request<JobListResponse>(`/jobs${params}`);
|
||||
}
|
||||
|
||||
async listArtifacts(filter: ArtifactFilterRequest = {}): Promise<ArtifactListResponse> {
|
||||
return this.request<ArtifactListResponse>(`/artifacts${artifactQuery(filter)}`);
|
||||
}
|
||||
|
||||
async openArtifactDownload(id: string): Promise<ArtifactDownloadReferenceResponse> {
|
||||
return this.request<ArtifactDownloadReferenceResponse>(`/artifacts/${encodeURIComponent(id)}/download`, { method: "POST", body: {} });
|
||||
}
|
||||
|
||||
async readArtifactContent(id: string, offset = 0, limit?: number): Promise<ArtifactContentChunk> {
|
||||
const params = new URLSearchParams({ offset: String(offset) });
|
||||
if (limit !== undefined) {
|
||||
params.set("limit", String(limit));
|
||||
}
|
||||
const headers = new Headers();
|
||||
const sessionToken = this.sessionTokenProvider();
|
||||
if (sessionToken) {
|
||||
headers.set("Authorization", `Bearer ${sessionToken}`);
|
||||
}
|
||||
const response = await fetch(`${this.baseUrl}/artifacts/${encodeURIComponent(id)}/content?${params.toString()}`, { headers });
|
||||
if (!response.ok) {
|
||||
const apiError = await safeReadError(response);
|
||||
throw new Error(apiError?.message ?? `request failed: ${response.status}`);
|
||||
}
|
||||
const payload = await response.arrayBuffer();
|
||||
return {
|
||||
artifactId: response.headers.get("X-Artifact-Id") ?? undefined,
|
||||
payload,
|
||||
contentType: response.headers.get("Content-Type") ?? "application/octet-stream",
|
||||
contentLength: Number(response.headers.get("Content-Length") ?? payload.byteLength),
|
||||
contentRange: response.headers.get("Content-Range") ?? undefined,
|
||||
checksum: response.headers.get("X-Artifact-Checksum") ?? undefined,
|
||||
contentChecksum: response.headers.get("X-Artifact-Content-Checksum") ?? undefined,
|
||||
storageBehavior: response.headers.get("X-Artifact-Storage") ?? undefined
|
||||
};
|
||||
}
|
||||
|
||||
async getJob(id: string): Promise<JobResponse> {
|
||||
return this.request<JobResponse>(`/jobs/${encodeURIComponent(id)}`);
|
||||
}
|
||||
|
||||
async createJob(request: JobCreateRequest): Promise<JobResponse> {
|
||||
return this.request<JobResponse>("/jobs", { method: "POST", body: request });
|
||||
}
|
||||
|
||||
async register(request: RegisterRequest): Promise<AuthSessionResponse> {
|
||||
return this.request<AuthSessionResponse>("/auth/register", { method: "POST", body: request });
|
||||
}
|
||||
|
||||
async login(request: LoginRequest): Promise<AuthSessionResponse> {
|
||||
return this.request<AuthSessionResponse>("/auth/login", { method: "POST", body: request });
|
||||
}
|
||||
|
||||
async logout(): Promise<void> {
|
||||
await this.request<void>("/auth/logout", { method: "POST", parseJson: false });
|
||||
}
|
||||
|
||||
async getCurrentUser(): Promise<CurrentUserResponse> {
|
||||
return this.request<CurrentUserResponse>("/users/current");
|
||||
}
|
||||
|
||||
async listUsers(): Promise<UserListResponse> {
|
||||
return this.request<UserListResponse>("/users");
|
||||
}
|
||||
|
||||
async createUser(request: UserCreateRequest): Promise<UserResponse> {
|
||||
return this.request<UserResponse>("/users", { method: "POST", body: request });
|
||||
}
|
||||
|
||||
async updateUser(id: string, request: UserUpdateRequest): Promise<UserResponse> {
|
||||
return this.request<UserResponse>(`/users/${encodeURIComponent(id)}`, { method: "PUT", body: request });
|
||||
}
|
||||
|
||||
async updateCurrentUserProfile(request: UserProfileUpdateRequest): Promise<CurrentUserResponse> {
|
||||
return this.request<CurrentUserResponse>("/users/current/profile", { method: "PUT", body: request });
|
||||
}
|
||||
|
||||
async updateCurrentUserTheme(request: UserThemePreferenceRequest): Promise<UserThemePreferenceResponse> {
|
||||
return this.request<UserThemePreferenceResponse>("/users/current/theme", { method: "PUT", body: request });
|
||||
}
|
||||
|
||||
async getServerInstance(id: string): Promise<ServerInstanceResponse> {
|
||||
return this.request<ServerInstanceResponse>(`/server-instances/${encodeURIComponent(id)}`);
|
||||
}
|
||||
|
||||
async getPlatformResourceUsage(): Promise<PlatformResourceUsageResponse> {
|
||||
return this.request<PlatformResourceUsageResponse>("/metrics/platform");
|
||||
}
|
||||
|
||||
async listServerMetrics(): Promise<ServerMetricsListResponse> {
|
||||
return this.request<ServerMetricsListResponse>("/metrics/server-instances");
|
||||
}
|
||||
|
||||
async getServerConfig(id: string): Promise<ServerConfigResponse> {
|
||||
return this.request<ServerConfigResponse>(`/server-instances/${encodeURIComponent(id)}/config`);
|
||||
}
|
||||
|
||||
async previewServerConfigDiff(id: string, request: ServerConfigDiffPreviewRequest): Promise<ServerConfigDiffPreviewResponse> {
|
||||
return this.request<ServerConfigDiffPreviewResponse>(`/server-instances/${encodeURIComponent(id)}/config/diff`, {
|
||||
method: "POST",
|
||||
body: request
|
||||
});
|
||||
}
|
||||
|
||||
async approveServerConfigWrite(id: string, request: ServerConfigWriteApprovalRequest): Promise<ServerConfigWriteDispatchResponse> {
|
||||
return this.request<ServerConfigWriteDispatchResponse>(`/server-instances/${encodeURIComponent(id)}/config/approve`, {
|
||||
method: "POST",
|
||||
body: request
|
||||
});
|
||||
}
|
||||
|
||||
async dispatchFileOperation(request: FileOperationDispatchRequest): Promise<FileOperationDispatchResponse> {
|
||||
return this.request<FileOperationDispatchResponse>("/file-operations/dispatch", {
|
||||
method: "POST",
|
||||
body: request
|
||||
});
|
||||
}
|
||||
|
||||
async listLogStreams(): Promise<LogStreamListResponse> {
|
||||
return this.request<LogStreamListResponse>("/log-streams");
|
||||
}
|
||||
|
||||
async queryLogStream(request: LogStreamCursorRequest): Promise<LogStreamCursorResponse> {
|
||||
return this.request<LogStreamCursorResponse>("/log-streams/query", { method: "POST", body: request });
|
||||
}
|
||||
|
||||
async listAuditEvents(): Promise<AuditEventListResponse> {
|
||||
return this.request<AuditEventListResponse>("/audit-events");
|
||||
}
|
||||
|
||||
async suggestServerConfig(request: LlmConfigSuggestionRequest): Promise<LlmConfigSuggestionResponse> {
|
||||
return this.request<LlmConfigSuggestionResponse>("/ai/config-suggestions", { method: "POST", body: request });
|
||||
}
|
||||
|
||||
async invokeAI(request: AIInvocationRequest): Promise<AIInvocationResponse> {
|
||||
return this.request<AIInvocationResponse>("/ai/invocations", { method: "POST", body: request });
|
||||
}
|
||||
|
||||
async authorizePluginBridge(request: PluginBridgeAuthorizeRequest): Promise<PluginBridgeAuthorizeResponse> {
|
||||
return this.request<PluginBridgeAuthorizeResponse>("/plugin-bridge/authorize", {
|
||||
method: "POST",
|
||||
body: request
|
||||
});
|
||||
}
|
||||
|
||||
async executePluginBridge(request: PluginBridgeExecuteRequest): Promise<PluginBridgeExecuteResponse> {
|
||||
return this.request<PluginBridgeExecuteResponse>("/plugin-bridge/execute", {
|
||||
method: "POST",
|
||||
body: request
|
||||
});
|
||||
}
|
||||
|
||||
async listAiProviders(): Promise<AiProviderListResponse> {
|
||||
return this.request<AiProviderListResponse>("/ai-providers");
|
||||
}
|
||||
|
||||
async createAiProvider(request: AiProviderRequest): Promise<AiProviderResponse> {
|
||||
return this.request<AiProviderResponse>("/ai-providers", {
|
||||
method: "POST",
|
||||
body: request
|
||||
});
|
||||
}
|
||||
|
||||
async updateAiProvider(id: string, request: AiProviderUpdateRequest): Promise<AiProviderResponse> {
|
||||
return this.request<AiProviderResponse>(`/ai-providers/${encodeURIComponent(id)}`, {
|
||||
method: "PUT",
|
||||
body: request
|
||||
});
|
||||
}
|
||||
|
||||
async setAiProviderStatus(id: string, request: AiProviderStatusRequest): Promise<AiProviderResponse> {
|
||||
return this.request<AiProviderResponse>(`/ai-providers/${encodeURIComponent(id)}/status`, {
|
||||
method: "POST",
|
||||
body: request
|
||||
});
|
||||
}
|
||||
|
||||
async testAiProvider(id: string): Promise<AiProviderTestResponse> {
|
||||
return this.request<AiProviderTestResponse>(`/ai-providers/${encodeURIComponent(id)}/test`, {
|
||||
method: "POST"
|
||||
});
|
||||
}
|
||||
|
||||
async listAiProviderModels(id: string): Promise<AiProviderModelsResponse> {
|
||||
return this.request<AiProviderModelsResponse>(`/ai-providers/${encodeURIComponent(id)}/models`);
|
||||
}
|
||||
|
||||
private async request<T>(path: string, options: ApiRequestOptions = {}): Promise<T> {
|
||||
const headers = new Headers(options.init?.headers);
|
||||
if (options.body !== undefined) {
|
||||
headers.set("Content-Type", "application/json");
|
||||
}
|
||||
const sessionToken = this.sessionTokenProvider();
|
||||
if (sessionToken && !headers.has("Authorization")) {
|
||||
headers.set("Authorization", `Bearer ${sessionToken}`);
|
||||
}
|
||||
|
||||
const response = await fetch(options.absolute ? path : `${this.baseUrl}${path}`, {
|
||||
...options.init,
|
||||
method: options.method ?? options.init?.method ?? "GET",
|
||||
headers,
|
||||
body: options.body === undefined ? options.init?.body : JSON.stringify(options.body)
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const apiError = await safeReadError(response);
|
||||
throw new Error(apiError?.message ?? `request failed: ${response.status}`);
|
||||
}
|
||||
|
||||
if (options.parseJson === false || response.status === 204) {
|
||||
return undefined as T;
|
||||
}
|
||||
|
||||
return response.json() as Promise<T>;
|
||||
}
|
||||
}
|
||||
|
||||
interface ApiRequestOptions {
|
||||
absolute?: boolean;
|
||||
method?: string;
|
||||
body?: unknown;
|
||||
init?: RequestInit;
|
||||
parseJson?: boolean;
|
||||
}
|
||||
|
||||
async function safeReadError(response: Response): Promise<ApiErrorResponse | null> {
|
||||
try {
|
||||
return (await response.json()) as ApiErrorResponse;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function marketplaceQuery(filter: MarketplacePluginFilterRequest): string {
|
||||
const params = new URLSearchParams();
|
||||
if (filter.status && filter.status !== "all") {
|
||||
params.set("status", filter.status);
|
||||
}
|
||||
if (filter.serverType) {
|
||||
params.set("serverType", filter.serverType);
|
||||
}
|
||||
if (filter.capability) {
|
||||
params.set("capability", filter.capability);
|
||||
}
|
||||
if (filter.keyword) {
|
||||
params.set("keyword", filter.keyword);
|
||||
}
|
||||
const query = params.toString();
|
||||
return query ? `?${query}` : "";
|
||||
}
|
||||
|
||||
function artifactQuery(filter: ArtifactFilterRequest): string {
|
||||
const params = new URLSearchParams();
|
||||
if (filter.ownerKind) {
|
||||
params.set("ownerKind", filter.ownerKind);
|
||||
}
|
||||
if (filter.ownerId) {
|
||||
params.set("ownerId", filter.ownerId);
|
||||
}
|
||||
if (filter.state) {
|
||||
params.set("state", filter.state);
|
||||
}
|
||||
const query = params.toString();
|
||||
return query ? `?${query}` : "";
|
||||
}
|
||||
|
||||
export const platformApiClient = new PlatformApiClient(readWebRuntimeEnv().platformApiBaseUrl);
|
||||
@@ -0,0 +1,50 @@
|
||||
# Frontend API Contracts
|
||||
|
||||
API clients and DTO types live here, not inside page components.
|
||||
|
||||
## Client Groups
|
||||
|
||||
- `users`: user and role APIs.
|
||||
- `serverPlugins`: plugin marketplace and installed plugin APIs.
|
||||
- `serverInstances`: create server, lifecycle, config read, config diff/approval, scoped files, logs, and detail APIs.
|
||||
- `aiProviders`: provider CRUD, test, and model APIs.
|
||||
- `jobs`: job status and operation APIs.
|
||||
- `runEndpoints`: run endpoint status, lifecycle capabilities, and capacity APIs.
|
||||
- `artifacts`: artifact upload/download APIs.
|
||||
- `logs`: historical query and tail APIs.
|
||||
- `pluginPageBridge`: safe bridge APIs for hosted plugin page.
|
||||
|
||||
Every API client must use named request and response types.
|
||||
|
||||
## Server Management Workflows
|
||||
|
||||
- `createServerWorkflow` posts `ServerLifecycleCreateRequest` to `/server-instances/workflows/create` and receives the accepted instance plus install job.
|
||||
- `startServerInstance` and `stopServerInstance` post `ServerLifecycleCommandRequest` with the current config version and receive the lifecycle job response.
|
||||
- `listServerAdministratorCandidates`, `addServerAdministrator`, and `removeServerAdministrator` call server membership endpoints so server owners can invite or remove active non-platform-admin server administrators.
|
||||
- `getServerConfig`, `previewServerConfigDiff`, and `approveServerConfigWrite` call platform-mediated config routes. ServerDetailPage must preview the platform diff first, keep the explicit confirmation step, and dispatch writes only through the approval API.
|
||||
- `dispatchFileOperation` posts `FileOperationDispatchRequest` to `/file-operations/dispatch` using logical file keys and scoped refs rather than raw host paths.
|
||||
- `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.
|
||||
- `invokeAI` posts `AIInvocationRequest` to `/ai/invocations` for platform-mediated AI assistance. Responses carry redacted recommendations, usage metadata, optional reviewable config suggestions, and safe errors; they must not include provider base URLs, key refs, raw keys, or direct provider transport details.
|
||||
- `listRunEndpoints` and `listJobs` provide refresh data for endpoint availability, capacity, and pending lifecycle status.
|
||||
- Server management DTOs may include bounded `ownerUserId` and `adminUserIds` metadata, but must not include raw run credentials, host paths, direct socket details, user password hashes, or AI provider keys.
|
||||
|
||||
## Redesign Contract Gaps (redesign-platform-web-interactions)
|
||||
|
||||
Existing platform APIs already cover server lifecycle, jobs, log stream metadata and cursor query, audit events, users, run endpoints, game plugins, plugin bridge authorization, and AI provider health/test. The redesigned UI additionally declares the following frontend contracts; where the platform backend does not yet serve them, the UI must degrade to a clearly labeled local/unavailable state instead of failing silently:
|
||||
|
||||
- `POST /api/v1/auth/register` (`RegisterRequest`/`AuthSessionResponse`): visitor registration. Implemented: the first registered user becomes an active platform administrator; later self-registered users become pending server-scoped users and do not receive platform administrator privileges.
|
||||
- `POST /api/v1/auth/login` (`LoginRequest`/`AuthSessionResponse`) and `POST /api/v1/auth/logout`: implemented bearer session lifecycle for authenticated workspace entry.
|
||||
- `GET /api/v1/users/current` (`CurrentUserResponse`): implemented current session identity, roles, profile summary, and theme preference reference for role-aware navigation and default landing.
|
||||
- `PUT /api/v1/users/current/profile` (`UserProfileUpdateRequest`/`CurrentUserResponse`): implemented current-user profile updates such as display name, avatar reference, phone, QQ, and bounded contact fields.
|
||||
- `PUT /api/v1/users/current/theme` (`UserThemePreferenceRequest`/`UserThemePreferenceResponse`): implemented per-user theme preferences, including selected palette IDs such as `mecha-black` or `magical-girl`, uploaded background reference or safe persisted data URL metadata, and readable overlay preference.
|
||||
- `GET /api/v1/metrics/platform` (`PlatformResourceUsageResponse`): implemented platform-level CPU/memory/disk usage and LLM connectivity summary for the overview first screen.
|
||||
- `GET /api/v1/metrics/server-instances` (`ServerMetricsListResponse`): implemented per-server online state, player count, TPS, latency, CPU/memory/disk for server cards and the server detail header.
|
||||
- `GET /api/v1/server-instances/{id}/config` (`ServerConfigResponse`): implemented readable configuration content for diff-based editing.
|
||||
- `POST /api/v1/server-instances/{id}/config/diff` (`ServerConfigDiffPreviewRequest`/`ServerConfigDiffPreviewResponse`) and `POST /api/v1/server-instances/{id}/config/approve` (`ServerConfigWriteApprovalRequest`/`ServerConfigWriteDispatchResponse`): implemented platform-mediated config write review and approval. Manual config edits and AI suggestion applies must not create generic `config.write` jobs through `POST /api/v1/jobs`.
|
||||
- `POST /api/v1/file-operations/dispatch` (`FileOperationDispatchRequest`/`FileOperationDispatchResponse`): implemented scoped file operation dispatch using logical keys and refs only.
|
||||
- `POST /api/v1/ai/config-suggestions` (`LlmConfigSuggestionRequest`/`LlmConfigSuggestionResponse`) and `POST /api/v1/ai/invocations` (`AIInvocationRequest`/`AIInvocationResponse`): platform-mediated AI recommendation or diff scoped to one server. Provider keys stay in `platform/`; responses carry only recommendation text, usage metadata, and reviewable suggestions, never keys or provider secrets.
|
||||
- Per-server plugin controls are rendered from installed plugin manifests (`bridgeActions`, `lifecycleActions`, `pages`, `declaredPermissions`); a richer declared-control schema remains a future plugin contract. Hosted bridge execution uses `POST /api/v1/plugin-bridge/execute` for server context, scoped file, log, job, artifact reference, and AI action envelopes instead of direct plugin fetches to platform internals.
|
||||
- Operation/job traceability reuses `GET /api/v1/jobs`, `GET /api/v1/jobs/{id}`, `POST /api/v1/jobs/{id}/cancel`, and `GET /api/v1/audit-events`; the frontend wraps these in one visible operation lifecycle per user intent.
|
||||
- Log filtering by level/keyword/time/source is applied client-side over `POST /api/v1/log-streams/query` (`LogStreamCursorRequest`) results until the platform exposes server-side filters.
|
||||
@@ -0,0 +1,679 @@
|
||||
export interface HealthResponse {
|
||||
service: string;
|
||||
status: "ok" | "degraded";
|
||||
version: string;
|
||||
time: string;
|
||||
}
|
||||
|
||||
export type GamePluginStatus = "installed" | "disabled" | "invalid" | "updating";
|
||||
export type ServerInstanceState = "draft" | "installing" | "ready" | "running" | "stopped" | "failed" | "deleted";
|
||||
export type RunEndpointStatus = "online" | "offline" | "degraded" | "disabled";
|
||||
export type JobState = "queued" | "accepted" | "running" | "succeeded" | "failed" | "cancelled";
|
||||
export type ServerLifecycleAction = "create" | "start" | "stop";
|
||||
|
||||
export interface PluginPermissionsResponse {
|
||||
ai: boolean;
|
||||
logs: boolean;
|
||||
files: boolean;
|
||||
jobs: boolean;
|
||||
artifacts: boolean;
|
||||
}
|
||||
|
||||
export interface GamePluginPageResponse {
|
||||
key: string;
|
||||
title: string;
|
||||
path: string;
|
||||
permissions: string[];
|
||||
bridgeActions?: string[];
|
||||
}
|
||||
|
||||
export interface GamePluginResponse {
|
||||
id: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
version: string;
|
||||
serverType: string;
|
||||
serverDisplayName?: string;
|
||||
supportedOs?: string[];
|
||||
manifestRef: string;
|
||||
createFormSchemaRef: string;
|
||||
requiredRunCapabilities: string[];
|
||||
declaredPermissions: string[];
|
||||
permissions: PluginPermissionsResponse;
|
||||
lifecycleActions: Record<string, string>;
|
||||
bridgeActions: string[];
|
||||
pages: GamePluginPageResponse[];
|
||||
tags: string[];
|
||||
aiPurposes: string[];
|
||||
validationViolations?: string[];
|
||||
status: GamePluginStatus;
|
||||
}
|
||||
|
||||
export interface GamePluginListResponse {
|
||||
items: GamePluginResponse[];
|
||||
count: number;
|
||||
}
|
||||
|
||||
export type MarketplacePluginStateAction = "install" | "enable" | "disable";
|
||||
|
||||
export interface MarketplacePluginResponse {
|
||||
id: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
version: string;
|
||||
serverType: string;
|
||||
serverDisplayName?: string;
|
||||
supportedOs?: string[];
|
||||
manifestRef: string;
|
||||
createFormSchemaRef: string;
|
||||
capabilities: string[];
|
||||
declaredPermissions: string[];
|
||||
permissions: PluginPermissionsResponse;
|
||||
lifecycleActions: Record<string, string>;
|
||||
bridgeActions: string[];
|
||||
pages: GamePluginPageResponse[];
|
||||
tags: string[];
|
||||
aiPurposes: string[];
|
||||
validationViolations?: string[];
|
||||
status: GamePluginStatus;
|
||||
source: string;
|
||||
}
|
||||
|
||||
export interface MarketplacePluginListResponse {
|
||||
items: MarketplacePluginResponse[];
|
||||
count: number;
|
||||
}
|
||||
|
||||
export interface MarketplacePluginFilterRequest {
|
||||
status?: GamePluginStatus | "all";
|
||||
serverType?: string;
|
||||
capability?: string;
|
||||
keyword?: string;
|
||||
}
|
||||
|
||||
export interface MarketplacePluginStateRequest {
|
||||
action: MarketplacePluginStateAction;
|
||||
}
|
||||
|
||||
export interface ServerInstanceResponse {
|
||||
id: string;
|
||||
pluginId: string;
|
||||
pluginVersion: string;
|
||||
runEndpointId: string;
|
||||
name: string;
|
||||
ownerUserId?: string;
|
||||
adminUserIds: string[];
|
||||
state: ServerInstanceState;
|
||||
configVersion: number;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface ServerInstanceListResponse {
|
||||
items: ServerInstanceResponse[];
|
||||
count: number;
|
||||
}
|
||||
|
||||
export interface ServerLifecycleCreateRequest {
|
||||
id: string;
|
||||
pluginId: string;
|
||||
runEndpointId: string;
|
||||
name: string;
|
||||
idempotencyKey: string;
|
||||
}
|
||||
|
||||
export interface ServerLifecycleCommandRequest {
|
||||
expectedConfigVersion: number;
|
||||
idempotencyKey: string;
|
||||
}
|
||||
|
||||
export interface ServerLifecycleResponse {
|
||||
accepted: boolean;
|
||||
action: ServerLifecycleAction;
|
||||
instance: ServerInstanceResponse;
|
||||
job: JobResponse;
|
||||
}
|
||||
|
||||
export interface RunCapacityResponse {
|
||||
maxJobs: number;
|
||||
runningJobs: number;
|
||||
queuedJobs: number;
|
||||
summary?: string;
|
||||
}
|
||||
|
||||
export interface RunEndpointResponse {
|
||||
id: string;
|
||||
displayName: string;
|
||||
version: string;
|
||||
status: RunEndpointStatus;
|
||||
capabilities: string[];
|
||||
capacity: RunCapacityResponse;
|
||||
lastHeartbeatAt: string;
|
||||
}
|
||||
|
||||
export interface RunEndpointListResponse {
|
||||
items: RunEndpointResponse[];
|
||||
count: number;
|
||||
}
|
||||
|
||||
export interface JobProgressBody {
|
||||
percent: number;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
export interface JobResponse {
|
||||
id: string;
|
||||
serverInstanceId?: string;
|
||||
runEndpointId: string;
|
||||
capability: string;
|
||||
targetKey?: string;
|
||||
inputRef?: string;
|
||||
idempotencyKey: string;
|
||||
state: JobState;
|
||||
progress: JobProgressBody;
|
||||
resultRef?: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface JobListResponse {
|
||||
items: JobResponse[];
|
||||
count: number;
|
||||
}
|
||||
|
||||
export type ArtifactOwnerKind = "platform" | "plugin" | "server-instance" | "job";
|
||||
export type ArtifactState = "uploading" | "available" | "expired" | "failed";
|
||||
|
||||
export interface ArtifactResponse {
|
||||
id: string;
|
||||
ownerKind: ArtifactOwnerKind;
|
||||
ownerId: string;
|
||||
sizeBytes: number;
|
||||
checksum: string;
|
||||
state: ArtifactState;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface ArtifactListResponse {
|
||||
items: ArtifactResponse[];
|
||||
count: number;
|
||||
}
|
||||
|
||||
export interface ArtifactFilterRequest {
|
||||
ownerKind?: ArtifactOwnerKind;
|
||||
ownerId?: string;
|
||||
state?: ArtifactState;
|
||||
}
|
||||
|
||||
export interface ArtifactDownloadReferenceResponse {
|
||||
artifactId: string;
|
||||
ownerKind: ArtifactOwnerKind;
|
||||
ownerId: string;
|
||||
filename: string;
|
||||
contentType: string;
|
||||
sizeBytes: number;
|
||||
checksum: string;
|
||||
state: ArtifactState;
|
||||
downloadUrl: string;
|
||||
expiresAt: string;
|
||||
rangeSupported: boolean;
|
||||
chunkSizeBytes: number;
|
||||
storageBehavior: string;
|
||||
}
|
||||
|
||||
export interface ArtifactContentChunk {
|
||||
artifactId?: string;
|
||||
payload: ArrayBuffer;
|
||||
contentType: string;
|
||||
contentLength: number;
|
||||
contentRange?: string;
|
||||
checksum?: string;
|
||||
contentChecksum?: string;
|
||||
storageBehavior?: string;
|
||||
}
|
||||
|
||||
export interface PluginBridgeAuthorizeRequest {
|
||||
pluginId: string;
|
||||
routeKey: string;
|
||||
serverInstanceId?: string;
|
||||
action: string;
|
||||
aiPurpose?: string;
|
||||
}
|
||||
|
||||
export interface PluginBridgeAuthorizeResponse {
|
||||
pluginId: string;
|
||||
routeKey: string;
|
||||
serverInstanceId?: string;
|
||||
action: string;
|
||||
allowed: boolean;
|
||||
requiredPermissions: string[];
|
||||
effectivePermissions: string[];
|
||||
reason?: string;
|
||||
}
|
||||
|
||||
export interface PluginBridgeExecuteRequest {
|
||||
requestId: string;
|
||||
pluginId: string;
|
||||
routeKey: string;
|
||||
serverInstanceId?: string;
|
||||
action: string;
|
||||
aiPurpose?: string;
|
||||
payload?: Record<string, string>;
|
||||
}
|
||||
|
||||
export interface PluginBridgeSafeErrorResponse {
|
||||
code: string;
|
||||
message: string;
|
||||
details?: string[];
|
||||
}
|
||||
|
||||
export interface PluginBridgeExecuteResponse {
|
||||
requestId: string;
|
||||
pluginId: string;
|
||||
routeKey: string;
|
||||
serverInstanceId?: string;
|
||||
action: string;
|
||||
status: "ok" | "queued" | "denied" | "unsupported" | "cancelled" | "error" | string;
|
||||
result?: Record<string, string>;
|
||||
error?: PluginBridgeSafeErrorResponse;
|
||||
}
|
||||
|
||||
export type AiProviderKind = "openai-compatible" | "openai" | "claude" | "gemini" | "ollama" | "custom";
|
||||
export type AiRelayMode = "direct" | "relay" | "local";
|
||||
export type AiProviderStatus = "active" | "disabled" | "error";
|
||||
|
||||
export interface AiProviderRequest {
|
||||
id: string;
|
||||
name: string;
|
||||
kind: AiProviderKind;
|
||||
baseUrl: string;
|
||||
apiKeyRef: string;
|
||||
models: string[];
|
||||
defaultModel?: string;
|
||||
relayMode: AiRelayMode;
|
||||
timeoutMs: number;
|
||||
redactionPolicy: string;
|
||||
}
|
||||
|
||||
export type AiProviderUpdateRequest = Omit<AiProviderRequest, "id">;
|
||||
|
||||
export interface AiProviderStatusRequest {
|
||||
status: Extract<AiProviderStatus, "active" | "disabled">;
|
||||
}
|
||||
|
||||
export interface AiProviderResponse {
|
||||
id: string;
|
||||
name: string;
|
||||
kind: AiProviderKind;
|
||||
baseUrl: string;
|
||||
apiKeyRef: string;
|
||||
models: string[];
|
||||
defaultModel?: string;
|
||||
relayMode: AiRelayMode;
|
||||
timeoutMs: number;
|
||||
status: AiProviderStatus;
|
||||
redactionPolicy: string;
|
||||
}
|
||||
|
||||
export interface AiProviderListResponse {
|
||||
items: AiProviderResponse[];
|
||||
count: number;
|
||||
}
|
||||
|
||||
export interface AiProviderTestResponse {
|
||||
providerId: string;
|
||||
mode: "metadata";
|
||||
success: boolean;
|
||||
message: string;
|
||||
violations?: string[];
|
||||
}
|
||||
|
||||
export interface AiProviderModelsResponse {
|
||||
providerId: string;
|
||||
defaultModel?: string;
|
||||
models: string[];
|
||||
}
|
||||
|
||||
export interface ApiErrorResponse {
|
||||
code: string;
|
||||
message: string;
|
||||
details?: string[];
|
||||
}
|
||||
|
||||
export type UserStatus = "active" | "disabled" | "pending";
|
||||
export type UserThemePersistence = "api" | "local";
|
||||
|
||||
export interface UserContactProfile {
|
||||
avatarUrl?: string;
|
||||
phone?: string;
|
||||
qq?: string;
|
||||
contactNote?: string;
|
||||
}
|
||||
|
||||
export interface UserThemePreferenceRequest {
|
||||
paletteId: string;
|
||||
backgroundPresetId: string;
|
||||
backgroundImage?: string | null;
|
||||
}
|
||||
|
||||
export interface UserThemePreferenceResponse extends UserThemePreferenceRequest {
|
||||
userId: string;
|
||||
persistence: UserThemePersistence;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface UserResponse {
|
||||
id: string;
|
||||
displayName: string;
|
||||
email?: string;
|
||||
status: UserStatus;
|
||||
roles: string[];
|
||||
profile?: UserContactProfile;
|
||||
themePreference?: UserThemePreferenceResponse;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface ServerMemberResponse {
|
||||
id: string;
|
||||
displayName: string;
|
||||
email?: string;
|
||||
status: UserStatus;
|
||||
roles: string[];
|
||||
profile?: UserContactProfile;
|
||||
}
|
||||
|
||||
export interface ServerMemberListResponse {
|
||||
items: ServerMemberResponse[];
|
||||
count: number;
|
||||
}
|
||||
|
||||
export interface ServerMemberRequest {
|
||||
userId: string;
|
||||
}
|
||||
|
||||
export interface UserListResponse {
|
||||
items: UserResponse[];
|
||||
count: number;
|
||||
}
|
||||
|
||||
export interface CurrentUserResponse {
|
||||
id: string;
|
||||
displayName: string;
|
||||
email?: string;
|
||||
status?: UserStatus;
|
||||
roles: string[];
|
||||
capabilities?: string[];
|
||||
profile?: UserContactProfile;
|
||||
themePreference?: UserThemePreferenceResponse;
|
||||
}
|
||||
|
||||
export interface LoginRequest {
|
||||
account: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
export interface RegisterRequest {
|
||||
displayName: string;
|
||||
email: string;
|
||||
password: string;
|
||||
phone?: string;
|
||||
qq?: string;
|
||||
}
|
||||
|
||||
export interface AuthSessionResponse {
|
||||
user: CurrentUserResponse;
|
||||
sessionId?: string;
|
||||
status: "authenticated" | "pending";
|
||||
message?: string;
|
||||
}
|
||||
|
||||
export interface UserProfileUpdateRequest {
|
||||
displayName: string;
|
||||
avatarUrl?: string;
|
||||
phone?: string;
|
||||
qq?: string;
|
||||
contactNote?: string;
|
||||
}
|
||||
|
||||
export interface UserCreateRequest {
|
||||
displayName: string;
|
||||
email?: string;
|
||||
roles: string[];
|
||||
status: UserStatus;
|
||||
profile?: UserContactProfile;
|
||||
}
|
||||
|
||||
export interface UserUpdateRequest {
|
||||
displayName?: string;
|
||||
email?: string;
|
||||
roles?: string[];
|
||||
status?: UserStatus;
|
||||
profile?: UserContactProfile;
|
||||
}
|
||||
|
||||
export interface PlatformResourceUsageResponse {
|
||||
cpuPercent: number;
|
||||
memoryPercent: number;
|
||||
diskPercent: number;
|
||||
source?: string;
|
||||
collectedAt: string;
|
||||
}
|
||||
|
||||
export interface ServerMetricsResponse {
|
||||
serverInstanceId: string;
|
||||
online: boolean;
|
||||
playerCount?: number;
|
||||
maxPlayers?: number;
|
||||
tps?: number;
|
||||
latencyMs?: number;
|
||||
cpuPercent?: number;
|
||||
memoryPercent?: number;
|
||||
diskPercent?: number;
|
||||
source?: string;
|
||||
collectedAt: string;
|
||||
}
|
||||
|
||||
export interface ServerMetricsListResponse {
|
||||
items: ServerMetricsResponse[];
|
||||
count: number;
|
||||
}
|
||||
|
||||
export interface ServerConfigResponse {
|
||||
serverInstanceId: string;
|
||||
configVersion: number;
|
||||
format: string;
|
||||
key?: string;
|
||||
content: string;
|
||||
source?: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export type ConfigDiffLineKind = "context" | "added" | "removed";
|
||||
|
||||
export interface ConfigDiffLineResponse {
|
||||
kind: ConfigDiffLineKind;
|
||||
oldNumber?: number;
|
||||
newNumber?: number;
|
||||
content: string;
|
||||
}
|
||||
|
||||
export interface ServerConfigDiffPreviewRequest {
|
||||
expectedConfigVersion: number;
|
||||
key: string;
|
||||
proposedContent?: string;
|
||||
proposedContentInputRef?: string;
|
||||
}
|
||||
|
||||
export interface ServerConfigDiffPreviewResponse {
|
||||
serverInstanceId: string;
|
||||
configVersion: number;
|
||||
key: string;
|
||||
currentContent: string;
|
||||
proposedContent?: string;
|
||||
proposedContentInputRef?: string;
|
||||
diff: ConfigDiffLineResponse[];
|
||||
hasChanges: boolean;
|
||||
source: string;
|
||||
reviewedAt: string;
|
||||
}
|
||||
|
||||
export interface ServerConfigWriteApprovalRequest {
|
||||
expectedConfigVersion: number;
|
||||
key: string;
|
||||
proposedContent?: string;
|
||||
proposedContentInputRef?: string;
|
||||
idempotencyKey: string;
|
||||
}
|
||||
|
||||
export interface ServerConfigWriteDispatchResponse {
|
||||
status: string;
|
||||
preview: ServerConfigDiffPreviewResponse;
|
||||
job: JobResponse;
|
||||
}
|
||||
|
||||
export type FileOperationKind = "read" | "write";
|
||||
|
||||
export interface FileOperationDispatchRequest {
|
||||
serverInstanceId: string;
|
||||
pluginId?: string;
|
||||
operation: FileOperationKind;
|
||||
key: string;
|
||||
inputRef?: string;
|
||||
expectedConfigVersion?: number;
|
||||
idempotencyKey: string;
|
||||
}
|
||||
|
||||
export interface FileOperationDispatchResponse {
|
||||
status: string;
|
||||
serverInstanceId: string;
|
||||
pluginId?: string;
|
||||
operation: FileOperationKind;
|
||||
key: string;
|
||||
inputRef?: string;
|
||||
job: JobResponse;
|
||||
}
|
||||
|
||||
export interface LogStreamResponse {
|
||||
id: string;
|
||||
serverInstanceId: string;
|
||||
source: string;
|
||||
streamKey: string;
|
||||
latestSeq: number;
|
||||
storageBackend: string;
|
||||
retentionPolicy: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface LogStreamListResponse {
|
||||
items: LogStreamResponse[];
|
||||
count: number;
|
||||
}
|
||||
|
||||
export interface LogEntryBody {
|
||||
seq: number;
|
||||
timestamp: string;
|
||||
level?: string;
|
||||
line: string;
|
||||
fields?: Record<string, string>;
|
||||
redacted: boolean;
|
||||
}
|
||||
|
||||
export interface LogStreamCursorRequest {
|
||||
logStreamId: string;
|
||||
afterSeq: number;
|
||||
limit: number;
|
||||
}
|
||||
|
||||
export interface LogStreamCursorResponse {
|
||||
logStreamId: string;
|
||||
entries: LogEntryBody[];
|
||||
nextSeq: number;
|
||||
latestSeq: number;
|
||||
}
|
||||
|
||||
export interface AuditEventResponse {
|
||||
id: string;
|
||||
actorId: string;
|
||||
action: string;
|
||||
resourceKind: string;
|
||||
resourceId: string;
|
||||
result: string;
|
||||
summary: string;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export interface AuditEventListResponse {
|
||||
items: AuditEventResponse[];
|
||||
count: number;
|
||||
}
|
||||
|
||||
export interface JobCreateRequest {
|
||||
id: string;
|
||||
serverInstanceId?: string;
|
||||
runEndpointId: string;
|
||||
capability: string;
|
||||
targetKey?: string;
|
||||
inputRef?: string;
|
||||
idempotencyKey: string;
|
||||
progress?: JobProgressBody;
|
||||
}
|
||||
|
||||
export interface LlmConfigSuggestionRequest {
|
||||
serverInstanceId: string;
|
||||
prompt: string;
|
||||
currentConfig: string;
|
||||
}
|
||||
|
||||
export interface LlmConfigSuggestionResponse {
|
||||
serverInstanceId: string;
|
||||
recommendation: string;
|
||||
suggestedConfig?: string;
|
||||
}
|
||||
|
||||
export interface AIInvocationRequest {
|
||||
requestId: string;
|
||||
pluginId?: string;
|
||||
routeKey?: string;
|
||||
serverInstanceId?: string;
|
||||
purpose: string;
|
||||
providerId?: string;
|
||||
model?: string;
|
||||
prompt: string;
|
||||
currentConfig?: string;
|
||||
contextRefs?: Record<string, string>;
|
||||
}
|
||||
|
||||
export interface AIInvocationUsageResponse {
|
||||
providerId: string;
|
||||
model: string;
|
||||
inputTokens: number;
|
||||
outputTokens: number;
|
||||
mocked: boolean;
|
||||
}
|
||||
|
||||
export interface AIConfigRecommendationResponse {
|
||||
key: string;
|
||||
suggestedConfig?: string;
|
||||
diffSummary: string;
|
||||
}
|
||||
|
||||
export interface AIInvocationSafeErrorResponse {
|
||||
code: string;
|
||||
message: string;
|
||||
details?: string[];
|
||||
}
|
||||
|
||||
export interface AIInvocationResponse {
|
||||
requestId: string;
|
||||
purpose: string;
|
||||
providerId?: string;
|
||||
model?: string;
|
||||
status: "ok" | "denied" | "error" | string;
|
||||
recommendation?: string;
|
||||
configRecommendation?: AIConfigRecommendationResponse;
|
||||
usage: AIInvocationUsageResponse;
|
||||
error?: AIInvocationSafeErrorResponse;
|
||||
}
|
||||
Reference in New Issue
Block a user