import type { AdapterExecutionResult } from './adapters/base' export type MonitorAccountFailoverReason = | 'risk_control' | 'challenge_required' | 'authorization_failed' | 'empty_result' | 'missing_citations' | 'unknown_result' | 'adapter_failure' export type MonitorAccountPoolEntry = { id: string authState: string hasLocalAuthorization: boolean } type MonitorAccountCooldown = { reason: MonitorAccountFailoverReason until: number } const lastUsedAt = new Map() const cooldowns = new Map() const blockedAuthStates = new Set(['challenge_required', 'expired', 'revoked']) const deterministicFailurePattern = /adapter_aborted|stale_business_date|desktop_monitor_task_stale|desktop_task_adapter_missing|question_text_missing|playwright_required|playwright_cdp_(?:unavailable|disabled)/i const riskControlPattern = /risk[_ -]?control|风控|频繁|访问受限|操作受限/i const challengePattern = /challenge|required|captcha|验证码|人机验证|人工验证|安全验证/i const authorizationPattern = /authorization|unauthorized|login[_ -]?(?:required|expired)|not logged in|登录态|未登录|请先登录|授权过期/i export function resetMonitorAccountPoolState(): void { lastUsedAt.clear() cooldowns.clear() } export function isMonitorAccountAuthBlocked(authState: string): boolean { return blockedAuthStates.has(authState) } export function selectMonitorAccountCandidates( entries: readonly MonitorAccountPoolEntry[], preferredAccountId: string | null, now = Date.now(), ): MonitorAccountPoolEntry[] { for (const [accountId, cooldown] of cooldowns.entries()) { if (cooldown.until <= now) { cooldowns.delete(accountId) } } return entries .filter( (entry) => entry.hasLocalAuthorization && !isMonitorAccountAuthBlocked(entry.authState) && (cooldowns.get(entry.id)?.until ?? 0) <= now, ) .sort((left, right) => { if (left.id === preferredAccountId && right.id !== preferredAccountId) { return -1 } if (right.id === preferredAccountId && left.id !== preferredAccountId) { return 1 } const usedDelta = (lastUsedAt.get(left.id) ?? 0) - (lastUsedAt.get(right.id) ?? 0) return usedDelta || left.id.localeCompare(right.id) }) } export function noteMonitorAccountAttempt(accountId: string, now = Date.now()): void { lastUsedAt.set(accountId, now) } export function clearMonitorAccountCooldown(accountId: string): void { cooldowns.delete(accountId) } export function monitorAccountCooldownMs(reason: MonitorAccountFailoverReason): number { switch (reason) { case 'risk_control': case 'challenge_required': case 'authorization_failed': return 30 * 60_000 case 'empty_result': case 'unknown_result': return 3 * 60_000 case 'missing_citations': return 0 case 'adapter_failure': return 60_000 } } export function coolDownMonitorAccount( accountId: string, reason: MonitorAccountFailoverReason, now = Date.now(), ): void { cooldowns.set(accountId, { reason, until: now + monitorAccountCooldownMs(reason), }) } export function classifyMonitorAccountFailover( result: AdapterExecutionResult, ): MonitorAccountFailoverReason | null { const answerCandidates = [ result.payload?.answer, result.payload?.answer_text, result.payload?.content, result.payload?.response_text, ] const hasAnswer = answerCandidates.some( (value) => typeof value === 'string' && value.trim().length > 0, ) if (result.status === 'succeeded' && hasAnswer) { const hasCitationSources = [result.payload?.citations, result.payload?.search_results].some( (value) => Array.isArray(value) && value.length > 0, ) return hasCitationSources ? null : 'missing_citations' } if (result.status === 'succeeded') { return 'empty_result' } if (result.status === 'unknown') { return 'unknown_result' } const errorCode = typeof result.error?.code === 'string' ? result.error.code : '' const errorMessage = typeof result.error?.message === 'string' ? result.error.message : '' const failureText = `${errorCode} ${errorMessage} ${result.summary}` if (deterministicFailurePattern.test(failureText)) { return null } if (riskControlPattern.test(failureText)) { return 'risk_control' } if (challengePattern.test(failureText)) { return 'challenge_required' } if (authorizationPattern.test(failureText)) { return 'authorization_failed' } return 'adapter_failure' } export function getMonitorAccountPoolSnapshot(now = Date.now()) { return { trackedAccountCount: lastUsedAt.size, cooldowns: [...cooldowns.entries()] .filter(([, cooldown]) => cooldown.until > now) .map(([accountId, cooldown]) => ({ accountId, reason: cooldown.reason, until: cooldown.until, })), } }