SCUM 停止/重启/更新以前只有“结束进程”这一条路,插件没有声明任何优雅关闭方式,
平台也没有把停止后重新启动串起来。现在插件声明自己的关闭脚本,run 先执行它,
平台在停止或更新成功后再自动拉起服务。
run:
- lifecycle stop 支持插件声明的 gracefulStop(可执行文件、参数、环境、超时、
fallback=report|terminate);关闭命令超时且声明 report 时任务失败,不再默默杀进程。
- 新增 steam.update 依赖探针:调用 steamcmd +app_info_print 获取公开分支 buildid,
与本地 steamapps/appmanifest_<appid>.acf 的 buildid 比较,输出
installed/latest/update=yes|no|unknown。
platform:
- 新增 POST /api/v1/server-instances/{id}/restart 与 /update。
- restart 派发插件 stop 动作(走优雅关闭),终态成功后入队 start 作业。
- update 派发插件 install 动作;插件在更新前必须先优雅关闭 SCUM,关闭失败直接拒绝
SteamCMD 更新,成功后平台再拉起服务。
- 依赖检查输入带上插件声明的服务器安装根目录,供 steam.update 读取 appmanifest。
plugin (SCUM server plugin 0.1.16):
- bin/scum-stop.cmd:解析已声明的可执行文件路径,定位同路径正在运行的 SCUMServer.exe,
通过本地 RCON 公告并发送关闭命令,等待进程自行退出;不再使用 taskkill。
- bin/scum-rcon.ps1:插件自有的 Source RCON 客户端,从 UE4SS mod config.ini 读取
密码/端口,密钥不离开本机。
- actions/stop.json 声明 gracefulStop;actions/install.json 更新前先执行同一关闭脚本。
platform_web:
- 服务器详情新增“重启”按钮和“SCUM 版本更新”面板;点“检查更新”查询公开分支版本,
只有检测到更新时“更新版本”按钮才会置为可用并高亮,点击后先确认再派发更新任务。
1013 lines
43 KiB
TypeScript
1013 lines
43 KiB
TypeScript
import type {
|
||
AiProviderListResponse,
|
||
AiProviderModelsResponse,
|
||
AiProviderRequest,
|
||
AiProviderResponse,
|
||
AiProviderStatusRequest,
|
||
AiProviderTestResponse,
|
||
AiProviderUpdateRequest,
|
||
AIInvocationRequest,
|
||
AIInvocationResponse,
|
||
AIConfigDiffApprovalResponse,
|
||
AIConfigDiffListResponse,
|
||
ApiErrorResponse,
|
||
ArtifactContentChunk,
|
||
ArtifactDownloadReferenceResponse,
|
||
ArtifactFilterRequest,
|
||
ArtifactListResponse,
|
||
AuthSessionResponse,
|
||
ComponentKeyResponse,
|
||
CurrentUserResponse,
|
||
DependencyCatalogResponse,
|
||
DependencyJobRequest,
|
||
DeclaredFileReadSnapshotResponse,
|
||
FileOperationDispatchRequest,
|
||
FileOperationDispatchResponse,
|
||
GameClientBridgeCancelRequest,
|
||
GameClientBridgeCancelResponse,
|
||
GameClientBridgeCommandFilterRequest,
|
||
GameClientBridgeCommandListResponse,
|
||
GameClientBridgeCommandResponse,
|
||
GameClientBridgeQueueRequest,
|
||
GameClientBridgeSnapshotListResponse,
|
||
GameClientBridgeSnapshotQuery,
|
||
GameClientBridgeStatusResponse,
|
||
GamePluginListResponse,
|
||
HealthResponse,
|
||
JobCreateRequest,
|
||
JobListResponse,
|
||
JobResponse,
|
||
JobState,
|
||
LlmConfigSuggestionRequest,
|
||
LlmConfigSuggestionResponse,
|
||
LogStreamCursorRequest,
|
||
LogStreamCursorResponse,
|
||
LogStreamListResponse,
|
||
LoginRequest,
|
||
MarketplacePluginFilterRequest,
|
||
MarketplacePluginListResponse,
|
||
MarketplacePluginResponse,
|
||
MarketplacePluginStateRequest,
|
||
PlatformResourceUsageResponse,
|
||
PluginLifecycleActionRequest,
|
||
PluginLifecycleActionResponse,
|
||
PluginLifecycleListResponse,
|
||
PluginBridgeAuthorizeRequest,
|
||
PluginBridgeAuthorizeResponse,
|
||
PluginBridgeExecuteRequest,
|
||
PluginBridgeExecuteResponse,
|
||
RegisterRequest,
|
||
RunDistributionGenerateRequest,
|
||
RunDistributionResponse,
|
||
RunEndpointListResponse,
|
||
RunEndpointStatus,
|
||
RunUpdateJobResponse,
|
||
RunUpdateJobListResponse,
|
||
RunUpdateRequest,
|
||
RuntimeBindingResponse,
|
||
RuntimeBindingUpdateRequest,
|
||
ServerLifecycleCommandRequest,
|
||
ServerLifecycleCreateRequest,
|
||
ServerLifecycleResponse,
|
||
ServerDeploymentRequest,
|
||
ServerDeploymentRevealResponse,
|
||
ServerDeploymentResponse,
|
||
ServerInstanceListResponse,
|
||
ServerDeletionRequest,
|
||
ServerFileDownloadRequest,
|
||
ServerFileDownloadResponse,
|
||
ServerFileListRequest,
|
||
ServerFileListResponse,
|
||
ServerFileReadRequest,
|
||
ServerFileUploadResponse,
|
||
ServerFileWorkspaceResponse,
|
||
ServerFileWriteRequest,
|
||
ServerInstanceUpdateRequest,
|
||
ServerInstanceResponse,
|
||
ServerMemberListResponse,
|
||
ServerMemberRequest,
|
||
ServerMetricsListResponse,
|
||
ScumListRequest,
|
||
ScumUserListResponse,
|
||
ScumSurfaceResponse,
|
||
ScumUserTrajectoryListResponse,
|
||
ScumVehicleLockListResponse,
|
||
ScumVehicleListResponse,
|
||
ScumVehicleTrajectoryListResponse,
|
||
SourceRCONCommandRequest,
|
||
SourceRCONCommandResponse,
|
||
MetricSampleListResponse,
|
||
BackupListResponse,
|
||
BackupResponse,
|
||
RemoteAdapterDeclarationListResponse,
|
||
RemoteAdapterRequest,
|
||
RemoteAdapterResponse,
|
||
ServerRuntimeActionsResponse,
|
||
UserCreateRequest,
|
||
UserListResponse,
|
||
UserProfileUpdateRequest,
|
||
UserResponse,
|
||
UserThemePreferenceRequest,
|
||
UserThemePreferenceResponse,
|
||
UserUpdateRequest
|
||
} from "./types";
|
||
import { readWebRuntimeEnv } from "../schemas/env";
|
||
import { parseSafeDependencyCatalog, parseSafeRunUpdate, parseSafeRunUpdateList } from "../schemas/runtimeUpdates";
|
||
import {
|
||
parseSafeGameClientBridgeCancellation,
|
||
parseSafeGameClientBridgeCommand,
|
||
parseSafeGameClientBridgeCommandList,
|
||
parseSafeGameClientBridgeSnapshotList,
|
||
parseSafeGameClientBridgeStatus
|
||
} from "../schemas/gameClientBridge";
|
||
import { safeDiagnosticText } from "../utils/safeDiagnosticText";
|
||
|
||
let platformApiSessionToken: string | null = null;
|
||
let platformApiAuthFailureHandler: ((error: PlatformApiError) => void) | null = null;
|
||
|
||
export function setPlatformApiSessionToken(token: string | null) {
|
||
platformApiSessionToken = token;
|
||
}
|
||
|
||
export function setPlatformApiAuthFailureHandler(handler: ((error: PlatformApiError) => void) | null) {
|
||
platformApiAuthFailureHandler = handler;
|
||
}
|
||
|
||
export class PlatformApiError extends Error {
|
||
constructor(
|
||
readonly status: number,
|
||
readonly code: string,
|
||
message: string
|
||
) {
|
||
super(message);
|
||
this.name = "PlatformApiError";
|
||
}
|
||
}
|
||
|
||
export interface PlatformEventStream {
|
||
onerror: ((event: Event) => void) | null;
|
||
addEventListener(type: string, listener: (event: MessageEvent) => void): void;
|
||
removeEventListener(type: string, listener: (event: MessageEvent) => void): void;
|
||
close(): void;
|
||
}
|
||
|
||
interface RunEndpointListFilter {
|
||
status?: RunEndpointStatus;
|
||
}
|
||
|
||
interface JobListFilter {
|
||
runEndpointId?: string;
|
||
state?: JobState;
|
||
states?: JobState[];
|
||
limit?: number;
|
||
}
|
||
|
||
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 getServerDeployment(id: string): Promise<ServerDeploymentResponse> {
|
||
return this.request<ServerDeploymentResponse>(`/server-instances/${encodeURIComponent(id)}/deployment`);
|
||
}
|
||
|
||
async revealServerDeployment(id: string): Promise<ServerDeploymentRevealResponse> {
|
||
return this.request<ServerDeploymentRevealResponse>(`/server-instances/${encodeURIComponent(id)}/deployment/reveal`);
|
||
}
|
||
|
||
async updateServerDeployment(id: string, request: ServerDeploymentRequest): Promise<ServerDeploymentResponse> {
|
||
return this.request<ServerDeploymentResponse>(`/server-instances/${encodeURIComponent(id)}/deployment`, { method: "PUT", body: request });
|
||
}
|
||
|
||
async deployServerInstance(id: string, request: ServerLifecycleCommandRequest): Promise<ServerLifecycleResponse> {
|
||
return this.request<ServerLifecycleResponse>(`/server-instances/${encodeURIComponent(id)}/deploy`, { method: "POST", body: request });
|
||
}
|
||
|
||
async getServerRuntimeBinding(id: string): Promise<RuntimeBindingResponse> {
|
||
return this.request<RuntimeBindingResponse>(`/server-instances/${encodeURIComponent(id)}/runtime-binding`);
|
||
}
|
||
|
||
async updateServerRuntimeBinding(id: string, request: RuntimeBindingUpdateRequest): Promise<RuntimeBindingResponse> {
|
||
return this.request<RuntimeBindingResponse>(`/server-instances/${encodeURIComponent(id)}/runtime-binding`, { method: "PUT", 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 restartServerInstance(id: string, request: ServerLifecycleCommandRequest): Promise<ServerLifecycleResponse> {
|
||
return this.request<ServerLifecycleResponse>(`/server-instances/${encodeURIComponent(id)}/restart`, {
|
||
method: "POST",
|
||
body: request
|
||
});
|
||
}
|
||
|
||
async updateServerGame(id: string, request: ServerLifecycleCommandRequest): Promise<ServerLifecycleResponse> {
|
||
return this.request<ServerLifecycleResponse>(`/server-instances/${encodeURIComponent(id)}/update`, {
|
||
method: "POST",
|
||
body: request
|
||
});
|
||
}
|
||
|
||
async queryServerProcessStatus(id: string, request: ServerLifecycleCommandRequest): Promise<ServerLifecycleResponse> {
|
||
return this.request<ServerLifecycleResponse>(`/server-instances/${encodeURIComponent(id)}/process/status`, {
|
||
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(filter: RunEndpointListFilter = {}): Promise<RunEndpointListResponse> {
|
||
return this.request<RunEndpointListResponse>(`/run/endpoints${runEndpointQuery(filter)}`);
|
||
}
|
||
|
||
async listJobs(serverInstanceId?: string, filter: JobListFilter = {}): Promise<JobListResponse> {
|
||
return this.request<JobListResponse>(`/jobs${jobListQuery(serverInstanceId, filter)}`);
|
||
}
|
||
|
||
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, credentials: "same-origin" });
|
||
if (!response.ok) {
|
||
throw await responseError(response);
|
||
}
|
||
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 downloadArtifactContent(id: string): Promise<Response> {
|
||
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`, { headers, credentials: "same-origin" });
|
||
if (!response.ok) {
|
||
throw await responseError(response);
|
||
}
|
||
return response;
|
||
}
|
||
|
||
async getJob(id: string): Promise<JobResponse> {
|
||
return this.request<JobResponse>(`/jobs/${encodeURIComponent(id)}`);
|
||
}
|
||
|
||
async getServerRuntimeActions(id: string): Promise<ServerRuntimeActionsResponse> {
|
||
return this.request<ServerRuntimeActionsResponse>(`/server-instances/${encodeURIComponent(id)}/runtime/actions`);
|
||
}
|
||
|
||
async generateRunDistribution(id: string, request: RunDistributionGenerateRequest): Promise<RunDistributionResponse> {
|
||
return this.request<RunDistributionResponse>(`/server-instances/${encodeURIComponent(id)}/run/generate`, {
|
||
method: "POST",
|
||
body: request
|
||
});
|
||
}
|
||
|
||
async downloadLatestRunDistribution(id: string): Promise<ArtifactDownloadReferenceResponse> {
|
||
return this.request<ArtifactDownloadReferenceResponse>(`/server-instances/${encodeURIComponent(id)}/run/download`, {
|
||
method: "POST",
|
||
body: {}
|
||
});
|
||
}
|
||
|
||
async resetRunKey(id: string): Promise<ComponentKeyResponse> {
|
||
return this.request<ComponentKeyResponse>(`/server-instances/${encodeURIComponent(id)}/run/key/reset`, {
|
||
method: "POST",
|
||
body: {}
|
||
});
|
||
}
|
||
|
||
async pushRunUpdate(id: string, request: RunUpdateRequest): Promise<RunUpdateJobResponse> {
|
||
const response = await this.request<unknown>(`/server-instances/${encodeURIComponent(id)}/run/update`, {
|
||
method: "POST",
|
||
body: request
|
||
});
|
||
return parseSafeRunUpdate(response);
|
||
}
|
||
|
||
async listRunUpdates(id: string): Promise<RunUpdateJobListResponse> {
|
||
return parseSafeRunUpdateList(await this.request<unknown>(`/server-instances/${encodeURIComponent(id)}/run/update`));
|
||
}
|
||
|
||
async getGameClientBridgeStatus(id: string): Promise<GameClientBridgeStatusResponse> {
|
||
return parseSafeGameClientBridgeStatus(await this.request<unknown>(`/server-instances/${encodeURIComponent(id)}/game-client-bridge`));
|
||
}
|
||
|
||
async listGameClientBridgeCommands(id: string, filter: GameClientBridgeCommandFilterRequest = {}): Promise<GameClientBridgeCommandListResponse> {
|
||
const params = new URLSearchParams();
|
||
if (filter.profileKey) params.set("profileKey", filter.profileKey);
|
||
if (filter.state) params.set("state", filter.state);
|
||
if (filter.commandType) params.set("commandType", filter.commandType);
|
||
const query = params.toString();
|
||
return parseSafeGameClientBridgeCommandList(await this.request<unknown>(`/server-instances/${encodeURIComponent(id)}/game-client-bridge/commands${query ? `?${query}` : ""}`));
|
||
}
|
||
|
||
async queueGameClientBridgeCommand(id: string, request: GameClientBridgeQueueRequest): Promise<GameClientBridgeCommandResponse> {
|
||
return parseSafeGameClientBridgeCommand(await this.request<unknown>(`/server-instances/${encodeURIComponent(id)}/game-client-bridge/commands`, { method: "POST", body: request }));
|
||
}
|
||
|
||
async dispatchSourceRCONCommand(id: string, request: SourceRCONCommandRequest): Promise<SourceRCONCommandResponse> {
|
||
return this.request<SourceRCONCommandResponse>(`/server-instances/${encodeURIComponent(id)}/rcon/commands`, { method: "POST", body: request });
|
||
}
|
||
|
||
async getGameClientBridgeCommand(id: string, commandId: string): Promise<GameClientBridgeCommandResponse> {
|
||
return parseSafeGameClientBridgeCommand(await this.request<unknown>(`/server-instances/${encodeURIComponent(id)}/game-client-bridge/commands/${encodeURIComponent(commandId)}`));
|
||
}
|
||
|
||
async cancelGameClientBridgeCommand(id: string, commandId: string, request: GameClientBridgeCancelRequest = {}): Promise<GameClientBridgeCancelResponse> {
|
||
return parseSafeGameClientBridgeCancellation(await this.request<unknown>(`/server-instances/${encodeURIComponent(id)}/game-client-bridge/commands/${encodeURIComponent(commandId)}/cancel`, { method: "POST", body: request }));
|
||
}
|
||
|
||
async listGameClientBridgeSnapshots(id: string, query: GameClientBridgeSnapshotQuery = {}): Promise<GameClientBridgeSnapshotListResponse> {
|
||
const params = new URLSearchParams();
|
||
if (query.profileKey) params.set("profileKey", query.profileKey);
|
||
if (query.type) params.set("type", query.type);
|
||
if (query.streamKey) params.set("streamKey", query.streamKey);
|
||
if (query.observedAfter) params.set("observedAfter", query.observedAfter);
|
||
if (query.limit !== undefined) params.set("limit", String(query.limit));
|
||
const search = params.toString();
|
||
return parseSafeGameClientBridgeSnapshotList(await this.request<unknown>(`/server-instances/${encodeURIComponent(id)}/game-client-bridge/snapshots${search ? `?${search}` : ""}`));
|
||
}
|
||
|
||
async checkDependencies(id: string, request: DependencyJobRequest): Promise<JobResponse> {
|
||
return this.request<JobResponse>(`/server-instances/${encodeURIComponent(id)}/dependencies/check`, {
|
||
method: "POST",
|
||
body: request
|
||
});
|
||
}
|
||
|
||
async getDependencyCatalog(id: string): Promise<DependencyCatalogResponse> {
|
||
return parseSafeDependencyCatalog(await this.request<unknown>(`/server-instances/${encodeURIComponent(id)}/dependencies`));
|
||
}
|
||
|
||
async installDependencies(id: string, request: DependencyJobRequest): Promise<JobResponse> {
|
||
return this.request<JobResponse>(`/server-instances/${encodeURIComponent(id)}/dependencies/install`, {
|
||
method: "POST",
|
||
body: request
|
||
});
|
||
}
|
||
|
||
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 updateServerInstance(id: string, request: ServerInstanceUpdateRequest): Promise<ServerInstanceResponse> {
|
||
return this.request<ServerInstanceResponse>(`/server-instances/${encodeURIComponent(id)}`, {
|
||
method: "PUT",
|
||
body: request
|
||
});
|
||
}
|
||
|
||
async deleteServerInstance(id: string, request: ServerDeletionRequest): Promise<void> {
|
||
return this.request<void>(`/server-instances/${encodeURIComponent(id)}`, { method: "DELETE", body: request, parseJson: false });
|
||
}
|
||
|
||
async getPlatformResourceUsage(): Promise<PlatformResourceUsageResponse> {
|
||
return this.request<PlatformResourceUsageResponse>("/metrics/platform");
|
||
}
|
||
|
||
async listPluginLifecycles(filter: { pluginId?: string; serverInstanceId?: string; currentState?: string } = {}): Promise<PluginLifecycleListResponse> {
|
||
const params = new URLSearchParams();
|
||
Object.entries(filter).forEach(([key, value]) => { if (value) params.set(key, value); });
|
||
const query = params.toString();
|
||
return this.request<PluginLifecycleListResponse>(`/plugin-lifecycles${query ? `?${query}` : ""}`);
|
||
}
|
||
|
||
async runPluginLifecycle(pluginId: string, request: PluginLifecycleActionRequest): Promise<PluginLifecycleActionResponse> {
|
||
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");
|
||
}
|
||
|
||
async listMetricHistory(serverInstanceId: string, limit = 100): Promise<MetricSampleListResponse> {
|
||
const params = new URLSearchParams({ serverInstanceId, limit: String(limit) });
|
||
return this.request<MetricSampleListResponse>(`/metrics/server-instances/history?${params.toString()}`);
|
||
}
|
||
|
||
async listBackups(serverInstanceId: string): Promise<BackupListResponse> {
|
||
return this.request<BackupListResponse>(`/backups?serverInstanceId=${encodeURIComponent(serverInstanceId)}`);
|
||
}
|
||
|
||
async getBackup(id: string): Promise<BackupResponse> {
|
||
return this.request<BackupResponse>(`/backups/${encodeURIComponent(id)}`);
|
||
}
|
||
|
||
async listRemoteAdapters(serverInstanceId: string): Promise<RemoteAdapterDeclarationListResponse> {
|
||
return this.request<RemoteAdapterDeclarationListResponse>(`/server-instances/${encodeURIComponent(serverInstanceId)}/remote-adapters`);
|
||
}
|
||
|
||
async listScumUsers(serverInstanceId: string, request: ScumListRequest = {}): Promise<ScumUserListResponse> {
|
||
return this.request<ScumUserListResponse>(`/server-instances/${encodeURIComponent(serverInstanceId)}/scum/users${scumListQuery(request)}`);
|
||
}
|
||
|
||
async listScumUserTrajectories(serverInstanceId: string, request: ScumListRequest = {}): Promise<ScumUserTrajectoryListResponse> {
|
||
return this.request<ScumUserTrajectoryListResponse>(`/server-instances/${encodeURIComponent(serverInstanceId)}/scum/user-trajectories${scumListQuery(request)}`);
|
||
}
|
||
|
||
async listScumVehicles(serverInstanceId: string, request: ScumListRequest = {}): Promise<ScumVehicleListResponse> {
|
||
return this.request<ScumVehicleListResponse>(`/server-instances/${encodeURIComponent(serverInstanceId)}/scum/vehicles${scumListQuery(request)}`);
|
||
}
|
||
|
||
async listScumVehicleTrajectories(serverInstanceId: string, request: ScumListRequest = {}): Promise<ScumVehicleTrajectoryListResponse> {
|
||
return this.request<ScumVehicleTrajectoryListResponse>(`/server-instances/${encodeURIComponent(serverInstanceId)}/scum/vehicle-trajectories${scumListQuery(request)}`);
|
||
}
|
||
|
||
async listScumVehicleLocks(serverInstanceId: string, request: ScumListRequest = {}): Promise<ScumVehicleLockListResponse> {
|
||
return this.request<ScumVehicleLockListResponse>(`/server-instances/${encodeURIComponent(serverInstanceId)}/scum/vehicle-locks${scumListQuery(request)}`);
|
||
}
|
||
|
||
async getScumSurface(serverInstanceId: string): Promise<ScumSurfaceResponse> {
|
||
return this.request<ScumSurfaceResponse>(`/server-instances/${encodeURIComponent(serverInstanceId)}/scum/surface`);
|
||
}
|
||
|
||
async listPluginData(serverInstanceId: string, collection: string, key?: string): Promise<{ items: Array<{ key: string; value: Record<string, unknown> }>; count: number }> {
|
||
const params = new URLSearchParams();
|
||
if (key) params.set("key", key);
|
||
const query = params.toString();
|
||
return this.request(`/server-instances/${encodeURIComponent(serverInstanceId)}/plugin-data/${encodeURIComponent(collection)}${query ? `?${query}` : ""}`);
|
||
}
|
||
|
||
async putPluginData(serverInstanceId: string, collection: string, key: string, value: Record<string, unknown>): Promise<{ key: string; value: Record<string, unknown> }> {
|
||
return this.request(`/server-instances/${encodeURIComponent(serverInstanceId)}/plugin-data/${encodeURIComponent(collection)}`, { method: "PUT", body: { key, value } });
|
||
}
|
||
|
||
async deletePluginData(serverInstanceId: string, collection: string, key: string): Promise<void> {
|
||
const query = new URLSearchParams({ key }).toString();
|
||
return this.request(`/server-instances/${encodeURIComponent(serverInstanceId)}/plugin-data/${encodeURIComponent(collection)}?${query}`, { method: "DELETE" });
|
||
}
|
||
|
||
async transactPluginData(serverInstanceId: string, collection: string, mutations: Array<{ operation: "put" | "delete"; key: string; value?: Record<string, unknown> }>): Promise<{ items: Array<{ key: string; value: Record<string, unknown> }>; count: number }> {
|
||
return this.request(`/server-instances/${encodeURIComponent(serverInstanceId)}/plugin-data/${encodeURIComponent(collection)}/transaction`, { method: "POST", body: { mutations } });
|
||
}
|
||
|
||
async requestRemoteAdapter(serverInstanceId: string, request: RemoteAdapterRequest): Promise<RemoteAdapterResponse> {
|
||
return this.request<RemoteAdapterResponse>(`/server-instances/${encodeURIComponent(serverInstanceId)}/remote-adapters`, { method: "POST", body: request });
|
||
}
|
||
|
||
async dispatchFileOperation(request: FileOperationDispatchRequest): Promise<FileOperationDispatchResponse> {
|
||
return this.request<FileOperationDispatchResponse>("/file-operations/dispatch", {
|
||
method: "POST",
|
||
body: request
|
||
});
|
||
}
|
||
|
||
async getServerFileWorkspace(serverInstanceId: string): Promise<ServerFileWorkspaceResponse> {
|
||
return normalizeServerFileWorkspace(await this.request<ServerFileWorkspaceResponse>(`/server-instances/${encodeURIComponent(serverInstanceId)}/files/workspace`));
|
||
}
|
||
|
||
async listServerFiles(serverInstanceId: string, request: Partial<ServerFileListRequest> = {}): Promise<ServerFileListResponse> {
|
||
const params = serverFileListQuery(request);
|
||
return normalizeServerFileList(await this.request<ServerFileListResponse>(`/server-instances/${encodeURIComponent(serverInstanceId)}/files/list${params}`));
|
||
}
|
||
|
||
async refreshServerFiles(serverInstanceId: string, request: ServerFileListRequest): Promise<ServerFileListResponse> {
|
||
return normalizeServerFileList(await this.request<ServerFileListResponse>(`/server-instances/${encodeURIComponent(serverInstanceId)}/files/refresh`, { method: "POST", body: request }));
|
||
}
|
||
|
||
async browseServerFiles(serverInstanceId: string, request: ServerFileListRequest): Promise<ServerFileListResponse> {
|
||
return normalizeServerFileList(await this.request<ServerFileListResponse>(`/server-instances/${encodeURIComponent(serverInstanceId)}/files/browse`, { method: "POST", body: request }));
|
||
}
|
||
|
||
async readServerFile(serverInstanceId: string, request: ServerFileReadRequest): Promise<FileOperationDispatchResponse> {
|
||
return this.request<FileOperationDispatchResponse>(`/server-instances/${encodeURIComponent(serverInstanceId)}/files/read`, { method: "POST", body: request });
|
||
}
|
||
|
||
async getServerFileReadSnapshot(serverInstanceId: string, key: string): Promise<DeclaredFileReadSnapshotResponse> {
|
||
const params = new URLSearchParams({ key });
|
||
return this.request<DeclaredFileReadSnapshotResponse>(`/server-instances/${encodeURIComponent(serverInstanceId)}/files/read-snapshot?${params.toString()}`);
|
||
}
|
||
|
||
async writeServerFile(serverInstanceId: string, request: ServerFileWriteRequest): Promise<FileOperationDispatchResponse> {
|
||
return this.request<FileOperationDispatchResponse>(`/server-instances/${encodeURIComponent(serverInstanceId)}/files/write`, { method: "POST", body: request });
|
||
}
|
||
|
||
async uploadServerFile(serverInstanceId: string, request: { directoryKey: string; relativePath?: string; file: File; idempotencyKey: string }): Promise<ServerFileUploadResponse> {
|
||
const body = new FormData();
|
||
body.set("directoryKey", request.directoryKey);
|
||
body.set("relativePath", request.relativePath ?? "");
|
||
body.set("filename", request.file.name);
|
||
body.set("idempotencyKey", request.idempotencyKey);
|
||
body.set("file", request.file);
|
||
const headers = new Headers();
|
||
const sessionToken = this.sessionTokenProvider();
|
||
if (sessionToken) headers.set("Authorization", `Bearer ${sessionToken}`);
|
||
const response = await fetch(`${this.baseUrl}/server-instances/${encodeURIComponent(serverInstanceId)}/files/upload`, { method: "POST", credentials: "same-origin", headers, body });
|
||
if (!response.ok) throw await responseError(response);
|
||
return response.json() as Promise<ServerFileUploadResponse>;
|
||
}
|
||
|
||
async prepareServerFileDownload(serverInstanceId: string, request: ServerFileDownloadRequest): Promise<ServerFileDownloadResponse> {
|
||
return this.request<ServerFileDownloadResponse>(`/server-instances/${encodeURIComponent(serverInstanceId)}/files/download`, { method: "POST", body: request });
|
||
}
|
||
|
||
async listLogStreams(serverInstanceId?: string): Promise<LogStreamListResponse> {
|
||
const query = serverInstanceId ? `?serverInstanceId=${encodeURIComponent(serverInstanceId)}` : "";
|
||
return this.request<LogStreamListResponse>(`/log-streams${query}`);
|
||
}
|
||
|
||
openServerLogEvents(id: string): PlatformEventStream {
|
||
const url = this.serverLogEventsUrl(id);
|
||
const sessionToken = this.sessionTokenProvider();
|
||
if (!sessionToken) {
|
||
return new EventSource(url, { withCredentials: true });
|
||
}
|
||
return new FetchServerSentEventStream(url, sessionToken);
|
||
}
|
||
|
||
serverLogEventsUrl(id: string): string {
|
||
return `${this.baseUrl}/server-instances/${encodeURIComponent(id)}/logs/events`;
|
||
}
|
||
|
||
async queryLogStream(request: LogStreamCursorRequest): Promise<LogStreamCursorResponse> {
|
||
return this.request<LogStreamCursorResponse>("/log-streams/query", { method: "POST", body: request });
|
||
}
|
||
|
||
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,
|
||
credentials: options.init?.credentials ?? "same-origin",
|
||
method: options.method ?? options.init?.method ?? "GET",
|
||
headers,
|
||
body: options.body === undefined ? options.init?.body : JSON.stringify(options.body)
|
||
});
|
||
|
||
if (!response.ok) {
|
||
throw await responseError(response);
|
||
}
|
||
|
||
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;
|
||
}
|
||
}
|
||
|
||
async function responseError(response: Response): Promise<PlatformApiError> {
|
||
const apiError = await safeReadError(response);
|
||
const message = response.status === 401
|
||
? "会话已失效,请重新登录。"
|
||
: apiError?.code === "validation_failed"
|
||
? safeValidationMessage(apiError)
|
||
: response.status === 403
|
||
? safeForbiddenMessage(apiError?.message)
|
||
: apiError?.message ?? `request failed: ${response.status}`;
|
||
const error = new PlatformApiError(response.status, apiError?.code ?? "request_failed", message);
|
||
if (response.status === 401) {
|
||
platformApiSessionToken = null;
|
||
platformApiAuthFailureHandler?.(error);
|
||
}
|
||
return error;
|
||
}
|
||
|
||
class FetchServerSentEventStream implements PlatformEventStream {
|
||
onerror: ((event: Event) => void) | null = null;
|
||
private lastEventId = "";
|
||
private readonly controller = new AbortController();
|
||
private readonly listeners = new Map<string, Set<(event: MessageEvent) => void>>();
|
||
|
||
constructor(private readonly url: string, private readonly sessionToken: string) {
|
||
void this.connectLoop();
|
||
}
|
||
|
||
addEventListener(type: string, listener: (event: MessageEvent) => void): void {
|
||
const listeners = this.listeners.get(type) ?? new Set<(event: MessageEvent) => void>();
|
||
listeners.add(listener);
|
||
this.listeners.set(type, listeners);
|
||
}
|
||
|
||
removeEventListener(type: string, listener: (event: MessageEvent) => void): void {
|
||
this.listeners.get(type)?.delete(listener);
|
||
}
|
||
|
||
close(): void {
|
||
this.controller.abort();
|
||
this.listeners.clear();
|
||
}
|
||
|
||
private async connectLoop(): Promise<void> {
|
||
while (!this.controller.signal.aborted) {
|
||
try {
|
||
const headers = new Headers({ Accept: "text/event-stream", Authorization: `Bearer ${this.sessionToken}` });
|
||
if (this.lastEventId) headers.set("Last-Event-ID", this.lastEventId);
|
||
const response = await fetch(this.url, { method: "GET", credentials: "include", headers, signal: this.controller.signal });
|
||
if (!response.ok || !response.body) {
|
||
throw new Error(`event stream failed: ${response.status}`);
|
||
}
|
||
await this.read(response.body);
|
||
} catch {
|
||
if (this.controller.signal.aborted) return;
|
||
this.onerror?.(new Event("error"));
|
||
}
|
||
await this.reconnectPause();
|
||
}
|
||
}
|
||
|
||
private async read(body: ReadableStream<Uint8Array>): Promise<void> {
|
||
const reader = body.getReader();
|
||
const decoder = new TextDecoder();
|
||
let buffer = "";
|
||
let eventName = "message";
|
||
let eventId = "";
|
||
let dataLines: string[] = [];
|
||
const processLine = (rawLine: string) => {
|
||
const line = rawLine.endsWith("\r") ? rawLine.slice(0, -1) : rawLine;
|
||
if (line === "") {
|
||
if (dataLines.length > 0) {
|
||
this.dispatch(eventName, dataLines.join("\n"), eventId);
|
||
}
|
||
eventName = "message";
|
||
eventId = "";
|
||
dataLines = [];
|
||
return;
|
||
}
|
||
if (line.startsWith(":")) return;
|
||
const separator = line.indexOf(":");
|
||
const field = separator === -1 ? line : line.slice(0, separator);
|
||
const value = separator === -1 ? "" : line.slice(separator + 1).replace(/^ /, "");
|
||
if (field === "event") eventName = value || "message";
|
||
if (field === "id") eventId = value;
|
||
if (field === "data") dataLines.push(value);
|
||
};
|
||
try {
|
||
while (!this.controller.signal.aborted) {
|
||
const { value, done } = await reader.read();
|
||
if (done) break;
|
||
buffer += decoder.decode(value, { stream: true });
|
||
const lines = buffer.split("\n");
|
||
buffer = lines.pop() ?? "";
|
||
lines.forEach(processLine);
|
||
}
|
||
buffer += decoder.decode();
|
||
if (buffer) processLine(buffer);
|
||
processLine("");
|
||
} finally {
|
||
reader.releaseLock();
|
||
}
|
||
}
|
||
|
||
private dispatch(type: string, data: string, lastEventId: string): void {
|
||
if (lastEventId) this.lastEventId = lastEventId;
|
||
const event = new MessageEvent(type, { data, lastEventId });
|
||
this.listeners.get(type)?.forEach((listener) => listener(event));
|
||
if (type !== "message") {
|
||
this.listeners.get("message")?.forEach((listener) => listener(event));
|
||
}
|
||
}
|
||
|
||
private reconnectPause(): Promise<void> {
|
||
if (this.controller.signal.aborted) return Promise.resolve();
|
||
return new Promise((resolve) => {
|
||
const timeout = globalThis.setTimeout(resolve, 1000);
|
||
this.controller.signal.addEventListener("abort", () => { globalThis.clearTimeout(timeout); resolve(); }, { once: true });
|
||
});
|
||
}
|
||
}
|
||
|
||
function safeValidationMessage(apiError?: ApiErrorResponse | null): string {
|
||
const details = apiError?.details
|
||
?.map((detail) => safeValidationDetail(detail))
|
||
.filter((detail): detail is string => Boolean(detail));
|
||
if (details?.length) {
|
||
return details.slice(0, 3).join(";");
|
||
}
|
||
const sanitized = safeDiagnosticText(apiError?.message, "")?.trim();
|
||
return sanitized || "请求校验失败。";
|
||
}
|
||
|
||
function safeValidationDetail(detail: string): string | undefined {
|
||
const sanitized = safeDiagnosticText(detail, "")?.trim();
|
||
switch (sanitized) {
|
||
case "password is required":
|
||
return "请输入当前登录密码。";
|
||
case "running or installing server instances must be stopped before delete":
|
||
return "运行中或安装中的服务器必须先停止再删除。";
|
||
case "running or installing server instances require forced-delete confirmation":
|
||
return "运行中或安装中的服务器需要强制删除确认。";
|
||
default:
|
||
return sanitized || undefined;
|
||
}
|
||
}
|
||
|
||
function safeForbiddenMessage(apiMessage?: string): string {
|
||
const sanitized = safeDiagnosticText(apiMessage, "")?.trim();
|
||
if (!sanitized || sanitized === "account is not allowed to access this resource") {
|
||
return "没有权限访问该资源。";
|
||
}
|
||
if (sanitized === "password confirmation failed") {
|
||
return "当前登录密码不正确,请重新输入。";
|
||
}
|
||
const missingPermission = sanitized.match(/^plugin does not declare required permission:\s*([a-z0-9._-]+)$/i);
|
||
if (missingPermission) {
|
||
return `插件未声明所需权限:${missingPermission[1]}`;
|
||
}
|
||
if (sanitized === "plugin is not installed") {
|
||
return "插件未安装,不能执行该操作。";
|
||
}
|
||
return "没有权限访问该资源。";
|
||
}
|
||
|
||
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}` : "";
|
||
}
|
||
|
||
function runEndpointQuery(filter: RunEndpointListFilter): string {
|
||
const params = new URLSearchParams();
|
||
if (filter.status) params.set("status", filter.status);
|
||
const query = params.toString();
|
||
return query ? `?${query}` : "";
|
||
}
|
||
|
||
function jobListQuery(serverInstanceId: string | undefined, filter: JobListFilter): string {
|
||
const params = new URLSearchParams();
|
||
if (serverInstanceId) params.set("serverInstanceId", serverInstanceId);
|
||
if (filter.runEndpointId) params.set("runEndpointId", filter.runEndpointId);
|
||
if (filter.state) params.set("state", filter.state);
|
||
if (filter.states?.length) params.set("states", filter.states.join(","));
|
||
if (filter.limit !== undefined) params.set("limit", String(filter.limit));
|
||
const query = params.toString();
|
||
return query ? `?${query}` : "";
|
||
}
|
||
|
||
function scumListQuery(request: ScumListRequest): string {
|
||
const params = new URLSearchParams();
|
||
if (request.limit !== undefined) params.set("limit", String(request.limit));
|
||
if (request.changedAfter) params.set("changedAfter", request.changedAfter);
|
||
if (request.after) params.set("after", request.after);
|
||
if (request.online !== undefined) params.set("online", String(request.online));
|
||
if (request.exists !== undefined) params.set("exists", String(request.exists));
|
||
if (request.steamId) params.set("steamId", request.steamId);
|
||
if (request.scumUserId) params.set("scumUserId", request.scumUserId);
|
||
if (request.gameVehicleId) params.set("gameVehicleId", request.gameVehicleId);
|
||
if (request.scumVehicleId) params.set("scumVehicleId", request.scumVehicleId);
|
||
const query = params.toString();
|
||
return query ? `?${query}` : "";
|
||
}
|
||
|
||
function serverFileListQuery(request: Partial<ServerFileListRequest>): string {
|
||
const params = new URLSearchParams();
|
||
if (request.directoryKey) params.set("directoryKey", request.directoryKey);
|
||
if (request.path) params.set("path", request.path);
|
||
if (request.query) params.set("query", request.query);
|
||
if (request.recursive) params.set("recursive", "true");
|
||
const query = params.toString();
|
||
return query ? `?${query}` : "";
|
||
}
|
||
|
||
function normalizeServerFileWorkspace(response: ServerFileWorkspaceResponse): ServerFileWorkspaceResponse {
|
||
return { ...response, directories: response.directories ?? [], files: response.files ?? [], configFields: response.configFields ?? [], transfer: { ...response.transfer, notes: response.transfer.notes ?? [] } };
|
||
}
|
||
|
||
function normalizeServerFileList(response: ServerFileListResponse): ServerFileListResponse {
|
||
return { ...response, entries: response.entries ?? [] };
|
||
}
|
||
|
||
export const platformApiClient = new PlatformApiClient(readWebRuntimeEnv().platformApiBaseUrl);
|