import { useEffect, useState } from 'react'; import { ArrowRight, Check, ExternalLink, Hand, MessageSquare, RefreshCw, UserCheck } from 'lucide-react'; import { displayTime } from '../taskView'; import type { Task } from '../types'; type ObservedProfile = { source_id: string; profile_url: string; nickname?: string; xiaohongshu_id?: string; observed_at: string; evidence?: string }; type ObservedSource = { source_id: string; author_profile_url?: string; author?: string; content?: string }; export type OutreachRequest = { action: 'follow' | 'message'; source_ids: string[]; content?: string; account_profile_url: string }; type Target = ObservedProfile & { author?: string; quote?: string; profileId: string }; function text(value: unknown): string { return typeof value === 'string' ? value : ''; } function object(value: unknown): Record | null { return value && typeof value === 'object' && !Array.isArray(value) ? value as Record : null; } function profileId(value: unknown): string | null { if (typeof value !== 'string') return null; try { const url = new URL(value); if (url.protocol !== 'https:' || !['www.xiaohongshu.com', 'xiaohongshu.com'].includes(url.hostname) || url.username || url.password) return null; return /^\/user\/profile\/([a-f0-9]{24})\/?$/i.exec(url.pathname)?.[1].toLowerCase() || null; } catch { return null; } } export function verifiedOutreachTargets(task: Task): Target[] { const sources = Array.isArray(task.artifacts.sources) ? task.artifacts.sources as ObservedSource[] : []; const profiles = Array.isArray(task.artifacts.profiles) ? task.artifacts.profiles as ObservedProfile[] : []; const targets: Target[] = []; const seen = new Set(); for (const profile of profiles) { const source = sources.find(item => item.source_id === profile.source_id); const identifier = profileId(profile.profile_url); if (!source || !identifier || !profile.observed_at || profileId(source.author_profile_url) !== identifier) continue; if (profile.evidence !== 'matched_source_author_link_and_visible_profile_header' || seen.has(identifier)) continue; seen.add(identifier); targets.push({ ...profile, profileId: identifier, author: source.author, quote: source.content }); } return targets; } export function OutreachPlan({ task, busy, onAuthorize, onRecheck, onAcknowledge }: { task: Task; busy: boolean; onAuthorize: (request: OutreachRequest) => Promise; onRecheck: () => Promise; onAcknowledge: (accountProfileUrl: string) => Promise; }) { const intent = task.inputs.intent; const [selected, setSelected] = useState>(new Set()); const [content, setContent] = useState(text(task.inputs.message_text)); const goalVersion = typeof task.inputs._goal_version === 'number' ? task.inputs._goal_version : 1; useEffect(() => { setSelected(new Set()); }, [task.id, goalVersion, intent]); useEffect(() => { setContent(text(task.inputs.message_text)); }, [task.id, task.inputs.message_text]); if (intent !== 'follow' && intent !== 'message') return null; const account = object(task.artifacts.outreach_account); const accountUrl = text(account?.account_profile_url); const accountId = profileId(accountUrl); const verifiedAccount = Boolean(accountId && accountId === account?.account_id && account?.goal_version === goalVersion && text(account?.observed_at)); const targets = verifiedOutreachTargets(task).filter(item => item.profileId !== accountId); const selectedIds = targets.filter(item => selected.has(item.source_id)).map(item => item.source_id); const pause = object(task.artifacts.outreach_pause); const paused = intent === 'message' && Boolean(pause); const observedAt = text(account?.observed_at); const pausedAt = text(pause?.at); const zonedTime = /(?:Z|[+-]\d{2}:\d{2})$/i; const freshObservation = zonedTime.test(observedAt) && zonedTime.test(pausedAt) && Date.parse(observedAt) > Date.parse(pausedAt); const canAcknowledge = paused && verifiedAccount && freshObservation && account?.send_error === false && account?.daily_limit === false; const actionable = verifiedAccount && selectedIds.length > 0 && (intent === 'follow' || !!content.trim()) && !paused && task.status !== 'running'; const pending: string[] = []; if (!verifiedAccount) pending.push('先在实际页面核对当前操作账号,目标修改后需要重新核对。'); if (!targets.length) pending.push('先沿来源作者入口核对目标主页,未核对的对象不会进入可选名单。'); else if (!selectedIds.length) pending.push('选择本次要操作的对象,名单不会默认全选。'); if (intent === 'message' && !content.trim()) pending.push('补充这次要发送的私信内容。'); if (paused) pending.push(text(pause?.message) || '当前账号的私信阶段已暂停,请先核对页面限制和未确认发送结果。'); async function authorize(): Promise { if (!actionable || busy) return; const done = await onAuthorize({ action: intent as 'follow' | 'message', source_ids: selectedIds, account_profile_url: accountUrl, ...(intent === 'message' ? { content: content.trim() } : {}) }); if (done) setSelected(new Set()); } async function acknowledge(): Promise { if (!canAcknowledge || busy || task.status === 'running') return; if (await onAcknowledge(accountUrl)) setSelected(new Set()); } return
{intent === 'message' ? : }

{intent === 'message' ? '核对这次私信计划' : '核对这次关注计划'}

{text(task.inputs.action_scope) &&

目标范围{text(task.inputs.action_scope)}

}
{verifiedAccount ? : }{intent === 'message' ? '发送账号' : '操作账号'}{verifiedAccount ? <>查看已核对账号主页{accountUrl}页面核对时间:{displayTime(text(account?.observed_at))} :

当前账号尚未通过实际页面核对。

}
{targets.length > 0 &&
本次对象 已选择 {selectedIds.length} 位{targets.map(target => )}
} {intent === 'message' &&