import type { JsonValue, MonitoringHeartbeatPlatformPayload, MonitoringHeartbeatResponse, MonitoringLeaseTask, MonitoringLeaseTasksPayload, MonitoringLeaseTasksResponse, MonitoringLocalPlatformState, MonitoringPluginPingResponse, MonitoringResumeTasksPayload, MonitoringResumeTasksResponse, MonitoringSkipTaskPayload, MonitoringSkipTaskResponse, MonitoringTaskResultPayload, MonitoringTaskResultResponse, } from "@geo/shared-types"; import { browser } from "wxt/browser"; import { canQueryMonitoringPlatform, detectMonitoringPlatformState, queryMonitoringTask } from "./monitoring-adapters"; import type { MonitoringAdapterTaskContext } from "./monitoring-adapters/types"; import type { StoredMonitoringPlatformState } from "./monitoring-platforms"; import { getExtensionState, patchExtensionState, type ExtensionStorageState, type StoredMonitoringScheduledTask } from "./storage"; export type MonitoringAction = "ping" | "detectPlatforms" | "syncHeartbeat" | "runCycle" | "getState"; export type MonitoringCycleMode = "manual" | "automatic"; const monitoringCycleBatchLeaseLimit = 50; const monitoringAutomaticDelayMinMs = 5 * 60 * 1000; const monitoringAutomaticDelayMaxMs = 10 * 60 * 1000; const monitoringRemoteHeartbeatFreshnessMs = 10 * 60 * 1000; const monitoringBusinessTimeZone = "Asia/Shanghai"; export interface MonitoringHeartbeatSyncResult { success: boolean; source: "remote" | "local"; platform_states: MonitoringLocalPlatformState[]; heartbeat?: MonitoringHeartbeatResponse | null; message?: string | null; } export interface MonitoringCycleResult { success: boolean; leased_task_count: number; processed_task_count: number; failed_task_count: number; skipped_task_count: number; message?: string | null; } export interface MonitoringRuntimeStateSnapshot { installation_key: string; plugin_installation_id: number | null; api_base_url: string | null; monitoring_last_heartbeat_at: string | null; monitoring_last_heartbeat_status: string | null; monitoring_last_cycle_at: string | null; monitoring_last_cycle_status: string | null; monitoring_platforms: StoredMonitoringPlatformState[]; } function normalizeBaseUrl(baseUrl: string): string { return baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`; } function normalizeText(value?: string | null): string | null { const trimmed = value?.trim(); return trimmed ? trimmed : null; } function formatMonitoringBusinessDate(value: Date): string { const parts = new Intl.DateTimeFormat("en-US", { timeZone: monitoringBusinessTimeZone, year: "numeric", month: "2-digit", day: "2-digit", }).formatToParts(value); const year = parts.find((part) => part.type === "year")?.value; const month = parts.find((part) => part.type === "month")?.value; const day = parts.find((part) => part.type === "day")?.value; if (!year || !month || !day) { throw new Error("monitoring_business_date_format_failed"); } return `${year}-${month}-${day}`; } function currentMonitoringBusinessDate(): string { return formatMonitoringBusinessDate(new Date()); } function currentTimestampMs(): number { return Date.now(); } function toTimestampMs(value: string | null | undefined): number | null { if (!value) { return null; } const timestamp = new Date(value).getTime(); return Number.isNaN(timestamp) ? null : timestamp; } function randomMonitoringDelayMs(): number { return monitoringAutomaticDelayMinMs + Math.floor(Math.random() * (monitoringAutomaticDelayMaxMs - monitoringAutomaticDelayMinMs + 1)); } function hasFreshSuccessfulMonitoringHeartbeat(state: ExtensionStorageState): boolean { if (state.monitoring_last_heartbeat_status !== "success") { return false; } const lastHeartbeatAt = toTimestampMs(state.monitoring_last_heartbeat_at); if (lastHeartbeatAt == null) { return false; } return currentTimestampMs() - lastHeartbeatAt < monitoringRemoteHeartbeatFreshnessMs; } function isImmediateMonitoringTask(task: MonitoringLeaseTask | StoredMonitoringScheduledTask): boolean { return task.trigger_source === "manual"; } function sortMonitoringTaskQueue(tasks: StoredMonitoringScheduledTask[]): StoredMonitoringScheduledTask[] { return [...tasks].sort((left, right) => { const leftTime = toTimestampMs(left.next_execute_at) ?? 0; const rightTime = toTimestampMs(right.next_execute_at) ?? 0; if (leftTime !== rightTime) { return leftTime - rightTime; } return left.task_id - right.task_id; }); } function isMonitoringNotLoggedIn(platform: StoredMonitoringPlatformState): boolean { return platform.reason_code === "not_logged_in" || (platform.access_requirement === "login_required" && !platform.connected); } function resolveAccessStatus(platform: StoredMonitoringPlatformState): string { if (platform.accessible) { return "accessible"; } if (isMonitoringNotLoggedIn(platform)) { return "not_logged_in"; } return "unavailable"; } function toMonitoringPlatformPayload(platform: StoredMonitoringPlatformState): MonitoringHeartbeatPlatformPayload { return { ai_platform_id: platform.ai_platform_id, access_status: resolveAccessStatus(platform), connected: platform.connected, accessible: platform.accessible, detected_at: platform.detected_at ?? null, reason_code: platform.reason_code ?? null, reason_message: platform.reason_message ?? null, }; } async function postMonitoringApi(path: string, payload: unknown): Promise { void path; void payload; throw new Error("browser_extension_monitoring_removed_use_desktop_client"); } async function detectAllMonitoringPlatforms(): Promise { const state = await getExtensionState(); return await Promise.all(state.monitoring_platforms.map((platform) => detectMonitoringPlatformState(platform))); } async function persistMonitoringPlatforms(platforms: StoredMonitoringPlatformState[]): Promise { const nextState = await patchExtensionState({ monitoring_platforms: platforms, }); return nextState.monitoring_platforms; } function projectMonitoringPlatforms(platforms: StoredMonitoringPlatformState[]): MonitoringLocalPlatformState[] { return platforms.map((platform) => ({ ai_platform_id: platform.ai_platform_id, connected: platform.connected, accessible: platform.accessible, detected_at: platform.detected_at ?? null, reason_code: platform.reason_code ?? null, reason_message: platform.reason_message ?? null, message: platform.message ?? null, })); } function asMonitoringTaskContext(task: MonitoringLeaseTask): MonitoringAdapterTaskContext { return { task_id: task.task_id, brand_id: task.brand_id, question_id: task.question_id, question_hash: task.question_hash, question_text: task.question_text, ai_platform_id: task.ai_platform_id, platform_name: task.platform_name, collector_type: task.collector_type, run_mode: task.run_mode, trigger_source: task.trigger_source, business_date: task.business_date, lease_token: task.lease_token, lease_expires_at: task.lease_expires_at, }; } function asTaskResultPayload(task: MonitoringLeaseTask, result: Awaited>): MonitoringTaskResultPayload { return { lease_token: task.lease_token, status: "succeeded", provider_model: normalizeText(result.provider_model), provider_request_id: normalizeText(result.provider_request_id), request_id: normalizeText(result.request_id), answer: normalizeText(result.answer), raw_response_json: (result.raw_response_json as Record | undefined) ?? {}, citations: result.citations ?? [], search_results: result.search_results ?? [], brand_mentioned: result.brand_mentioned ?? null, brand_mention_position: normalizeText(result.brand_mention_position), first_recommended: result.first_recommended ?? null, sentiment_label: normalizeText(result.sentiment_label), matched_brand_terms: result.matched_brand_terms ?? [], }; } async function markMonitoringTaskSkipped(task: MonitoringLeaseTask, reason: string, errorMessage?: string | null): Promise { const payload: MonitoringSkipTaskPayload = { lease_token: task.lease_token, skip_reason: reason, error_message: normalizeText(errorMessage), completed_at: new Date().toISOString(), }; return await postMonitoringApi("skip_task", payload); } async function submitMonitoringTaskResult(task: MonitoringLeaseTask, payload: MonitoringTaskResultPayload): Promise { void task; return await postMonitoringApi("submit_result", payload); } async function processMonitoringLeasedTask( platform: StoredMonitoringPlatformState, task: MonitoringLeaseTask, ): Promise<"processed" | "failed" | "skipped"> { if (!(await canQueryMonitoringPlatform(platform))) { await markMonitoringTaskSkipped(task, "adapter_not_ready", `monitoring adapter is not ready for ${platform.ai_platform_id}`); return "skipped"; } try { const result = await queryMonitoringTask(platform, asMonitoringTaskContext(task)); await submitMonitoringTaskResult(task, asTaskResultPayload(task, result)); return "processed"; } catch (error) { const message = error instanceof Error ? error.message : "monitoring_query_failed"; try { await markMonitoringTaskSkipped(task, "query_failed", message); } catch { // Ignore secondary skip failures so the caller can continue draining the queue. } return "failed"; } } async function resumeMonitoringTasks(aiPlatformIDs: string[]): Promise { const payload: MonitoringResumeTasksPayload = { ai_platform_ids: aiPlatformIDs, }; const resumed = await postMonitoringApi("resume_tasks", payload); return resumed.tasks; } async function leaseMonitoringTasks(aiPlatformIDs: string[]): Promise { const payload: MonitoringLeaseTasksPayload = { limit: monitoringCycleBatchLeaseLimit, ai_platform_ids: aiPlatformIDs, }; return await postMonitoringApi("lease_tasks", payload); } function mergeMonitoringQueueTasks( existingQueue: StoredMonitoringScheduledTask[], incomingTasks: MonitoringLeaseTask[], ): StoredMonitoringScheduledTask[] { if (incomingTasks.length === 0) { return sortMonitoringTaskQueue(existingQueue); } const nowMs = currentTimestampMs(); const queueByTaskID = new Map(existingQueue.map((task) => [task.task_id, task])); let latestDelayedExecuteAt = existingQueue.reduce((latest, task) => { if (task.pacing_mode !== "delayed") { return latest; } const executeAt = toTimestampMs(task.next_execute_at); return executeAt == null ? latest : Math.max(latest, executeAt); }, nowMs); for (const task of incomingTasks) { if (task.business_date !== currentMonitoringBusinessDate()) { continue; } const existing = queueByTaskID.get(task.task_id); if (existing) { queueByTaskID.set(task.task_id, { ...existing, ...task, }); continue; } const immediate = isImmediateMonitoringTask(task); if (!immediate) { latestDelayedExecuteAt = Math.max(latestDelayedExecuteAt, nowMs) + randomMonitoringDelayMs(); } queueByTaskID.set(task.task_id, { ...task, pacing_mode: immediate ? "immediate" : "delayed", scheduled_at: new Date(nowMs).toISOString(), next_execute_at: immediate ? new Date(nowMs).toISOString() : new Date(latestDelayedExecuteAt).toISOString(), }); } return sortMonitoringTaskQueue(Array.from(queueByTaskID.values())); } async function persistMonitoringTaskQueue(queue: StoredMonitoringScheduledTask[]): Promise { const nextState = await patchExtensionState({ monitoring_task_queue: sortMonitoringTaskQueue(queue), }); return nextState.monitoring_task_queue; } async function pruneMonitoringTaskQueue(): Promise { const state = await getExtensionState(); const today = currentMonitoringBusinessDate(); const retained: StoredMonitoringScheduledTask[] = []; for (const task of state.monitoring_task_queue) { if (task.business_date === today) { retained.push(task); continue; } try { await markMonitoringTaskSkipped( task, "stale_business_date", `monitoring task business_date ${task.business_date} is not the current business date`, ); } catch { // Best-effort cleanup for locally cached stale tasks. } } return await persistMonitoringTaskQueue(retained); } async function syncMonitoringTaskQueue(aiPlatformIDs: string[]): Promise { const localQueue = await pruneMonitoringTaskQueue(); const resumedTasks = await resumeMonitoringTasks(aiPlatformIDs); const resumedTaskMap = new Map(resumedTasks.map((task) => [task.task_id, task])); const matchedQueue: StoredMonitoringScheduledTask[] = []; for (const task of localQueue) { const resumedTask = resumedTaskMap.get(task.task_id); if (!resumedTask) { continue; } matchedQueue.push({ ...task, ...resumedTask, }); resumedTaskMap.delete(task.task_id); } const mergedQueue = mergeMonitoringQueueTasks(matchedQueue, Array.from(resumedTaskMap.values())); return await persistMonitoringTaskQueue(mergedQueue); } async function appendMonitoringTasksToQueue(tasks: MonitoringLeaseTask[]): Promise { const state = await getExtensionState(); const mergedQueue = mergeMonitoringQueueTasks(state.monitoring_task_queue, tasks); return await persistMonitoringTaskQueue(mergedQueue); } async function removeMonitoringTaskFromQueue(taskID: number): Promise { const state = await getExtensionState(); return await persistMonitoringTaskQueue(state.monitoring_task_queue.filter((task) => task.task_id !== taskID)); } function isMonitoringTaskDue(task: StoredMonitoringScheduledTask, nowMs: number): boolean { const executeAt = toTimestampMs(task.next_execute_at); return executeAt == null || executeAt <= nowMs; } async function processQueuedMonitoringTask( task: StoredMonitoringScheduledTask, platformMap: Map, ): Promise<"processed" | "failed" | "skipped"> { if (task.business_date !== currentMonitoringBusinessDate()) { await markMonitoringTaskSkipped( task, "stale_business_date", `monitoring task business_date ${task.business_date} is not the current business date`, ); return "skipped"; } const platform = platformMap.get(task.ai_platform_id); if (!platform) { await markMonitoringTaskSkipped(task, "platform_not_configured", `monitoring platform ${task.ai_platform_id} is not configured locally`); return "skipped"; } return await processMonitoringLeasedTask(platform, task); } async function processImmediateMonitoringTasks( platformMap: Map, ): Promise<{ processedCount: number; failedCount: number; skippedCount: number }> { let processedCount = 0; let failedCount = 0; let skippedCount = 0; for (;;) { const state = await getExtensionState(); const dueImmediateTask = state.monitoring_task_queue.find( (task) => task.pacing_mode === "immediate" && isMonitoringTaskDue(task, currentTimestampMs()), ); if (!dueImmediateTask) { break; } const outcome = await processQueuedMonitoringTask(dueImmediateTask, platformMap); await removeMonitoringTaskFromQueue(dueImmediateTask.task_id); if (outcome === "processed") { processedCount += 1; continue; } if (outcome === "failed") { failedCount += 1; continue; } skippedCount += 1; } return { processedCount, failedCount, skippedCount, }; } async function processNextDelayedMonitoringTask( platformMap: Map, ): Promise<{ processedCount: number; failedCount: number; skippedCount: number }> { const state = await getExtensionState(); const delayedTask = state.monitoring_task_queue.find( (task) => task.pacing_mode === "delayed" && isMonitoringTaskDue(task, currentTimestampMs()), ); if (!delayedTask) { return { processedCount: 0, failedCount: 0, skippedCount: 0, }; } const outcome = await processQueuedMonitoringTask(delayedTask, platformMap); await removeMonitoringTaskFromQueue(delayedTask.task_id); if (outcome === "processed") { return { processedCount: 1, failedCount: 0, skippedCount: 0 }; } if (outcome === "failed") { return { processedCount: 0, failedCount: 1, skippedCount: 0 }; } return { processedCount: 0, failedCount: 0, skippedCount: 1 }; } export async function handleMonitoringAction( action: MonitoringAction, ): Promise< MonitoringPluginPingResponse | MonitoringLocalPlatformState[] | MonitoringHeartbeatSyncResult | MonitoringCycleResult | MonitoringRuntimeStateSnapshot > { switch (action) { case "ping": return await handleMonitoringPing(); case "detectPlatforms": return await handleMonitoringDetectPlatforms(); case "syncHeartbeat": return await handleMonitoringSyncHeartbeat(false); case "runCycle": return await runMonitoringCycle("manual"); case "getState": return await handleMonitoringGetState(); default: throw new Error("monitoring_action_not_supported"); } } export async function runMonitoringCycle(mode: MonitoringCycleMode): Promise { return await handleMonitoringRunCycle(mode); } async function handleMonitoringPing(): Promise { const state = await getExtensionState(); return { installed: true, version: browser.runtime.getManifest().version, capabilities: ["ping", "detect", "heartbeat", "lease", "background"], installation_key: state.installation_key, plugin_installation_id: state.plugin_installation_id, }; } async function handleMonitoringDetectPlatforms(): Promise { const platforms = await detectAllMonitoringPlatforms(); const persisted = await persistMonitoringPlatforms(platforms); return projectMonitoringPlatforms(persisted); } async function handleMonitoringSyncHeartbeat(allowCachedRemote: boolean): Promise { const detectedPlatforms = await detectAllMonitoringPlatforms(); const persistedPlatforms = await persistMonitoringPlatforms(detectedPlatforms); const projectedPlatforms = projectMonitoringPlatforms(persistedPlatforms); const heartbeatAt = new Date().toISOString(); const state = await getExtensionState(); if (!state.plugin_installation_id || !state.installation_token || !state.api_base_url) { await patchExtensionState({ monitoring_last_heartbeat_at: heartbeatAt, monitoring_last_heartbeat_status: "local_only", }); return { success: true, source: "local", platform_states: projectedPlatforms, heartbeat: null, message: "monitoring installation is not registered yet", }; } if (allowCachedRemote && hasFreshSuccessfulMonitoringHeartbeat(state)) { return { success: true, source: "local", platform_states: projectedPlatforms, heartbeat: null, message: "reused recent monitoring heartbeat", }; } try { const heartbeat = await postMonitoringApi("heartbeat", { platforms: persistedPlatforms.map(toMonitoringPlatformPayload), }); await patchExtensionState({ monitoring_last_heartbeat_at: heartbeatAt, monitoring_last_heartbeat_status: "success", }); return { success: true, source: "remote", platform_states: projectedPlatforms, heartbeat, message: null, }; } catch (error) { await patchExtensionState({ monitoring_last_heartbeat_at: heartbeatAt, monitoring_last_heartbeat_status: "failed", }); throw error; } } async function handleMonitoringRunCycle(mode: MonitoringCycleMode): Promise { let heartbeatResult: MonitoringHeartbeatSyncResult; try { heartbeatResult = await handleMonitoringSyncHeartbeat(mode === "automatic"); } catch (error) { await patchExtensionState({ monitoring_last_cycle_at: new Date().toISOString(), monitoring_last_cycle_status: "heartbeat_failed", }); throw error; } const state = await getExtensionState(); const accessiblePlatforms = state.monitoring_platforms.filter((platform) => platform.accessible); const queryReadyPlatforms = ( await Promise.all( accessiblePlatforms.map(async (platform) => ({ platform, canQuery: await canQueryMonitoringPlatform(platform), })), ) ) .filter((item) => item.canQuery) .map((item) => item.platform); if (!state.plugin_installation_id || !state.installation_token || !state.api_base_url) { await patchExtensionState({ monitoring_last_cycle_at: new Date().toISOString(), monitoring_last_cycle_status: "not_configured", }); return { success: false, leased_task_count: 0, processed_task_count: 0, failed_task_count: 0, skipped_task_count: 0, message: "monitoring installation is not registered yet", }; } if (queryReadyPlatforms.length === 0) { await patchExtensionState({ monitoring_last_cycle_at: new Date().toISOString(), monitoring_last_cycle_status: "no_query_capable_platforms", }); return { success: heartbeatResult.success, leased_task_count: 0, processed_task_count: 0, failed_task_count: 0, skipped_task_count: 0, message: "monitoring adapters are not query-ready yet", }; } const queryReadyPlatformIDs = queryReadyPlatforms.map((platform) => platform.ai_platform_id); const platformMap = new Map(queryReadyPlatforms.map((platform) => [platform.ai_platform_id, platform] as const)); let leasedTaskCount = 0; let processedCount = 0; let failedCount = 0; let skippedCount = 0; let queue = await syncMonitoringTaskQueue(queryReadyPlatformIDs); if (mode === "manual" || queue.length === 0) { const leased = await leaseMonitoringTasks(queryReadyPlatformIDs); leasedTaskCount += leased.lease_count; if (leased.tasks.length > 0) { queue = await appendMonitoringTasksToQueue(leased.tasks); } } const immediateResult = await processImmediateMonitoringTasks(platformMap); processedCount += immediateResult.processedCount; failedCount += immediateResult.failedCount; skippedCount += immediateResult.skippedCount; if (mode === "automatic" && immediateResult.processedCount === 0) { const delayedResult = await processNextDelayedMonitoringTask(platformMap); processedCount += delayedResult.processedCount; failedCount += delayedResult.failedCount; skippedCount += delayedResult.skippedCount; } const remainingQueue = (await getExtensionState()).monitoring_task_queue; const cycleStatus = failedCount > 0 ? "partial_failed" : processedCount > 0 ? "success" : skippedCount > 0 ? "skipped" : remainingQueue.length > 0 ? "scheduled" : "idle"; await patchExtensionState({ monitoring_last_cycle_at: new Date().toISOString(), monitoring_last_cycle_status: cycleStatus, }); return { success: failedCount === 0, leased_task_count: leasedTaskCount, processed_task_count: processedCount, failed_task_count: failedCount, skipped_task_count: skippedCount, message: remainingQueue.length > 0 ? `scheduled ${remainingQueue.length} monitoring tasks` : null, }; } async function handleMonitoringGetState(): Promise { const state = await getExtensionState(); return { installation_key: state.installation_key, plugin_installation_id: state.plugin_installation_id, api_base_url: state.api_base_url, monitoring_last_heartbeat_at: state.monitoring_last_heartbeat_at, monitoring_last_heartbeat_status: state.monitoring_last_heartbeat_status, monitoring_last_cycle_at: state.monitoring_last_cycle_at, monitoring_last_cycle_status: state.monitoring_last_cycle_status, monitoring_platforms: state.monitoring_platforms, }; }