4142c53fa6
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>
148 lines
3.4 KiB
TypeScript
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,
|
|
};
|
|
}
|