import { resolveXhsPublicAccountDetails, resolveXhsAccountProfileFromMcp, resolveXhsProfileDetailsFromMcp, type CollectionMcpConfig, } from "./mcp-collection-client"; import { hashText } from "./mvp-db"; import type { DatabaseClient } from "./database"; type DistributionAccountRow = { id: string; account_id: string | null; publish_url: string | null; }; type BackfillRow = DistributionAccountRow & { nickname: string | null; platform: string | null; platform_uid: string | null; public_account_id: string | null; profile_url: string | null; followers: number | null; }; function isVerifiedXhsProfileUrl(value: string | null) { if (!value) return false; try { const url = new URL(value); return ( url.protocol === "https:" && (url.hostname === "xiaohongshu.com" || url.hostname.endsWith(".xiaohongshu.com")) && url.pathname.startsWith("/user/profile/") ); } catch { return false; } } export async function enrichDistributionAccount( db: DatabaseClient, distributionId: string, publishUrl: string, fallbackNickname: string, mcpConfig: CollectionMcpConfig, ) { const profile = await resolveXhsAccountProfileFromMcp( publishUrl, fallbackNickname, mcpConfig, ); const current = await db .prepare( `SELECT id, account_id, publish_url FROM distributions WHERE id = ?`, ) .bind(distributionId) .first(); if (!current || current.publish_url !== publishUrl) { return { updated: false, reason: "stale" as const }; } const canonicalAccountId = `account-${hashText( `小红书:${profile.platformUid}`, )}`; if (current.account_id === canonicalAccountId) { await db .prepare( `UPDATE accounts SET nickname = ?, profile_url = ?, public_account_id = CASE WHEN ? != '' THEN ? ELSE public_account_id END, ip_location = CASE WHEN ? != '' AND ? != '待识别' THEN ? ELSE ip_location END, followers = CASE WHEN ? IS NOT NULL AND (? > 0 OR followers = 0) THEN ? ELSE followers END, last_seen_at = CURRENT_TIMESTAMP WHERE id = ?`, ) .bind( profile.nickname || fallbackNickname, profile.profileUrl, profile.redId, profile.redId, profile.ipLocation, profile.ipLocation, profile.ipLocation, profile.followers, profile.followers, profile.followers, canonicalAccountId, ) .run(); return { updated: true, accountId: canonicalAccountId, profileUrl: profile.profileUrl, }; } const provisionalAccountId = current.account_id; await db.batch([ db .prepare( `INSERT INTO accounts (id, platform, platform_uid, public_account_id, nickname, profile_url, ip_location, followers, post_count) VALUES (?, '小红书', ?, ?, ?, ?, ?, ?, 1) ON CONFLICT(platform, platform_uid) DO UPDATE SET public_account_id = CASE WHEN excluded.public_account_id != '' THEN excluded.public_account_id ELSE accounts.public_account_id END, nickname = excluded.nickname, profile_url = excluded.profile_url, ip_location = CASE WHEN excluded.ip_location != '' AND excluded.ip_location != '待识别' THEN excluded.ip_location ELSE accounts.ip_location END, followers = CASE WHEN excluded.followers > 0 OR accounts.followers = 0 THEN excluded.followers ELSE accounts.followers END, post_count = accounts.post_count + 1, last_seen_at = CURRENT_TIMESTAMP`, ) .bind( canonicalAccountId, profile.platformUid, profile.redId, profile.nickname || fallbackNickname, profile.profileUrl, profile.ipLocation, profile.followers ?? 0, ), db .prepare( `UPDATE distributions SET account_id = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ? AND publish_url = ?`, ) .bind(canonicalAccountId, distributionId, publishUrl), ]); if (provisionalAccountId) { await db .prepare( `DELETE FROM accounts WHERE id = ? AND id != ? AND NOT EXISTS ( SELECT 1 FROM distributions WHERE account_id = ? )`, ) .bind( provisionalAccountId, canonicalAccountId, provisionalAccountId, ) .run(); } return { updated: true, accountId: canonicalAccountId, profileUrl: profile.profileUrl, }; } export async function backfillAccountProfiles( db: DatabaseClient, mcpConfig: CollectionMcpConfig, limit = 10, ) { const rows = await db .prepare( `SELECT d.id, d.account_id, d.publish_url, a.nickname, a.platform, a.platform_uid, a.public_account_id, a.profile_url, a.followers FROM distributions d LEFT JOIN accounts a ON a.id = d.account_id WHERE d.publish_url IS NOT NULL AND d.publish_url != '' ORDER BY d.updated_at DESC LIMIT 100`, ) .all(); let attempted = 0; let updated = 0; let failed = 0; for (const row of rows.results) { if (attempted >= Math.max(1, Math.min(25, limit))) break; const noteId = (() => { try { const url = new URL(row.publish_url ?? ""); return ( url.pathname.match( /\/(?:discovery\/item|explore)\/([A-Za-z0-9_-]{8,80})/, )?.[1] ?? "" ); } catch { return ""; } })(); const isDemoAccount = row.platform_uid?.startsWith("xhs-"); let attemptedThisRow = false; let publicProfileUpdated = false; if ( row.platform === "小红书" && !isDemoAccount && isVerifiedXhsProfileUrl(row.profile_url) && (!row.public_account_id || Number(row.followers ?? 0) === 0) ) { attempted += 1; attemptedThisRow = true; const details = await resolveXhsProfileDetailsFromMcp( row.profile_url ?? "", mcpConfig, ).catch(() => resolveXhsPublicAccountDetails(row.profile_url ?? ""), ); if ( row.account_id && (details.redId || details.followers !== null) ) { await db .prepare( `UPDATE accounts SET public_account_id = CASE WHEN ? != '' THEN ? ELSE public_account_id END, followers = CASE WHEN ? IS NOT NULL AND (? > 0 OR followers = 0) THEN ? ELSE followers END, ip_location = CASE WHEN ? != '' AND ? != '待识别' THEN ? ELSE ip_location END, last_seen_at = CURRENT_TIMESTAMP WHERE id = ?`, ) .bind( details.redId, details.redId, details.followers, details.followers, details.followers, details.ipLocation ?? "", details.ipLocation ?? "", details.ipLocation ?? "", row.account_id, ) .run(); publicProfileUpdated = true; } if ( (details.redId || row.public_account_id) && Number(details.followers ?? row.followers ?? 0) > 0 ) { updated += 1; continue; } if (attempted >= Math.max(1, Math.min(25, limit))) { if (publicProfileUpdated) updated += 1; continue; } } const needsEnrichment = !isDemoAccount && (!row.account_id || (row.platform === "小红书" && !isVerifiedXhsProfileUrl(row.profile_url)) || (row.platform === "小红书" && !row.public_account_id) || (row.platform === "小红书" && Number(row.followers ?? 0) === 0) || row.platform_uid?.startsWith("pending-") || Boolean(noteId && row.platform_uid === noteId)); if (!needsEnrichment || !row.publish_url) { if (publicProfileUpdated) updated += 1; continue; } if (!attemptedThisRow) attempted += 1; try { const result = await enrichDistributionAccount( db, row.id, row.publish_url, row.nickname || "待识别账号", mcpConfig, ); if (result.updated || publicProfileUpdated) updated += 1; } catch { if (publicProfileUpdated) updated += 1; else failed += 1; } } return { attempted, updated, failed }; }