feat(monitoring): decouple AI platforms from media_platforms and expand catalog to 6

Add ai_platforms table + shared catalog (yuanbao/kimi/wenxin/deepseek/doubao/qwen),
drop FKs from platform_accounts and desktop_tasks to media_platforms, and wire
target_account_id + platform into DesktopTaskEvent so the desktop runtime no
longer has to infer them from local state. Desktop client gains generic AI page
detection for binding, drops stale-business-date monitor tasks at the edge, and
refactors AccountsView/AiPlatformsView around the shared catalog. Admin tracking
view surfaces per-platform sampling status cards.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-20 15:40:18 +08:00
parent 25dad49ed3
commit 09295d11a1
21 changed files with 2073 additions and 843 deletions
@@ -11,6 +11,7 @@ import type {
JsonValue,
LeaseDesktopTaskResponse,
} from "@geo/shared-types";
import { isAIPlatformId } from "@geo/shared-types";
import {
doubaoAdapter,
@@ -80,6 +81,7 @@ const heartbeatIntervalMs = 25_000;
const pullIntervalMs = 60_000;
const leaseExtendIntervalMs = 60_000;
const maxActivityItems = 60;
const monitorBusinessTimeZone = "Asia/Shanghai";
interface RuntimeTaskRecord {
id: string;
@@ -456,10 +458,10 @@ function handleTaskEvent(event: DesktopTaskEventMessage): void {
const next: RuntimeTaskRecord = {
id: event.task_id,
jobId: event.job_id,
title: existing?.title ?? defaultTaskTitle(event.kind, existing?.platform),
title: existing?.title ?? defaultTaskTitle(event.kind, event.platform || existing?.platform),
kind: event.kind,
platform: existing?.platform ?? inferPlatformFromAccount(existing?.accountId ?? null),
accountId: existing?.accountId ?? "",
platform: event.platform || existing?.platform || inferPlatformFromAccount(event.target_account_id),
accountId: event.target_account_id || existing?.accountId || "",
accountName: existing?.accountName ?? "待同步账号",
clientId: event.target_client_id,
status: event.status,
@@ -548,6 +550,7 @@ async function syncAccounts(source: "startup" | "manual"): Promise<void> {
state.accountProfiles.clear();
const syncedAccounts: Array<DesktopAccountInfo | null> = await Promise.all(accounts.map(async (account) => {
const normalizedPlatformUid = normalizeAccountPlatformUid(account.platform_uid) || account.platform_uid;
const isAIPlatform = isAIPlatformId(account.platform);
const identity = {
id: account.id,
platform: account.platform,
@@ -560,7 +563,18 @@ async function syncAccounts(source: "startup" | "manual"): Promise<void> {
return null;
}
if (local.profile && !sameAccountPlatformUid(local.profile.platformUid, normalizedPlatformUid)) {
if (local.profile) {
state.accountProfiles.set(account.id, local.profile);
}
const normalizedLocalPlatformUid = local.profile
? (normalizeAccountPlatformUid(local.profile.platformUid) || local.profile.platformUid)
: null;
const hasPlatformUidMismatch = Boolean(
normalizedLocalPlatformUid && !sameAccountPlatformUid(normalizedLocalPlatformUid, normalizedPlatformUid),
);
if (hasPlatformUidMismatch && !isAIPlatform) {
return {
...account,
platform_uid: normalizedPlatformUid,
@@ -571,6 +585,8 @@ async function syncAccounts(source: "startup" | "manual"): Promise<void> {
let resolvedAccount: DesktopAccountInfo = {
...account,
platform_uid: normalizedPlatformUid,
display_name: local.profile?.displayName ?? account.display_name,
avatar_url: local.profile?.avatarUrl ?? account.avatar_url,
health: local.availability === "expired" ? ("expired" as const) : ("live" as const),
};
@@ -967,6 +983,24 @@ async function executeTaskAdapter(
}
}
const staleBusinessDate = resolveStaleMonitoringBusinessDate(payload);
if (staleBusinessDate) {
return {
status: "unknown",
payload: {
...payload,
dropped_by_client: true,
dropped_reason: "stale_business_date",
},
error: {
code: "desktop_monitor_task_stale",
message: "monitor task is stale and has been dropped by desktop client",
business_date: staleBusinessDate,
},
summary: `${task.title} 已过业务日 ${staleBusinessDate},按漏采策略直接丢弃。`,
};
}
const adapter = selectMonitorAdapter(task.platform);
if (!adapter) {
return buildScaffoldResult(task, payload, "monitor adapter is not implemented yet");
@@ -1017,7 +1051,7 @@ function buildScaffoldResult(
message: "desktop runtime adapter is not implemented for this platform",
detail,
},
summary: `${task.title} 执行失败:当前平台的 desktop 发布适配器尚未实现。`,
summary: `${task.title} 执行失败:当前平台的 desktop ${task.kind === "publish" ? "发布" : "监测"}适配器尚未实现。`,
};
}
@@ -1062,6 +1096,50 @@ function toPositiveInt(value: JsonValue | null): number | null {
return null;
}
function resolveStaleMonitoringBusinessDate(payload: Record<string, JsonValue>): string | null {
const businessDate = resolveMonitoringBusinessDate(payload);
if (!businessDate) {
return null;
}
return businessDate < currentMonitoringBusinessDate() ? businessDate : null;
}
function resolveMonitoringBusinessDate(payload: Record<string, JsonValue>): string | null {
const candidates = [
payload.business_date,
payload.businessDate,
payload.metric_date,
payload.date,
];
for (const candidate of candidates) {
if (typeof candidate !== "string") {
continue;
}
const normalized = candidate.trim();
if (/^\d{4}-\d{2}-\d{2}$/.test(normalized)) {
return normalized;
}
}
return null;
}
function currentMonitoringBusinessDate(now = new Date()): string {
const parts = new Intl.DateTimeFormat("en-US", {
timeZone: monitorBusinessTimeZone,
year: "numeric",
month: "2-digit",
day: "2-digit",
}).formatToParts(now);
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 ?? "";
return `${year}-${month}-${day}`;
}
function updateTaskProgress(taskId: string, summary: string): void {
const existing = state.tasks.get(taskId);
if (!existing) {
@@ -1354,7 +1432,9 @@ function isDesktopTaskEvent(value: unknown): value is DesktopTaskEventMessage {
typeof event.type === "string"
&& typeof event.task_id === "string"
&& typeof event.job_id === "string"
&& typeof event.target_account_id === "string"
&& typeof event.target_client_id === "string"
&& typeof event.platform === "string"
&& typeof event.kind === "string"
&& typeof event.status === "string"
&& typeof event.updated_at === "string",