fix(desktop-client): stop false manual-verification states and auto-revive probes after sleep/offline
Challenge detection previously regex-matched the entire page innerText, so chat transcripts or console copy containing words like 验证码/安全验证/verify flagged healthy accounts as challenge_required. Detection is now scoped to real evidence only (visible captcha vendor widgets, short modal copy, sparse interstitial pages) via a shared challenge-signals module used by the generic AI probe, doubao/kimi/qwen auth rules, and the deepseek runtime snapshot. Platform API error codes (e.g. baijiahao_challenge_required) keep working. Account health also could not recover after lid-close sleep or a network drop: offline probes misread cached console pages as expired, and expired records null out nextProbeAt so they were never re-probed. Probing is now skipped while offline (network_error backoff instead), and powerMonitor resume/unlock plus an offline-to-online transition watch schedule jittered recovery probes for all tracked accounts, including expired ones. Challenge rechecks wait 8s so transient widgets during page load no longer stick. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -2,7 +2,7 @@ import { mkdirSync, readFileSync, writeFileSync } from 'node:fs'
|
||||
import { dirname, join } from 'node:path'
|
||||
|
||||
import type { DesktopAccountInfo } from '@geo/shared-types'
|
||||
import { app } from 'electron/main'
|
||||
import { app, net, powerMonitor } from 'electron/main'
|
||||
|
||||
import type { PublishAccountIdentity } from './account-binder'
|
||||
import type {
|
||||
@@ -35,6 +35,11 @@ const MANUAL_PROBE_CACHE_TTL_MS = 5 * 60_000
|
||||
const MANUAL_PROBE_MIN_INTERVAL_MS = 15_000
|
||||
const ACCOUNT_PROBE_RETRY_BACKOFF_MS = [1_000, 3_000, 10_000] as const
|
||||
const CHALLENGE_RECHECK_ATTEMPTS = 1
|
||||
// 复核前留出缓冲:页面加载中的瞬态验证组件(无感验证/加载占位)几秒内会自行消失,
|
||||
// 立即复核会把瞬态误报固化成"需人工验证"。
|
||||
const CHALLENGE_RECHECK_DELAY_MS = process.env.VITEST ? 25 : 8_000
|
||||
const RECOVERY_PROBE_MIN_MS = 2_000
|
||||
const RECOVERY_PROBE_MAX_MS = 15_000
|
||||
|
||||
type ProbeTrigger = 'scheduled' | 'manual' | 'preflight' | 'confirm'
|
||||
|
||||
@@ -85,6 +90,20 @@ const probeQueue: string[] = []
|
||||
let initialized = false
|
||||
let probeTimer: ReturnType<typeof setInterval> | null = null
|
||||
let activeProbeCount = 0
|
||||
let recoveryWatchersInstalled = false
|
||||
let lastKnownOnline = true
|
||||
|
||||
function sleep(ms: number): Promise<void> {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms))
|
||||
}
|
||||
|
||||
function isNetworkOnline(): boolean {
|
||||
try {
|
||||
return net.isOnline()
|
||||
} catch {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
function randomInt(min: number, max: number): number {
|
||||
if (max <= min) {
|
||||
@@ -176,13 +195,60 @@ function ensureInitialized(): void {
|
||||
loadPersistedRecords()
|
||||
}
|
||||
|
||||
// 休眠唤醒/断网恢复后,给所有被跟踪账号(包括已标记 expired、nextProbeAt 已被
|
||||
// 置空的账号)重新排一次短延迟探测,让会话状态自动回到真实值,而不是停留在
|
||||
// 断网期间产生的离线/失效快照上。
|
||||
function scheduleRecoveryProbes(reason: string): void {
|
||||
ensureInitialized()
|
||||
const now = Date.now()
|
||||
let touched = false
|
||||
|
||||
for (const accountId of trackedAccounts.keys()) {
|
||||
const record = records.get(accountId)
|
||||
if (!record || isProbeInFlight(record)) {
|
||||
continue
|
||||
}
|
||||
record.nextProbeAt = now + randomInt(RECOVERY_PROBE_MIN_MS, RECOVERY_PROBE_MAX_MS)
|
||||
touched = true
|
||||
}
|
||||
|
||||
if (touched) {
|
||||
persistRecords()
|
||||
console.info('[desktop-account-health] recovery probes scheduled', { reason })
|
||||
}
|
||||
}
|
||||
|
||||
function ensureRecoveryWatchers(): void {
|
||||
if (recoveryWatchersInstalled) {
|
||||
return
|
||||
}
|
||||
recoveryWatchersInstalled = true
|
||||
lastKnownOnline = isNetworkOnline()
|
||||
|
||||
try {
|
||||
powerMonitor.on('resume', () => scheduleRecoveryProbes('power_resume'))
|
||||
powerMonitor.on('unlock-screen', () => scheduleRecoveryProbes('unlock_screen'))
|
||||
} catch {
|
||||
// powerMonitor 在 app ready 之前或测试环境不可用;此时仅依赖下方的网络轮询恢复
|
||||
}
|
||||
}
|
||||
|
||||
function ensureProbeTimer(): void {
|
||||
ensureInitialized()
|
||||
ensureRecoveryWatchers()
|
||||
if (probeTimer) {
|
||||
return
|
||||
}
|
||||
|
||||
probeTimer = setInterval(() => {
|
||||
const onlineNow = isNetworkOnline()
|
||||
if (onlineNow && !lastKnownOnline) {
|
||||
scheduleRecoveryProbes('network_restored')
|
||||
}
|
||||
lastKnownOnline = onlineNow
|
||||
if (!onlineNow) {
|
||||
return
|
||||
}
|
||||
void runDueProbes()
|
||||
}, PROBE_TICK_MS)
|
||||
}
|
||||
@@ -400,6 +466,7 @@ async function recheckChallengeResult(
|
||||
|
||||
let confirmed = result
|
||||
for (let attempt = 0; attempt < CHALLENGE_RECHECK_ATTEMPTS; attempt += 1) {
|
||||
await sleep(CHALLENGE_RECHECK_DELAY_MS)
|
||||
confirmed = await probeAdapter(adapter, account, partition)
|
||||
if (confirmed.verdict !== 'challenge_required') {
|
||||
return confirmed
|
||||
@@ -442,6 +509,12 @@ async function performProbeLocked(
|
||||
record.probeState = 'probing'
|
||||
commitRecord(record, previousSignature)
|
||||
|
||||
// 离线时页面探测只能得到缓存渲染或加载失败,任何"过期/需验证"的结论都不可信,
|
||||
// 直接按网络错误退避,等网络恢复后由 recovery 探测纠正。
|
||||
if (!isNetworkOnline()) {
|
||||
return applyNetworkResult(record, networkProbeResult(), Date.now(), options.trigger)
|
||||
}
|
||||
|
||||
const partition = resolveKnownPartition(account.id)
|
||||
if (!partition) {
|
||||
return applyExpiredResult(
|
||||
|
||||
Reference in New Issue
Block a user