Files
geo/apps/desktop-client/src/main/view-pool.ts
T
root 4142c53fa6 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>
2026-04-22 00:24:21 +08:00

148 lines
3.4 KiB
TypeScript

import { WebContentsView } from "electron/main";
import type { WebContentsView as ElectronWebContentsView } from "electron/main";
import { maybeOpenExecutionDevtools } from "./execution-devtools";
export interface HotViewHandle {
accountId: string;
view: ElectronWebContentsView;
activatedAt: number;
lastReleasedAt: number | null;
retainCount: number;
}
const hotViews = new Map<string, HotViewHandle>();
const HOT_VIEW_IDLE_TTL_MS = 5 * 60_000;
const HOT_VIEW_REAPER_INTERVAL_MS = 60_000;
let reaperHandle: ReturnType<typeof setInterval> | null = null;
function closeView(handle: HotViewHandle): void {
if (!handle.view.webContents.isDestroyed()) {
handle.view.webContents.close();
}
}
function touchHandle(handle: HotViewHandle): HotViewHandle {
handle.activatedAt = Date.now();
return handle;
}
function demoteViewInternal(accountId: string): boolean {
const handle = hotViews.get(accountId);
if (!handle) {
return false;
}
closeView(handle);
hotViews.delete(accountId);
return true;
}
export function startHotViewReaper(): void {
if (reaperHandle) {
return;
}
reaperHandle = setInterval(() => {
reapIdleHotViews();
}, HOT_VIEW_REAPER_INTERVAL_MS);
reaperHandle.unref?.();
}
export function acquireHotView(accountId: string): HotViewHandle {
startHotViewReaper();
const existing = hotViews.get(accountId);
if (existing) {
return touchHandle(existing);
}
const handle: HotViewHandle = {
accountId,
view: new WebContentsView({
webPreferences: {
sandbox: true,
contextIsolation: true,
nodeIntegration: false,
},
}),
activatedAt: Date.now(),
lastReleasedAt: Date.now(),
retainCount: 0,
};
maybeOpenExecutionDevtools(handle.view.webContents, `hot-view:${accountId}`, { background: true });
hotViews.set(accountId, handle);
return handle;
}
export function retainHotView(accountId: string): HotViewHandle {
const handle = touchHandle(acquireHotView(accountId));
handle.retainCount += 1;
handle.lastReleasedAt = null;
return handle;
}
export function releaseHotView(accountId: string): void {
const handle = hotViews.get(accountId);
if (!handle) {
return;
}
if (handle.retainCount > 0) {
handle.retainCount -= 1;
}
if (handle.retainCount === 0) {
handle.lastReleasedAt = Date.now();
}
}
export function demoteView(accountId: string): boolean {
const handle = hotViews.get(accountId);
if (!handle || handle.retainCount > 0) {
return false;
}
return demoteViewInternal(accountId);
}
export function reapIdleHotViews(
now = Date.now(),
idleTTLms = HOT_VIEW_IDLE_TTL_MS,
): string[] {
const reclaimed: string[] = [];
for (const [accountId, handle] of hotViews.entries()) {
if (handle.retainCount > 0) {
continue;
}
const idleSince = handle.lastReleasedAt ?? handle.activatedAt;
if (now - idleSince < idleTTLms) {
continue;
}
if (demoteViewInternal(accountId)) {
reclaimed.push(accountId);
}
}
return reclaimed;
}
export function listHotViews(): Array<Pick<HotViewHandle, "accountId" | "activatedAt" | "lastReleasedAt" | "retainCount">> {
return [...hotViews.values()].map(({ accountId, activatedAt, lastReleasedAt, retainCount }) => ({
accountId,
activatedAt,
lastReleasedAt,
retainCount,
}));
}
export function getHotViewPoolPolicy() {
return {
idleTTLms: HOT_VIEW_IDLE_TTL_MS,
reapIntervalMs: HOT_VIEW_REAPER_INTERVAL_MS,
};
}