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:
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user