Remove AI config approval flow

This commit is contained in:
npc0-hue
2026-09-22 15:33:32 +08:00
parent 20008b4043
commit a8b5d483a3
54 changed files with 364 additions and 875 deletions
+5 -3
View File
@@ -565,8 +565,9 @@ describe("PlatformApiClient AI providers", () => {
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" },
recommendation: "Configuration changes are dispatched automatically.",
configRecommendation: { key: "server.properties", suggestedConfig: "server.name=Example Survival #1\npvp=false\n", diffSummary: "AI config write queued" },
configExecution: { status: "queued", job: { ...job, id: "job-ai-config-1", capability: "config.write", targetKey: "server.properties" } },
usage: { providerId: "ai.openai", model: "gpt-4.1", inputTokens: 20, outputTokens: 12, mocked: true }
});
}
@@ -645,7 +646,7 @@ describe("PlatformApiClient AI providers", () => {
});
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" } });
).resolves.toMatchObject({ status: "ok", usage: { mocked: true }, configRecommendation: { diffSummary: "AI config write queued" }, configExecution: { status: "queued", job: { id: "job-ai-config-1", capability: "config.write" } } });
expect(fetchMock).toHaveBeenCalledTimes(51);
});
@@ -799,6 +800,7 @@ describe("PlatformApiClient AI providers", () => {
const client = new PlatformApiClient("/api/v1");
expect(client.serverLogEventsUrl("server/scum 1")).toBe("/api/v1/server-instances/server%2Fscum%201/logs/events");
expect(client.serverLogEventsUrl("server-1", { jobId: "job/config write" })).toBe("/api/v1/server-instances/server-1/logs/events?jobId=job%2Fconfig%20write");
expect(client.serverLogEventsUrl("server-1")).toBe("/api/v1/server-instances/server-1/logs/events");
});
+5 -17
View File
@@ -8,8 +8,6 @@ import type {
AiProviderUpdateRequest,
AIInvocationRequest,
AIInvocationResponse,
AIConfigDiffApprovalResponse,
AIConfigDiffListResponse,
ApiErrorResponse,
ArtifactContentChunk,
ArtifactDownloadReferenceResponse,
@@ -500,17 +498,6 @@ export class PlatformApiClient {
return this.request<PluginLifecycleActionResponse>(`/plugin-lifecycles/${encodeURIComponent(pluginId)}/actions`, { method: "POST", body: request });
}
async listAIConfigDiffs(filter: { serverInstanceId?: string; pluginId?: string; state?: string } = {}): Promise<AIConfigDiffListResponse> {
const params = new URLSearchParams();
Object.entries(filter).forEach(([key, value]) => { if (value) params.set(key, value); });
const query = params.toString();
return this.request<AIConfigDiffListResponse>(`/ai/config-diffs${query ? `?${query}` : ""}`);
}
async approveAIConfigDiff(id: string, idempotencyKey: string): Promise<AIConfigDiffApprovalResponse> {
return this.request<AIConfigDiffApprovalResponse>(`/ai/config-diffs/${encodeURIComponent(id)}/approve`, { method: "POST", body: { idempotencyKey } });
}
async listServerMetrics(): Promise<ServerMetricsListResponse> {
return this.request<ServerMetricsListResponse>("/metrics/server-instances");
}
@@ -641,8 +628,8 @@ export class PlatformApiClient {
return this.request<LogStreamListResponse>(`/log-streams${query}`);
}
openServerLogEvents(id: string): PlatformEventStream {
const url = this.serverLogEventsUrl(id);
openServerLogEvents(id: string, options: { jobId?: string } = {}): PlatformEventStream {
const url = this.serverLogEventsUrl(id, options);
const sessionToken = this.sessionTokenProvider();
if (!sessionToken) {
return new EventSource(url, { withCredentials: true });
@@ -650,8 +637,9 @@ export class PlatformApiClient {
return new FetchServerSentEventStream(url, sessionToken);
}
serverLogEventsUrl(id: string): string {
return `${this.baseUrl}/server-instances/${encodeURIComponent(id)}/logs/events`;
serverLogEventsUrl(id: string, options: { jobId?: string } = {}): string {
const query = options.jobId ? `?jobId=${encodeURIComponent(options.jobId)}` : "";
return `${this.baseUrl}/server-instances/${encodeURIComponent(id)}/logs/events${query}`;
}
async queryLogStream(request: LogStreamCursorRequest): Promise<LogStreamCursorResponse> {
+3 -3
View File
@@ -32,7 +32,7 @@ Normal browser login uses the platform's HttpOnly SameSite cookie and `credentia
- `listArtifacts`, `openArtifactDownload`, `downloadArtifactContent`, and `readArtifactContent` use platform artifact routes for available job/server artifacts. Browser downloads stream the full body through `/artifacts/{id}/content`; explicit range reads may still use bounded `offset`/`limit` chunks 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.
- `invokeAI` posts `AIInvocationRequest` to `/ai/invocations` for platform-mediated AI assistance. Config suggestions return redacted recommendation text, the proposed config, and `configExecution.job` metadata for the job dispatched immediately after Platform validation; responses 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 durable lifecycle status. Job projections include `retrying`, attempt/max-attempt counts, next retry timing, safe ack/lease deadlines, cancellation timestamps/reason, terminal time, and reconciliation outcome/count.
- `getDependencyCatalog` reads `GET /server-instances/{id}/dependencies` and returns only target-matched probe state/evidence, typed plan step summaries, approved download hosts, and immutable SHA-256 `planDigest` values. Install requests must submit the selected digest; the browser never receives bindings, commands, paths, credentials, tokens, or private download refs.
- `listRunUpdates` reads `GET /server-instances/{id}/run/update` and returns only target, artifact checksum, release identity, phase, bounded status message, rollback flag, and timestamps. The UI treats `restart-requested`/`activating` as non-terminal until a later safe projection confirms health.
@@ -53,9 +53,9 @@ Existing platform APIs already cover server lifecycle, jobs, log stream metadata
- `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 on the server list.
- Server-scoped raw config routes (`GET /api/v1/server-instances/{id}/config`, `POST .../config/diff`, `POST .../config/approve`) are removed from the product API. AI configuration assistance uses `/api/v1/ai/invocations` plus reviewable AI config-diff approval APIs; plugin pages do not receive raw config text.
- Server-scoped raw config routes (`GET /api/v1/server-instances/{id}/config`, `POST .../config/diff`, `POST .../config/approve`) are removed from the product API. AI configuration assistance uses `/api/v1/ai/invocations` and direct typed `configExecution` job metadata; plugin pages do not receive provider credentials.
- `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.
- `POST /api/v1/ai/config-suggestions` (`LlmConfigSuggestionRequest`/`LlmConfigSuggestionResponse`) and `POST /api/v1/ai/invocations` (`AIInvocationRequest`/`AIInvocationResponse`): platform-mediated AI recommendation scoped to one server. Config suggestions dispatch a bounded `config.write` job immediately after validation and return its job metadata; provider keys stay in `platform/` and never reach the browser or plugin pages.
- 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}`, and `POST /api/v1/jobs/{id}/cancel`; the frontend wraps these in one visible operation lifecycle per user intent.
+1 -5
View File
@@ -15,14 +15,10 @@ describe("PlatformApiClient plugin operations", () => {
await client.listPluginLifecycles({ pluginId: "game.scum" });
await client.runPluginLifecycle("game.scum", { serverInstanceId: "server-1", operation: "upgrade", targetVersion: "1.2.0", idempotencyKey: "upgrade-1" });
await client.listAIConfigDiffs({ state: "pending" });
await client.approveAIConfigDiff("diff-1", "approve-1");
expect(calls.map((call) => `${call.method} ${call.url}`)).toEqual([
"GET /api/v1/plugin-lifecycles?pluginId=game.scum",
"POST /api/v1/plugin-lifecycles/game.scum/actions",
"GET /api/v1/ai/config-diffs?state=pending",
"POST /api/v1/ai/config-diffs/diff-1/approve"
"POST /api/v1/plugin-lifecycles/game.scum/actions"
]);
const serialized = JSON.stringify(calls);
expect(serialized).not.toMatch(/apiKey|token|secret|providerBaseUrl|runSocket|runEndpointUrl|hostPath|credential|dsn|rcon/i);
+7 -26
View File
@@ -1575,6 +1575,7 @@ export interface LlmConfigSuggestionResponse {
serverInstanceId: string;
recommendation: string;
suggestedConfig?: string;
configExecution?: AIConfigExecutionResponse;
}
export interface AIInvocationRequest {
@@ -1602,8 +1603,11 @@ export interface AIConfigRecommendationResponse {
key: string;
suggestedConfig?: string;
diffSummary: string;
diffId: string;
expiresAt: string;
}
export interface AIConfigExecutionResponse {
status: string;
job: JobResponse;
}
export interface PluginProductionLifecycleDeclaration {
@@ -1633,30 +1637,6 @@ export interface PluginLifecycleListResponse { items: PluginLifecycleInstallatio
export interface PluginLifecycleActionRequest { serverInstanceId: string; operation: PluginLifecycleOperation; targetVersion?: string; idempotencyKey: string; }
export interface PluginLifecycleActionResponse { status: string; installation: PluginLifecycleInstallationResponse; job: JobResponse; }
export interface AIConfigDiffPreviewResponse {
id: string;
requestId: string;
createdBy: string;
serverInstanceId: string;
pluginId?: string;
providerId?: string;
model?: string;
key: string;
configVersion: number;
currentConfigChecksum?: string;
proposedConfig?: string;
diffSummary: string;
state: "pending" | "approved" | "cancelled" | "expired";
expiresAt: string;
approvedBy?: string;
approvedAt?: string;
jobId?: string;
createdAt: string;
updatedAt: string;
}
export interface AIConfigDiffListResponse { items: AIConfigDiffPreviewResponse[]; count: number; }
export interface AIConfigDiffApprovalResponse { preview: AIConfigDiffPreviewResponse; dispatch: ServerConfigWriteDispatchResponse; }
export interface AIInvocationSafeErrorResponse {
code: string;
message: string;
@@ -1671,6 +1651,7 @@ export interface AIInvocationResponse {
status: "ok" | "denied" | "error" | string;
recommendation?: string;
configRecommendation?: AIConfigRecommendationResponse;
configExecution?: AIConfigExecutionResponse;
usage: AIInvocationUsageResponse;
error?: AIInvocationSafeErrorResponse;
}