6066f43a7d
- Implement monitoring service with heartbeat, lease tasks, resume tasks, and task result handling. - Create monitoring time utilities for business date calculations. - Add unit tests for date window resolution and business day handling. - Define database schema for monitoring-related tables including quotas, daily reports, and task management. - Establish migration scripts for creating and dropping monitoring tables.
256 lines
7.5 KiB
TypeScript
256 lines
7.5 KiB
TypeScript
import type { MonitoringCycleKickoffResponse } from "@geo/shared-types";
|
|
import { browser } from "wxt/browser";
|
|
import { defineBackground } from "wxt/utils/define-background";
|
|
|
|
import {
|
|
patchDoubaoMonitoringRuntimeState,
|
|
readDoubaoMonitoringRuntimePatchFromRequestUrl,
|
|
} from "../src/monitoring-adapters/doubao-state";
|
|
import { handleMonitoringAction, runMonitoringCycle, type MonitoringCycleMode } from "../src/monitoring-runtime";
|
|
import { handlePublisherAction } from "../src/runtime";
|
|
import { getExtensionState, type ExtensionStorageState } from "../src/storage";
|
|
|
|
type BackgroundSuccessResponse = {
|
|
ok: true;
|
|
data: unknown;
|
|
};
|
|
|
|
type BackgroundErrorResponse = {
|
|
ok: false;
|
|
error: string;
|
|
};
|
|
|
|
const PUBLISHER_HEARTBEAT_ALARM = "geo.publisher.background-heartbeat";
|
|
const MONITORING_HEARTBEAT_ALARM = "geo.monitoring.background-heartbeat";
|
|
const MONITORING_QUEUE_ALARM = "geo.monitoring.queue-heartbeat";
|
|
const MONITORING_WARMUP_INTERVAL_MS = 10 * 60 * 1000;
|
|
const MONITORING_HEARTBEAT_INTERVAL_MINUTES = 15;
|
|
const MONITORING_HEARTBEAT_START_JITTER_MS = 3 * 60 * 1000;
|
|
const DOUBAO_COMPLETION_REQUEST_FILTER = {
|
|
urls: ["https://www.doubao.com/chat/completion*", "https://doubao.com/chat/completion*"],
|
|
};
|
|
|
|
let monitoringCycleInFlight: Promise<unknown> | null = null;
|
|
|
|
function randomMonitoringHeartbeatStartDelayMs(): number {
|
|
return Math.floor(Math.random() * (MONITORING_HEARTBEAT_START_JITTER_MS + 1));
|
|
}
|
|
|
|
async function ensureBackgroundAlarms(): Promise<void> {
|
|
browser.alarms.create(PUBLISHER_HEARTBEAT_ALARM, {
|
|
periodInMinutes: 60,
|
|
});
|
|
browser.alarms.create(MONITORING_HEARTBEAT_ALARM, {
|
|
when: Date.now() + randomMonitoringHeartbeatStartDelayMs(),
|
|
periodInMinutes: MONITORING_HEARTBEAT_INTERVAL_MINUTES,
|
|
});
|
|
}
|
|
|
|
function shouldWarmupMonitoring(state: ExtensionStorageState): boolean {
|
|
if (!state.plugin_installation_id || !state.installation_token || !state.api_base_url) {
|
|
return false;
|
|
}
|
|
|
|
if (!state.monitoring_last_heartbeat_at) {
|
|
return true;
|
|
}
|
|
|
|
const lastHeartbeatAt = new Date(state.monitoring_last_heartbeat_at).getTime();
|
|
if (Number.isNaN(lastHeartbeatAt)) {
|
|
return true;
|
|
}
|
|
|
|
return Date.now() - lastHeartbeatAt >= MONITORING_WARMUP_INTERVAL_MS;
|
|
}
|
|
|
|
function registerMonitoringRuntimeCapture(): void {
|
|
if (!browser.webRequest?.onBeforeRequest) {
|
|
return;
|
|
}
|
|
|
|
browser.webRequest.onBeforeRequest.addListener(
|
|
(details) => {
|
|
const patch = readDoubaoMonitoringRuntimePatchFromRequestUrl(details.url);
|
|
if (!patch) {
|
|
return undefined;
|
|
}
|
|
void patchDoubaoMonitoringRuntimeState(patch).catch((error) => {
|
|
console.warn("[geo-monitoring] failed to persist doubao runtime patch", error);
|
|
});
|
|
return undefined;
|
|
},
|
|
DOUBAO_COMPLETION_REQUEST_FILTER,
|
|
);
|
|
}
|
|
|
|
async function warmupMonitoringRuntime(): Promise<void> {
|
|
const state = await getExtensionState();
|
|
if (!shouldWarmupMonitoring(state)) {
|
|
return;
|
|
}
|
|
|
|
await runMonitoringCycleSingleFlight("automatic");
|
|
}
|
|
|
|
async function syncMonitoringQueueAlarm(): Promise<void> {
|
|
const state = await getExtensionState();
|
|
const nextExecuteAt = state.monitoring_task_queue.reduce<number | null>((earliest, task) => {
|
|
const timestamp = new Date(task.next_execute_at).getTime();
|
|
if (Number.isNaN(timestamp)) {
|
|
return earliest;
|
|
}
|
|
if (earliest == null) {
|
|
return timestamp;
|
|
}
|
|
return Math.min(earliest, timestamp);
|
|
}, null);
|
|
|
|
await browser.alarms.clear(MONITORING_QUEUE_ALARM);
|
|
if (nextExecuteAt == null) {
|
|
return;
|
|
}
|
|
|
|
browser.alarms.create(MONITORING_QUEUE_ALARM, {
|
|
when: Math.max(Date.now() + 1000, nextExecuteAt),
|
|
});
|
|
}
|
|
|
|
function runMonitoringCycleSingleFlight(mode: MonitoringCycleMode): Promise<unknown> {
|
|
if (monitoringCycleInFlight) {
|
|
return monitoringCycleInFlight;
|
|
}
|
|
|
|
monitoringCycleInFlight = runMonitoringCycle(mode).finally(() => {
|
|
monitoringCycleInFlight = null;
|
|
void syncMonitoringQueueAlarm().catch((error) => {
|
|
console.warn("[geo-monitoring] failed to sync queue alarm", error);
|
|
});
|
|
});
|
|
return monitoringCycleInFlight;
|
|
}
|
|
|
|
function kickoffMonitoringCycle(): MonitoringCycleKickoffResponse {
|
|
if (monitoringCycleInFlight) {
|
|
return {
|
|
accepted: true,
|
|
status: "already_running",
|
|
message: "monitoring cycle is already running in background",
|
|
};
|
|
}
|
|
|
|
void runMonitoringCycleSingleFlight("manual").catch((error) => {
|
|
console.warn("[geo-monitoring] background cycle failed", error);
|
|
});
|
|
|
|
return {
|
|
accepted: true,
|
|
status: "started",
|
|
message: "monitoring cycle started in background",
|
|
};
|
|
}
|
|
|
|
export default defineBackground(() => {
|
|
void getExtensionState();
|
|
void ensureBackgroundAlarms();
|
|
registerMonitoringRuntimeCapture();
|
|
void warmupMonitoringRuntime().catch((error) => {
|
|
console.warn("[geo-monitoring] warmup cycle failed", error);
|
|
});
|
|
void syncMonitoringQueueAlarm();
|
|
|
|
browser.runtime.onInstalled.addListener(() => {
|
|
void getExtensionState();
|
|
void ensureBackgroundAlarms();
|
|
void warmupMonitoringRuntime().catch((error) => {
|
|
console.warn("[geo-monitoring] install warmup cycle failed", error);
|
|
});
|
|
void syncMonitoringQueueAlarm();
|
|
});
|
|
|
|
browser.runtime.onStartup.addListener(() => {
|
|
void getExtensionState();
|
|
void ensureBackgroundAlarms();
|
|
void warmupMonitoringRuntime().catch((error) => {
|
|
console.warn("[geo-monitoring] startup warmup cycle failed", error);
|
|
});
|
|
void syncMonitoringQueueAlarm();
|
|
});
|
|
|
|
browser.alarms.onAlarm.addListener(async (alarm) => {
|
|
if (alarm.name === PUBLISHER_HEARTBEAT_ALARM) {
|
|
await getExtensionState();
|
|
return;
|
|
}
|
|
|
|
if (alarm.name === MONITORING_HEARTBEAT_ALARM) {
|
|
try {
|
|
await runMonitoringCycleSingleFlight("automatic");
|
|
} catch (error) {
|
|
console.warn("[geo-monitoring] cycle failed", error);
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (alarm.name === MONITORING_QUEUE_ALARM) {
|
|
try {
|
|
await runMonitoringCycleSingleFlight("automatic");
|
|
} catch (error) {
|
|
console.warn("[geo-monitoring] queue cycle failed", error);
|
|
}
|
|
}
|
|
});
|
|
|
|
chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
|
|
if (!message) {
|
|
return undefined;
|
|
}
|
|
|
|
if (message.type === "geo.publisher.request") {
|
|
void handlePublisherAction(message.action, message.payload)
|
|
.then((data) => {
|
|
sendResponse({
|
|
ok: true,
|
|
data,
|
|
} satisfies BackgroundSuccessResponse);
|
|
})
|
|
.catch((error: unknown) => {
|
|
sendResponse({
|
|
ok: false,
|
|
error: error instanceof Error ? error.message : "publisher_plugin_unknown_error",
|
|
} satisfies BackgroundErrorResponse);
|
|
});
|
|
|
|
return true;
|
|
}
|
|
|
|
if (message.type === "geo.monitoring.request") {
|
|
if (message.action === "kickoffCycle") {
|
|
sendResponse({
|
|
ok: true,
|
|
data: kickoffMonitoringCycle(),
|
|
} satisfies BackgroundSuccessResponse);
|
|
return true;
|
|
}
|
|
|
|
const action = message.action === "runCycle" ? runMonitoringCycleSingleFlight("manual") : handleMonitoringAction(message.action);
|
|
void action
|
|
.then((data) => {
|
|
sendResponse({
|
|
ok: true,
|
|
data,
|
|
} satisfies BackgroundSuccessResponse);
|
|
})
|
|
.catch((error: unknown) => {
|
|
sendResponse({
|
|
ok: false,
|
|
error: error instanceof Error ? error.message : "monitoring_plugin_unknown_error",
|
|
} satisfies BackgroundErrorResponse);
|
|
});
|
|
|
|
return true;
|
|
}
|
|
|
|
return undefined;
|
|
});
|
|
});
|