feat: move code queue to d601

This commit is contained in:
Codex
2026-05-14 19:13:43 +00:00
parent cbbed004a6
commit c930607316
28 changed files with 359 additions and 242 deletions
+14
View File
@@ -12,6 +12,7 @@ export interface UniDeskConfig {
frontend: { port: number; containerPort: number };
database: { port: number; containerPort: number };
providerIngress: { port: number; containerPort: number };
restrictedHostAccess?: { bindHost: string; allowedSourceCidrs: string[] };
};
auth: { username: string; password: string; sessionSecret: string; sessionTtlSeconds: number };
database: { user: string; password: string; name: string; volume: string; volumeSize: string };
@@ -127,6 +128,18 @@ function stringArrayField(obj: Record<string, unknown>, key: string, path: strin
return value as string[];
}
function optionalRestrictedHostAccess(network: Record<string, unknown>): UniDeskConfig["network"]["restrictedHostAccess"] {
const value = network.restrictedHostAccess;
if (value === undefined) return undefined;
const record = asRecord(value, "network.restrictedHostAccess");
const allowedSourceCidrs = stringArrayField(record, "allowedSourceCidrs", "network.restrictedHostAccess");
if (allowedSourceCidrs.length === 0) throw new Error("network.restrictedHostAccess.allowedSourceCidrs must not be empty");
return {
bindHost: stringField(record, "bindHost", "network.restrictedHostAccess"),
allowedSourceCidrs,
};
}
function microserviceConfig(item: Record<string, unknown>, index: number): UniDeskMicroserviceConfig {
const path = `microservices[${index}]`;
const repository = asRecord(item.repository, `${path}.repository`);
@@ -200,6 +213,7 @@ export function readConfig(): UniDeskConfig {
frontend: portPair(network, "frontend"),
database: portPair(network, "database"),
providerIngress: portPair(network, "providerIngress"),
restrictedHostAccess: optionalRestrictedHostAccess(network),
},
database: {
user: stringField(database, "user", "database"),
+2 -1
View File
@@ -136,7 +136,8 @@ export async function debugHealth(config: UniDeskConfig): Promise<unknown> {
providerIngressPublic: await readJson(`http://127.0.0.1:${config.network.providerIngress.port}/health`),
publicExposureBoundary: {
coreHostPort: { port: config.network.core.port, expected: "not-exposed" },
databaseHostPort: { port: config.network.database.port, expected: "not-exposed" },
databaseHostPort: { port: config.network.database.port, expected: "restricted-to-code-queue-provider" },
oaEventFlowHostPort: { port: 4255, expected: "restricted-to-code-queue-provider" },
},
};
}
+69 -9
View File
@@ -19,7 +19,7 @@ export interface ContainerStatus {
ports: string;
}
const rebuildableServices = ["backend-core", "frontend", "provider-gateway", "todo-note", "code-queue", "project-manager", "baidu-netdisk", "oa-event-flow"] as const;
const rebuildableServices = ["backend-core", "frontend", "provider-gateway", "todo-note", "project-manager", "baidu-netdisk", "oa-event-flow"] as const;
export type RebuildableService = typeof rebuildableServices[number];
export function isRebuildableService(value: string | undefined): value is RebuildableService {
@@ -60,7 +60,7 @@ export function writeComposeEnv(config: UniDeskConfig, freshLogPrefix: boolean):
const previousValue = (key: string): string => previousRaw.match(new RegExp(`^${key}=(.*)$`, "m"))?.[1]?.replace(/^"|"$/g, "") ?? "";
const runtimeSecret = (key: string): string => process.env[key] ?? previousValue(key);
const runtimeSecretWithDefault = (key: string, defaultValue: string, legacyDefault = ""): string => {
if (process.env[key] !== undefined) return process.env[key] ?? defaultValue;
if (process.env[key] !== undefined) return process.env[key] || defaultValue;
const previous = previousValue(key);
if (previous.length > 0 && previous !== legacyDefault) return previous;
return defaultValue;
@@ -86,11 +86,13 @@ export function writeComposeEnv(config: UniDeskConfig, freshLogPrefix: boolean):
chmodSync(join(logRoot, logDay), 0o777);
const labels = JSON.stringify(config.providerGateway.labels);
const microservices = JSON.stringify(config.microservices);
const restrictedHostBind = config.network.restrictedHostAccess?.bindHost || "127.0.0.1";
const lines = {
UNIDESK_PUBLIC_HOST: config.network.publicHost,
UNIDESK_CORE_PORT: String(config.network.core.port),
UNIDESK_FRONTEND_PORT: String(config.network.frontend.port),
UNIDESK_DATABASE_PORT: String(config.network.database.port),
UNIDESK_DATABASE_BIND_HOST: runtimeSecretWithDefault("UNIDESK_DATABASE_BIND_HOST", restrictedHostBind, "127.0.0.1"),
UNIDESK_PROVIDER_INGRESS_PORT: String(config.network.providerIngress.port),
UNIDESK_DATABASE_USER: config.database.user,
UNIDESK_DATABASE_PASSWORD: config.database.password,
@@ -147,6 +149,8 @@ export function writeComposeEnv(config: UniDeskConfig, freshLogPrefix: boolean):
UNIDESK_CODE_QUEUE_DEV_CONTAINER_IMAGE: runtimeSecret("UNIDESK_CODE_QUEUE_DEV_CONTAINER_IMAGE"),
UNIDESK_CODE_QUEUE_DEV_CONTAINER_WORKDIR: runtimeSecret("UNIDESK_CODE_QUEUE_DEV_CONTAINER_WORKDIR") || "/home/ubuntu",
UNIDESK_OA_EVENT_FLOW_BASE_URL: runtimeSecret("UNIDESK_OA_EVENT_FLOW_BASE_URL") || "http://oa-event-flow:4255",
UNIDESK_OA_EVENT_FLOW_PORT: runtimeSecret("UNIDESK_OA_EVENT_FLOW_PORT") || "4255",
UNIDESK_OA_EVENT_FLOW_BIND_HOST: runtimeSecretWithDefault("UNIDESK_OA_EVENT_FLOW_BIND_HOST", restrictedHostBind, "127.0.0.1"),
UNIDESK_CODE_QUEUE_NOTIFY_CLAUDEQQ_ENABLED: runtimeSecret("UNIDESK_CODE_QUEUE_NOTIFY_CLAUDEQQ_ENABLED") || "true",
UNIDESK_CODE_QUEUE_NOTIFY_CLAUDEQQ_BASE_URL: runtimeSecret("UNIDESK_CODE_QUEUE_NOTIFY_CLAUDEQQ_BASE_URL") || "http://backend-core:8080/api/microservices/claudeqq/proxy",
UNIDESK_CODE_QUEUE_NOTIFY_CLAUDEQQ_TARGET_TYPE: runtimeSecret("UNIDESK_CODE_QUEUE_NOTIFY_CLAUDEQQ_TARGET_TYPE") || "private",
@@ -182,7 +186,12 @@ export function startStack(config: UniDeskConfig): unknown {
throw new Error(`Fixed UniDesk port is occupied before start: ${occupiedPorts.map((p) => `${p.name}:${p.port}`).join(", ")}`);
}
const upCommand = [...compose, "up", "-d", "--build", "--remove-orphans"];
const command = ["bash", "-lc", composeLockedScript(`set -euo pipefail; ${shellJoin(upCommand)}`)];
const script = [
"set -euo pipefail",
restrictedHostAccessScript(config),
shellJoin(upCommand),
].filter((line) => line.length > 0).join("\n");
const command = ["bash", "-lc", composeLockedScript(script)];
const job = startJob("server_start", command, "Build and start UniDesk services without tearing down running queue containers");
return { job, runtimeEnv, command, ports: fixedPorts(config) };
}
@@ -246,14 +255,14 @@ export function rebuildService(config: UniDeskConfig, service: RebuildableServic
const script = [
"set -euo pipefail",
`echo ${shellJoin(["rebuild_service", service, "build_first_then_force_recreate_with_validation"])}`,
restrictedHostAccessScript(config),
shellJoin(buildCommand),
`nohup bash -lc ${shellQuote(watchdogScript)} >/dev/null 2>&1 &`,
shellJoin(upCommand),
validateScript,
].join("\n");
const command = ["bash", "-lc", composeLockedScript(script)];
const jobRunner = service === "code-queue" ? { runner: "docker" as const, dockerImage: "unidesk-code-queue:latest" } : {};
const job = startJob("server_rebuild", command, `Rebuild and validate UniDesk ${service} with serialized Docker Compose mutation`, jobRunner);
const job = startJob("server_rebuild", command, `Rebuild and validate UniDesk ${service} with serialized Docker Compose mutation`);
return {
job,
runtimeEnv,
@@ -268,7 +277,7 @@ export function rebuildService(config: UniDeskConfig, service: RebuildableServic
noDeps: true,
forceRecreate: true,
composeMutationLock: rootPath(".state", "locks", "server-compose.lock"),
jobRunner: service === "code-queue" ? "docker" : "local",
jobRunner: "local",
postUpValidation: true,
namedVolumesPreserved: true,
},
@@ -282,6 +291,36 @@ function fixedPorts(config: UniDeskConfig): Array<{ name: string; port: number;
];
}
function restrictedHostAccessScript(config: UniDeskConfig): string {
const access = config.network.restrictedHostAccess;
if (access === undefined || access.bindHost === "127.0.0.1") return "";
const ports = [
{ name: "database", containerPort: config.network.database.containerPort },
{ name: "oa-event-flow", containerPort: 4255 },
];
return [
"iptables -N DOCKER-USER 2>/dev/null || true",
...ports.flatMap((port) => [
...access.allowedSourceCidrs.map((source) => [
`iptables -C DOCKER-USER -s ${shellQuote(source)} -p tcp --dport ${port.containerPort} -j ACCEPT 2>/dev/null`,
` || iptables -I DOCKER-USER 1 -s ${shellQuote(source)} -p tcp --dport ${port.containerPort} -j ACCEPT`,
].join(" \\\n")),
[
`iptables -C DOCKER-USER -p tcp --dport ${port.containerPort} -j DROP 2>/dev/null`,
" || {",
" return_line=$(iptables -L DOCKER-USER --line-numbers | awk '$2==\"RETURN\" {print $1; exit}')",
" if [ -n \"$return_line\" ]; then",
` iptables -I DOCKER-USER "$return_line" -p tcp --dport ${port.containerPort} -j DROP`,
" else",
` iptables -A DOCKER-USER -p tcp --dport ${port.containerPort} -j DROP`,
" fi",
" }",
].join("\n"),
`echo ${shellJoin(["restricted_host_access", port.name, String(port.containerPort), access.allowedSourceCidrs.join(",")])}`,
]),
].join("\n");
}
function shellJoin(args: string[]): string {
return args.map(shellQuote).join(" ");
}
@@ -366,6 +405,11 @@ function dockerExec(config: UniDeskConfig, container: string, command: string[])
export async function stackStatus(config: UniDeskConfig): Promise<unknown> {
const runtimeEnv = writeComposeEnv(config, false);
const runtimeRaw = existsSync(runtimeEnv.envFile) ? readFileSync(runtimeEnv.envFile, "utf8") : "";
const runtimeValue = (key: string): string => runtimeRaw.match(new RegExp(`^${key}=(.*)$`, "m"))?.[1]?.replace(/^"|"$/g, "") ?? "";
const databaseBindHost = runtimeValue("UNIDESK_DATABASE_BIND_HOST") || "127.0.0.1";
const oaEventFlowBindHost = runtimeValue("UNIDESK_OA_EVENT_FLOW_BIND_HOST") || "127.0.0.1";
const oaEventFlowPort = Number(runtimeValue("UNIDESK_OA_EVENT_FLOW_PORT") || "4255");
const coreHealth = dockerExecJson("unidesk-backend-core", "fetch('http://127.0.0.1:8080/health').then(r=>r.json()).then(j=>console.log(JSON.stringify({ok:true,status:200,body:j}))).catch(e=>{console.log(JSON.stringify({ok:false,error:String(e)}));process.exit(1)})");
const overview = dockerExecJson("unidesk-backend-core", "fetch('http://127.0.0.1:8080/api/overview').then(r=>r.json()).then(j=>console.log(JSON.stringify({ok:true,status:200,body:j}))).catch(e=>{console.log(JSON.stringify({ok:false,error:String(e)}));process.exit(1)})");
return {
@@ -373,12 +417,28 @@ export async function stackStatus(config: UniDeskConfig): Promise<unknown> {
publicPorts: fixedPorts(config),
blockedPublicPorts: [
{ name: "backend-core-rest", port: config.network.core.port, listening: isPortListening(config.network.core.port), expected: "not-listening" },
{ name: "database", port: config.network.database.port, listening: isPortListening(config.network.database.port), expected: "not-listening" },
],
restrictedHostPorts: [
{
name: "database",
bindHost: databaseBindHost,
hostPort: config.network.database.port,
containerPort: config.network.database.containerPort,
listening: isPortListening(config.network.database.port),
expected: databaseBindHost === "127.0.0.1" ? "local-only" : "restricted-to-code-queue-provider",
},
{
name: "oa-event-flow",
bindHost: oaEventFlowBindHost,
hostPort: oaEventFlowPort,
containerPort: 4255,
listening: isPortListening(oaEventFlowPort),
expected: oaEventFlowBindHost === "127.0.0.1" ? "local-only" : "restricted-to-code-queue-provider",
},
],
internalPorts: [
{ name: "backend-core", containerPort: config.network.core.containerPort, hostPort: null },
{ name: "database", containerPort: config.network.database.containerPort, hostPort: null },
{ name: "code-queue", containerPort: 4222, hostPort: null },
{ name: "project-manager", containerPort: 4233, hostPort: null },
{ name: "baidu-netdisk", containerPort: 4244, hostPort: null },
{ name: "oa-event-flow", containerPort: 4255, hostPort: null },
@@ -419,7 +479,7 @@ export function stackLogs(config: UniDeskConfig, tailBytes: number): unknown {
const currentFiles = allFiles.filter((path) => basename(path).startsWith(runtimeEnv.logPrefix));
const selectedFiles = (currentFiles.length > 0 ? currentFiles : allFiles.slice(-12)).slice(-12);
const files = selectedFiles.map((path) => ({ path, name: basename(path), tail: tailFile(path, tailBytes) }));
const containerNames = ["unidesk-database", "unidesk-backend-core", "unidesk-frontend", "unidesk-provider-gateway-main", "todo-note-backend", "code-queue-backend", "project-manager-backend", "baidu-netdisk-backend", "oa-event-flow-backend"];
const containerNames = ["unidesk-database", "unidesk-backend-core", "unidesk-frontend", "unidesk-provider-gateway-main", "todo-note-backend", "project-manager-backend", "baidu-netdisk-backend", "oa-event-flow-backend"];
const docker = containerNames.map((name) => {
const result = runCommand(["docker", "logs", "--tail", "40", name], repoRoot);
return { name, exitCode: result.exitCode, stdoutTail: result.stdout.slice(-tailBytes), stderrTail: result.stderr.slice(-tailBytes) };
+39 -6
View File
@@ -841,6 +841,36 @@ function dockerPortSummary(): unknown {
};
}
function dockerUserPortRestriction(containerPort: number, allowedSourceCidrs: string[] = []): unknown {
const result = runCommand(["iptables", "-S", "DOCKER-USER"], repoRoot);
const lines = result.stdout.split("\n").map((line) => line.trim()).filter(Boolean);
const dportPattern = new RegExp(`--dport\\s+${containerPort}\\b`);
const portLines = lines.filter((line) => dportPattern.test(line));
const acceptSources = portLines
.filter((line) => line.includes("-j ACCEPT"))
.map((line) => line.match(/(?:^|\s)-s\s+(\S+)/)?.[1] ?? "")
.filter((source) => source.length > 0);
const hasDrop = portLines.some((line) => line.includes("-j DROP"));
const requiredSourcesPresent = allowedSourceCidrs.length === 0
? acceptSources.length > 0
: allowedSourceCidrs.every((source) => acceptSources.includes(source));
return {
ok: result.exitCode === 0 && hasDrop && requiredSourcesPresent,
exitCode: result.exitCode,
containerPort,
acceptSources,
allowedSourceCidrs,
requiredSourcesPresent,
hasDrop,
lines: portLines,
stderr: result.stderr.slice(-1200),
};
}
function publicProbeBlockedOrRestricted(probe: unknown, restriction: unknown): boolean {
return (probe as { reachable?: boolean }).reachable === false || (restriction as { ok?: boolean }).ok === true;
}
function dockerStatusCheckDetail(dockerStatus: unknown, providerId: string): unknown {
const response = dockerStatus as { ok?: boolean; status?: number; body?: { dockerStatuses?: Array<{ providerId?: string; name?: string; nodeStatus?: string; updatedAt?: string; dockerStatus?: { ok?: boolean; socketPresent?: boolean; collectedAt?: string; counts?: unknown; daemon?: unknown; containers?: Array<{ id?: string; name?: string; image?: string; state?: string; status?: string; ports?: string; restartPolicy?: string; pidMode?: string }> } }> } };
const item = response.body?.dockerStatuses?.find((entry) => entry.providerId === providerId);
@@ -940,22 +970,25 @@ async function exposureChecks(config: UniDeskConfig, urls: PublicUrls, checks: E
const portsText = (portSummary.rows ?? []).map((row) => `${row.name} ${row.ports}`).join("\n");
const corePublic = await fetchProbe(`${urls.blockedCoreUrl}/health`, 2500);
const databasePublic = await tcpProbe(urls.blockedDatabaseHost, urls.blockedDatabasePort);
const allowedSourceCidrs = config.network.restrictedHostAccess?.allowedSourceCidrs ?? [];
const databaseRestriction = dockerUserPortRestriction(config.network.database.containerPort, allowedSourceCidrs);
const findjobPublic = await fetchProbe(`http://${config.network.publicHost}:3254/api/health`, 2500);
const metNonlinearPublic = await fetchProbe(`http://${config.network.publicHost}:3288/health`, 2500);
const claudeqqPublic = await fetchProbe(`http://${config.network.publicHost}:3290/health`, 2500);
const todoNotePublic = await fetchProbe(`http://${config.network.publicHost}:4211/api/health`, 2500);
const codeQueuePublic = await fetchProbe(`http://${config.network.publicHost}:14222/health`, 2500);
const oaEventFlowPublic = await fetchProbe(`http://${config.network.publicHost}:4255/health`, 2500);
const oaEventFlowRestriction = dockerUserPortRestriction(4255, allowedSourceCidrs);
const filebrowserPublic = await fetchProbe(`http://${config.network.publicHost}:4251/health`, 2500);
addSelectedCheck(checks, options, "network:only-frontend-provider-ports", !portsText.includes(`:${config.network.core.port}->`) && !portsText.includes(`:${config.network.database.port}->`) && !portsText.includes(":14222->"), portSummary);
addSelectedCheck(checks, options, "network:only-frontend-provider-ports", !portsText.includes(`:${config.network.core.port}->`) && !portsText.includes(":14222->"), portSummary);
addSelectedCheck(checks, options, "network:core-public-blocked", (corePublic as { reachable?: boolean }).reachable === false, corePublic);
addSelectedCheck(checks, options, "network:database-public-blocked", (databasePublic as { reachable?: boolean }).reachable === false, databasePublic);
addSelectedCheck(checks, options, "network:database-public-blocked", publicProbeBlockedOrRestricted(databasePublic, databaseRestriction), { publicProbe: databasePublic, restriction: databaseRestriction });
addSelectedCheck(checks, options, "network:findjob-public-blocked", (findjobPublic as { reachable?: boolean }).reachable === false, findjobPublic);
addSelectedCheck(checks, options, "network:met-nonlinear-public-blocked", (metNonlinearPublic as { reachable?: boolean }).reachable === false, metNonlinearPublic);
addSelectedCheck(checks, options, "network:claudeqq-public-blocked", (claudeqqPublic as { reachable?: boolean }).reachable === false, claudeqqPublic);
addSelectedCheck(checks, options, "network:todo-note-public-blocked", (todoNotePublic as { reachable?: boolean }).reachable === false, todoNotePublic);
addSelectedCheck(checks, options, "network:code-queue-public-blocked", (codeQueuePublic as { reachable?: boolean }).reachable === false, codeQueuePublic);
addSelectedCheck(checks, options, "network:oa-event-flow-public-blocked", (oaEventFlowPublic as { reachable?: boolean }).reachable === false, oaEventFlowPublic);
addSelectedCheck(checks, options, "network:oa-event-flow-public-blocked", publicProbeBlockedOrRestricted(oaEventFlowPublic, oaEventFlowRestriction), { publicProbe: oaEventFlowPublic, restriction: oaEventFlowRestriction });
addSelectedCheck(checks, options, "network:filebrowser-public-blocked", (filebrowserPublic as { reachable?: boolean }).reachable === false, filebrowserPublic);
}
@@ -1099,7 +1132,7 @@ async function serviceChecks(config: UniDeskConfig, urls: PublicUrls, checks: E2
addSelectedCheck(checks, options, "microservice:catalog-claudeqq", (microservices as { ok?: boolean }).ok === true && claudeqq?.providerId === "D601" && claudeqq.backend?.public === false && claudeqq.runtime?.container?.name === "claudeqq-backend", { microservices });
addSelectedCheck(checks, options, "microservice:catalog-todo-note", (microservices as { ok?: boolean }).ok === true && todoNote?.providerId === config.providerGateway.id && todoNote.backend?.public === false && todoNote.runtime?.container?.name === "todo-note-backend", { microservices });
addSelectedCheck(checks, options, "microservice:catalog-oa-event-flow", (microservices as { ok?: boolean }).ok === true && oaEventFlow?.providerId === config.providerGateway.id && oaEventFlow.backend?.public === false && oaEventFlow.runtime?.container?.name === "oa-event-flow-backend", { microservices });
addSelectedCheck(checks, options, "microservice:catalog-code-queue", (microservices as { ok?: boolean }).ok === true && codeQueue?.providerId === config.providerGateway.id && codeQueue.backend?.public === false && codeQueue.runtime?.container?.name === "code-queue-backend", { microservices });
addSelectedCheck(checks, options, "microservice:catalog-code-queue", (microservices as { ok?: boolean }).ok === true && codeQueue?.providerId === "D601" && codeQueue.backend?.public === false && codeQueue.runtime?.container?.name === "code-queue-backend", { microservices });
addSelectedCheck(checks, options, "microservice:catalog-filebrowser", (microservices as { ok?: boolean }).ok === true
&& filebrowser?.providerId === "D518"
&& filebrowser.backend?.public === false
@@ -1167,7 +1200,7 @@ async function serviceChecks(config: UniDeskConfig, urls: PublicUrls, checks: E2
sampleEvents: oaEventFlowPipelineEventsBody?.events?.slice(0, 5),
});
addSelectedCheck(checks, options, "microservice:oa-event-flow-stats", (oaEventFlowStats as { ok?: boolean }).ok === true && oaEventFlowStatsBody?.ok === true && Array.isArray(oaEventFlowStatsBody.stats), oaEventFlowStats);
addSelectedCheck(checks, options, "microservice:code-queue-status", (codeQueueStatus as { ok?: boolean }).ok === true && (codeQueueStatus as { body?: { microservice?: { id?: string; providerId?: string } } }).body?.microservice?.providerId === config.providerGateway.id, codeQueueStatus);
addSelectedCheck(checks, options, "microservice:code-queue-status", (codeQueueStatus as { ok?: boolean }).ok === true && (codeQueueStatus as { body?: { microservice?: { id?: string; providerId?: string } } }).body?.microservice?.providerId === "D601", codeQueueStatus);
addSelectedCheck(checks, options, "microservice:code-queue-health", (codeQueueHealth as { ok?: boolean }).ok === true && codeQueueHealthBody?.ok === true && codeQueueHealthBody.queue?.defaultModel === "gpt-5.5" && codeQueueHealthBody.queue?.modelReasoningEfforts?.["gpt-5.5"] === "xhigh", codeQueueHealth);
addSelectedCheck(checks, options, "microservice:code-queue-tasks", (codeQueueTasks as { ok?: boolean }).ok === true && codeQueueTasksBody?.ok === true && Array.isArray(codeQueueTasksBody.tasks) && codeQueueTasksBody.queue?.defaultModel === "gpt-5.5" && codeQueueTasksBody.queue?.modelReasoningEfforts?.["gpt-5.5"] === "xhigh", codeQueueTasks);
const upgradeDispatch = dockerCoreJson("/api/dispatch", {
@@ -2688,7 +2721,7 @@ async function frontendCheck(config: UniDeskConfig, urls: PublicUrls, checks: E2
&& Number(oaEventFlowMetrics.rawButtonCount || 0) >= 2
&& !oaEventFlowText.includes("{\n"),
{ oaEventFlowMetrics, oaEventFlowTextPreview: oaEventFlowText.slice(0, 1400) });
addSelectedCheck(checks, options, "frontend:code-queue-integrated-visible", codeQueueTextLower.includes("code queue") && codeQueueText.includes("gpt-5.4-mini") && codeQueueText.includes("gpt-5.4") && codeQueueText.includes("gpt-5.5") && codeQueueText.includes("提交任务") && codeQueueText.includes("执行 Provider") && codeQueueText.includes("入队份数") && codeQueueText.includes("追加 prompt") && codeQueueText.includes("打断") && codeQueueTextLower.includes("查看 queue") && codeQueueText.includes("创建 queue") && codeQueueText.includes("合并 queue") && codeQueueOptions.some((text) => text.includes("All queues")) && codeQueueTracePlacement.firstChildIsTrace === true && codeQueueTracePlacement.noPageTopStatus === true && codeQueueTracePlacement.filterInsideTracePanel === true && codeQueueTracePlacement.taskSearchVisible === true && codeQueueTracePlacement.traceStatusVisible === true && codeQueueTracePlacement.markAllReadVisible === true && codeQueueGlobalStatus.activeMicroserviceVisible === true && codeQueueSidebarUpdateMetrics.hasRecentUpdateLabel === true && codeQueueHtmlGuard.rootAttrMissing === true && codeQueueHtmlGuard.sourceAttrMissing === true && codeQueueHtmlGuard.sourceNoBasePrompt === true && codeQueueSubmitQueueControl.tagName === "select" && codeQueueSubmitQueueControl.createButtonVisible === true && codeQueueSubmitQueueControl.mergeButtonVisible === true && codeQueueSubmitQueueControl.mergeSourceInlineMissing === true && codeQueueSubmitQueueControl.mergeDialogMissingBeforeClick === true && (codeQueueSubmitQueueControl.mergeButtonDisabled === true || (codeQueueSubmitQueueControl.mergeDialogVisible === true && codeQueueSubmitQueueControl.mergeDialogSelectVisible === true && Number(codeQueueSubmitQueueControl.mergeDialogSourceOptionCount || 0) > 1 && codeQueueSubmitQueueControl.mergeDialogSelectInsideSubmitForm !== true && codeQueueSubmitQueueControl.mergeDialogUsesCommonComponent === true && codeQueueSubmitQueueControl.mergeDialogDeleteNoteVisible === true)) && codeQueueSubmitQueueControl.oldInputMissing === true && codeQueueSubmitQueueControl.providerValue === "main-server" && codeQueueSubmitQueueControl.cwdValue === "/root/unidesk" && Array.isArray(codeQueueSubmitQueueControl.providerOptions) && codeQueueSubmitQueueControl.providerOptions.some((item: any) => item.value === "D601" && String(item.text || "").includes("/home/ubuntu")) && codeQueueSubmitQueueControl.maxAttemptsMax === "99" && codeQueueSubmitQueueControl.maxAttemptsValue === "99" && codeQueueSubmitQueueControl.moveQueueVisible === true && codeQueuePromptDefaultEmpty === true && codeQueueSubmitGuard.batchRowVisible === true && codeQueueSubmitGuard.checkboxVisible === true && codeQueueSubmitGuard.disabledBeforeConfirm === true && codeQueueSubmitGuard.enabledAfterConfirm === true && codeQueueSubmitGuard.waitElementMissingBeforeSubmit === true && codeQueueScrollbarMetrics.transcriptThin === true && codeQueueScrollbarMetrics.toolHorizontalHidden === true && (codeQueueSwitchMetrics.optionCount <= 1 || codeQueueSwitchMetrics.switched === true) && codeQueueTextLower.includes("attempts") && codeQueueText.includes("仅 UniDesk frontend 代理访问") && (codeQueueTaskCount === 0 || codeQueueOutputText.includes("Submitted prompt")), { codeQueueTaskCount, codeQueueOptions, codeQueueSwitchMetrics, codeQueueSubmitQueueControl, codeQueueSubmitGuard, codeQueueScrollbarMetrics, codeQueuePromptDefaultEmpty, codeQueueTracePlacement, codeQueueGlobalStatus, codeQueueSidebarUpdateMetrics, codeQueueHtmlGuard, codeQueueOutputPreview: codeQueueOutputText.slice(0, 900), codeQueueTextPreview: codeQueueText.slice(0, 1400) });
addSelectedCheck(checks, options, "frontend:code-queue-integrated-visible", codeQueueTextLower.includes("code queue") && codeQueueText.includes("gpt-5.4-mini") && codeQueueText.includes("gpt-5.4") && codeQueueText.includes("gpt-5.5") && codeQueueText.includes("提交任务") && codeQueueText.includes("执行 Provider") && codeQueueText.includes("入队份数") && codeQueueText.includes("追加 prompt") && codeQueueText.includes("打断") && codeQueueTextLower.includes("查看 queue") && codeQueueText.includes("创建 queue") && codeQueueText.includes("合并 queue") && codeQueueOptions.some((text) => text.includes("All queues")) && codeQueueTracePlacement.firstChildIsTrace === true && codeQueueTracePlacement.noPageTopStatus === true && codeQueueTracePlacement.filterInsideTracePanel === true && codeQueueTracePlacement.taskSearchVisible === true && codeQueueTracePlacement.traceStatusVisible === true && codeQueueTracePlacement.markAllReadVisible === true && codeQueueGlobalStatus.activeMicroserviceVisible === true && codeQueueSidebarUpdateMetrics.hasRecentUpdateLabel === true && codeQueueHtmlGuard.rootAttrMissing === true && codeQueueHtmlGuard.sourceAttrMissing === true && codeQueueHtmlGuard.sourceNoBasePrompt === true && codeQueueSubmitQueueControl.tagName === "select" && codeQueueSubmitQueueControl.createButtonVisible === true && codeQueueSubmitQueueControl.mergeButtonVisible === true && codeQueueSubmitQueueControl.mergeSourceInlineMissing === true && codeQueueSubmitQueueControl.mergeDialogMissingBeforeClick === true && (codeQueueSubmitQueueControl.mergeButtonDisabled === true || (codeQueueSubmitQueueControl.mergeDialogVisible === true && codeQueueSubmitQueueControl.mergeDialogSelectVisible === true && Number(codeQueueSubmitQueueControl.mergeDialogSourceOptionCount || 0) > 1 && codeQueueSubmitQueueControl.mergeDialogSelectInsideSubmitForm !== true && codeQueueSubmitQueueControl.mergeDialogUsesCommonComponent === true && codeQueueSubmitQueueControl.mergeDialogDeleteNoteVisible === true)) && codeQueueSubmitQueueControl.oldInputMissing === true && codeQueueSubmitQueueControl.providerValue === "D601" && codeQueueSubmitQueueControl.cwdValue === "/workspace" && Array.isArray(codeQueueSubmitQueueControl.providerOptions) && codeQueueSubmitQueueControl.providerOptions.some((item: any) => item.value === "D601" && String(item.text || "").includes("/workspace")) && codeQueueSubmitQueueControl.maxAttemptsMax === "99" && codeQueueSubmitQueueControl.maxAttemptsValue === "99" && codeQueueSubmitQueueControl.moveQueueVisible === true && codeQueuePromptDefaultEmpty === true && codeQueueSubmitGuard.batchRowVisible === true && codeQueueSubmitGuard.checkboxVisible === true && codeQueueSubmitGuard.disabledBeforeConfirm === true && codeQueueSubmitGuard.enabledAfterConfirm === true && codeQueueSubmitGuard.waitElementMissingBeforeSubmit === true && codeQueueScrollbarMetrics.transcriptThin === true && codeQueueScrollbarMetrics.toolHorizontalHidden === true && (codeQueueSwitchMetrics.optionCount <= 1 || codeQueueSwitchMetrics.switched === true) && codeQueueTextLower.includes("attempts") && codeQueueText.includes("仅 UniDesk frontend 代理访问") && (codeQueueTaskCount === 0 || codeQueueOutputText.includes("Submitted prompt")), { codeQueueTaskCount, codeQueueOptions, codeQueueSwitchMetrics, codeQueueSubmitQueueControl, codeQueueSubmitGuard, codeQueueScrollbarMetrics, codeQueuePromptDefaultEmpty, codeQueueTracePlacement, codeQueueGlobalStatus, codeQueueSidebarUpdateMetrics, codeQueueHtmlGuard, codeQueueOutputPreview: codeQueueOutputText.slice(0, 900), codeQueueTextPreview: codeQueueText.slice(0, 1400) });
addSelectedCheck(checks, options, "frontend:code-queue-enqueue-await-smoke",
codeQueueEnqueueAwaitSmoke.checked === true
&& codeQueueEnqueueAwaitSmoke.delayedPostCount === 1
+2 -1
View File
@@ -416,7 +416,8 @@ async function remoteHealth(session: FrontendSession, config: UniDeskConfig): Pr
dockerStatusInternal: summarizeDockerStatus(await frontendJson(session, "/api/nodes/docker-status")),
publicExposureBoundary: {
coreHostPort: { port: config.network.core.port, expected: "not-exposed" },
databaseHostPort: { port: config.network.database.port, expected: "not-exposed" },
databaseHostPort: { port: config.network.database.port, expected: "restricted-to-code-queue-provider" },
oaEventFlowHostPort: { port: 4255, expected: "restricted-to-code-queue-provider" },
},
};
}