feat: move distribution builds to platform Docker builder

This commit is contained in:
npc0-hue
2026-07-30 19:25:50 +08:00
parent 1e004dc9ec
commit e614a17fe3
45 changed files with 4492 additions and 294 deletions
+45 -12
View File
@@ -40,6 +40,7 @@ const fallbackFragments = [
async function main() {
await mkdir(evidenceDir, { recursive: true });
const smokeSeed = await loadSmokeSeed();
const session = await loginApi();
const authHeaders = { Authorization: `Bearer ${session.sessionId}` };
await ensureAiProvider(authHeaders);
@@ -47,7 +48,7 @@ async function main() {
const [instances, endpoints, jobs, plugins, marketplace, users, providers, logStreams, artifacts, usage] = await Promise.all([
getJson("/server-instances", authHeaders),
getJson("/run/endpoints?status=online", authHeaders),
getJson("/jobs?serverInstanceId=server-local-debug", authHeaders),
getJson(`/jobs?serverInstanceId=${encodeURIComponent(smokeSeed.serverLocalId)}`, authHeaders),
getJson("/game-plugins", authHeaders),
getJson("/plugin-marketplace/plugins", authHeaders),
getJson("/users", authHeaders),
@@ -57,15 +58,18 @@ async function main() {
getJson("/metrics/platform", authHeaders)
]);
const server = findRequired(instances.items, (item) => item.id === "server-local-debug", "server-local-debug instance");
const runEndpoint = findRequired(endpoints.items, (item) => item.id === "run-local-debug", "run-local-debug endpoint");
const server = findRequired(instances.items, (item) => item.id === smokeSeed.serverLocalId, `${smokeSeed.serverLocalId} instance`);
const runEndpoint = findRequired(endpoints.items, (item) => item.id === server.runEndpointId, `${server.runEndpointId} generated endpoint`);
const plugin = findRequired(plugins.items, (item) => item.id === "game.example", "game.example plugin");
const marketplacePlugin = findRequired(marketplace.items, (item) => item.id === "game.example", "game.example marketplace plugin");
const operator = findRequired(users.items, (item) => item.email === "operator.local@example.test", "operator local user");
const aiProvider = findRequired(providers.items, (item) => item.id === "ai.openai" || item.apiKeyConfigured === true, "redacted AI provider");
assertEqual(server.pluginId, "game.example", "server is backed by game.example");
assertEqual(server.runEndpointId, "run-local-debug", "server is assigned to run-local-debug");
assertEqual(server.runEndpointId, smokeSeed.generatedRunEndpointId, "server is assigned to its generated Run");
if (runEndpoint.capabilities.includes("distribution.build")) {
throw new Error("generated Run unexpectedly advertises distribution.build");
}
assertIncludes(runEndpoint.capabilities, "process.install", "run endpoint exposes process.install");
assertIncludes(runEndpoint.capabilities, "process.start", "run endpoint exposes process.start");
assertIncludes(runEndpoint.capabilities, "process.stop", "run endpoint exposes process.stop");
@@ -90,6 +94,7 @@ async function main() {
platformUrl,
webUrl,
localDebugRoot,
smokeSeed,
seedEvidenceDir: path.join(localDebugRoot, "smoke"),
session: {
userId: session.user.id,
@@ -169,7 +174,7 @@ async function main() {
},
{
name: "服务器详情",
hash: "#/servers/server-local-debug",
hash: `#/servers/${encodeURIComponent(server.id)}`,
markers: [
server.name,
`${server.id} · 插件 ${server.pluginId}@${server.pluginVersion} · 节点 ${server.runEndpointId}`,
@@ -225,6 +230,28 @@ async function main() {
console.log(`evidence file: ${evidencePath}`);
}
async function loadSmokeSeed() {
const configPath = path.join(localDebugRoot, "smoke", "run-build-config.env");
const contents = await readFile(configPath, "utf8");
const values = {};
for (const line of contents.split(/\r?\n/)) {
const separator = line.indexOf("=");
if (separator <= 0) continue;
values[line.slice(0, separator)] = line.slice(separator + 1);
}
for (const key of ["SMOKE_INVOCATION_ID", "SERVER_LOCAL_ID", "SCUM_ALPHA_ID", "SCUM_BETA_ID", "SCUM_DYNAMIC_ID", "GENERATED_RUN_ENDPOINT_ID"]) {
if (!values[key]) throw new Error(`smoke seed configuration is missing ${key}`);
}
return {
invocationId: values.SMOKE_INVOCATION_ID,
serverLocalId: values.SERVER_LOCAL_ID,
scumAlphaId: values.SCUM_ALPHA_ID,
scumBetaId: values.SCUM_BETA_ID,
scumDynamicId: values.SCUM_DYNAMIC_ID,
generatedRunEndpointId: values.GENERATED_RUN_ENDPOINT_ID
};
}
async function loginApi() {
const response = await postJson("/auth/login", {
account: "operator.local@example.test",
@@ -362,9 +389,9 @@ async function verifyResponsiveThemeWalkthroughs(chrome, routeChecks, server) {
}
}
await chrome.evaluate(() => {
window.location.hash = "#/servers/server-local-debug";
});
await chrome.evaluate((serverID) => {
window.location.hash = `#/servers/${encodeURIComponent(serverID)}`;
}, server.id);
await chrome.waitForText([server.name, "插件控制"], `${scenario.name} / server detail tabs`);
const pluginControls = await clickAndVerify(chrome, "插件控制", ["生产生命周期", "Logs 桥接执行", "server.logs.read", "server.artifacts.read", "读取"]);
const pluginLayout = await chrome.layoutSnapshot();
@@ -438,7 +465,7 @@ async function clickAndVerify(chrome, buttonText, markers) {
}
async function verifyServerQuickRuntimeMenu(chrome, label) {
const markers = ["生成 run", "下载 run", "推送更新", "生成客户端", "依赖检查", "依赖安装", "实时日志", "历史日志"];
const markers = ["生成 run", "下载 run", "更新 run", "生成客户端", "依赖检查", "依赖安装", "实时日志", "历史日志"];
await chrome.evaluate(() => {
const trigger = Array.from(document.querySelectorAll("button")).find((item) => item.textContent?.includes("运行操作"));
if (!(trigger instanceof HTMLButtonElement)) {
@@ -787,13 +814,13 @@ async function verifyLifecycleOperation(headers, server, chrome) {
if (result.job.capability !== expectedCapability) {
throw new Error(`lifecycle job used unexpected capability ${result.job.capability}`);
}
if (result.job.runEndpointId !== "run-local-debug") {
if (result.job.runEndpointId !== currentServer.runEndpointId) {
throw new Error(`lifecycle job used unexpected run endpoint ${result.job.runEndpointId}`);
}
const job = await waitForJob(headers, currentServer.id, result.job.id);
await chrome.navigate(`${webUrl}/#/servers/server-local-debug`);
await chrome.navigate(`${webUrl}/#/servers/${encodeURIComponent(currentServer.id)}`);
await chrome.waitForText([currentServer.name, "操作历史"], "server detail after lifecycle operation");
const historyState = await clickAndVerify(chrome, "操作历史", ["操作历史", "平台任务记录", "server-lifecycle", "process."]);
@@ -953,6 +980,8 @@ async function startChrome() {
const clipped = intersection(rectFromDomRect(rect), clipForElement(element));
const visibleWidth = Math.max(0, clipped.right - clipped.left);
const visibleHeight = Math.max(0, clipped.bottom - clipped.top);
const floatingMenu = element.closest(".runtime-action-popover");
const floatingMenuRect = floatingMenu?.getBoundingClientRect();
return {
tag: element.tagName.toLowerCase(),
text: (element.textContent || element.getAttribute("aria-label") || "").trim().slice(0, 60),
@@ -961,7 +990,8 @@ async function startChrome() {
right: Math.round(clipped.right),
bottom: Math.round(clipped.bottom),
width: Math.round(visibleWidth),
height: Math.round(visibleHeight)
height: Math.round(visibleHeight),
boundedFloatingMenu: Boolean(floatingMenuRect && floatingMenuRect.width <= 320 && floatingMenuRect.height <= 320)
};
})
.filter((control) => control.width > 0 && control.height > 0);
@@ -969,6 +999,9 @@ async function startChrome() {
const overlappingControls = [];
for (let index = 0; index < controls.length; index += 1) {
for (let otherIndex = index + 1; otherIndex < controls.length; otherIndex += 1) {
if (controls[index].boundedFloatingMenu !== controls[otherIndex].boundedFloatingMenu && (controls[index].boundedFloatingMenu || controls[otherIndex].boundedFloatingMenu)) {
continue;
}
const left = Math.max(controls[index].left, controls[otherIndex].left);
const top = Math.max(controls[index].top, controls[otherIndex].top);
const right = Math.min(controls[index].right, controls[otherIndex].right);