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:
@@ -2,6 +2,7 @@ import { ApiClientError, createApiClient, type ApiClient } from "@geo/http-clien
|
||||
import type {
|
||||
ApiEnvelope,
|
||||
ApiErrorEnvelope,
|
||||
CancelDesktopTaskRequest,
|
||||
CompleteDesktopTaskRequest,
|
||||
CreatePublishJobResponse,
|
||||
DesktopArticleContent,
|
||||
@@ -15,19 +16,50 @@ import type {
|
||||
LeaseDesktopTaskRequest,
|
||||
LeaseDesktopTaskResponse,
|
||||
ListDesktopPublishTasksParams,
|
||||
MonitoringLeaseTasksPayload,
|
||||
MonitoringLeaseTasksResponse,
|
||||
MonitoringResumeTasksPayload,
|
||||
MonitoringResumeTasksResponse,
|
||||
MonitoringSkipTaskPayload,
|
||||
MonitoringSkipTaskResponse,
|
||||
MonitoringTaskResultPayload,
|
||||
MonitoringTaskResultResponse,
|
||||
UpsertDesktopAccountRequest,
|
||||
} from "@geo/shared-types";
|
||||
|
||||
import { canUseRendererDevtoolsProxy, rendererDevtoolsFetch } from "../renderer-devtools-proxy";
|
||||
import {
|
||||
failObservedRequest,
|
||||
resolveObservedRequest,
|
||||
startObservedRequest,
|
||||
} from "../network-observer";
|
||||
|
||||
type TransportAuthState = "pending" | "registered" | "expired";
|
||||
type TransportSseState = "idle" | "connecting" | "streaming";
|
||||
type TransportDispatchState = "idle" | "connecting" | "streaming";
|
||||
|
||||
type ObservedAxiosConfig = {
|
||||
method?: string;
|
||||
url?: string;
|
||||
baseURL?: string;
|
||||
headers?: Record<string, unknown>;
|
||||
__observedRequestId?: string;
|
||||
};
|
||||
|
||||
interface DesktopTransportSession {
|
||||
baseURL: string;
|
||||
clientToken: string | null;
|
||||
}
|
||||
|
||||
interface MonitoringCancelTaskPayload {
|
||||
lease_token: string;
|
||||
reason?: string | null;
|
||||
}
|
||||
|
||||
interface MonitoringCancelTaskResponse {
|
||||
task_id: number;
|
||||
task_status: string;
|
||||
}
|
||||
|
||||
let desktopApiClient: ApiClient | null = null;
|
||||
let transportSession: DesktopTransportSession = {
|
||||
baseURL: process.env.DESKTOP_API_BASE_URL ?? "http://localhost:8080",
|
||||
@@ -40,7 +72,7 @@ const transportState = {
|
||||
mode: "rabbitmq-first" as const,
|
||||
requestDebugMode: "main-process" as "main-process" | "renderer-devtools",
|
||||
auth: "pending" as TransportAuthState,
|
||||
sseState: "idle" as TransportSseState,
|
||||
dispatchState: "idle" as TransportDispatchState,
|
||||
lastHeartbeatAt: 0,
|
||||
lastHeartbeatStatus: "idle" as "idle" | "success" | "failed",
|
||||
lastPullAt: 0,
|
||||
@@ -58,9 +90,48 @@ function createDesktopClient(baseURL: string): ApiClient {
|
||||
config.headers = config.headers ?? {};
|
||||
(config.headers as Record<string, string>).Authorization = `Bearer ${transportSession.clientToken}`;
|
||||
}
|
||||
|
||||
const observedConfig = config as ObservedAxiosConfig;
|
||||
observedConfig.__observedRequestId = startObservedRequest({
|
||||
source: "transport",
|
||||
label: "desktop.api",
|
||||
method: config.method,
|
||||
url: resolveObservedRequestURL(config.url, config.baseURL ?? baseURL),
|
||||
});
|
||||
return config;
|
||||
});
|
||||
|
||||
client.raw.interceptors.response.use(
|
||||
(response) => {
|
||||
const observedConfig = response.config as ObservedAxiosConfig;
|
||||
if (observedConfig.__observedRequestId) {
|
||||
resolveObservedRequest(observedConfig.__observedRequestId, {
|
||||
phase: "response",
|
||||
status: response.status,
|
||||
});
|
||||
}
|
||||
return response;
|
||||
},
|
||||
(error: unknown) => {
|
||||
const responseError = error as {
|
||||
config?: ObservedAxiosConfig;
|
||||
response?: { status?: number };
|
||||
};
|
||||
const requestId = responseError.config?.__observedRequestId;
|
||||
if (requestId) {
|
||||
if (typeof responseError.response?.status === "number") {
|
||||
resolveObservedRequest(requestId, {
|
||||
phase: "response",
|
||||
status: responseError.response.status,
|
||||
});
|
||||
} else {
|
||||
failObservedRequest(requestId, error);
|
||||
}
|
||||
}
|
||||
return Promise.reject(error);
|
||||
},
|
||||
);
|
||||
|
||||
return client;
|
||||
}
|
||||
|
||||
@@ -72,6 +143,33 @@ function resolveDesktopURL(path: string): string {
|
||||
return new URL(path, transportSession.baseURL).toString();
|
||||
}
|
||||
|
||||
function resolveObservedRequestURL(path: string | undefined, baseURL: string): string {
|
||||
if (!path) {
|
||||
return baseURL;
|
||||
}
|
||||
|
||||
try {
|
||||
return new URL(path, baseURL).toString();
|
||||
} catch {
|
||||
return path;
|
||||
}
|
||||
}
|
||||
|
||||
function shouldFallbackToMainProcess(error: unknown): boolean {
|
||||
if (error instanceof ApiClientError) {
|
||||
if (typeof error.status === "number" && error.status > 0) {
|
||||
return false;
|
||||
}
|
||||
return /Failed to fetch|renderer_devtools_proxy_/i.test(error.message);
|
||||
}
|
||||
|
||||
if (error instanceof Error) {
|
||||
return /Failed to fetch|renderer_devtools_proxy_/i.test(error.message);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
async function proxyDesktopRequest<T, B = unknown>(
|
||||
method: "GET" | "POST" | "DELETE",
|
||||
path: string,
|
||||
@@ -134,7 +232,14 @@ async function proxyDesktopRequest<T, B = unknown>(
|
||||
async function desktopGet<T>(path: string): Promise<T> {
|
||||
transportState.requestDebugMode = currentRequestDebugMode();
|
||||
if (transportState.requestDebugMode === "renderer-devtools") {
|
||||
return proxyDesktopRequest<T>("GET", path);
|
||||
try {
|
||||
return await proxyDesktopRequest<T>("GET", path);
|
||||
} catch (error) {
|
||||
if (!shouldFallbackToMainProcess(error)) {
|
||||
throw error;
|
||||
}
|
||||
transportState.requestDebugMode = "main-process";
|
||||
}
|
||||
}
|
||||
return getDesktopApiClient().get<T>(path);
|
||||
}
|
||||
@@ -142,7 +247,14 @@ async function desktopGet<T>(path: string): Promise<T> {
|
||||
async function desktopPost<T, B = unknown>(path: string, body?: B): Promise<T> {
|
||||
transportState.requestDebugMode = currentRequestDebugMode();
|
||||
if (transportState.requestDebugMode === "renderer-devtools") {
|
||||
return proxyDesktopRequest<T, B>("POST", path, body);
|
||||
try {
|
||||
return await proxyDesktopRequest<T, B>("POST", path, body);
|
||||
} catch (error) {
|
||||
if (!shouldFallbackToMainProcess(error)) {
|
||||
throw error;
|
||||
}
|
||||
transportState.requestDebugMode = "main-process";
|
||||
}
|
||||
}
|
||||
return getDesktopApiClient().post<T, B>(path, body);
|
||||
}
|
||||
@@ -150,7 +262,14 @@ async function desktopPost<T, B = unknown>(path: string, body?: B): Promise<T> {
|
||||
async function desktopDelete<T>(path: string): Promise<T> {
|
||||
transportState.requestDebugMode = currentRequestDebugMode();
|
||||
if (transportState.requestDebugMode === "renderer-devtools") {
|
||||
return proxyDesktopRequest<T>("DELETE", path);
|
||||
try {
|
||||
return await proxyDesktopRequest<T>("DELETE", path);
|
||||
} catch (error) {
|
||||
if (!shouldFallbackToMainProcess(error)) {
|
||||
throw error;
|
||||
}
|
||||
transportState.requestDebugMode = "main-process";
|
||||
}
|
||||
}
|
||||
return getDesktopApiClient().remove<T>(path);
|
||||
}
|
||||
@@ -176,6 +295,7 @@ export function configureTransport(session: DesktopRuntimeSessionSyncRequest | n
|
||||
transportState.baseURL = transportSession.baseURL;
|
||||
transportState.requestDebugMode = currentRequestDebugMode();
|
||||
transportState.auth = "pending";
|
||||
transportState.dispatchState = "idle";
|
||||
desktopApiClient = null;
|
||||
return null;
|
||||
}
|
||||
@@ -187,6 +307,7 @@ export function configureTransport(session: DesktopRuntimeSessionSyncRequest | n
|
||||
transportState.baseURL = transportSession.baseURL;
|
||||
transportState.requestDebugMode = currentRequestDebugMode();
|
||||
transportState.auth = session.client_token ? "registered" : "pending";
|
||||
transportState.dispatchState = session.client_token ? "connecting" : "idle";
|
||||
transportState.initializedAt = Date.now();
|
||||
desktopApiClient = createDesktopClient(transportSession.baseURL);
|
||||
return desktopApiClient;
|
||||
@@ -196,8 +317,8 @@ export function setTransportAuthState(next: TransportAuthState): void {
|
||||
transportState.auth = next;
|
||||
}
|
||||
|
||||
export function setTransportSseState(next: TransportSseState): void {
|
||||
transportState.sseState = next;
|
||||
export function setTransportDispatchState(next: TransportDispatchState): void {
|
||||
transportState.dispatchState = next;
|
||||
}
|
||||
|
||||
export function noteTransportHeartbeat(success: boolean): void {
|
||||
@@ -328,3 +449,61 @@ export async function completeDesktopTask(
|
||||
payload,
|
||||
);
|
||||
}
|
||||
|
||||
export async function cancelDesktopTask(
|
||||
taskId: string,
|
||||
payload: CancelDesktopTaskRequest,
|
||||
): Promise<DesktopTaskInfo> {
|
||||
return desktopPost<DesktopTaskInfo, CancelDesktopTaskRequest>(
|
||||
`/api/desktop/tasks/${taskId}/cancel`,
|
||||
payload,
|
||||
);
|
||||
}
|
||||
|
||||
export async function leaseMonitoringTasks(
|
||||
payload: MonitoringLeaseTasksPayload = {},
|
||||
): Promise<MonitoringLeaseTasksResponse> {
|
||||
return desktopPost<MonitoringLeaseTasksResponse, MonitoringLeaseTasksPayload>(
|
||||
"/api/desktop/monitoring/tasks/lease",
|
||||
payload,
|
||||
);
|
||||
}
|
||||
|
||||
export async function resumeMonitoringTasks(
|
||||
payload: MonitoringResumeTasksPayload = {},
|
||||
): Promise<MonitoringResumeTasksResponse> {
|
||||
return desktopPost<MonitoringResumeTasksResponse, MonitoringResumeTasksPayload>(
|
||||
"/api/desktop/monitoring/tasks/resume",
|
||||
payload,
|
||||
);
|
||||
}
|
||||
|
||||
export async function submitMonitoringTaskResult(
|
||||
taskId: number,
|
||||
payload: MonitoringTaskResultPayload,
|
||||
): Promise<MonitoringTaskResultResponse> {
|
||||
return desktopPost<MonitoringTaskResultResponse, MonitoringTaskResultPayload>(
|
||||
`/api/desktop/monitoring/tasks/${taskId}/result`,
|
||||
payload,
|
||||
);
|
||||
}
|
||||
|
||||
export async function skipMonitoringTask(
|
||||
taskId: number,
|
||||
payload: MonitoringSkipTaskPayload,
|
||||
): Promise<MonitoringSkipTaskResponse> {
|
||||
return desktopPost<MonitoringSkipTaskResponse, MonitoringSkipTaskPayload>(
|
||||
`/api/desktop/monitoring/tasks/${taskId}/skip`,
|
||||
payload,
|
||||
);
|
||||
}
|
||||
|
||||
export async function cancelMonitoringTask(
|
||||
taskId: number,
|
||||
payload: MonitoringCancelTaskPayload,
|
||||
): Promise<MonitoringCancelTaskResponse> {
|
||||
return desktopPost<MonitoringCancelTaskResponse, MonitoringCancelTaskPayload>(
|
||||
`/api/desktop/monitoring/tasks/${taskId}/cancel`,
|
||||
payload,
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user