first commit

This commit is contained in:
npc0-hue
2026-07-11 14:56:10 +08:00
commit 7e05d0a4e7
660 changed files with 78119 additions and 0 deletions
+27
View File
@@ -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));
});
});
+506
View File
@@ -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" }
});
}
+414
View File
@@ -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);
+50
View File
@@ -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.
+679
View File
@@ -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;
}