feat(microservices): manage code queue through v3s

This commit is contained in:
Codex
2026-05-15 11:54:41 +00:00
parent c334c4f082
commit 00add260e3
42 changed files with 2010 additions and 354 deletions
+2
View File
@@ -34,6 +34,7 @@ function unifiedLogRotationItem(): CheckItem {
"src/components/frontend/src/index.ts",
"src/components/provider-gateway/src/index.ts",
"src/components/microservices/code-queue/src/index.ts",
"src/components/microservices/v3sctl-adapter/src/index.ts",
"src/components/microservices/project-manager/src/index.ts",
"src/components/microservices/baidu-netdisk/src/index.ts",
"src/components/microservices/oa-event-flow/src/index.ts",
@@ -66,6 +67,7 @@ export function runChecks(config: UniDeskConfig): { ok: boolean; items: CheckIte
fileItem("src/components/frontend/src/index.ts"),
fileItem("src/components/provider-gateway/src/index.ts"),
fileItem("src/components/microservices/oa-event-flow/src/index.ts"),
fileItem("src/components/microservices/v3sctl-adapter/src/index.ts"),
fileItem("scripts/src/e2e.ts"),
unifiedLogRotationItem(),
commandItem("bun:version", ["bun", "--version"]),
+36
View File
@@ -66,6 +66,14 @@ export interface UniDeskMicroserviceConfig {
healthPath: string;
timeoutMs: number;
};
deployment: {
mode: "unidesk-direct" | "v3sctl-managed";
adapterServiceId?: string;
v3sServiceId?: string;
namespace?: string;
expectedNodeIds?: string[];
activeNodeId?: string;
};
development: {
providerId: string;
sshPassthrough: boolean;
@@ -114,12 +122,25 @@ function booleanField(obj: Record<string, unknown>, key: string, path: string):
return value;
}
function optionalStringField(obj: Record<string, unknown>, key: string, path: string): string | undefined {
const value = obj[key];
if (value === undefined) return undefined;
if (typeof value !== "string" || value.length === 0) throw new Error(`${path}.${key} must be a non-empty string`);
return value;
}
function optionalArray(value: unknown, name: string): Record<string, unknown>[] {
if (value === undefined) return [];
if (!Array.isArray(value)) throw new Error(`${name} must be an array`);
return value.map((item, index) => asRecord(item, `${name}[${index}]`));
}
function optionalStringArrayField(obj: Record<string, unknown>, key: string, path: string): string[] | undefined {
const value = obj[key];
if (value === undefined) return undefined;
return stringArrayField(obj, key, path);
}
function stringArrayField(obj: Record<string, unknown>, key: string, path: string): string[] {
const value = obj[key];
if (!Array.isArray(value) || value.some((item) => typeof item !== "string" || item.length === 0)) {
@@ -146,6 +167,11 @@ function microserviceConfig(item: Record<string, unknown>, index: number): UniDe
const backend = asRecord(item.backend, `${path}.backend`);
const development = asRecord(item.development, `${path}.development`);
const frontend = asRecord(item.frontend, `${path}.frontend`);
const deployment = item.deployment === undefined ? undefined : asRecord(item.deployment, `${path}.deployment`);
const deploymentMode = deployment === undefined ? "unidesk-direct" : stringField(deployment, "mode", `${path}.deployment`);
if (deploymentMode !== "unidesk-direct" && deploymentMode !== "v3sctl-managed") {
throw new Error(`${path}.deployment.mode must be unidesk-direct or v3sctl-managed`);
}
return {
id: stringField(item, "id", path),
name: stringField(item, "name", path),
@@ -171,6 +197,16 @@ function microserviceConfig(item: Record<string, unknown>, index: number): UniDe
healthPath: stringField(backend, "healthPath", `${path}.backend`),
timeoutMs: numberField(backend, "timeoutMs", `${path}.backend`),
},
deployment: deployment === undefined
? { mode: "unidesk-direct" }
: {
mode: deploymentMode,
adapterServiceId: optionalStringField(deployment, "adapterServiceId", `${path}.deployment`),
v3sServiceId: optionalStringField(deployment, "v3sServiceId", `${path}.deployment`),
namespace: optionalStringField(deployment, "namespace", `${path}.deployment`),
expectedNodeIds: optionalStringArrayField(deployment, "expectedNodeIds", `${path}.deployment`),
activeNodeId: optionalStringField(deployment, "activeNodeId", `${path}.deployment`),
},
development: {
providerId: stringField(development, "providerId", `${path}.development`),
sshPassthrough: booleanField(development, "sshPassthrough", `${path}.development`),
+1 -1
View File
@@ -306,7 +306,7 @@ function restrictedHostAccessScript(config: UniDeskConfig): string {
` || 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`,
`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",
+20 -12
View File
@@ -554,7 +554,7 @@ function providerGatewayPackageVersion(): string {
function isCodeQueueTaskEnqueueRequest(url: string, method: string): boolean {
try {
const parsed = new URL(url);
return method === "POST" && parsed.pathname === "/api/code-queue-direct/api/tasks";
return method === "POST" && parsed.pathname === "/api/microservices/code-queue/proxy/api/tasks";
} catch {
return false;
}
@@ -568,7 +568,7 @@ async function runCodeQueueEnqueueAwaitSmoke(page: Page): Promise<any> {
"This task is created by the frontend E2E smoke test to verify that the enqueue submit path awaits the backend response before unlocking the form.",
].join("\n");
let delayedPostCount = 0;
const routePattern = "**/api/code-queue-direct/api/tasks**";
const routePattern = "**/api/microservices/code-queue/proxy/api/tasks**";
const routeHandler = async (route: any, request: any): Promise<void> => {
if (!isCodeQueueTaskEnqueueRequest(request.url(), request.method())) {
await route.continue();
@@ -635,14 +635,14 @@ async function runCodeQueueEnqueueAwaitSmoke(page: Page): Promise<any> {
};
}, taskId);
const storedTask = await page.evaluate(async (id) => {
const response = await fetch(`/api/code-queue-direct/api/tasks/${encodeURIComponent(String(id))}?meta=1`, { credentials: "same-origin" });
const response = await fetch(`/api/microservices/code-queue/proxy/api/tasks/${encodeURIComponent(String(id))}?meta=1`, { credentials: "same-origin" });
const text = await response.text();
let body: any = null;
try { body = text ? JSON.parse(text) : null; } catch { body = { text }; }
return { ok: response.ok, status: response.status, body };
}, taskId);
const interrupt = await page.evaluate(async (id) => {
const response = await fetch(`/api/code-queue-direct/api/tasks/${encodeURIComponent(String(id))}/interrupt`, {
const response = await fetch(`/api/microservices/code-queue/proxy/api/tasks/${encodeURIComponent(String(id))}/interrupt`, {
method: "POST",
credentials: "same-origin",
headers: { "content-type": "application/json" },
@@ -1028,7 +1028,7 @@ async function serviceChecks(config: UniDeskConfig, urls: PublicUrls, checks: E2
const oaEventFlowStats = dockerCoreJson("/api/microservices/oa-event-flow/proxy/api/stats/trace?limit=10");
const codeQueueStatus = dockerCoreJson("/api/microservices/code-queue/status");
const codeQueueHealth = dockerCoreJson("/api/microservices/code-queue/health");
const codeQueueTasks = dockerCoreJson("/api/microservices/code-queue/proxy/api/tasks?limit=5");
const codeQueueTasks = dockerCoreJson("/api/microservices/code-queue/proxy/api/tasks/overview?limit=5&transcriptLimit=1&compact=1&afterSeq=0&preferId=");
const filebrowserHealth = dockerCoreJson("/api/microservices/filebrowser/health");
const filebrowserWebui = dockerCoreJson("/api/microservices/filebrowser/proxy/");
const filebrowserD601Health = dockerCoreJson("/api/microservices/filebrowser-d601/health");
@@ -1071,7 +1071,7 @@ async function serviceChecks(config: UniDeskConfig, urls: PublicUrls, checks: E2
addSelectedCheck(checks, options, "provider:process-resource-status", mainProcesses.length > 0 && mainSystem?.current?.processSummary?.defaultSort === "memory_desc" && processMemoryDescending && mainProcesses.some((row) => Number.isFinite(row.pid) && Number.isFinite(row.rssBytes) && Number.isFinite(row.cpuPercent) && typeof row.command === "string"), { providerId: config.providerGateway.id, processSummary: mainSystem?.current?.processSummary, sample: mainProcesses.slice(0, 5) });
addSelectedCheck(checks, options, "provider:docker-status", (dockerStatus as { ok?: boolean }).ok === true && mainDocker?.dockerStatus !== undefined && ((mainDocker.dockerStatus.counts?.containers ?? 0) > 0 || (mainDocker.dockerStatus.containers?.length ?? 0) > 0), dockerStatusCheckDetail(dockerStatus, config.providerGateway.id));
addSelectedCheck(checks, options, "provider:gateway-restart-policy", providerGatewayRuntimeGuard.ok, providerGatewayRuntimeGuard);
const microserviceList = (microservices as { body?: { microservices?: Array<{ id?: string; providerId?: string; backend?: { public?: boolean }; runtime?: { providerStatus?: string; container?: { name?: string; state?: string } } }> } }).body?.microservices ?? [];
const microserviceList = (microservices as { body?: { microservices?: Array<{ id?: string; providerId?: string; backend?: { public?: boolean; proxyMode?: string }; deployment?: { mode?: string }; runtime?: { orchestrator?: string; providerStatus?: string; container?: { name?: string; state?: string } | null } }> } }).body?.microservices ?? [];
const findjob = microserviceList.find((service) => service.id === "findjob");
const pipeline = microserviceList.find((service) => service.id === "pipeline");
const metNonlinear = microserviceList.find((service) => service.id === "met-nonlinear");
@@ -1132,7 +1132,15 @@ 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 === "D601" && 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.backend?.proxyMode === "v3sctl-adapter-http"
&& codeQueue.deployment?.mode === "v3sctl-managed"
&& codeQueue.runtime?.orchestrator === "v3sctl"
&& codeQueue.runtime?.container === null,
{ microservices });
addSelectedCheck(checks, options, "microservice:catalog-filebrowser", (microservices as { ok?: boolean }).ok === true
&& filebrowser?.providerId === "D518"
&& filebrowser.backend?.public === false
@@ -1954,7 +1962,7 @@ async function frontendCheck(config: UniDeskConfig, urls: PublicUrls, checks: E2
codeQueueTaskCount = await page.locator('[data-testid^="codex-task-codex_"]').count();
if (wants("frontend:code-queue-initial-prompt-full-expand")) {
codexInitialPromptFullMetrics = await page.evaluate(async () => {
const tasksResponse = await fetch("/api/microservices/code-queue/proxy/api/tasks?limit=300&lite=1&devReady=0", { credentials: "same-origin" });
const tasksResponse = await fetch("/api/microservices/code-queue/proxy/api/tasks/overview?limit=120&transcriptLimit=0&compact=1&selected=0&includeActive=1&stats=0&afterSeq=0&preferId=", { credentials: "same-origin" });
const tasksPayload = await tasksResponse.json().catch(() => null);
const tasks = Array.isArray(tasksPayload?.tasks) ? tasksPayload.tasks : [];
const candidate = tasks.find((task: any) => Array.isArray(task?.referenceTaskIds) && task.referenceTaskIds.length > 0);
@@ -1995,14 +2003,14 @@ async function frontendCheck(config: UniDeskConfig, urls: PublicUrls, checks: E2
}
if (wants("frontend:code-queue-trace-full-load")) {
codexTraceFullMetrics = await page.evaluate(async () => {
const tasksResponse = await fetch("/api/code-queue-direct/api/tasks?limit=300&lite=1&devReady=0", { credentials: "same-origin" });
const tasksResponse = await fetch("/api/microservices/code-queue/proxy/api/tasks/overview?limit=120&transcriptLimit=0&compact=1&selected=0&includeActive=1&stats=0&afterSeq=0&preferId=", { credentials: "same-origin" });
const tasksPayload = await tasksResponse.json().catch(() => null);
const tasks = Array.isArray(tasksPayload?.tasks) ? tasksPayload.tasks : [];
const terminal = new Set(["succeeded", "failed", "canceled"]);
for (const task of tasks) {
const taskId = String(task?.id || "");
if (!taskId || !terminal.has(String(task?.status || "")) || Number(task?.outputCount || 0) < 20) continue;
const transcriptResponse = await fetch(`/api/code-queue-direct/api/tasks/${encodeURIComponent(taskId)}/transcript?afterSeq=0&limit=120&fullText=1`, { credentials: "same-origin" });
const transcriptResponse = await fetch(`/api/microservices/code-queue/proxy/api/tasks/${encodeURIComponent(taskId)}/transcript?afterSeq=0&limit=120&fullText=1`, { credentials: "same-origin" });
const transcriptPayload = await transcriptResponse.json().catch(() => null);
const transcript = Array.isArray(transcriptPayload?.transcript) ? transcriptPayload.transcript : [];
const toolCount = transcript.filter((line: any) => ["ran", "explored", "edited"].includes(String(line?.kind || ""))).length;
@@ -2065,7 +2073,7 @@ async function frontendCheck(config: UniDeskConfig, urls: PublicUrls, checks: E2
const response = await fetch(new URL(path, urls.frontendUrl), { headers: authHeaders });
return response.json().catch(() => null);
};
const tasksPayload = await fetchJson("/api/code-queue-direct/api/tasks?limit=300&lite=1&devReady=0");
const tasksPayload = await fetchJson("/api/microservices/code-queue/proxy/api/tasks/overview?limit=120&transcriptLimit=0&compact=1&selected=0&includeActive=1&stats=0&afterSeq=0&preferId=");
const tasks = Array.isArray(tasksPayload?.tasks) ? tasksPayload.tasks : [];
const prioritized = tasks
.filter((task: any) => ["failed", "canceled", "running", "judging", "succeeded"].includes(String(task?.status || "")))
@@ -2073,7 +2081,7 @@ async function frontendCheck(config: UniDeskConfig, urls: PublicUrls, checks: E2
for (const task of prioritized) {
const taskId = String(task?.id || "");
if (!taskId) continue;
const summaryPayload = await fetchJson(`/api/code-queue-direct/api/tasks/${encodeURIComponent(taskId)}/trace-summary`);
const summaryPayload = await fetchJson(`/api/microservices/code-queue/proxy/api/tasks/${encodeURIComponent(taskId)}/trace-summary`);
const summary = summaryPayload?.summary || null;
const attempts = Array.isArray(summary?.attempts) ? summary.attempts : [];
const errorAttempt = attempts.find((attempt: any) => Number(attempt?.errorCount ?? 0) > 0) || null;
+2
View File
@@ -113,6 +113,8 @@ function composeContent(options: ProviderAttachOptions): string {
' pid: "host"',
" env_file:",
` - ${relativeEnv}`,
" ports:",
' - "127.0.0.1:${PROVIDER_EGRESS_PROXY_HOST_PORT:-18789}:${PROVIDER_EGRESS_PROXY_PORT:-18789}"',
" volumes:",
" - /var/run/docker.sock:/var/run/docker.sock",
" - .:/workspace:ro",