Remove SCUM server-management client path

This commit is contained in:
npc0-hue
2026-09-02 17:56:49 +08:00
parent 6e614d3fa3
commit a82f1ff01a
52 changed files with 245 additions and 5131 deletions
+2 -3
View File
@@ -82,12 +82,11 @@ Server list surfaces expose runtime package actions through platform APIs, while
- download the latest authorized run package.
- push a self-update job to an online run endpoint when the endpoint reports `run.self-update`.
- reset the current run key, which invalidates older run packages until regenerated.
- generate and download plugin-declared client-manager packages, including SCUM-style companion managers.
- reset the current client-manager key separately from the run key.
- use plugin-declared remote access actions such as RCON through Platform bridge envelopes.
- request dependency checks and typed dependency install jobs.
- open live server log stream metadata and request historical log backfill jobs.
These screens show safe availability reasons, run online/offline status, job/build/dependency progress, artifact IDs, checksums, key generations, fingerprints, and redacted `secret://runtime-keys/.../current` refs. They must not render raw run/client-manager keys, FTP passwords, database DSNs, RCON passwords, host paths, direct run sockets, backend storage URLs, or large inline log bodies.
These screens show safe availability reasons, run online/offline status, job/build/dependency progress, artifact IDs, checksums, key generations, fingerprints, and redacted `secret://runtime-keys/.../current` refs. They must not render raw run/client-manager keys, FTP passwords, database DSNs, RCON passwords, host paths, direct run sockets, backend storage URLs, or large inline log bodies. Log bodies are displayed verbatim when the plugin declares them; the browser does not redact or reinterpret them.
Manual UI smoke checklist:
+15 -15
View File
@@ -465,10 +465,10 @@ describe("PlatformApiClient AI providers", () => {
}
if (url.endsWith("/api/v1/server-instances/server-1/client-managers/generate") && init?.method === "POST") {
expect(JSON.parse(String(init.body))).toEqual({
profileKey: "scum-client-manager",
profileKey: "example-client-manager",
targetOs: "windows",
targetArch: "amd64",
repositoryUrl: "https://github.com/F88888/scum_client.git",
repositoryUrl: "https://example.test/client-manager.git",
sourceRevision: "main",
idempotencyKey: "idem-client-generate"
});
@@ -476,33 +476,33 @@ describe("PlatformApiClient AI providers", () => {
id: "client-dist-1",
serverInstanceId: server.id,
pluginId: plugin.id,
profileKey: "scum-client-manager",
profileKey: "example-client-manager",
targetOs: "windows",
targetArch: "amd64",
repositoryUrl: "https://github.com/F88888/scum_client.git",
repositoryUrl: "https://example.test/client-manager.git",
sourceRevision: "main",
buildJobId: "client-build-1",
artifactId: "artifact-client-1",
checksum: "sha256:clientchecksum",
keyGeneration: 1,
secretRef: "secret://runtime-keys/server-1/client-manager/scum-client-manager/current",
secretRef: "secret://runtime-keys/server-1/client-manager/example-client-manager/current",
status: "available",
createdAt: "2026-07-03T00:00:00Z",
updatedAt: "2026-07-03T00:00:00Z"
});
}
if (url.endsWith("/api/v1/server-instances/server-1/client-managers/download") && init?.method === "POST") {
expect(JSON.parse(String(init.body))).toEqual({ profileKey: "scum-client-manager" });
return jsonResponse({ ...runtimeDownload, artifactId: "artifact-client-1", filename: "scum-client-manager.exe" });
expect(JSON.parse(String(init.body))).toEqual({ profileKey: "example-client-manager" });
return jsonResponse({ ...runtimeDownload, artifactId: "artifact-client-1", filename: "example-client-manager.exe" });
}
if (url.endsWith("/api/v1/server-instances/server-1/client-managers/key/reset") && init?.method === "POST") {
expect(JSON.parse(String(init.body))).toEqual({ componentKind: "client-manager", componentKey: "scum-client-manager" });
expect(JSON.parse(String(init.body))).toEqual({ componentKind: "client-manager", componentKey: "example-client-manager" });
return jsonResponse({
id: "runtime-key-server-1-client-2",
serverInstanceId: server.id,
componentKind: "client-manager",
componentKey: "scum-client-manager",
secretRef: "secret://runtime-keys/server-1/client-manager/scum-client-manager/current",
componentKey: "example-client-manager",
secretRef: "secret://runtime-keys/server-1/client-manager/example-client-manager/current",
fingerprint: "def456abc123",
generation: 2,
status: "active",
@@ -640,16 +640,16 @@ describe("PlatformApiClient AI providers", () => {
await expect(client.listRunUpdates(server.id)).resolves.toMatchObject({ count: 1, items: [{ phase: "restart-requested", rollback: false }] });
await expect(
client.generateClientManager(server.id, {
profileKey: "scum-client-manager",
profileKey: "example-client-manager",
targetOs: "windows",
targetArch: "amd64",
repositoryUrl: "https://github.com/F88888/scum_client.git",
repositoryUrl: "https://example.test/client-manager.git",
sourceRevision: "main",
idempotencyKey: "idem-client-generate"
})
).resolves.toMatchObject({ artifactId: "artifact-client-1", profileKey: "scum-client-manager" });
await expect(client.downloadLatestClientManager(server.id, { profileKey: "scum-client-manager" })).resolves.toMatchObject({ artifactId: "artifact-client-1" });
await expect(client.resetClientManagerKey(server.id, { componentKind: "client-manager", componentKey: "scum-client-manager" })).resolves.toMatchObject({ generation: 2 });
).resolves.toMatchObject({ artifactId: "artifact-client-1", profileKey: "example-client-manager" });
await expect(client.downloadLatestClientManager(server.id, { profileKey: "example-client-manager" })).resolves.toMatchObject({ artifactId: "artifact-client-1" });
await expect(client.resetClientManagerKey(server.id, { componentKind: "client-manager", componentKey: "example-client-manager" })).resolves.toMatchObject({ generation: 2 });
await expect(client.checkDependencies(server.id, { probeKey: "java-21", idempotencyKey: "idem-dep-check" })).resolves.toMatchObject({ capability: "dependencies.check" });
await expect(client.getDependencyCatalog(server.id)).resolves.toMatchObject({ targetOs: "linux", plans: [{ digest: runtimeDigest }] });
await expect(client.installDependencies(server.id, { probeKey: "java-21", installPlanKey: "install-java-linux", planDigest: runtimeDigest, idempotencyKey: "idem-dep-install" })).resolves.toMatchObject({
@@ -3,7 +3,7 @@ import { afterEach, describe, expect, it, vi } from "vitest";
import { PlatformApiClient } from "./client";
const lifecycle = {
id: "installation-1", serverInstanceId: "server-1", pluginId: "game.scum", profileKey: "scum-client-manager", targetOs: "windows", targetArch: "amd64",
id: "installation-1", serverInstanceId: "server-1", pluginId: "game.example", profileKey: "example-client-manager", targetOs: "windows", targetArch: "amd64",
status: "available", phase: "artifact available", desiredVersion: "1.0.0", desiredRevision: "rev-1", desiredArtifactId: "artifact-1", keyGeneration: 1,
deploymentGeneration: 0, health: "unknown", healthReason: "component is not installed", retryable: false, requiresRedeploy: false, updatedAt: "2026-07-18T08:00:00Z",
distribution: { id: "distribution-1", artifactId: "artifact-1", sourceRevision: "rev-1", targetOs: "windows", targetArch: "amd64", checksum: `sha256:${"a".repeat(64)}`, keyGeneration: 1, status: "available" },
@@ -23,17 +23,17 @@ describe("PlatformApiClient Client Manager lifecycle", () => {
const client = new PlatformApiClient("/api/v1", () => "session-token");
await expect(client.listClientManagerLifecycles("server-1")).resolves.toMatchObject({ count: 1 });
await expect(client.getClientManagerLifecycle("server-1", "scum-client-manager")).resolves.toMatchObject({ profileKey: "scum-client-manager" });
await client.deployClientManager("server-1", { profileKey: "scum-client-manager", distributionId: "distribution-1", expectedDeploymentGeneration: 0, idempotencyKey: "deploy-1" });
await client.controlClientManager("server-1", { profileKey: "scum-client-manager", operation: "start", expectedDeploymentGeneration: 1, idempotencyKey: "start-1" });
await client.updateClientManager("server-1", { profileKey: "scum-client-manager", distributionId: "distribution-2", expectedDeploymentGeneration: 1, approved: true, idempotencyKey: "update-1" });
await client.retryClientManagerLifecycle("server-1", { profileKey: "scum-client-manager", expectedDeploymentGeneration: 2, idempotencyKey: "retry-1" });
await client.revokeClientManagerSession("server-1", { profileKey: "scum-client-manager", reason: "operator revoked component session" });
await client.uninstallClientManager("server-1", { profileKey: "scum-client-manager", expectedDeploymentGeneration: 2, confirmed: true, idempotencyKey: "uninstall-1" });
await expect(client.getClientManagerLifecycle("server-1", "example-client-manager")).resolves.toMatchObject({ profileKey: "example-client-manager" });
await client.deployClientManager("server-1", { profileKey: "example-client-manager", distributionId: "distribution-1", expectedDeploymentGeneration: 0, idempotencyKey: "deploy-1" });
await client.controlClientManager("server-1", { profileKey: "example-client-manager", operation: "start", expectedDeploymentGeneration: 1, idempotencyKey: "start-1" });
await client.updateClientManager("server-1", { profileKey: "example-client-manager", distributionId: "distribution-2", expectedDeploymentGeneration: 1, approved: true, idempotencyKey: "update-1" });
await client.retryClientManagerLifecycle("server-1", { profileKey: "example-client-manager", expectedDeploymentGeneration: 2, idempotencyKey: "retry-1" });
await client.revokeClientManagerSession("server-1", { profileKey: "example-client-manager", reason: "operator revoked component session" });
await client.uninstallClientManager("server-1", { profileKey: "example-client-manager", expectedDeploymentGeneration: 2, confirmed: true, idempotencyKey: "uninstall-1" });
expect(calls.map((call) => `${call.method} ${call.url}`)).toEqual([
"GET /api/v1/server-instances/server-1/client-managers",
"GET /api/v1/server-instances/server-1/client-managers/scum-client-manager",
"GET /api/v1/server-instances/server-1/client-managers/example-client-manager",
"POST /api/v1/server-instances/server-1/client-managers/deploy",
"POST /api/v1/server-instances/server-1/client-managers/control",
"POST /api/v1/server-instances/server-1/client-managers/update",
@@ -72,10 +72,10 @@ const plugin: GamePluginResponse = {
productionLifecycle: { operations: ["install", "enable", "disable", "upgrade", "rollback", "retire", "dependency-check"], dependencyPolicy: "required" },
gameClientBridge: {
commands: [{ type: "diagnostic.ping", title: "Diagnostic ping", permission: "server.game-client.command", payloadSchemaRef: "schemas/bridge/diagnostic-ping.json", timeoutSeconds: 30, maxPayloadBytes: 4096 }],
snapshots: [{ type: "companion.health", schemaVersion: "1", schemaRef: "schemas/bridge/health.json", keepForSeconds: 3600, maxRecords: 24 }],
snapshots: [{ type: "bridge.health", schemaVersion: "1", schemaRef: "schemas/bridge/health.json", keepForSeconds: 3600, maxRecords: 24 }],
commandRetentionSeconds: 86400,
maxCommands: 1000,
pages: [{ pageKey: "players", commandTypes: ["diagnostic.ping"], snapshotTypes: ["companion.health"] }]
pages: [{ pageKey: "players", commandTypes: ["diagnostic.ping"], snapshotTypes: ["bridge.health"] }]
},
status: "installed"
};
@@ -193,14 +193,14 @@ describe("PluginPageHostPage", () => {
{ operation: "delete" as const, key: "user-2" }
];
const queueRequest = {
profileKey: "scum-client",
profileKey: "example-client",
commandType: "reward.deliver",
payload: { steamId: "redacted", giftKey: "starter" },
payload: { steamId: "steam-1", giftKey: "starter" },
idempotencyKey: "gift:user-1:starter",
expiresAt: "2026-08-15T12:00:00Z"
};
const commandFilter = { profileKey: "scum-client", state: "pending" as const, commandType: "reward.deliver" };
const snapshotQuery = { profileKey: "scum-client", type: "scum.positions", streamKey: "current", limit: 50 };
const commandFilter = { profileKey: "example-client", state: "pending" as const, commandType: "reward.deliver" };
const snapshotQuery = { profileKey: "example-client", type: "example.positions", streamKey: "current", limit: 50 };
const dispatchEnvelope = { requestId: "request-1", action: "server.instances.read" as const, payload: { view: "summary" } };
await actions?.pluginData?.list("scum_users", "user-1");
+3 -3
View File
@@ -29,7 +29,7 @@ export function PluginPageHostPage({ params, onNavigate, initialPlugin, embedded
const [state, setState] = useState<PluginPageState>(() => initialPlugin ? { status: "ready", plugin: initialPlugin } : { status: "loading" });
const [bundle, setBundle] = useState<ComponentType<{ context: ReturnType<typeof createPluginBridgeHostContext>; workspaceActions?: PluginPageWorkspaceActions; availability: PluginPageAvailability }> | null>(null);
const [bundleError, setBundleError] = useState("");
const [availability, setAvailability] = useState<PluginPageAvailability>({ available: false, reason: "正在验证 Companion 可用性。" });
const [availability, setAvailability] = useState<PluginPageAvailability>({ available: false, reason: "正在验证插件桥接可用性。" });
const readyPluginRef = useRef<GamePluginResponse | undefined>(undefined);
const hostContextRef = useRef<ReturnType<typeof createPluginBridgeHostContext> | undefined>(undefined);
@@ -99,7 +99,7 @@ export function PluginPageHostPage({ params, onNavigate, initialPlugin, embedded
if (!serverId) { setAvailability({ available: false, reason: "插件页面没有绑定服务器。" }); return () => { active = false; }; }
void platformApiClient.getGameClientBridgeStatus(serverId).then((status) => {
if (active) setAvailability({ available: status.available, reason: status.reason, features: status.features });
}).catch((error) => { if (active) setAvailability({ available: false, reason: error instanceof Error ? error.message : "无法验证 Companion 可用性。" }); });
}).catch((error) => { if (active) setAvailability({ available: false, reason: error instanceof Error ? error.message : "无法验证插件桥接可用性。" }); });
return () => { active = false; };
}, [bundleLoadKey, loadableBundlePage, serverId]);
@@ -148,7 +148,7 @@ export function PluginPageHostPage({ params, onNavigate, initialPlugin, embedded
</button>
</div>
<div className="action-list" aria-label="plugin page declarations"><span><strong>Bundle</strong> {page.bundleKey ? `${page.bundleKey}@${page.bundleVersion}` : "未声明"}</span><span><strong></strong> {page.bundleIntegritySha256 ? `${page.bundleIntegritySha256.slice(0, 18)}` : "未声明"}</span><span><strong>Companion</strong> {availability.available ? "可用" : "不可用"}</span></div>
<div className="action-list" aria-label="plugin page declarations"><span><strong>Bundle</strong> {page.bundleKey ? `${page.bundleKey}@${page.bundleVersion}` : "未声明"}</span><span><strong></strong> {page.bundleIntegritySha256 ? `${page.bundleIntegritySha256.slice(0, 18)}` : "未声明"}</span><span><strong></strong> {availability.available ? "可用" : "不可用"}</span></div>
</section>
{bundleError && <ErrorState title="插件页面不可用" reason={bundleError} />}
{!bundle && !bundleError && <LoadingState label="正在校验并加载插件页面 bundle…" />}
+3 -70
View File
@@ -7,7 +7,6 @@ import type { GamePluginResponse, JobResponse, RunEndpointResponse, ServerInstan
import {
RuntimeTaskProgressDialog,
type RuntimeTaskDialogAction,
runtimeBuildStages,
runtimeDependencyStages,
runtimeDownloadStages,
runtimeKeyResetStages,
@@ -29,7 +28,6 @@ import {
import { summarizeServerOperations } from "../contracts/operationsConsole";
import { filterServerCards, serverIsOnline, type ServerCardView, type ServerStatusFilter } from "../contracts/workspace";
import {
clientManagerBuildRequest,
dependencyJobRequest,
runDistributionGenerateRequest,
runUpdateRequest,
@@ -298,35 +296,10 @@ export function ServersPage({ session, operations, onNavigate }: PageComponentPr
const intent = quickRuntimeActionLabel(action);
const operationId = operations.begin({ intent, targetKind: "server", targetId: `${instance.id}:${action}`, requester: session.displayName });
setRuntimeTaskActions([]);
let generatedClientProfile: string | null = null;
try {
await requireQuickRuntimeActionAvailable(instance.id, action);
let message: string;
if (action === "generate-client-manager") {
const distribution = await runtimeTask.runTrackedTask({
title: intent,
description: quickRuntimeTaskDescription(instance, action),
stages: runtimeBuildStages,
start: async () => {
const distribution = await platformApiClient.generateClientManager(
instance.id,
clientManagerBuildRequest({
serverInstanceId: instance.id,
profileKey: defaults.clientProfileKey,
targetOs: defaults.clientOs,
targetArch: "amd64",
repositoryUrl: defaults.repositoryUrl,
sourceRevision: "main"
})
);
return { value: distribution, jobId: distribution.buildJobId };
},
poll: (jobId) => platformApiClient.getJob(jobId)
});
generatedClientProfile = defaults.clientProfileKey;
message = `客户端二进制已构建并上传,artifact ${distribution.artifactId}`;
} else {
message = await runtimeTask.runTask({
message = await runtimeTask.runTask({
title: intent,
description: quickRuntimeTaskDescription(instance, action),
stages: quickRuntimeStages(action),
@@ -361,19 +334,8 @@ export function ServersPage({ session, operations, onNavigate }: PageComponentPr
throw new Error("该运行操作已下线");
}
});
}
operations.succeed(operationId, message);
runtimeTask.succeedTask(message);
if (generatedClientProfile) {
const clientProfile = generatedClientProfile;
setRuntimeTaskActions([
{
label: "下载客户端",
kind: "primary",
onClick: () => void downloadGeneratedClient(instance, clientProfile)
}
]);
}
await refresh();
} catch (error) {
const message = error instanceof Error ? error.message : "运行操作失败";
@@ -426,26 +388,6 @@ export function ServersPage({ session, operations, onNavigate }: PageComponentPr
}
}
async function downloadGeneratedClient(instance: ServerInstanceResponse, profileKey: string) {
setRuntimeTaskActions([]);
try {
const message = await runtimeTask.runTask({
title: "下载客户端",
description: `${instance.name} 的客户端管理器已生成,正在创建下载引用。`,
stages: runtimeDownloadStages,
executeStageIndex: 1,
execute: async () => {
const reference = await platformApiClient.downloadLatestClientManager(instance.id, { profileKey });
await downloadArtifactReference(reference, (artifactId, offset, limit) => platformApiClient.readArtifactContent(artifactId, offset, limit));
return `客户端下载已开始,文件 ${safeArtifactFilename(reference.filename)}`;
}
});
runtimeTask.succeedTask(message);
} catch (error) {
runtimeTask.failTask(error instanceof Error ? error.message : "客户端下载失败");
}
}
const latestCreate = operations.operations.find((operation) => operation.intent === "创建服务器");
const runTargetPlugin = runTargetSelection ? plugins.find((plugin) => plugin.id === runTargetSelection.instance.pluginId) : undefined;
const runTargetOsOptions = runPlatformOptions(runTargetPlugin, runTargetSelection?.targetOs ?? "linux");
@@ -665,7 +607,6 @@ type ServerQuickRuntimeAction =
| "download-run"
| "push-run-update"
| "reset-run-key"
| "generate-client-manager"
| "dependencies-check"
| "dependencies-install";
@@ -902,7 +843,7 @@ function ServerCard({ card, metricsPending, metricsUnavailable, canManage, delet
const serverQuickActionGroups: Array<{ label: string; actions: ServerQuickRuntimeAction[] }> = [
{
label: "运行分发",
actions: ["generate-run", "download-run", "push-run-update", "generate-client-manager"]
actions: ["generate-run", "download-run", "push-run-update"]
},
{
label: "诊断维护",
@@ -920,8 +861,6 @@ function quickRuntimeActionLabel(action: ServerQuickRuntimeAction): string {
return "更新 run";
case "reset-run-key":
return "重置 run 密钥";
case "generate-client-manager":
return "生成客户端";
case "dependencies-check":
return "依赖检查";
case "dependencies-install":
@@ -933,9 +872,6 @@ function quickRuntimeStages(action: ServerQuickRuntimeAction) {
if (action === "generate-run") {
return runtimeRunBuildStages;
}
if (action === "generate-client-manager") {
return runtimeBuildStages;
}
if (action === "download-run") {
return runtimeDownloadStages;
}
@@ -952,7 +888,7 @@ function quickRuntimeStages(action: ServerQuickRuntimeAction) {
}
function quickRuntimeExecuteStageIndex(action: ServerQuickRuntimeAction): number {
if (action === "generate-run" || action === "generate-client-manager") {
if (action === "generate-run") {
return 3;
}
if (action === "push-run-update") {
@@ -973,9 +909,6 @@ function quickRuntimeDefaultsForPlugin(pluginId: string) {
const isScum = pluginId.toLowerCase().includes("scum");
return {
runOs: isScum ? "windows" : "linux",
clientOs: "windows",
clientProfileKey: isScum ? "scum-client-manager" : "client-manager",
repositoryUrl: isScum ? "https://github.com/F88888/scum_client.git" : "https://github.com/example/client-manager.git",
probeKey: isScum ? "steamcmd" : "java-21",
installPlanKey: isScum ? "install-steamcmd-linux" : "install-java-linux"
};