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:
2026-06-10 11:06:51 +08:00
parent c7bad83496
commit abacc1f421
7 changed files with 287 additions and 77 deletions
@@ -2379,20 +2379,72 @@ async function readDeepSeekPageSnapshot(
}
const bodyText = normalize(document.body?.innerText || '')
// 登录/验证判定不能直接对整页 innerText 匹配:聊天问题与回答里出现
// "登录/验证码/安全验证" 这类词会把正常会话误判成需要人工处理。
// 只认三类证据:sign_in 路由、可见弹层内的短文案、稀疏拦截页。
const loginPhrasePattern =
/(log in|send code|scan with wechat to login|phone number|continue with deepseek|和 deepseek 继续聊|登录|发送验证码|手机号|验证码)/i
const challengePhrasePattern =
/(人机验证|安全验证|请完成验证|完成下方验证|拖动滑块|滑动验证|向右滑动|安全检测|访问异常|环境异常|verification required|security check|unusual traffic|not a robot|请输入(?:图形)?验证码)/i
const findVisibleSurfaceText = (
selectors: string,
pattern: RegExp,
maxTextLength: number,
): boolean => {
for (const node of Array.from(document.querySelectorAll(selectors)).slice(0, 50)) {
if (!isVisible(node)) {
continue
}
const text = normalize(node.innerText || node.textContent)
if (text && text.length <= maxTextLength && pattern.test(text)) {
return true
}
}
return false
}
const hasChallengeVendorWidget = (() => {
const vendorSelectors = [
'[class*="geetest_"]',
'[id^="nc_"][id$="_wrapper"]',
'.nc-container',
'[class*="nocaptcha"]',
'[class*="yidun_modal"]',
'[class*="yidun_popup"]',
'[class*="vaptcha"]',
'[class*="tcaptcha"]',
'#tcaptcha_iframe',
'[class*="captcha_verify"]',
'[class*="secsdk-captcha"]',
'iframe[src*="captcha"]',
'iframe[src*="geetest"]',
'iframe[src*="turnstile"]',
'iframe[src*="hcaptcha"]',
'iframe[src*="recaptcha"]',
'iframe[title*="验证"]',
].join(',')
for (const node of Array.from(document.querySelectorAll(vendorSelectors)).slice(0, 50)) {
if (isVisible(node)) {
return true
}
}
return false
})()
const loginRequired =
window.location.pathname.includes('/sign_in') ||
Boolean(
bodyText &&
/(log in|send code|scan with wechat to login|phone number|continue with deepseek|和 deepseek 继续聊|登录|发送验证码|手机号|验证码)/i.test(
bodyText,
),
)
const challengeRequired = Boolean(
bodyText &&
/(verification required|captcha|security check|verify|challenge|人机验证|安全验证|请完成验证|验证码|风控|环境异常)/i.test(
bodyText,
),
)
findVisibleSurfaceText(
'[role="dialog"], [aria-modal="true"], [class*="modal"], [class*="login"], [class*="sign"], [class*="auth"]',
loginPhrasePattern,
300,
) ||
Boolean(bodyText && bodyText.length <= 600 && loginPhrasePattern.test(bodyText))
const challengeRequired =
hasChallengeVendorWidget ||
findVisibleSurfaceText(
'[role="dialog"], [aria-modal="true"], [class*="captcha"], [id*="captcha"], [class*="verify"], [class*="Verify"]',
challengePhrasePattern,
240,
) ||
Boolean(bodyText && bodyText.length <= 400 && challengePhrasePattern.test(bodyText))
const busyDescriptors: string[] = []
for (const node of Array.from(
@@ -1,6 +1,7 @@
import type { Cookie, Session, WebContents } from 'electron/main'
import type { AccountHealthProfile, AuthProbeResult } from '../../auth-types'
import { PAGE_CHALLENGE_SIGNAL_COUNT_SNIPPET } from '../../challenge-signals'
import type { GenericAIPageState } from '../../generic-ai-auth'
import { normalizeRemoteUrl } from '../common'
@@ -269,9 +270,7 @@ export async function readDoubaoAuthPageState(
const loggedOutSignalCount = /(未登录|登录后继续|请先登录|扫码登录|手机号登录)/.test(bodyText)
? 1
: 0;
const challengeSignalCount = /(验证码|滑块|人机验证|安全验证|短信验证|captcha|verification)/i.test(bodyText)
? 1
: 0;
const challengeSignalCount = ${PAGE_CHALLENGE_SIGNAL_COUNT_SNIPPET};
const account = pickAccountTrigger();
const displayName = account?.displayName || null;
const avatarUrl = isAccountAvatarUrl(account?.avatarUrl || null)
@@ -1,6 +1,7 @@
import type { Cookie, Session, WebContents } from 'electron/main'
import type { AccountHealthProfile, AuthProbeResult } from '../../auth-types'
import { PAGE_CHALLENGE_SIGNAL_COUNT_SNIPPET } from '../../challenge-signals'
import type { GenericAIPageState } from '../../generic-ai-auth'
import { normalizeRemoteUrl } from '../common'
@@ -195,9 +196,7 @@ export async function readKimiAuthPageState(
const loggedOutSignalCount = /(登录以同步历史会话|微信扫码登录|手机号快捷登录|请输入手机号|请输入验证码|扫码登录|未登录|登录后继续|请先登录)/.test(bodyText)
? 1
: 0;
const challengeSignalCount = /(验证码|滑块|人机验证|安全验证|短信验证|captcha|verification|NECAPTCHA)/i.test(bodyText)
? 1
: 0;
const challengeSignalCount = ${PAGE_CHALLENGE_SIGNAL_COUNT_SNIPPET};
const stopWords = /^(Kimi|Kimi Code|Kimi Claw|历史会话|新建会话|查看全部|设置|会员计划|关于我们|Language|用户反馈|获取应用程序|升级|升级套餐|登录|注册|虚拟用户)$/i;
const isKimiAvatarUrl = (src) => {
if (!src) return false;
@@ -1,6 +1,7 @@
import type { Cookie, Session, WebContents } from 'electron/main'
import type { AccountHealthProfile, AuthProbeResult } from '../../auth-types'
import { PAGE_CHALLENGE_SIGNAL_COUNT_SNIPPET } from '../../challenge-signals'
import type { GenericAIPageState } from '../../generic-ai-auth'
import { normalizeRemoteUrl } from '../common'
@@ -370,9 +371,7 @@ export async function readQwenAuthPageState(
const loggedOutSignalCount = /(未登录|登录后继续|请先登录|扫码登录|手机号登录|登录可同步历史对话)/.test(bodyText)
? 1
: 0;
const challengeSignalCount = /(验证码|滑块|人机验证|安全验证|短信验证|captcha|verification)/i.test(bodyText)
? 1
: 0;
const challengeSignalCount = ${PAGE_CHALLENGE_SIGNAL_COUNT_SNIPPET};
return {
displayName: sidebarAccount?.displayName || user?.displayName || null,
avatarUrl: sidebarAccount?.avatarUrl || user?.avatarUrl || null,