feat(monitoring): dispatch monitoring tasks to desktop via AMQP outbox
Replace SSE /desktop/events with priority AMQP dispatch for monitoring
runs, and add phase1/phase2 infrastructure so desktop workers can lease,
resume, report, skip, and cancel monitoring tasks over the existing
dispatch WebSocket.
- Add monitoring collect outbox worker and phase2 desktop task fields
- Add /desktop/monitoring/tasks/{lease,resume,result,skip,cancel} routes
- Introduce execution-devtools and network-observer on desktop runtime
- Refactor runtime-controller, LoginView, and doubao adapter for the new flow
- Add channelName column to tracking views
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -10,6 +10,14 @@ const defaultConnectTimeoutMs = 10_000;
|
||||
const defaultPageTimeoutMs = 45_000;
|
||||
const cdpPort = Number.parseInt(process.env.GEO_ELECTRON_CDP_PORT ?? "9339", 10);
|
||||
const cdpEndpointURL = `http://127.0.0.1:${cdpPort}`;
|
||||
const cdpTargetsListURL = `${cdpEndpointURL}/json/list`;
|
||||
|
||||
interface CDPTargetDescriptor {
|
||||
id?: string;
|
||||
type?: string;
|
||||
title?: string;
|
||||
url?: string;
|
||||
}
|
||||
|
||||
interface HiddenPlaywrightRecord {
|
||||
key: string;
|
||||
@@ -220,17 +228,22 @@ async function connectBrowserOverCDP(): Promise<PlaywrightBrowser> {
|
||||
connectPromise = (async () => {
|
||||
await waitForCDPEndpoint();
|
||||
const { chromium } = await import("playwright-core");
|
||||
const browser = await chromium.connectOverCDP(cdpEndpointURL, {
|
||||
timeout: defaultConnectTimeoutMs,
|
||||
});
|
||||
await pruneStaleCDPTargets();
|
||||
|
||||
browser.on("disconnected", () => {
|
||||
connectedBrowser = null;
|
||||
connectPromise = null;
|
||||
});
|
||||
try {
|
||||
return await openBrowserOverCDP(chromium);
|
||||
} catch (error) {
|
||||
if (!isConnectOverCDPTimeout(error)) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
connectedBrowser = browser;
|
||||
return browser;
|
||||
const prunedTargetCount = await pruneStaleCDPTargets();
|
||||
if (prunedTargetCount <= 0) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return openBrowserOverCDP(chromium);
|
||||
}
|
||||
})();
|
||||
|
||||
try {
|
||||
@@ -260,6 +273,76 @@ async function waitForCDPEndpoint(timeoutMs = defaultConnectTimeoutMs): Promise<
|
||||
throw new Error("playwright_cdp_endpoint_unavailable");
|
||||
}
|
||||
|
||||
async function openBrowserOverCDP(
|
||||
chromium: typeof import("playwright-core").chromium,
|
||||
): Promise<PlaywrightBrowser> {
|
||||
const browser = await chromium.connectOverCDP(cdpEndpointURL, {
|
||||
timeout: defaultConnectTimeoutMs,
|
||||
});
|
||||
|
||||
browser.on("disconnected", () => {
|
||||
connectedBrowser = null;
|
||||
connectPromise = null;
|
||||
});
|
||||
|
||||
connectedBrowser = browser;
|
||||
return browser;
|
||||
}
|
||||
|
||||
async function pruneStaleCDPTargets(): Promise<number> {
|
||||
let targets: CDPTargetDescriptor[] = [];
|
||||
try {
|
||||
const response = await fetch(cdpTargetsListURL);
|
||||
if (!response.ok) {
|
||||
return 0;
|
||||
}
|
||||
const payload = await response.json();
|
||||
if (Array.isArray(payload)) {
|
||||
targets = payload as CDPTargetDescriptor[];
|
||||
}
|
||||
} catch {
|
||||
return 0;
|
||||
}
|
||||
|
||||
let closedCount = 0;
|
||||
for (const target of targets) {
|
||||
if (!isStaleCDPTarget(target)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
const closeURL = `${cdpEndpointURL}/json/close/${encodeURIComponent(target.id ?? "")}`;
|
||||
const response = await fetch(closeURL);
|
||||
if (response.ok) {
|
||||
closedCount += 1;
|
||||
}
|
||||
} catch {
|
||||
// Ignore best-effort stale target cleanup failures.
|
||||
}
|
||||
}
|
||||
|
||||
if (closedCount > 0) {
|
||||
await wait(150);
|
||||
}
|
||||
return closedCount;
|
||||
}
|
||||
|
||||
function isStaleCDPTarget(target: CDPTargetDescriptor): boolean {
|
||||
if ((target.type ?? "").trim() !== "page") {
|
||||
return false;
|
||||
}
|
||||
if (!(target.id ?? "").trim()) {
|
||||
return false;
|
||||
}
|
||||
return !(target.title ?? "").trim() && !(target.url ?? "").trim();
|
||||
}
|
||||
|
||||
function isConnectOverCDPTimeout(error: unknown): boolean {
|
||||
return error instanceof Error
|
||||
&& error.name === "TimeoutError"
|
||||
&& error.message.includes("connectOverCDP");
|
||||
}
|
||||
|
||||
async function waitForNewPage(
|
||||
browser: PlaywrightBrowser,
|
||||
knownPages: Set<PlaywrightPage>,
|
||||
@@ -270,6 +353,10 @@ async function waitForNewPage(
|
||||
while (Date.now() - startedAt < defaultPageTimeoutMs) {
|
||||
const nextPage = context.pages().find((page) => !knownPages.has(page));
|
||||
if (nextPage) {
|
||||
if (isDevtoolsPage(nextPage)) {
|
||||
knownPages.add(nextPage);
|
||||
continue;
|
||||
}
|
||||
return nextPage;
|
||||
}
|
||||
await wait(100);
|
||||
@@ -298,13 +385,12 @@ function safePageURL(page: PlaywrightPage): string {
|
||||
}
|
||||
}
|
||||
|
||||
function closeRecord(record: HiddenPlaywrightRecord): void {
|
||||
try {
|
||||
record.page.close().catch(() => undefined);
|
||||
} catch {
|
||||
// Ignore and fall through to window close.
|
||||
}
|
||||
function isDevtoolsPage(page: PlaywrightPage): boolean {
|
||||
const url = safePageURL(page);
|
||||
return url.startsWith("devtools://");
|
||||
}
|
||||
|
||||
function closeRecord(record: HiddenPlaywrightRecord): void {
|
||||
if (!record.window.isDestroyed()) {
|
||||
record.window.close();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user