feat: add unified OA event flow

This commit is contained in:
Codex
2026-05-14 09:41:55 +00:00
parent b36d7f94d7
commit 9f483b002c
37 changed files with 3417 additions and 502 deletions
+3 -3
View File
@@ -29,7 +29,7 @@ function help(): unknown {
{ command: "server stop", description: "Fire-and-forget docker-compose down for the fixed UniDesk stack." },
{ command: "server status", description: "Show fixed ports, containers, service health, and public URLs." },
{ command: "server logs [--tail-bytes N]", description: "Return bounded tails from file logs and docker logs." },
{ command: "server rebuild <backend-core|frontend|provider-gateway|todo-note|code-queue|project-manager|baidu-netdisk>", description: "Build first, then serialize, force-recreate, and validate one Compose service." },
{ command: "server rebuild <backend-core|frontend|provider-gateway|todo-note|code-queue|project-manager|baidu-netdisk|oa-event-flow>", description: "Build first, then serialize, force-recreate, and validate one Compose service." },
{ command: "provider attach <providerId> [--master-server URL] [--up] [--force]", description: "Generate the minimal external provider-gateway env/compose bundle; only master server URL and provider id are required." },
{ command: "ssh <providerId> [ssh-like args...]", description: "Open a Host SSH / WSL SSH maintenance session through the provider-gateway bridge with built-in remote helper tools in PATH." },
{ command: "ssh <providerId> apply-patch [tool args...] < patch.diff", description: "Invoke the injected remote apply_patch helper directly over SSH passthrough and stream the patch from local stdin." },
@@ -46,7 +46,7 @@ function help(): unknown {
{ command: "schedule upsert-pgdata-backup [--time HH:MM] [--remote-base /SERVER_DATA/UNIDESK_PG_DATA]", description: "Create or update the daily PGDATA physical backup task that uploads monthly rotated archives to Baidu Netdisk." },
{ command: "codex task <taskId> [--trace --tail|--from-start|--after-seq N|--before-seq N --limit N] [--full]", description: "Fetch a compact Code Queue task summary; trace rows are opt-in and paged with next/previous commands to avoid output explosion." },
{ command: "codex output <taskId> [--tail|--from-start|--after-seq N|--before-seq N --limit N] [--full-text]", description: "Fetch paged raw Code Queue output records by seq when a trace row has omitted command/output text." },
{ command: "codex (queues | queue create <queueId> | queue merge <sourceQueueId> --into <targetQueueId> | move <taskId> --queue <queueId>)", description: "List/create/merge Code Queue lanes and move a queued task; merge preserves task queue time order and keeps source queue records." },
{ command: "codex (queues | queue create <queueId> | queue merge <sourceQueueId> --into <targetQueueId> | move <taskId> --queue <queueId>)", description: "List/create/merge Code Queue lanes and move a queued task; merge preserves task queue time order and deletes the source queue record." },
{ command: "job list", description: "List async jobs from .state/jobs." },
{ command: "job status <jobId|latest> [--tail-bytes N]", description: "Show job state with bounded stdout/stderr tails." },
{ command: "debug health", description: "Probe internal core, nodes, system/Docker status, frontend, provider ingress, and public boundary." },
@@ -164,7 +164,7 @@ async function main(): Promise<void> {
}
if (sub === "rebuild") {
if (!isRebuildableService(third)) {
throw new Error("server rebuild requires one of: backend-core, frontend, provider-gateway, todo-note, code-queue, project-manager, baidu-netdisk");
throw new Error("server rebuild requires one of: backend-core, frontend, provider-gateway, todo-note, code-queue, project-manager, baidu-netdisk, oa-event-flow");
}
emitJson(commandName, rebuildService(config, third));
return;
+2
View File
@@ -36,6 +36,7 @@ function unifiedLogRotationItem(): CheckItem {
"src/components/microservices/code-queue/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",
];
const offenders = serviceFiles.flatMap((path) => {
const text = readFileSync(rootPath(path), "utf8");
@@ -64,6 +65,7 @@ export function runChecks(config: UniDeskConfig): { ok: boolean; items: CheckIte
fileItem("src/components/backend-core/src/index.ts"),
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("scripts/src/e2e.ts"),
unifiedLogRotationItem(),
commandItem("bun:version", ["bun", "--version"]),
+12 -10
View File
@@ -307,18 +307,16 @@ function compactAttemptCycle(value: unknown, full: boolean): Record<string, unkn
};
}
function traceStepCounts(steps: Record<string, unknown>[]): Record<string, number> {
return steps.reduce<Record<string, number>>((counts, step) => {
const kind = String(step.kind || "message");
counts[kind] = (counts[kind] ?? 0) + 1;
return counts;
}, {});
}
function renderTraceConsoleRows(summary: Record<string, unknown>, steps: Record<string, unknown>[]): string[] {
const rows: string[] = [];
const execution = asRecord(summary.execution) ?? {};
const counts = traceStepCounts(steps);
const stats = asRecord(execution.traceStats) ?? asRecord(summary.traceStats);
const statsSource = String(execution.statsSource || summary.statsSource || "");
const stat = (key: string): string | number => {
if (!stats || statsSource !== "oa-event-flow") return "--";
const value = Number(stats[key]);
return Number.isFinite(value) && value >= 0 ? Math.floor(value) : "--";
};
rows.push([
`task=${summary.id ?? ""}`,
`status=${summary.status ?? ""}`,
@@ -326,7 +324,11 @@ function renderTraceConsoleRows(summary: Record<string, unknown>, steps: Record<
`model=${summary.model ?? ""}`,
`updated=${summary.updatedAt ?? ""}`,
].filter((item) => !item.endsWith("=")).join(" "));
rows.push(`progressive steps=${steps.length} tools=${execution.toolCallCount ?? 0} read=${execution.readCount ?? counts.explored ?? 0} edit=${execution.editCount ?? counts.edited ?? 0} run=${execution.runCount ?? counts.ran ?? 0} errors=${counts.error ?? 0}`);
const read = stat("readCount");
const edit = stat("editCount");
const run = stat("runCount");
const toolCount = typeof read === "number" || typeof edit === "number" || typeof run === "number" ? Number(read === "--" ? 0 : read) + Number(edit === "--" ? 0 : edit) + Number(run === "--" ? 0 : run) : "--";
rows.push(`progressive steps=${steps.length} tools=${toolCount} read=${read} edit=${edit} run=${run} errors=${stat("errorCount")}`);
for (const step of steps) {
const head = [`#${step.seq ?? "?"}`, `[${step.label ?? traceKindLabel(step.kind)}]`, step.title ?? ""]
.filter((item) => String(item).length > 0)
+4 -2
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"] as const;
const rebuildableServices = ["backend-core", "frontend", "provider-gateway", "todo-note", "code-queue", "project-manager", "baidu-netdisk", "oa-event-flow"] as const;
export type RebuildableService = typeof rebuildableServices[number];
export function isRebuildableService(value: string | undefined): value is RebuildableService {
@@ -140,6 +140,7 @@ export function writeComposeEnv(config: UniDeskConfig, freshLogPrefix: boolean):
UNIDESK_CODE_QUEUE_DEV_CONTAINER_DEFAULT_PROVIDER_ID: runtimeSecret("UNIDESK_CODE_QUEUE_DEV_CONTAINER_DEFAULT_PROVIDER_ID") || "D601",
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_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",
@@ -374,6 +375,7 @@ export async function stackStatus(config: UniDeskConfig): Promise<unknown> {
{ 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 },
],
containers: dockerContainers(config),
health: {
@@ -411,7 +413,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"];
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 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) };
+110 -5
View File
@@ -38,6 +38,7 @@ const NETWORK_CHECK_NAMES = [
"network:claudeqq-public-blocked",
"network:todo-note-public-blocked",
"network:code-queue-public-blocked",
"network:oa-event-flow-public-blocked",
"network:filebrowser-public-blocked",
] as const;
@@ -58,6 +59,7 @@ const SERVICE_CHECK_NAMES = [
"microservice:catalog-met-nonlinear",
"microservice:catalog-claudeqq",
"microservice:catalog-todo-note",
"microservice:catalog-oa-event-flow",
"microservice:catalog-code-queue",
"microservice:catalog-filebrowser",
"microservice:filebrowser-health",
@@ -88,6 +90,12 @@ const SERVICE_CHECK_NAMES = [
"microservice:code-queue-status",
"microservice:code-queue-health",
"microservice:code-queue-tasks",
"microservice:oa-event-flow-status",
"microservice:oa-event-flow-health",
"microservice:oa-event-flow-diagnostics",
"microservice:oa-event-flow-events",
"microservice:oa-event-flow-pipeline-bridge",
"microservice:oa-event-flow-stats",
] as const;
const DATABASE_CHECK_NAMES = [
@@ -118,6 +126,7 @@ const FRONTEND_CHECK_NAMES = [
"frontend:microservice-catalog-visible",
"frontend:todo-note-integrated-visible",
"frontend:findjob-integrated-visible",
"frontend:oa-event-flow-visible",
"frontend:code-queue-integrated-visible",
"frontend:code-queue-enqueue-await-smoke",
"frontend:code-queue-summary-mobile-wrap",
@@ -309,6 +318,7 @@ const LAYOUT_OVERFLOW_PAGE_TEST_IDS: Record<string, string> = {
"/app/pipeline/": "pipeline-page",
"/app/met-nonlinear/": "met-nonlinear-page",
"/app/claudeqq/": "claudeqq-page",
"/app/oa-event-flow/": "oa-event-flow-page",
"/app/code-queue/": "code-queue-page",
};
@@ -935,6 +945,7 @@ async function exposureChecks(config: UniDeskConfig, urls: PublicUrls, checks: E
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 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:core-public-blocked", (corePublic as { reachable?: boolean }).reachable === false, corePublic);
@@ -944,6 +955,7 @@ async function exposureChecks(config: UniDeskConfig, urls: PublicUrls, checks: E
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:filebrowser-public-blocked", (filebrowserPublic as { reachable?: boolean }).reachable === false, filebrowserPublic);
}
@@ -975,6 +987,12 @@ async function serviceChecks(config: UniDeskConfig, urls: PublicUrls, checks: E2
const todoNoteStatus = dockerCoreJson("/api/microservices/todo-note/status");
const todoNoteHealth = dockerCoreJson("/api/microservices/todo-note/health");
const todoNoteInstances = dockerCoreJson("/api/microservices/todo-note/proxy/api/instances");
const oaEventFlowStatus = dockerCoreJson("/api/microservices/oa-event-flow/status");
const oaEventFlowHealth = dockerCoreJson("/api/microservices/oa-event-flow/health");
const oaEventFlowDiagnostics = dockerCoreJson("/api/microservices/oa-event-flow/proxy/api/diagnostics");
const oaEventFlowEvents = dockerCoreJson("/api/microservices/oa-event-flow/proxy/api/events?limit=10");
const oaEventFlowPipelineEvents = dockerCoreJson("/api/microservices/oa-event-flow/proxy/api/events?tags=service:pipeline&limit=10");
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");
@@ -1026,6 +1044,7 @@ async function serviceChecks(config: UniDeskConfig, urls: PublicUrls, checks: E2
const metNonlinear = microserviceList.find((service) => service.id === "met-nonlinear");
const claudeqq = microserviceList.find((service) => service.id === "claudeqq");
const todoNote = microserviceList.find((service) => service.id === "todo-note");
const oaEventFlow = microserviceList.find((service) => service.id === "oa-event-flow");
const codeQueue = microserviceList.find((service) => service.id === "code-queue");
const filebrowser = microserviceList.find((service) => service.id === "filebrowser");
const filebrowserD601 = microserviceList.find((service) => service.id === "filebrowser-d601");
@@ -1043,6 +1062,11 @@ async function serviceChecks(config: UniDeskConfig, urls: PublicUrls, checks: E2
const claudeqqNapcatLoginBody = (claudeqqNapcatLogin as { body?: { ok?: boolean; napcat?: { containerized?: boolean; connected?: boolean; httpConnected?: boolean; wsConnected?: boolean; loginState?: string; qrcode?: { available?: boolean; dataUrl?: string } }; login?: { ready?: boolean; state?: string } } }).body;
const claudeqqEventsBody = (claudeqqEvents as { body?: { ok?: boolean; events?: unknown[]; count?: number } }).body;
const claudeqqSubscriptionsBody = (claudeqqSubscriptions as { body?: { ok?: boolean; subscriptions?: unknown[]; count?: number } }).body;
const oaEventFlowHealthBody = (oaEventFlowHealth as { body?: { ok?: boolean; service?: string; databaseReady?: boolean; eventCount?: number; traceStatsCount?: number; latestSequence?: number; pipelineBridge?: { enabled?: boolean; inFlight?: boolean; lastFinishedAt?: string; lastError?: string; insertedCount?: number; duplicateCount?: number } } }).body;
const oaEventFlowDiagnosticsBody = (oaEventFlowDiagnostics as { body?: { ok?: boolean; service?: string; databaseReady?: boolean; eventCount?: number; traceStatsCount?: number; latestSequence?: number; eventTypes?: unknown[]; recentEvents?: unknown[]; recentTraceStats?: unknown[]; pipelineBridge?: { enabled?: boolean; lastFinishedAt?: string; lastError?: string; insertedCount?: number; duplicateCount?: number; runs?: Record<string, unknown> } } }).body;
const oaEventFlowEventsBody = (oaEventFlowEvents as { body?: { ok?: boolean; events?: unknown[]; returned?: number } }).body;
const oaEventFlowPipelineEventsBody = (oaEventFlowPipelineEvents as { body?: { ok?: boolean; events?: Array<{ tags?: unknown[]; sourceId?: string; type?: string; payload?: { runId?: string; pipelineId?: string } }>; returned?: number } }).body;
const oaEventFlowStatsBody = (oaEventFlowStats as { body?: { ok?: boolean; stats?: unknown[]; returned?: number } }).body;
const codeQueueHealthBody = (codeQueueHealth as { body?: { ok?: boolean; queue?: { defaultModel?: string; judgeConfigured?: boolean; modelReasoningEfforts?: Record<string, string> } } }).body;
const codeQueueTasksBody = (codeQueueTasks as { body?: { ok?: boolean; queue?: { defaultModel?: string; modelReasoningEfforts?: Record<string, string> }; tasks?: unknown[] } }).body;
const filebrowserHealthBody = (filebrowserHealth as { body?: { status?: string } }).body;
@@ -1074,6 +1098,7 @@ async function serviceChecks(config: UniDeskConfig, urls: PublicUrls, checks: E2
addSelectedCheck(checks, options, "microservice:catalog-met-nonlinear", (microservices as { ok?: boolean }).ok === true && metNonlinear?.providerId === "D601" && metNonlinear.backend?.public === false && metNonlinear.runtime?.container?.name === "met-nonlinear-ts", { microservices });
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-filebrowser", (microservices as { ok?: boolean }).ok === true
&& filebrowser?.providerId === "D518"
@@ -1126,6 +1151,22 @@ async function serviceChecks(config: UniDeskConfig, urls: PublicUrls, checks: E2
addSelectedCheck(checks, options, "microservice:todo-note-health", (todoNoteHealth as { ok?: boolean; body?: { ok?: boolean; storage?: string } }).ok === true && (todoNoteHealth as { body?: { ok?: boolean; storage?: string } }).body?.ok === true && (todoNoteHealth as { body?: { storage?: string } }).body?.storage === "postgres", todoNoteHealth);
addSelectedCheck(checks, options, "microservice:todo-note-migrated-data", (todoNoteInstances as { ok?: boolean }).ok === true && todoNoteRows.length >= 5 && ["CONSTAR", "大论文", "找工作", "小论文", "事务"].every((name) => todoNoteNames.includes(name)) && todoNoteRows.reduce((sum, row) => sum + Number(row.todoCount ?? 0), 0) >= 100, { todoNoteInstances });
addSelectedCheck(checks, options, "microservice:todo-note-write-path", (todoNoteCreate as { ok?: boolean }).ok === true && (todoNoteAdd as { ok?: boolean }).ok === true && (todoNoteToggle as { ok?: boolean }).ok === true && (todoNoteUndo as { ok?: boolean }).ok === true && (todoNoteDelete as { ok?: boolean }).ok === true, { todoNoteCreate, todoNoteAdd, todoNoteToggle, todoNoteUndo, todoNoteDelete });
addSelectedCheck(checks, options, "microservice:oa-event-flow-status", (oaEventFlowStatus as { ok?: boolean }).ok === true && (oaEventFlowStatus as { body?: { microservice?: { id?: string; providerId?: string } } }).body?.microservice?.providerId === config.providerGateway.id, oaEventFlowStatus);
addSelectedCheck(checks, options, "microservice:oa-event-flow-health", (oaEventFlowHealth as { ok?: boolean }).ok === true && oaEventFlowHealthBody?.ok === true && oaEventFlowHealthBody.service === "oa-event-flow" && oaEventFlowHealthBody.databaseReady === true, oaEventFlowHealth);
addSelectedCheck(checks, options, "microservice:oa-event-flow-diagnostics", (oaEventFlowDiagnostics as { ok?: boolean }).ok === true && oaEventFlowDiagnosticsBody?.ok === true && oaEventFlowDiagnosticsBody.service === "oa-event-flow" && oaEventFlowDiagnosticsBody.databaseReady === true && Number.isFinite(oaEventFlowDiagnosticsBody.eventCount) && Array.isArray(oaEventFlowDiagnosticsBody.eventTypes), oaEventFlowDiagnostics);
addSelectedCheck(checks, options, "microservice:oa-event-flow-events", (oaEventFlowEvents as { ok?: boolean }).ok === true && oaEventFlowEventsBody?.ok === true && Array.isArray(oaEventFlowEventsBody.events), oaEventFlowEvents);
addSelectedCheck(checks, options, "microservice:oa-event-flow-pipeline-bridge",
(oaEventFlowPipelineEvents as { ok?: boolean }).ok === true
&& oaEventFlowPipelineEventsBody?.ok === true
&& Array.isArray(oaEventFlowPipelineEventsBody.events)
&& oaEventFlowPipelineEventsBody.events.some((event) => Array.isArray(event.tags) && event.tags.includes("service:pipeline") && event.tags.some((tag) => typeof tag === "string" && tag.startsWith("epoch:")))
&& oaEventFlowDiagnosticsBody?.pipelineBridge?.enabled === true
&& String(oaEventFlowDiagnosticsBody.pipelineBridge.lastError || "").length === 0,
{
pipelineBridge: oaEventFlowDiagnosticsBody?.pipelineBridge,
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-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);
@@ -1226,6 +1267,7 @@ async function frontendCheck(config: UniDeskConfig, urls: PublicUrls, checks: E2
const needMicroserviceCatalog = wants("frontend:microservice-catalog-visible");
const needTodoNote = wants("frontend:todo-note-integrated-visible");
const needFindJob = wants("frontend:findjob-integrated-visible");
const needOaEventFlow = wants("frontend:oa-event-flow-visible");
const needCodeQueue = wantsAny([
"frontend:code-queue-integrated-visible",
"frontend:code-queue-enqueue-await-smoke",
@@ -1309,6 +1351,8 @@ async function frontendCheck(config: UniDeskConfig, urls: PublicUrls, checks: E2
let microserviceCatalogText = "";
let todoNoteText = "";
let findjobText = "";
let oaEventFlowText = "";
let oaEventFlowMetrics: any = { pageVisible: false, eventTableVisible: false, statsVisible: false, tagFilterValue: "", rawButtonCount: 0 };
let codeQueueText = "";
let codeQueueOutputText = "";
let codeQueueTaskCount = 0;
@@ -1524,9 +1568,9 @@ async function frontendCheck(config: UniDeskConfig, urls: PublicUrls, checks: E2
}
}
if (needMicroserviceCatalog || needTodoNote || needFindJob || needCodeQueue || needClaudeqq || needRouteDeepLink || needPipeline || needMetNonlinear) {
if (needMicroserviceCatalog || needTodoNote || needFindJob || needOaEventFlow || needCodeQueue || needClaudeqq || needRouteDeepLink || needPipeline || needMetNonlinear) {
await page.getByRole("button", { name: /用户服务/ }).click();
if (needMicroserviceCatalog || needTodoNote || needFindJob || needCodeQueue || needClaudeqq || needRouteDeepLink || needPipeline || needMetNonlinear) {
if (needMicroserviceCatalog || needTodoNote || needFindJob || needOaEventFlow || needCodeQueue || needClaudeqq || needRouteDeepLink || needPipeline || needMetNonlinear) {
await page.waitForSelector('[data-testid="microservice-catalog-page"]', { timeout: 10000 });
}
if (needMicroserviceCatalog) {
@@ -1535,6 +1579,7 @@ async function frontendCheck(config: UniDeskConfig, urls: PublicUrls, checks: E2
await page.waitForSelector('[data-testid="microservice-row-met-nonlinear"]', { timeout: 10000 });
await page.waitForSelector('[data-testid="microservice-row-claudeqq"]', { timeout: 10000 });
await page.waitForSelector('[data-testid="microservice-row-todo-note"]', { timeout: 10000 });
await page.waitForSelector('[data-testid="microservice-row-oa-event-flow"]', { timeout: 10000 });
await page.waitForSelector('[data-testid="microservice-row-code-queue"]', { timeout: 10000 });
microserviceCatalogText = await page.locator('[data-testid="microservice-catalog-page"]').innerText({ timeout: 5000 });
}
@@ -1591,8 +1636,29 @@ async function frontendCheck(config: UniDeskConfig, urls: PublicUrls, checks: E2
}, undefined, { timeout: 30000 });
findjobText = await page.locator('[data-testid="findjob-page"]').innerText({ timeout: 5000 });
}
if (needOaEventFlow) {
await page.getByRole("button", { name: /OA Event Flow/ }).click();
await page.waitForSelector('[data-testid="oa-event-flow-page"]', { timeout: 10000 });
await page.waitForSelector('[data-testid="oa-event-flow-event-table"]', { timeout: 30000 });
await page.waitForSelector('[data-testid="oa-event-flow-stats"]', { timeout: 30000 });
oaEventFlowText = await page.locator('[data-testid="oa-event-flow-page"]').innerText({ timeout: 5000 });
oaEventFlowMetrics = await page.evaluate(() => {
const pageRoot = document.querySelector('[data-testid="oa-event-flow-page"]') as HTMLElement | null;
const eventTable = document.querySelector('[data-testid="oa-event-flow-event-table"]') as HTMLElement | null;
const stats = document.querySelector('[data-testid="oa-event-flow-stats"]') as HTMLElement | null;
const filter = document.querySelector('[data-testid="oa-event-flow-tag-filter"]') as HTMLInputElement | null;
return {
pageVisible: Boolean(pageRoot),
eventTableVisible: Boolean(eventTable),
statsVisible: Boolean(stats),
tagFilterValue: filter?.value || "",
rawButtonCount: pageRoot?.querySelectorAll('[data-testid^="raw-oa-"]').length ?? 0,
textPreview: pageRoot?.innerText.slice(0, 800) || "",
};
});
}
if (needCodeQueue) {
await page.getByRole("button", { name: /Code Queue/ }).click();
await page.getByLabel("用户服务 子功能").getByRole("button", { name: "Code Queue" }).click();
await page.waitForSelector('[data-testid="code-queue-page"]', { timeout: 10000 });
await page.waitForFunction(() => {
const text = document.body.innerText;
@@ -1638,6 +1704,9 @@ async function frontendCheck(config: UniDeskConfig, urls: PublicUrls, checks: E2
optionCount: select?.options.length ?? 0,
createButtonVisible: Boolean(button && button.offsetParent !== null),
mergeButtonVisible: Boolean(mergeButton && mergeButton.offsetParent !== null),
mergeButtonDisabled: Boolean(mergeButton?.disabled),
mergeSourceInlineMissing: document.querySelector('[data-testid="code-queue-task-form"] [data-testid="codex-merge-source-queue-select"]') === null,
mergeDialogMissingBeforeClick: document.querySelector('[data-testid="codex-merge-queue-dialog"]') === null,
oldInputMissing: document.querySelector('[data-testid="code-queue-id-input"]') === null,
promptDefaultEmpty: (prompt?.value || "") === "",
providerValue: provider?.value || "",
@@ -2101,6 +2170,9 @@ async function frontendCheck(config: UniDeskConfig, urls: PublicUrls, checks: E2
optionCount: select?.options.length ?? 0,
createButtonVisible: Boolean(button && button.offsetParent !== null),
mergeButtonVisible: Boolean(mergeButton && mergeButton.offsetParent !== null),
mergeButtonDisabled: Boolean(mergeButton?.disabled),
mergeSourceInlineMissing: document.querySelector('[data-testid="code-queue-task-form"] [data-testid="codex-merge-source-queue-select"]') === null,
mergeDialogMissingBeforeClick: document.querySelector('[data-testid="codex-merge-queue-dialog"]') === null,
oldInputMissing: document.querySelector('[data-testid="code-queue-id-input"]') === null,
promptDefaultEmpty: (prompt?.value || "") === "",
providerValue: provider?.value || "",
@@ -2112,6 +2184,26 @@ async function frontendCheck(config: UniDeskConfig, urls: PublicUrls, checks: E2
};
});
codeQueuePromptDefaultEmpty = Boolean(codeQueueSubmitQueueControl.promptDefaultEmpty);
if (codeQueueSubmitQueueControl.mergeButtonVisible && !codeQueueSubmitQueueControl.mergeButtonDisabled) {
await page.getByTestId("codex-merge-queue-button").click();
await page.waitForSelector('[data-testid="codex-merge-queue-dialog"]', { timeout: 5000 });
const mergeDialogMetrics = await page.evaluate(() => {
const dialog = document.querySelector('[data-testid="codex-merge-queue-dialog"]') as HTMLElement | null;
const select = document.querySelector('[data-testid="codex-merge-queue-dialog"] [data-testid="codex-merge-source-queue-select"]') as HTMLSelectElement | null;
const text = dialog?.textContent || "";
return {
mergeDialogVisible: Boolean(dialog && dialog.offsetParent !== null),
mergeDialogSelectVisible: Boolean(select && select.offsetParent !== null),
mergeDialogSourceOptionCount: select?.options.length ?? 0,
mergeDialogSelectInsideSubmitForm: Boolean(document.querySelector('[data-testid="code-queue-task-form"] [data-testid="codex-merge-source-queue-select"]')),
mergeDialogUsesCommonComponent: Boolean(dialog?.classList.contains("unidesk-dialog")),
mergeDialogDeleteNoteVisible: text.includes("自动删除源 queue"),
};
});
codeQueueSubmitQueueControl = { ...codeQueueSubmitQueueControl, ...mergeDialogMetrics };
await page.getByTestId("codex-merge-queue-cancel").click();
await page.waitForSelector('[data-testid="codex-merge-queue-dialog"]', { state: "detached", timeout: 5000 }).catch(() => undefined);
}
codeQueueSwitchMetrics = await page.locator('[data-testid="code-queue-filter-select"] option').evaluateAll((options) => ({
optionCount: options.length,
queueValues: options.map((option) => (option as HTMLOptionElement).value).filter((value) => value !== "__all__"),
@@ -2580,10 +2672,23 @@ async function frontendCheck(config: UniDeskConfig, urls: PublicUrls, checks: E2
addSelectedCheck(checks, options, "frontend:gateway-duration-subsecond-visible", gatewayHasSubsecondDuration && !gatewayHasRoundedZeroDuration, { gatewayHasSubsecondDuration, gatewayHasRoundedZeroDuration, gatewayTextPreview: gatewayText.slice(0, 900) });
addSelectedCheck(checks, options, "frontend:provider-operation-availability-visible", sshAvailabilityTexts.length >= 1 && upgradeAvailabilityTexts.length >= 1 && sshAvailabilityTexts.every((text) => text.includes("SSH 透传")) && upgradeAvailabilityTexts.every((text) => text.includes("远程更新")) && upgradeAvailabilityTexts.some((text) => text.includes("always-enabled")), { sshAvailabilityTexts, upgradeAvailabilityTexts });
addSelectedCheck(checks, options, "frontend:overview-pgdata-visible", bodyText.includes("PGDATA") && bodyText.includes(config.database.volume), { bodyPreview: bodyText.slice(0, 800) });
addSelectedCheck(checks, options, "frontend:microservice-catalog-visible", microserviceCatalogTextLower.includes("findjob") && microserviceCatalogTextLower.includes("pipeline") && microserviceCatalogTextLower.includes("todo note") && microserviceCatalogTextLower.includes("met nonlinear") && microserviceCatalogTextLower.includes("claudeqq") && microserviceCatalogTextLower.includes("code queue") && microserviceCatalogText.includes("D601") && microserviceCatalogText.includes(config.providerGateway.id) && microserviceCatalogTextLower.includes("private") && microserviceCatalogText.includes("https://gitee.com/Lyon1998/findjob") && microserviceCatalogText.includes("https://github.com/pikasTech/pipeline") && microserviceCatalogText.includes("https://github.com/pikasTech/met_nonlinear") && microserviceCatalogText.includes("https://gitee.com/lyon1998/agent_skills") && microserviceCatalogText.includes("https://gitee.com/Lyon1998/todo_note") && microserviceCatalogText.includes("https://github.com/pikasTech/unidesk"), { microserviceCatalogPreview: microserviceCatalogText.slice(0, 2000) });
addSelectedCheck(checks, options, "frontend:microservice-catalog-visible", microserviceCatalogTextLower.includes("findjob") && microserviceCatalogTextLower.includes("pipeline") && microserviceCatalogTextLower.includes("todo note") && microserviceCatalogTextLower.includes("met nonlinear") && microserviceCatalogTextLower.includes("claudeqq") && microserviceCatalogTextLower.includes("oa event flow") && microserviceCatalogTextLower.includes("code queue") && microserviceCatalogText.includes("D601") && microserviceCatalogText.includes(config.providerGateway.id) && microserviceCatalogTextLower.includes("private") && microserviceCatalogText.includes("https://gitee.com/Lyon1998/findjob") && microserviceCatalogText.includes("https://github.com/pikasTech/pipeline") && microserviceCatalogText.includes("https://github.com/pikasTech/met_nonlinear") && microserviceCatalogText.includes("https://gitee.com/lyon1998/agent_skills") && microserviceCatalogText.includes("https://gitee.com/Lyon1998/todo_note") && microserviceCatalogText.includes("https://github.com/pikasTech/unidesk"), { microserviceCatalogPreview: microserviceCatalogText.slice(0, 2000) });
addSelectedCheck(checks, options, "frontend:todo-note-integrated-visible", todoNoteTextLower.includes("todo note 工作台") && todoNoteText.includes("CONSTAR") && todoNoteText.includes("大论文") && todoNoteText.includes("UI E2E smoke task") && todoNoteText.includes("撤销") && todoNoteText.includes("重做") && todoNoteText.includes("全部展开") && todoNoteText.includes("仅 UniDesk frontend 代理访问"), { todoNoteTextPreview: todoNoteText.slice(0, 1400) });
addSelectedCheck(checks, options, "frontend:findjob-integrated-visible", findjobTextLower.includes("findjob 工作台".toLowerCase()) && findjobText.includes("岗位总量") && findjobText.includes("D601") && findjobText.includes("近期岗位") && findjobText.includes("仅 UniDesk frontend 代理访问") && /岗位总量\s+\d+/.test(findjobText) && /health\s+ok/i.test(findjobText) && /[1-9]\d*\/[1-9]\d*\s+preview/i.test(findjobText), { findjobTextPreview: findjobText.slice(0, 1200) });
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.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:oa-event-flow-visible",
oaEventFlowText.toLowerCase().includes("oa event flow")
&& oaEventFlowText.includes("事件表")
&& oaEventFlowText.includes("统计中心")
&& oaEventFlowText.includes("service:code-queue")
&& oaEventFlowText.includes("仅 UniDesk frontend 代理访问")
&& oaEventFlowMetrics.pageVisible === true
&& oaEventFlowMetrics.eventTableVisible === true
&& oaEventFlowMetrics.statsVisible === true
&& oaEventFlowMetrics.tagFilterValue === "service:code-queue"
&& 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-enqueue-await-smoke",
codeQueueEnqueueAwaitSmoke.checked === true
&& codeQueueEnqueueAwaitSmoke.delayedPostCount === 1