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
+541 -25
View File
@@ -4,6 +4,7 @@ import { join } from "node:path";
import { BrowserWindow, app, nativeTheme, session as electronSession } from "electron/main";
import type { Session, WebContents } from "electron/main";
import { aiPlatformCatalog } from "@geo/shared-types";
import type { DesktopAccountInfo } from "@geo/shared-types";
import {
@@ -679,6 +680,423 @@ function sanitizeDetectedAccount(account: DetectedAccount): DetectedAccount {
};
}
interface GenericAIPageState {
displayName: string | null;
avatarUrl: string | null;
fingerprint: string | null;
credentialFingerprint: string | null;
currentPath: string | null;
strongAuthSignalCount: number;
loginSignalCount: number;
loggedOutSignalCount: number;
}
async function readGenericAIPageState(
webContents: WebContents | null | undefined,
): Promise<GenericAIPageState | null> {
if (!webContents || webContents.isDestroyed()) {
return null;
}
const state = await webContents.executeJavaScript(
`(() => {
try {
const normalize = (value) => {
if (typeof value !== "string") return null;
const trimmed = value.trim().replace(/\\s+/g, " ");
return trimmed || null;
};
const isVisible = (node) => {
if (!(node instanceof Element)) {
return false;
}
const style = window.getComputedStyle(node);
if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0') {
return false;
}
const rect = node.getBoundingClientRect();
return rect.width > 0 && rect.height > 0;
};
const bodyText = normalize(document.body?.innerText || "");
const stopWords = /^(登录|注册|设置|帮助|历史|聊天|新建|菜单|账户|账号|个人中心|联网搜索|深度思考|工具|更多|安装电脑版|下载电脑版|DeepSeek|元宝|混元|豆包|Kimi|Qwen|通义千问)$/i;
const loginPattern = /(登录|注册|立即登录|扫码登录|微信登录|QQ登录|手机号登录|请先登录)/i;
const loggedOutPattern = /(未登录|扫码登录|快捷登录|使用其他头像、昵称或账号|登录后继续|请先登录|立即登录|微信快捷登录)/i;
const credentialKeyPattern = /(token|session|auth|login|passport|ticket|jwt|refresh|access)/i;
const isInsideAuthSurface = (node) => {
if (!(node instanceof Element)) {
return false;
}
let current = node;
for (let depth = 0; current && depth < 8; depth += 1) {
const descriptor = [
current.getAttribute('role') || '',
current.getAttribute('aria-modal') || '',
current.getAttribute('data-testid') || '',
current.id || '',
current.className || '',
].join(' ');
if (/(dialog|modal|popup|popover|sheet|drawer|login|signin|auth|scan|qrcode|qrlogin)/i.test(descriptor)) {
return true;
}
const text = normalize(current.textContent || '');
if (text && text.length <= 120 && loggedOutPattern.test(text)) {
return true;
}
current = current.parentElement;
}
return false;
};
const pickGreetingName = () => {
if (!bodyText) {
return null;
}
const patterns = [
/(?:^|\\n)\\s*(?:hi|hello|你好|嗨)[,\\s]+([^\\n,]{1,20})/i,
/(?:^|\\n)\\s*欢迎回来[,\\s]*([^\\n]{1,20})/i,
];
for (const pattern of patterns) {
const matched = bodyText.match(pattern);
const candidate = normalize(matched?.[1] || "");
if (candidate && !stopWords.test(candidate)) {
return candidate;
}
}
return null;
};
const pickDisplayName = () => {
const selectors = [
'[data-testid*="user"]',
'[data-testid*="profile"]',
'[class*="user-name"]',
'[class*="username"]',
'[class*="nickname"]',
'[class*="profile"] [class*="name"]',
'[class*="avatar"] + *',
'[class*="account"] [class*="name"]',
'aside [class*="name"]',
'nav [class*="name"]',
];
for (const selector of selectors) {
const nodes = document.querySelectorAll(selector);
for (const node of nodes) {
if (!isVisible(node)) continue;
if (isInsideAuthSurface(node)) continue;
const text = normalize(node.textContent || "");
if (!text) continue;
if (text.length < 2 || text.length > 48) continue;
if (stopWords.test(text)) continue;
return text;
}
}
return null;
};
const pickAvatar = () => {
const selectors = [
'[class*="avatar"] img',
'[data-testid*="avatar"] img',
'[class*="profile"] img',
'[class*="account"] img',
'img[alt*="头像"]',
];
for (const selector of selectors) {
const node = document.querySelector(selector);
if (!node || !isVisible(node)) continue;
if (isInsideAuthSurface(node)) continue;
const src = normalize(node && 'src' in node ? node.src : null);
if (src && /(logo|icon|favicon)/i.test(src)) continue;
if (src) return src;
}
return null;
};
const collectLoginSignals = () => {
const matches = [];
if (bodyText && loginPattern.test(bodyText)) {
matches.push('body-text');
}
const selectors = ['button', 'a', '[role="button"]', '[aria-haspopup="dialog"]'];
for (const selector of selectors) {
const nodes = document.querySelectorAll(selector);
for (const node of nodes) {
if (!isVisible(node)) continue;
const text = normalize(node.textContent || "");
if (!text || !loginPattern.test(text)) continue;
matches.push(text);
if (matches.length >= 4) {
return matches;
}
}
}
return matches;
};
const collectLoggedOutSignals = () => {
const matches = [];
if (bodyText && loggedOutPattern.test(bodyText)) {
matches.push('body-text');
}
const selectors = ['button', 'a', '[role="button"]', '[aria-haspopup="dialog"]', '[role="dialog"]'];
for (const selector of selectors) {
const nodes = document.querySelectorAll(selector);
for (const node of nodes) {
if (!isVisible(node)) continue;
const text = normalize(node.textContent || "");
if (!text || !loggedOutPattern.test(text)) continue;
matches.push(text);
if (matches.length >= 4) {
return matches;
}
}
}
return matches;
};
const collectCredentialStorage = (storage, prefix) => {
const items = [];
try {
const limit = Math.min(storage.length, 40);
for (let index = 0; index < limit; index += 1) {
const key = storage.key(index);
if (!key || !credentialKeyPattern.test(key)) {
continue;
}
const value = storage.getItem(key);
const text = String(value || '').slice(0, 240).trim();
if (!text) {
continue;
}
items.push(prefix + ':' + key + '=' + text);
}
} catch {
return items;
}
return items;
};
const collectStorage = (storage, prefix) => {
const items = [];
try {
const limit = Math.min(storage.length, 40);
for (let index = 0; index < limit; index += 1) {
const key = storage.key(index);
if (!key || !/(user|uid|token|session|auth|account|profile|login|member|nick|name)/i.test(key)) {
continue;
}
const value = storage.getItem(key);
items.push(prefix + ':' + key + '=' + String(value || '').slice(0, 160));
}
} catch {
return items;
}
return items;
};
const displayName = pickGreetingName() || pickDisplayName();
const avatarUrl = pickAvatar();
const strongSignals = [];
if (displayName) {
strongSignals.push('displayName');
}
if (avatarUrl) {
strongSignals.push('avatar');
}
if (bodyText && /(?:^|\\n)\\s*(?:hi|hello|你好|嗨)[,\\s]+/i.test(bodyText)) {
strongSignals.push('greeting');
}
const loginSignals = collectLoginSignals();
const loggedOutSignals = collectLoggedOutSignals();
const credentialFingerprintParts = [
...collectCredentialStorage(window.localStorage, 'local'),
...collectCredentialStorage(window.sessionStorage, 'session'),
];
const fingerprintParts = [
document.cookie || '',
window.location.origin || '',
document.title || '',
...collectStorage(window.localStorage, 'local'),
...collectStorage(window.sessionStorage, 'session'),
].filter(Boolean);
return {
displayName,
avatarUrl,
fingerprint: fingerprintParts.length ? fingerprintParts.join('||') : null,
credentialFingerprint: credentialFingerprintParts.length
? credentialFingerprintParts.join('||')
: null,
currentPath: normalize(window.location.pathname || ''),
strongAuthSignalCount: strongSignals.length,
loginSignalCount: loginSignals.length,
loggedOutSignalCount: loggedOutSignals.length,
};
} catch {
return null;
}
})();`,
true,
).catch(() => null);
if (!state || typeof state !== "object") {
return null;
}
const typed = state as Partial<GenericAIPageState>;
return {
displayName: normalizeText(typed.displayName),
avatarUrl: normalizeRemoteUrl(typed.avatarUrl),
fingerprint: normalizeText(typed.fingerprint),
credentialFingerprint: normalizeText(typed.credentialFingerprint),
currentPath: normalizeText(typed.currentPath),
strongAuthSignalCount:
typeof typed.strongAuthSignalCount === "number" && Number.isFinite(typed.strongAuthSignalCount)
? typed.strongAuthSignalCount
: 0,
loginSignalCount:
typeof typed.loginSignalCount === "number" && Number.isFinite(typed.loginSignalCount)
? typed.loginSignalCount
: 0,
loggedOutSignalCount:
typeof typed.loggedOutSignalCount === "number" && Number.isFinite(typed.loggedOutSignalCount)
? typed.loggedOutSignalCount
: 0,
};
}
function isAIPlatformBinding(platformId: string): boolean {
return aiPlatformCatalog.some((item) => item.id === platformId);
}
function normalizeURLPath(pathname: string): string {
const normalized = pathname.replace(/\/+$/, "");
return normalized === "/" ? "" : normalized;
}
function safeParseURL(input: string): URL | null {
try {
return new URL(input);
} catch {
return null;
}
}
function genericAIConversationPath(pathname: string): boolean {
return /^\/(?:chat|c|conversation)(?:\/|$)/i.test(pathname);
}
function isLikelyGenericAICredentialName(name: string): boolean {
return /(token|session|auth|login|passport|ticket|jwt|refresh|access)/i.test(name);
}
function isLikelyGenericAICredentialValue(value: string | null | undefined): boolean {
const normalized = normalizeText(value);
if (!normalized) {
return false;
}
if (/^(true|false|null|undefined|0|1)$/i.test(normalized)) {
return false;
}
return normalized.length >= 8;
}
function genericAIPageLooksAuthenticated(currentURL: string, pageState: GenericAIPageState | null): boolean {
const current = safeParseURL(currentURL);
const currentPath = normalizeURLPath(current?.pathname ?? pageState?.currentPath ?? "");
const strongAuthSignalCount = pageState?.strongAuthSignalCount ?? 0;
const loginSignalCount = pageState?.loginSignalCount ?? 0;
const loggedOutSignalCount = pageState?.loggedOutSignalCount ?? 0;
const onConversationPath = genericAIConversationPath(currentPath);
if (loggedOutSignalCount > 0) {
return false;
}
if (loginSignalCount > 0 && strongAuthSignalCount === 0 && !onConversationPath) {
return false;
}
if (loginSignalCount > 0) {
return strongAuthSignalCount >= 2 && !onConversationPath;
}
return strongAuthSignalCount > 0 || onConversationPath;
}
async function buildGenericAISessionFingerprint(
platformId: string,
session: Session,
pageState?: GenericAIPageState | null,
): Promise<string | null> {
const parts: string[] = [];
if (pageState?.credentialFingerprint) {
parts.push(pageState.credentialFingerprint);
}
const platformMeta = aiPlatformCatalog.find((item) => item.id === platformId) ?? null;
if (platformMeta) {
const urls = new Set([platformMeta.loginUrl, platformMeta.consoleUrl]);
for (const url of urls) {
try {
const cookies = await session.cookies.get({ url });
const authCookies = cookies.filter((cookie) => {
return isLikelyGenericAICredentialName(cookie.name)
&& isLikelyGenericAICredentialValue(cookie.value);
});
if (authCookies.length > 0) {
const serialized = authCookies
.map((cookie) => `${cookie.domain}|${cookie.name}=${cookie.value}`)
.sort()
.join(";");
parts.push(`${url}:${serialized}`);
}
} catch {
continue;
}
}
}
if (parts.length === 0) {
return null;
}
return boundedIdentity(`${platformId}-session`, parts.join("||"));
}
async function detectGenericAIPlatform(
platformId: string,
label: string,
context: DetectContext,
): Promise<DetectedAccount | null> {
if (!context.webContents || context.webContents.isDestroyed()) {
return null;
}
const currentURL = context.webContents && !context.webContents.isDestroyed()
? context.webContents.getURL()
: "";
const platformMeta = aiPlatformCatalog.find((item) => item.id === platformId) ?? null;
if (!platformMeta) {
return null;
}
if (currentURL && looksLikeLoginRedirect(currentURL, platformMeta.loginUrl)) {
return null;
}
const pageState = await readGenericAIPageState(context.webContents).catch(() => null);
if (!genericAIPageLooksAuthenticated(currentURL, pageState)) {
return null;
}
const fingerprint = await buildGenericAISessionFingerprint(platformId, context.session, pageState);
if (!fingerprint) {
return null;
}
return sanitizeDetectedAccount({
platformUid: fingerprint,
displayName: pageState?.displayName ?? `${label} 会话`,
avatarUrl: pageState?.avatarUrl ?? null,
});
}
async function detectToutiao({ session, webContents }: DetectContext): Promise<DetectedAccount | null> {
const pageResponse = await pageFetchJson<ToutiaoMediaInfoResponse>(
webContents,
@@ -875,11 +1293,37 @@ async function resolvePublishAccountProfileFromHandle(
return await detectToutiaoFromSession(handle.session);
}
const definition = publishBindingDefinitions[account.platform];
const definition = platformBindingDefinitions[account.platform];
if (!definition) {
return null;
}
if (isAIPlatformBinding(account.platform)) {
const window = createBoundWindow(
`${definition.label} 静默识别`,
definition.consoleUrl,
handle.session,
{ show: false },
);
try {
await waitForWindowNavigationSettled(window);
if (window.isDestroyed()) {
return null;
}
const detected = await definition.detect({
session: handle.session,
webContents: window.webContents,
}).catch(() => null);
return toPublishAccountProfile(detected);
} finally {
if (!window.isDestroyed()) {
window.destroy();
}
}
}
const detected = await definition.detect({
session: handle.session,
webContents: undefined as unknown as WebContents,
@@ -994,6 +1438,30 @@ async function waitForWindowNavigationSettled(window: BrowserWindow, timeoutMs =
}
}
async function flushSessionPersistence(target: Session): Promise<void> {
const maybeFlushStorageData = (target as Session & {
flushStorageData?: () => Promise<void>;
}).flushStorageData;
if (typeof maybeFlushStorageData === "function") {
try {
await maybeFlushStorageData.call(target);
} catch (error) {
console.warn("[desktop-session] flushStorageData failed", {
message: error instanceof Error ? error.message : String(error),
});
}
}
try {
await target.cookies.flushStore();
} catch (error) {
console.warn("[desktop-session] flushStore failed", {
message: error instanceof Error ? error.message : String(error),
});
}
}
async function verifyToutiaoConsoleAccess(window: BrowserWindow, session: Session): Promise<boolean> {
if (window.isDestroyed()) {
return false;
@@ -1046,10 +1514,6 @@ function looksLikeLoginRedirect(currentURL: string, loginURL: string): boolean {
return false;
}
if (currentURL.startsWith(loginURL)) {
return true;
}
try {
const current = new URL(currentURL);
const login = new URL(loginURL);
@@ -1057,8 +1521,13 @@ function looksLikeLoginRedirect(currentURL: string, loginURL: string): boolean {
return false;
}
const currentPath = current.pathname.replace(/\/+$/, "");
const loginPath = login.pathname.replace(/\/+$/, "");
const currentPath = normalizeURLPath(current.pathname);
const loginPath = normalizeURLPath(login.pathname);
if (!loginPath) {
return /(^|[/?#=&._-])(login|signin|sign-in|signup|sign-up|register|auth|oauth|passport|scan|qrcode|qrlogin|wxlogin|sso)(?=$|[/?#=&._-])/i
.test(`${current.pathname}${current.search}${current.hash}`);
}
return currentPath === loginPath || currentPath.startsWith(`${loginPath}/`);
} catch {
return false;
@@ -1069,7 +1538,7 @@ async function verifyPublishAccountConsoleAccess(
account: PublishAccountIdentity,
handle: SessionHandle,
): Promise<boolean> {
const definition = publishBindingDefinitions[account.platform];
const definition = platformBindingDefinitions[account.platform];
if (!definition) {
return false;
}
@@ -1096,6 +1565,14 @@ async function verifyPublishAccountConsoleAccess(
return false;
}
if (isAIPlatformBinding(account.platform)) {
const detected = await definition.detect({
session: handle.session,
webContents: window.webContents,
}).catch(() => null);
return detected !== null;
}
return !looksLikeLoginRedirect(currentURL, definition.loginUrl);
} finally {
if (!window.isDestroyed()) {
@@ -1470,6 +1947,24 @@ const publishBindingDefinitions: Record<string, PublishPlatformBindingDefinition
},
};
const aiBindingDefinitions: Record<string, PublishPlatformBindingDefinition> = Object.fromEntries(
aiPlatformCatalog.map((platform) => [
platform.id,
{
id: platform.id,
label: platform.label,
loginUrl: platform.loginUrl,
consoleUrl: platform.consoleUrl,
detect: (context: DetectContext) => detectGenericAIPlatform(platform.id, platform.label, context),
},
]),
) as Record<string, PublishPlatformBindingDefinition>;
const platformBindingDefinitions: Record<string, PublishPlatformBindingDefinition> = {
...publishBindingDefinitions,
...aiBindingDefinitions,
};
function createBoundWindow(
title: string,
targetURL: string,
@@ -1542,9 +2037,9 @@ function clearActiveBindOperation(platformId: string, window: BrowserWindow): vo
}
export async function bindPublishAccount(platformId: string): Promise<DesktopAccountInfo> {
const definition = publishBindingDefinitions[platformId];
const definition = platformBindingDefinitions[platformId];
if (!definition) {
throw new Error(`desktop_publish_platform_not_supported:${platformId}`);
throw new Error(`desktop_platform_not_supported:${platformId}`);
}
cleanupInactiveBindOperations();
@@ -1615,7 +2110,12 @@ export async function bindPublishAccount(platformId: string): Promise<DesktopAcc
detectReady = false;
return;
}
detectReady = isWindowReadyForDetection(window);
const currentURL = window.webContents.getURL();
const fallbackReady =
/^https?:\/\//i.test(currentURL)
&& !currentURL.startsWith("data:text/html")
&& !window.webContents.isLoading();
detectReady = isWindowReadyForDetection(window) || fallbackReady;
};
window.webContents.on("did-finish-load", syncDetectionState);
@@ -1641,6 +2141,8 @@ export async function bindPublishAccount(platformId: string): Promise<DesktopAcc
console.info("[desktop-bind] detect tick", {
platform: definition.id,
detectReady,
canProbe,
loading: window.webContents.isLoading(),
url: currentURL,
});
const detected = await definition
@@ -1687,20 +2189,34 @@ export async function bindPublishAccount(platformId: string): Promise<DesktopAcc
normalizedDetected,
);
const account = await upsertDesktopAccount({
platform: definition.id,
platform_uid: normalizedDetected.platformUid,
display_name: normalizedDetected.displayName,
avatar_url: normalizedDetected.avatarUrl,
account_fingerprint: normalizedDetected.platformUid,
health: "live",
verified_at: new Date().toISOString(),
tags: [],
});
await flushSessionPersistence(handle.session);
settleSuccess(account);
try {
const account = await upsertDesktopAccount({
platform: definition.id,
platform_uid: normalizedDetected.platformUid,
display_name: normalizedDetected.displayName,
avatar_url: normalizedDetected.avatarUrl,
account_fingerprint: normalizedDetected.platformUid,
health: "live",
verified_at: new Date().toISOString(),
tags: [],
});
settleSuccess(account);
} catch (error) {
console.error("[desktop-bind] upsert failed", {
platform: definition.id,
url: currentURL,
message: error instanceof Error ? error.message : String(error),
});
if (!window.isDestroyed()) {
window.setTitle(`${definition.label} 绑定保存失败,保留窗口重试`);
}
return;
}
} catch (error) {
console.error("[desktop-bind] upsert failed", {
console.error("[desktop-bind] bind failed", {
platform: definition.id,
url: currentURL,
message: error instanceof Error ? error.message : String(error),
@@ -1739,9 +2255,9 @@ export async function bindPublishAccount(platformId: string): Promise<DesktopAcc
}
export async function openPublishAccountConsole(account: PublishAccountIdentity): Promise<void> {
const definition = publishBindingDefinitions[account.platform];
const definition = platformBindingDefinitions[account.platform];
if (!definition) {
throw new Error(`desktop_publish_platform_not_supported:${account.platform}`);
throw new Error(`desktop_platform_not_supported:${account.platform}`);
}
const handle = await ensurePublishAccountSessionHandle(account);
@@ -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",