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:
@@ -12,7 +12,7 @@ import { dirname, join } from 'node:path'
|
|||||||
import type { DesktopAccountInfo } from '@geo/shared-types'
|
import type { DesktopAccountInfo } from '@geo/shared-types'
|
||||||
import { aiPlatformCatalog } from '@geo/shared-types'
|
import { aiPlatformCatalog } from '@geo/shared-types'
|
||||||
import type { Cookie, Session, WebContents } from 'electron/main'
|
import type { Cookie, Session, WebContents } from 'electron/main'
|
||||||
import { app, BrowserWindow, session as electronSession, nativeTheme } from 'electron/main'
|
import { app, BrowserWindow, net, session as electronSession, nativeTheme } from 'electron/main'
|
||||||
|
|
||||||
import { normalizeAccountPlatformUid, sameAccountPlatformUid } from '../shared/account-identity'
|
import { normalizeAccountPlatformUid, sameAccountPlatformUid } from '../shared/account-identity'
|
||||||
import {
|
import {
|
||||||
@@ -49,6 +49,7 @@ import {
|
|||||||
import { readWenxinAccountIdentity } from './adapters/wenxin/account-identity'
|
import { readWenxinAccountIdentity } from './adapters/wenxin/account-identity'
|
||||||
import { readYuanbaoAccountIdentity } from './adapters/yuanbao/account-identity'
|
import { readYuanbaoAccountIdentity } from './adapters/yuanbao/account-identity'
|
||||||
import type { AccountHealthProfile, AuthProbeResult } from './auth-types'
|
import type { AccountHealthProfile, AuthProbeResult } from './auth-types'
|
||||||
|
import { PAGE_CHALLENGE_SIGNAL_COUNT_SNIPPET } from './challenge-signals'
|
||||||
import {
|
import {
|
||||||
attachWindowDiagnostics,
|
attachWindowDiagnostics,
|
||||||
isWindowReadyForDetection,
|
isWindowReadyForDetection,
|
||||||
@@ -1131,7 +1132,6 @@ async function readGenericAIPageState(
|
|||||||
const stopWords = /^(登录|注册|设置|帮助|历史|聊天|新建|菜单|账户|账号|个人中心|联网搜索|深度思考|工具|更多|安装电脑版|下载电脑版|DeepSeek|元宝|混元|豆包|Kimi|Qwen|通义千问)$/i;
|
const stopWords = /^(登录|注册|设置|帮助|历史|聊天|新建|菜单|账户|账号|个人中心|联网搜索|深度思考|工具|更多|安装电脑版|下载电脑版|DeepSeek|元宝|混元|豆包|Kimi|Qwen|通义千问)$/i;
|
||||||
const loginPattern = /(登录|注册|立即登录|扫码登录|微信登录|QQ登录|手机号登录|请先登录)/i;
|
const loginPattern = /(登录|注册|立即登录|扫码登录|微信登录|QQ登录|手机号登录|请先登录)/i;
|
||||||
const loggedOutPattern = /(未登录|扫码登录|快捷登录|使用其他头像、昵称或账号|登录后继续|请先登录|立即登录|微信快捷登录)/i;
|
const loggedOutPattern = /(未登录|扫码登录|快捷登录|使用其他头像、昵称或账号|登录后继续|请先登录|立即登录|微信快捷登录)/i;
|
||||||
const challengePattern = /(验证码|滑块|人机验证|安全验证|请完成验证|短信验证|手机验证码|扫码验证|verify|verification|captcha|security check)/i;
|
|
||||||
const credentialKeyPattern = /(token|session|auth|login|passport|ticket|jwt|refresh|access)/i;
|
const credentialKeyPattern = /(token|session|auth|login|passport|ticket|jwt|refresh|access)/i;
|
||||||
const isInsideAuthSurface = (node) => {
|
const isInsideAuthSurface = (node) => {
|
||||||
if (!(node instanceof Element)) {
|
if (!(node instanceof Element)) {
|
||||||
@@ -1261,26 +1261,6 @@ async function readGenericAIPageState(
|
|||||||
}
|
}
|
||||||
return matches;
|
return matches;
|
||||||
};
|
};
|
||||||
const collectChallengeSignals = () => {
|
|
||||||
const matches = [];
|
|
||||||
if (bodyText && challengePattern.test(bodyText)) {
|
|
||||||
matches.push('body-text');
|
|
||||||
}
|
|
||||||
const selectors = ['button', 'a', '[role="button"]', '[role="dialog"]', 'input', 'label'];
|
|
||||||
for (const selector of selectors) {
|
|
||||||
const nodes = document.querySelectorAll(selector);
|
|
||||||
for (const node of nodes) {
|
|
||||||
if (!isVisible(node)) continue;
|
|
||||||
const text = normalize(node.textContent || node.getAttribute?.('aria-label') || "");
|
|
||||||
if (!text || !challengePattern.test(text)) continue;
|
|
||||||
matches.push(text);
|
|
||||||
if (matches.length >= 4) {
|
|
||||||
return matches;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return matches;
|
|
||||||
};
|
|
||||||
const collectCredentialStorage = (storage, prefix) => {
|
const collectCredentialStorage = (storage, prefix) => {
|
||||||
const items = [];
|
const items = [];
|
||||||
try {
|
try {
|
||||||
@@ -1333,7 +1313,7 @@ async function readGenericAIPageState(
|
|||||||
}
|
}
|
||||||
const loginSignals = collectLoginSignals();
|
const loginSignals = collectLoginSignals();
|
||||||
const loggedOutSignals = collectLoggedOutSignals();
|
const loggedOutSignals = collectLoggedOutSignals();
|
||||||
const challengeSignals = collectChallengeSignals();
|
const challengeSignalCount = ${PAGE_CHALLENGE_SIGNAL_COUNT_SNIPPET};
|
||||||
const credentialFingerprintParts = [
|
const credentialFingerprintParts = [
|
||||||
...collectCredentialStorage(window.localStorage, 'local'),
|
...collectCredentialStorage(window.localStorage, 'local'),
|
||||||
...collectCredentialStorage(window.sessionStorage, 'session'),
|
...collectCredentialStorage(window.sessionStorage, 'session'),
|
||||||
@@ -1356,7 +1336,7 @@ async function readGenericAIPageState(
|
|||||||
strongAuthSignalCount: strongSignals.length,
|
strongAuthSignalCount: strongSignals.length,
|
||||||
loginSignalCount: loginSignals.length,
|
loginSignalCount: loginSignals.length,
|
||||||
loggedOutSignalCount: loggedOutSignals.length,
|
loggedOutSignalCount: loggedOutSignals.length,
|
||||||
challengeSignalCount: challengeSignals.length,
|
challengeSignalCount,
|
||||||
};
|
};
|
||||||
} catch {
|
} catch {
|
||||||
return null;
|
return null;
|
||||||
@@ -2378,6 +2358,27 @@ async function resolvePublishAccountProfileFromHandle(
|
|||||||
return toPublishAccountProfile(detected)
|
return toPublishAccountProfile(detected)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 断网时控制台页面可能命中本地缓存:页面渲染成功但账号接口全部失败,
|
||||||
|
// 探测会走到"未检出账号 → expired"的兜底分支,把好账号误判成已失效。
|
||||||
|
// expired 会停掉自动重探(nextProbeAt 置空),必须在兜底前先排除离线场景。
|
||||||
|
function offlineNetworkProbeResult(): AuthProbeResult | null {
|
||||||
|
try {
|
||||||
|
if (net.isOnline()) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
verdict: 'network_error',
|
||||||
|
reason: 'network_error',
|
||||||
|
profile: null,
|
||||||
|
sessionFingerprint: null,
|
||||||
|
evidence: { code: 'offline' },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export async function probeAIAccountSession(
|
export async function probeAIAccountSession(
|
||||||
account: PublishAccountIdentity,
|
account: PublishAccountIdentity,
|
||||||
partition?: string,
|
partition?: string,
|
||||||
@@ -2601,18 +2602,20 @@ export async function probeAIAccountSession(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return (
|
||||||
verdict: 'expired',
|
offlineNetworkProbeResult() ?? {
|
||||||
reason: 'login_redirect',
|
verdict: 'expired',
|
||||||
profile: null,
|
reason: 'login_redirect',
|
||||||
sessionFingerprint: null,
|
profile: null,
|
||||||
evidence: {
|
sessionFingerprint: null,
|
||||||
url: currentURL,
|
evidence: {
|
||||||
strongAuthSignalCount: pageState.strongAuthSignalCount,
|
url: currentURL,
|
||||||
loginSignalCount: pageState.loginSignalCount,
|
strongAuthSignalCount: pageState.strongAuthSignalCount,
|
||||||
loggedOutSignalCount: pageState.loggedOutSignalCount,
|
loginSignalCount: pageState.loginSignalCount,
|
||||||
},
|
loggedOutSignalCount: pageState.loggedOutSignalCount,
|
||||||
}
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return {
|
return {
|
||||||
verdict: 'network_error',
|
verdict: 'network_error',
|
||||||
@@ -2869,16 +2872,18 @@ export async function probePublishAccountSession(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (platformRequiresDetectedAccountForConsoleAccess(account.platform)) {
|
if (platformRequiresDetectedAccountForConsoleAccess(account.platform)) {
|
||||||
return {
|
return (
|
||||||
verdict: 'expired',
|
offlineNetworkProbeResult() ?? {
|
||||||
reason: 'login_redirect',
|
verdict: 'expired',
|
||||||
profile: null,
|
reason: 'login_redirect',
|
||||||
sessionFingerprint: null,
|
profile: null,
|
||||||
evidence: {
|
sessionFingerprint: null,
|
||||||
url: currentURL,
|
evidence: {
|
||||||
code: 'account_api_empty',
|
url: currentURL,
|
||||||
},
|
code: 'account_api_empty',
|
||||||
}
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const consoleReady =
|
const consoleReady =
|
||||||
@@ -2894,15 +2899,17 @@ export async function probePublishAccountSession(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return (
|
||||||
verdict: 'expired',
|
offlineNetworkProbeResult() ?? {
|
||||||
reason: 'login_redirect',
|
verdict: 'expired',
|
||||||
profile: null,
|
reason: 'login_redirect',
|
||||||
sessionFingerprint: null,
|
profile: null,
|
||||||
evidence: {
|
sessionFingerprint: null,
|
||||||
url: currentURL,
|
evidence: {
|
||||||
},
|
url: currentURL,
|
||||||
}
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return {
|
return {
|
||||||
verdict: 'network_error',
|
verdict: 'network_error',
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { mkdirSync, readFileSync, writeFileSync } from 'node:fs'
|
|||||||
import { dirname, join } from 'node:path'
|
import { dirname, join } from 'node:path'
|
||||||
|
|
||||||
import type { DesktopAccountInfo } from '@geo/shared-types'
|
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 { PublishAccountIdentity } from './account-binder'
|
||||||
import type {
|
import type {
|
||||||
@@ -35,6 +35,11 @@ const MANUAL_PROBE_CACHE_TTL_MS = 5 * 60_000
|
|||||||
const MANUAL_PROBE_MIN_INTERVAL_MS = 15_000
|
const MANUAL_PROBE_MIN_INTERVAL_MS = 15_000
|
||||||
const ACCOUNT_PROBE_RETRY_BACKOFF_MS = [1_000, 3_000, 10_000] as const
|
const ACCOUNT_PROBE_RETRY_BACKOFF_MS = [1_000, 3_000, 10_000] as const
|
||||||
const CHALLENGE_RECHECK_ATTEMPTS = 1
|
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'
|
type ProbeTrigger = 'scheduled' | 'manual' | 'preflight' | 'confirm'
|
||||||
|
|
||||||
@@ -85,6 +90,20 @@ const probeQueue: string[] = []
|
|||||||
let initialized = false
|
let initialized = false
|
||||||
let probeTimer: ReturnType<typeof setInterval> | null = null
|
let probeTimer: ReturnType<typeof setInterval> | null = null
|
||||||
let activeProbeCount = 0
|
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 {
|
function randomInt(min: number, max: number): number {
|
||||||
if (max <= min) {
|
if (max <= min) {
|
||||||
@@ -176,13 +195,60 @@ function ensureInitialized(): void {
|
|||||||
loadPersistedRecords()
|
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 {
|
function ensureProbeTimer(): void {
|
||||||
ensureInitialized()
|
ensureInitialized()
|
||||||
|
ensureRecoveryWatchers()
|
||||||
if (probeTimer) {
|
if (probeTimer) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
probeTimer = setInterval(() => {
|
probeTimer = setInterval(() => {
|
||||||
|
const onlineNow = isNetworkOnline()
|
||||||
|
if (onlineNow && !lastKnownOnline) {
|
||||||
|
scheduleRecoveryProbes('network_restored')
|
||||||
|
}
|
||||||
|
lastKnownOnline = onlineNow
|
||||||
|
if (!onlineNow) {
|
||||||
|
return
|
||||||
|
}
|
||||||
void runDueProbes()
|
void runDueProbes()
|
||||||
}, PROBE_TICK_MS)
|
}, PROBE_TICK_MS)
|
||||||
}
|
}
|
||||||
@@ -400,6 +466,7 @@ async function recheckChallengeResult(
|
|||||||
|
|
||||||
let confirmed = result
|
let confirmed = result
|
||||||
for (let attempt = 0; attempt < CHALLENGE_RECHECK_ATTEMPTS; attempt += 1) {
|
for (let attempt = 0; attempt < CHALLENGE_RECHECK_ATTEMPTS; attempt += 1) {
|
||||||
|
await sleep(CHALLENGE_RECHECK_DELAY_MS)
|
||||||
confirmed = await probeAdapter(adapter, account, partition)
|
confirmed = await probeAdapter(adapter, account, partition)
|
||||||
if (confirmed.verdict !== 'challenge_required') {
|
if (confirmed.verdict !== 'challenge_required') {
|
||||||
return confirmed
|
return confirmed
|
||||||
@@ -442,6 +509,12 @@ async function performProbeLocked(
|
|||||||
record.probeState = 'probing'
|
record.probeState = 'probing'
|
||||||
commitRecord(record, previousSignature)
|
commitRecord(record, previousSignature)
|
||||||
|
|
||||||
|
// 离线时页面探测只能得到缓存渲染或加载失败,任何"过期/需验证"的结论都不可信,
|
||||||
|
// 直接按网络错误退避,等网络恢复后由 recovery 探测纠正。
|
||||||
|
if (!isNetworkOnline()) {
|
||||||
|
return applyNetworkResult(record, networkProbeResult(), Date.now(), options.trigger)
|
||||||
|
}
|
||||||
|
|
||||||
const partition = resolveKnownPartition(account.id)
|
const partition = resolveKnownPartition(account.id)
|
||||||
if (!partition) {
|
if (!partition) {
|
||||||
return applyExpiredResult(
|
return applyExpiredResult(
|
||||||
|
|||||||
@@ -2379,20 +2379,72 @@ async function readDeepSeekPageSnapshot(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const bodyText = normalize(document.body?.innerText || '')
|
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 =
|
const loginRequired =
|
||||||
window.location.pathname.includes('/sign_in') ||
|
window.location.pathname.includes('/sign_in') ||
|
||||||
Boolean(
|
findVisibleSurfaceText(
|
||||||
bodyText &&
|
'[role="dialog"], [aria-modal="true"], [class*="modal"], [class*="login"], [class*="sign"], [class*="auth"]',
|
||||||
/(log in|send code|scan with wechat to login|phone number|continue with deepseek|和 deepseek 继续聊|登录|发送验证码|手机号|验证码)/i.test(
|
loginPhrasePattern,
|
||||||
bodyText,
|
300,
|
||||||
),
|
) ||
|
||||||
)
|
Boolean(bodyText && bodyText.length <= 600 && loginPhrasePattern.test(bodyText))
|
||||||
const challengeRequired = Boolean(
|
const challengeRequired =
|
||||||
bodyText &&
|
hasChallengeVendorWidget ||
|
||||||
/(verification required|captcha|security check|verify|challenge|人机验证|安全验证|请完成验证|验证码|风控|环境异常)/i.test(
|
findVisibleSurfaceText(
|
||||||
bodyText,
|
'[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[] = []
|
const busyDescriptors: string[] = []
|
||||||
for (const node of Array.from(
|
for (const node of Array.from(
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import type { Cookie, Session, WebContents } from 'electron/main'
|
import type { Cookie, Session, WebContents } from 'electron/main'
|
||||||
|
|
||||||
import type { AccountHealthProfile, AuthProbeResult } from '../../auth-types'
|
import type { AccountHealthProfile, AuthProbeResult } from '../../auth-types'
|
||||||
|
import { PAGE_CHALLENGE_SIGNAL_COUNT_SNIPPET } from '../../challenge-signals'
|
||||||
import type { GenericAIPageState } from '../../generic-ai-auth'
|
import type { GenericAIPageState } from '../../generic-ai-auth'
|
||||||
import { normalizeRemoteUrl } from '../common'
|
import { normalizeRemoteUrl } from '../common'
|
||||||
|
|
||||||
@@ -269,9 +270,7 @@ export async function readDoubaoAuthPageState(
|
|||||||
const loggedOutSignalCount = /(未登录|登录后继续|请先登录|扫码登录|手机号登录)/.test(bodyText)
|
const loggedOutSignalCount = /(未登录|登录后继续|请先登录|扫码登录|手机号登录)/.test(bodyText)
|
||||||
? 1
|
? 1
|
||||||
: 0;
|
: 0;
|
||||||
const challengeSignalCount = /(验证码|滑块|人机验证|安全验证|短信验证|captcha|verification)/i.test(bodyText)
|
const challengeSignalCount = ${PAGE_CHALLENGE_SIGNAL_COUNT_SNIPPET};
|
||||||
? 1
|
|
||||||
: 0;
|
|
||||||
const account = pickAccountTrigger();
|
const account = pickAccountTrigger();
|
||||||
const displayName = account?.displayName || null;
|
const displayName = account?.displayName || null;
|
||||||
const avatarUrl = isAccountAvatarUrl(account?.avatarUrl || null)
|
const avatarUrl = isAccountAvatarUrl(account?.avatarUrl || null)
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import type { Cookie, Session, WebContents } from 'electron/main'
|
import type { Cookie, Session, WebContents } from 'electron/main'
|
||||||
|
|
||||||
import type { AccountHealthProfile, AuthProbeResult } from '../../auth-types'
|
import type { AccountHealthProfile, AuthProbeResult } from '../../auth-types'
|
||||||
|
import { PAGE_CHALLENGE_SIGNAL_COUNT_SNIPPET } from '../../challenge-signals'
|
||||||
import type { GenericAIPageState } from '../../generic-ai-auth'
|
import type { GenericAIPageState } from '../../generic-ai-auth'
|
||||||
import { normalizeRemoteUrl } from '../common'
|
import { normalizeRemoteUrl } from '../common'
|
||||||
|
|
||||||
@@ -195,9 +196,7 @@ export async function readKimiAuthPageState(
|
|||||||
const loggedOutSignalCount = /(登录以同步历史会话|微信扫码登录|手机号快捷登录|请输入手机号|请输入验证码|扫码登录|未登录|登录后继续|请先登录)/.test(bodyText)
|
const loggedOutSignalCount = /(登录以同步历史会话|微信扫码登录|手机号快捷登录|请输入手机号|请输入验证码|扫码登录|未登录|登录后继续|请先登录)/.test(bodyText)
|
||||||
? 1
|
? 1
|
||||||
: 0;
|
: 0;
|
||||||
const challengeSignalCount = /(验证码|滑块|人机验证|安全验证|短信验证|captcha|verification|NECAPTCHA)/i.test(bodyText)
|
const challengeSignalCount = ${PAGE_CHALLENGE_SIGNAL_COUNT_SNIPPET};
|
||||||
? 1
|
|
||||||
: 0;
|
|
||||||
const stopWords = /^(Kimi|Kimi Code|Kimi Claw|历史会话|新建会话|查看全部|设置|会员计划|关于我们|Language|用户反馈|获取应用程序|升级|升级套餐|登录|注册|虚拟用户)$/i;
|
const stopWords = /^(Kimi|Kimi Code|Kimi Claw|历史会话|新建会话|查看全部|设置|会员计划|关于我们|Language|用户反馈|获取应用程序|升级|升级套餐|登录|注册|虚拟用户)$/i;
|
||||||
const isKimiAvatarUrl = (src) => {
|
const isKimiAvatarUrl = (src) => {
|
||||||
if (!src) return false;
|
if (!src) return false;
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import type { Cookie, Session, WebContents } from 'electron/main'
|
import type { Cookie, Session, WebContents } from 'electron/main'
|
||||||
|
|
||||||
import type { AccountHealthProfile, AuthProbeResult } from '../../auth-types'
|
import type { AccountHealthProfile, AuthProbeResult } from '../../auth-types'
|
||||||
|
import { PAGE_CHALLENGE_SIGNAL_COUNT_SNIPPET } from '../../challenge-signals'
|
||||||
import type { GenericAIPageState } from '../../generic-ai-auth'
|
import type { GenericAIPageState } from '../../generic-ai-auth'
|
||||||
import { normalizeRemoteUrl } from '../common'
|
import { normalizeRemoteUrl } from '../common'
|
||||||
|
|
||||||
@@ -370,9 +371,7 @@ export async function readQwenAuthPageState(
|
|||||||
const loggedOutSignalCount = /(未登录|登录后继续|请先登录|扫码登录|手机号登录|登录可同步历史对话)/.test(bodyText)
|
const loggedOutSignalCount = /(未登录|登录后继续|请先登录|扫码登录|手机号登录|登录可同步历史对话)/.test(bodyText)
|
||||||
? 1
|
? 1
|
||||||
: 0;
|
: 0;
|
||||||
const challengeSignalCount = /(验证码|滑块|人机验证|安全验证|短信验证|captcha|verification)/i.test(bodyText)
|
const challengeSignalCount = ${PAGE_CHALLENGE_SIGNAL_COUNT_SNIPPET};
|
||||||
? 1
|
|
||||||
: 0;
|
|
||||||
return {
|
return {
|
||||||
displayName: sidebarAccount?.displayName || user?.displayName || null,
|
displayName: sidebarAccount?.displayName || user?.displayName || null,
|
||||||
avatarUrl: sidebarAccount?.avatarUrl || user?.avatarUrl || null,
|
avatarUrl: sidebarAccount?.avatarUrl || user?.avatarUrl || null,
|
||||||
|
|||||||
@@ -0,0 +1,81 @@
|
|||||||
|
/**
|
||||||
|
* 页面内"人工验证"信号判定。
|
||||||
|
*
|
||||||
|
* 旧实现对整页 innerText 做宽泛关键词匹配(验证码/安全验证/captcha/verification…),
|
||||||
|
* 聊天正文、会话标题、帮助文案里出现这些词就会被误判为需要人工验证。
|
||||||
|
* 这里收紧为三类真实挑战证据,三者满足其一才计数:
|
||||||
|
* 1. 已知验证码厂商组件可见(极验/阿里云盾/网易易盾/腾讯防水墙/hCaptcha/reCAPTCHA/Turnstile…);
|
||||||
|
* 2. 可见弹层(dialog/modal/captcha 容器)自身文案命中挑战短语,且文案很短(排除聊天内容容器);
|
||||||
|
* 3. 整页是稀疏拦截页(正文极短且命中挑战短语,典型如风控拦截/盾页面)。
|
||||||
|
*/
|
||||||
|
export function pageChallengeSignalCount(): number {
|
||||||
|
try {
|
||||||
|
const phrasePattern =
|
||||||
|
/(人机验证|安全验证|请完成验证|完成下方验证|拖动滑块|滑动验证|向右滑动|安全检测|访问异常|环境异常|verification required|security check|unusual traffic|verify (?:you are|to continue)|not a robot|请输入(?:图形)?验证码)/i
|
||||||
|
const isVisible = (node: Element): boolean => {
|
||||||
|
const style = window.getComputedStyle(node)
|
||||||
|
if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0') {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
const rect = node.getBoundingClientRect()
|
||||||
|
return rect.width > 4 && rect.height > 4
|
||||||
|
}
|
||||||
|
const normalize = (value: string | null | undefined): string =>
|
||||||
|
(value || '').replace(/\s+/g, ' ').trim()
|
||||||
|
|
||||||
|
const vendorSelectors = [
|
||||||
|
'[class*="geetest_"]',
|
||||||
|
'[id^="nc_"][id$="_wrapper"]',
|
||||||
|
'.nc-container',
|
||||||
|
'[class*="nocaptcha"]',
|
||||||
|
'[class*="yidun_modal"]',
|
||||||
|
'[class*="yidun_popup"]',
|
||||||
|
'[class*="yidun_panel"]',
|
||||||
|
'[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 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const surfaceSelectors =
|
||||||
|
'[role="dialog"], [aria-modal="true"], [class*="captcha"], [id*="captcha"], [class*="verify"], [class*="Verify"]'
|
||||||
|
for (const node of Array.from(document.querySelectorAll(surfaceSelectors)).slice(0, 50)) {
|
||||||
|
if (!isVisible(node)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
const text = normalize(
|
||||||
|
node instanceof HTMLElement ? node.innerText : node.textContent || '',
|
||||||
|
)
|
||||||
|
if (text && text.length <= 240 && phrasePattern.test(text)) {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const bodyText = normalize(document.body ? document.body.innerText : '')
|
||||||
|
if (bodyText && bodyText.length <= 400 && phrasePattern.test(bodyText)) {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0
|
||||||
|
} catch {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 注入 webContents.executeJavaScript 模板字符串使用的表达式片段,
|
||||||
|
* 求值结果为 number(0 或 1)。
|
||||||
|
*/
|
||||||
|
export const PAGE_CHALLENGE_SIGNAL_COUNT_SNIPPET = `(${pageChallengeSignalCount.toString()})()`
|
||||||
Reference in New Issue
Block a user