Files
geo/apps/desktop-client/src/main/monitor-account-pool.ts
T
root 2706c07381 feat(desktop-monitor): multi-account pool with failover and cooldown
Add monitor-account-pool to select authorized, non-blocked candidates
for an AI-platform monitor task, classify failover reasons (risk
control, challenge, auth failure, empty/unknown result) and cool the
account down before rotating to the next. Wire runtime-controller to run
monitor tasks through the pool, retrying across accounts and annotating
the result with per-account attempt diagnostics and a failover count.
Expose the pool state via the runtime snapshot.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 14:17:27 +08:00

163 lines
4.9 KiB
TypeScript

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<string, number>()
const cooldowns = new Map<string, MonitorAccountCooldown>()
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,
})),
}
}