fix(desktop): isolate AI account auth probes
This commit is contained in:
@@ -17,6 +17,19 @@ import {
|
||||
sessionFetchJson,
|
||||
sessionFetchText,
|
||||
} from './adapters/common'
|
||||
import {
|
||||
buildDoubaoSessionCredentialEvidence,
|
||||
DOUBAO_AUTH_PROBE_URL,
|
||||
doubaoHasBoundAccountPageSignal,
|
||||
probeDoubaoAccountSession,
|
||||
readDoubaoAuthPageState,
|
||||
} from './adapters/doubao/auth-rules'
|
||||
import {
|
||||
detectKimiAccountFromSession,
|
||||
KIMI_AUTH_PROBE_URL,
|
||||
probeKimiAccountSession,
|
||||
readKimiAuthPageState,
|
||||
} from './adapters/kimi/auth-rules'
|
||||
import type { AccountHealthProfile, AuthProbeResult } from './auth-types'
|
||||
import {
|
||||
attachWindowDiagnostics,
|
||||
@@ -24,8 +37,9 @@ import {
|
||||
loadWindowURLSafely,
|
||||
} from './external-window'
|
||||
import {
|
||||
buildGenericAIPageFingerprintFallback,
|
||||
genericAIPageLooksAuthenticated,
|
||||
genericAIHasVisibleAccountSignal,
|
||||
genericAIPlatformRequiresVisibleCredentialSignal,
|
||||
isLikelyPlatformAICredentialCookie,
|
||||
type GenericAIPageState,
|
||||
} from './generic-ai-auth'
|
||||
import {
|
||||
@@ -948,6 +962,38 @@ function sanitizeDetectedAccount(account: DetectedAccount): DetectedAccount {
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeGenericAIPageStateSnapshot(state: unknown): GenericAIPageState | null {
|
||||
if (!state || typeof state !== 'object') {
|
||||
return null
|
||||
}
|
||||
|
||||
const typed = state as Partial<GenericAIPageState>
|
||||
return {
|
||||
displayName: normalizeText(typed.displayName),
|
||||
avatarUrl: normalizeRemoteUrl(typed.avatarUrl),
|
||||
fingerprint: normalizeText(typed.fingerprint),
|
||||
credentialFingerprint: normalizeText(typed.credentialFingerprint),
|
||||
currentPath: normalizeText(typed.currentPath),
|
||||
strongAuthSignalCount:
|
||||
typeof typed.strongAuthSignalCount === 'number' &&
|
||||
Number.isFinite(typed.strongAuthSignalCount)
|
||||
? typed.strongAuthSignalCount
|
||||
: 0,
|
||||
loginSignalCount:
|
||||
typeof typed.loginSignalCount === 'number' && Number.isFinite(typed.loginSignalCount)
|
||||
? typed.loginSignalCount
|
||||
: 0,
|
||||
loggedOutSignalCount:
|
||||
typeof typed.loggedOutSignalCount === 'number' && Number.isFinite(typed.loggedOutSignalCount)
|
||||
? typed.loggedOutSignalCount
|
||||
: 0,
|
||||
challengeSignalCount:
|
||||
typeof typed.challengeSignalCount === 'number' && Number.isFinite(typed.challengeSignalCount)
|
||||
? typed.challengeSignalCount
|
||||
: 0,
|
||||
}
|
||||
}
|
||||
|
||||
async function readGenericAIPageState(
|
||||
webContents: WebContents | null | undefined,
|
||||
): Promise<GenericAIPageState | null> {
|
||||
@@ -1214,35 +1260,135 @@ async function readGenericAIPageState(
|
||||
)
|
||||
.catch(() => null)
|
||||
|
||||
if (!state || typeof state !== 'object') {
|
||||
return normalizeGenericAIPageStateSnapshot(state)
|
||||
}
|
||||
|
||||
async function readQwenPageState(
|
||||
webContents: WebContents | null | undefined,
|
||||
): Promise<GenericAIPageState | null> {
|
||||
if (!webContents || webContents.isDestroyed()) {
|
||||
return null
|
||||
}
|
||||
|
||||
const typed = state as Partial<GenericAIPageState>
|
||||
return {
|
||||
displayName: normalizeText(typed.displayName),
|
||||
avatarUrl: normalizeRemoteUrl(typed.avatarUrl),
|
||||
fingerprint: normalizeText(typed.fingerprint),
|
||||
credentialFingerprint: normalizeText(typed.credentialFingerprint),
|
||||
currentPath: normalizeText(typed.currentPath),
|
||||
strongAuthSignalCount:
|
||||
typeof typed.strongAuthSignalCount === 'number' &&
|
||||
Number.isFinite(typed.strongAuthSignalCount)
|
||||
? typed.strongAuthSignalCount
|
||||
: 0,
|
||||
loginSignalCount:
|
||||
typeof typed.loginSignalCount === 'number' && Number.isFinite(typed.loginSignalCount)
|
||||
? typed.loginSignalCount
|
||||
: 0,
|
||||
loggedOutSignalCount:
|
||||
typeof typed.loggedOutSignalCount === 'number' && Number.isFinite(typed.loggedOutSignalCount)
|
||||
? typed.loggedOutSignalCount
|
||||
: 0,
|
||||
challengeSignalCount:
|
||||
typeof typed.challengeSignalCount === 'number' && Number.isFinite(typed.challengeSignalCount)
|
||||
? typed.challengeSignalCount
|
||||
: 0,
|
||||
}
|
||||
const state = await webContents
|
||||
.executeJavaScript(
|
||||
`(() => {
|
||||
try {
|
||||
const normalize = (value) => {
|
||||
if (typeof value !== "string") return null;
|
||||
const trimmed = value.trim().replace(/\\s+/g, " ");
|
||||
return trimmed || null;
|
||||
};
|
||||
const isVisible = (node) => {
|
||||
if (!(node instanceof Element)) return false;
|
||||
const style = window.getComputedStyle(node);
|
||||
if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0') {
|
||||
return false;
|
||||
}
|
||||
const rect = node.getBoundingClientRect();
|
||||
return rect.width > 0 && rect.height > 0;
|
||||
};
|
||||
const isInsideAuthSurface = (node) => {
|
||||
let current = node instanceof Element ? node : null;
|
||||
for (let depth = 0; current && depth < 8; depth += 1) {
|
||||
const descriptor = [
|
||||
current.getAttribute('role') || '',
|
||||
current.getAttribute('aria-modal') || '',
|
||||
current.getAttribute('data-testid') || '',
|
||||
current.id || '',
|
||||
current.className || '',
|
||||
].join(' ');
|
||||
if (/(dialog|modal|popup|popover|login|signin|auth|scan|qrcode)/i.test(descriptor)) {
|
||||
return true;
|
||||
}
|
||||
current = current.parentElement;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
const bodyText = normalize(document.body?.innerText || "") || "";
|
||||
const loginSignalCount = /(登录|注册|扫码登录|手机号登录|请先登录|立即登录)/.test(bodyText)
|
||||
? 1
|
||||
: 0;
|
||||
const loggedOutSignalCount = /(未登录|登录后继续|请先登录|扫码登录|手机号登录)/.test(bodyText)
|
||||
? 1
|
||||
: 0;
|
||||
const challengeSignalCount = /(验证码|滑块|人机验证|安全验证|短信验证|captcha|verification)/i.test(bodyText)
|
||||
? 1
|
||||
: 0;
|
||||
const stopWords = /^(千问|通义千问|Qwen|Qwen3\\.5-千问|新建对话|我的空间|智能体|新分组|最近对话|更多|设置|登录|注册)$/i;
|
||||
const pickSidebarAccount = () => {
|
||||
const lowerBandTop = window.innerHeight * 0.72;
|
||||
const candidates = [];
|
||||
for (const node of Array.from(document.querySelectorAll('aside *, nav *, [class*="side"] *, [class*="Sidebar"] *, body *'))) {
|
||||
if (!(node instanceof HTMLElement) || !isVisible(node) || isInsideAuthSurface(node)) {
|
||||
continue;
|
||||
}
|
||||
const rect = node.getBoundingClientRect();
|
||||
if (rect.top < lowerBandTop || rect.left > Math.max(360, window.innerWidth * 0.42)) {
|
||||
continue;
|
||||
}
|
||||
const text = normalize(node.textContent || "");
|
||||
if (!text || text.length < 2 || text.length > 32 || stopWords.test(text)) {
|
||||
continue;
|
||||
}
|
||||
if (!/(qwen|千问|通义|^[\\u4e00-\\u9fa5A-Za-z][\\u4e00-\\u9fa5A-Za-z0-9_-]{1,31}$)/i.test(text)) {
|
||||
continue;
|
||||
}
|
||||
candidates.push({ text, top: rect.top, left: rect.left });
|
||||
}
|
||||
candidates.sort((left, right) => right.top - left.top || left.left - right.left);
|
||||
return candidates[0]?.text || null;
|
||||
};
|
||||
const pickAvatar = () => {
|
||||
const lowerBandTop = window.innerHeight * 0.72;
|
||||
for (const node of Array.from(document.images)) {
|
||||
if (!isVisible(node) || isInsideAuthSurface(node)) continue;
|
||||
const rect = node.getBoundingClientRect();
|
||||
if (rect.top < lowerBandTop || rect.left > Math.max(360, window.innerWidth * 0.42)) {
|
||||
continue;
|
||||
}
|
||||
const src = normalize(node.currentSrc || node.src || "");
|
||||
if (src && !/(logo|icon|favicon)/i.test(src)) {
|
||||
return src;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
const displayName = pickSidebarAccount();
|
||||
const avatarUrl = pickAvatar();
|
||||
const credentialFingerprintParts = [];
|
||||
try {
|
||||
const state = window.__qianwenChatAPI?.sharedValues?.chatAPI?.state;
|
||||
const userInfo = state?.userInfo || state?.user || state?.account || null;
|
||||
if (userInfo && typeof userInfo === 'object') {
|
||||
credentialFingerprintParts.push('qwen-state:' + JSON.stringify(userInfo).slice(0, 240));
|
||||
}
|
||||
} catch {}
|
||||
const strongSignals = [];
|
||||
if (displayName) strongSignals.push('displayName');
|
||||
if (avatarUrl) strongSignals.push('avatar');
|
||||
return {
|
||||
displayName,
|
||||
avatarUrl,
|
||||
fingerprint: null,
|
||||
credentialFingerprint: credentialFingerprintParts.length
|
||||
? credentialFingerprintParts.join('||')
|
||||
: null,
|
||||
currentPath: normalize(window.location.pathname || ''),
|
||||
strongAuthSignalCount: strongSignals.length,
|
||||
loginSignalCount,
|
||||
loggedOutSignalCount,
|
||||
challengeSignalCount,
|
||||
};
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
})();`,
|
||||
true,
|
||||
)
|
||||
.catch(() => null)
|
||||
|
||||
return normalizeGenericAIPageStateSnapshot(state)
|
||||
}
|
||||
|
||||
function isAIPlatformBinding(platformId: string): boolean {
|
||||
@@ -1254,27 +1400,6 @@ function normalizeURLPath(pathname: string): string {
|
||||
return normalized === '/' ? '' : normalized
|
||||
}
|
||||
|
||||
function isLikelyGenericAICredentialName(name: string): boolean {
|
||||
const normalized = name.trim().toLowerCase()
|
||||
if (/^(x[-_]?csrf[-_]?token|xsrf[-_]?token|csrf[-_]?token|csrf|_csrf)$/i.test(normalized)) {
|
||||
return false
|
||||
}
|
||||
return /(token|session|auth|login|passport|ticket|jwt|refresh|access)/i.test(name)
|
||||
}
|
||||
|
||||
function isLikelyGenericAICredentialValue(value: string | null | undefined): boolean {
|
||||
const normalized = normalizeText(value)
|
||||
if (!normalized) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (/^(true|false|null|undefined|0|1)$/i.test(normalized)) {
|
||||
return false
|
||||
}
|
||||
|
||||
return normalized.length >= 8
|
||||
}
|
||||
|
||||
async function buildGenericAISessionFingerprint(
|
||||
platformId: string,
|
||||
session: Session,
|
||||
@@ -1283,6 +1408,12 @@ async function buildGenericAISessionFingerprint(
|
||||
const parts: string[] = []
|
||||
|
||||
if (pageState?.credentialFingerprint) {
|
||||
if (
|
||||
genericAIPlatformRequiresVisibleCredentialSignal(platformId) &&
|
||||
!genericAIHasVisibleAccountSignal(pageState)
|
||||
) {
|
||||
return null
|
||||
}
|
||||
parts.push(pageState.credentialFingerprint)
|
||||
}
|
||||
|
||||
@@ -1293,9 +1424,11 @@ async function buildGenericAISessionFingerprint(
|
||||
try {
|
||||
const cookies = await session.cookies.get({ url })
|
||||
const authCookies = cookies.filter((cookie) => {
|
||||
return (
|
||||
isLikelyGenericAICredentialName(cookie.name) &&
|
||||
isLikelyGenericAICredentialValue(cookie.value)
|
||||
return isLikelyPlatformAICredentialCookie(
|
||||
platformId,
|
||||
cookie.name,
|
||||
cookie.value,
|
||||
pageState,
|
||||
)
|
||||
})
|
||||
if (authCookies.length > 0) {
|
||||
@@ -1312,13 +1445,13 @@ async function buildGenericAISessionFingerprint(
|
||||
}
|
||||
|
||||
if (parts.length === 0) {
|
||||
return buildGenericAIPageFingerprintFallback(platformId, pageState)
|
||||
return null
|
||||
}
|
||||
|
||||
return boundedIdentity(`${platformId}-session`, parts.join('||'))
|
||||
}
|
||||
|
||||
async function detectGenericAIPlatform(
|
||||
async function detectCredentialBackedAIPlatform(
|
||||
platformId: string,
|
||||
label: string,
|
||||
context: DetectContext,
|
||||
@@ -1342,7 +1475,10 @@ async function detectGenericAIPlatform(
|
||||
if ((pageState?.challengeSignalCount ?? 0) > 0) {
|
||||
return null
|
||||
}
|
||||
if (!genericAIPageLooksAuthenticated(currentURL, pageState)) {
|
||||
if ((pageState?.loggedOutSignalCount ?? 0) > 0) {
|
||||
return null
|
||||
}
|
||||
if ((pageState?.loginSignalCount ?? 0) > 0 && !genericAIHasVisibleAccountSignal(pageState)) {
|
||||
return null
|
||||
}
|
||||
const fingerprint = await buildGenericAISessionFingerprint(platformId, context.session, pageState)
|
||||
@@ -1357,8 +1493,33 @@ async function detectGenericAIPlatform(
|
||||
})
|
||||
}
|
||||
|
||||
async function detectDeepSeekPlatform(
|
||||
platformId: string,
|
||||
label: string,
|
||||
context: DetectContext,
|
||||
): Promise<DetectedAccount | null> {
|
||||
return await detectCredentialBackedAIPlatform(platformId, label, context)
|
||||
}
|
||||
|
||||
async function detectYuanbaoPlatform(
|
||||
platformId: string,
|
||||
label: string,
|
||||
context: DetectContext,
|
||||
): Promise<DetectedAccount | null> {
|
||||
return await detectCredentialBackedAIPlatform(platformId, label, context)
|
||||
}
|
||||
|
||||
async function detectWenxinPlatform(
|
||||
platformId: string,
|
||||
label: string,
|
||||
context: DetectContext,
|
||||
): Promise<DetectedAccount | null> {
|
||||
return await detectCredentialBackedAIPlatform(platformId, label, context)
|
||||
}
|
||||
|
||||
async function detectQwenFromSession(
|
||||
session: Session,
|
||||
pageState: GenericAIPageState | null,
|
||||
hints: {
|
||||
displayName?: string | null
|
||||
avatarUrl?: string | null
|
||||
@@ -1382,6 +1543,9 @@ async function detectQwenFromSession(
|
||||
if (!platformUid) {
|
||||
return null
|
||||
}
|
||||
if (!genericAIHasVisibleAccountSignal(pageState)) {
|
||||
return null
|
||||
}
|
||||
|
||||
return sanitizeDetectedAccount({
|
||||
platformUid,
|
||||
@@ -1392,28 +1556,14 @@ async function detectQwenFromSession(
|
||||
|
||||
async function detectKimiFromSession(
|
||||
session: Session,
|
||||
pageState: GenericAIPageState | null,
|
||||
hints: {
|
||||
displayName?: string | null
|
||||
avatarUrl?: string | null
|
||||
} = {},
|
||||
): Promise<DetectedAccount | null> {
|
||||
const cookies = await session.cookies.get({ url: 'https://www.kimi.com/' }).catch(() => [])
|
||||
if (!cookies.length) {
|
||||
return null
|
||||
}
|
||||
|
||||
const authCookie = cookies.find(
|
||||
(cookie) => cookie.name === 'kimi-auth' && isLikelyGenericAICredentialValue(cookie.value),
|
||||
)
|
||||
if (!authCookie) {
|
||||
return null
|
||||
}
|
||||
|
||||
return sanitizeDetectedAccount({
|
||||
platformUid: boundedIdentity('kimi-auth', authCookie.value),
|
||||
displayName: hints.displayName ?? 'Kimi 会话',
|
||||
avatarUrl: hints.avatarUrl ?? null,
|
||||
})
|
||||
const detected = await detectKimiAccountFromSession(session, pageState, hints)
|
||||
return detected ? sanitizeDetectedAccount(detected) : null
|
||||
}
|
||||
|
||||
async function detectQwenPlatform(
|
||||
@@ -1421,11 +1571,6 @@ async function detectQwenPlatform(
|
||||
label: string,
|
||||
context: DetectContext,
|
||||
): Promise<DetectedAccount | null> {
|
||||
const genericDetected = await detectGenericAIPlatform(platformId, label, context)
|
||||
if (genericDetected) {
|
||||
return genericDetected
|
||||
}
|
||||
|
||||
if (!context.webContents || context.webContents.isDestroyed()) {
|
||||
return null
|
||||
}
|
||||
@@ -1440,7 +1585,7 @@ async function detectQwenPlatform(
|
||||
return null
|
||||
}
|
||||
|
||||
const pageState = await readGenericAIPageState(context.webContents).catch(() => null)
|
||||
const pageState = await readQwenPageState(context.webContents).catch(() => null)
|
||||
if ((pageState?.challengeSignalCount ?? 0) > 0) {
|
||||
return null
|
||||
}
|
||||
@@ -1448,7 +1593,7 @@ async function detectQwenPlatform(
|
||||
return null
|
||||
}
|
||||
|
||||
const fromSession = await detectQwenFromSession(context.session, {
|
||||
const fromSession = await detectQwenFromSession(context.session, pageState, {
|
||||
displayName: pageState?.displayName ?? `${label} 会话`,
|
||||
avatarUrl: pageState?.avatarUrl ?? null,
|
||||
})
|
||||
@@ -1456,6 +1601,10 @@ async function detectQwenPlatform(
|
||||
return fromSession
|
||||
}
|
||||
|
||||
if (!genericAIHasVisibleAccountSignal(pageState)) {
|
||||
return null
|
||||
}
|
||||
|
||||
const fingerprint = await buildGenericAISessionFingerprint(platformId, context.session, pageState)
|
||||
if (!fingerprint) {
|
||||
return null
|
||||
@@ -1463,7 +1612,57 @@ async function detectQwenPlatform(
|
||||
|
||||
return sanitizeDetectedAccount({
|
||||
platformUid: fingerprint,
|
||||
displayName: pageState?.displayName ?? `${label} 会话`,
|
||||
displayName: pageState?.displayName ?? `${label} 账号`,
|
||||
avatarUrl: pageState?.avatarUrl ?? null,
|
||||
})
|
||||
}
|
||||
|
||||
async function detectDoubaoPlatform(
|
||||
platformId: string,
|
||||
label: string,
|
||||
context: DetectContext,
|
||||
): Promise<DetectedAccount | null> {
|
||||
if (!context.webContents || context.webContents.isDestroyed()) {
|
||||
return null
|
||||
}
|
||||
|
||||
const currentURL = context.webContents.getURL()
|
||||
const platformMeta = aiPlatformCatalog.find((item) => item.id === platformId) ?? null
|
||||
if (!platformMeta) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (currentURL && looksLikeLoginRedirect(currentURL, platformMeta.loginUrl)) {
|
||||
return null
|
||||
}
|
||||
|
||||
const pageState = await readDoubaoAuthPageState(context.webContents).catch(() => null)
|
||||
if ((pageState?.challengeSignalCount ?? 0) > 0) {
|
||||
return null
|
||||
}
|
||||
if ((pageState?.loggedOutSignalCount ?? 0) > 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
// Doubao is intentionally platform-specific. It allows anonymous chat, but
|
||||
// our desktop binding requires the real sidebar account menu plus login cookies.
|
||||
if (!doubaoHasBoundAccountPageSignal(pageState)) {
|
||||
return null
|
||||
}
|
||||
|
||||
const urls = new Set([platformMeta.loginUrl, platformMeta.consoleUrl])
|
||||
const credentialEvidence = await buildDoubaoSessionCredentialEvidence(
|
||||
context.session,
|
||||
urls,
|
||||
pageState,
|
||||
)
|
||||
if (!credentialEvidence) {
|
||||
return null
|
||||
}
|
||||
|
||||
return sanitizeDetectedAccount({
|
||||
platformUid: boundedIdentity('doubao-session', credentialEvidence),
|
||||
displayName: pageState?.displayName ?? `${label} 账号`,
|
||||
avatarUrl: pageState?.avatarUrl ?? null,
|
||||
})
|
||||
}
|
||||
@@ -1473,11 +1672,6 @@ async function detectKimiPlatform(
|
||||
label: string,
|
||||
context: DetectContext,
|
||||
): Promise<DetectedAccount | null> {
|
||||
const genericDetected = await detectGenericAIPlatform(platformId, label, context)
|
||||
if (genericDetected) {
|
||||
return genericDetected
|
||||
}
|
||||
|
||||
if (!context.webContents || context.webContents.isDestroyed()) {
|
||||
return null
|
||||
}
|
||||
@@ -1492,7 +1686,7 @@ async function detectKimiPlatform(
|
||||
return null
|
||||
}
|
||||
|
||||
const pageState = await readGenericAIPageState(context.webContents).catch(() => null)
|
||||
const pageState = await readKimiAuthPageState(context.webContents).catch(() => null)
|
||||
if ((pageState?.challengeSignalCount ?? 0) > 0) {
|
||||
return null
|
||||
}
|
||||
@@ -1500,7 +1694,7 @@ async function detectKimiPlatform(
|
||||
return null
|
||||
}
|
||||
|
||||
const fromSession = await detectKimiFromSession(context.session, {
|
||||
const fromSession = await detectKimiFromSession(context.session, pageState, {
|
||||
displayName: pageState?.displayName ?? `${label} 会话`,
|
||||
avatarUrl: pageState?.avatarUrl ?? null,
|
||||
})
|
||||
@@ -1508,16 +1702,7 @@ async function detectKimiPlatform(
|
||||
return fromSession
|
||||
}
|
||||
|
||||
const fingerprint = await buildGenericAISessionFingerprint(platformId, context.session, pageState)
|
||||
if (!fingerprint) {
|
||||
return null
|
||||
}
|
||||
|
||||
return sanitizeDetectedAccount({
|
||||
platformUid: fingerprint,
|
||||
displayName: pageState?.displayName ?? `${label} 会话`,
|
||||
avatarUrl: pageState?.avatarUrl ?? null,
|
||||
})
|
||||
return null
|
||||
}
|
||||
|
||||
async function detectAIPlatformAccount(
|
||||
@@ -1529,11 +1714,27 @@ async function detectAIPlatformAccount(
|
||||
return await detectQwenPlatform(platformId, label, context)
|
||||
}
|
||||
|
||||
if (platformId === 'doubao') {
|
||||
return await detectDoubaoPlatform(platformId, label, context)
|
||||
}
|
||||
|
||||
if (platformId === 'kimi') {
|
||||
return await detectKimiPlatform(platformId, label, context)
|
||||
}
|
||||
|
||||
return await detectGenericAIPlatform(platformId, label, context)
|
||||
if (platformId === 'deepseek') {
|
||||
return await detectDeepSeekPlatform(platformId, label, context)
|
||||
}
|
||||
|
||||
if (platformId === 'yuanbao') {
|
||||
return await detectYuanbaoPlatform(platformId, label, context)
|
||||
}
|
||||
|
||||
if (platformId === 'wenxin') {
|
||||
return await detectWenxinPlatform(platformId, label, context)
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
async function detectToutiao({
|
||||
@@ -2093,10 +2294,252 @@ async function resolvePublishAccountProfileFromHandle(
|
||||
return toPublishAccountProfile(detected)
|
||||
}
|
||||
|
||||
export async function probeAIAccountSession(
|
||||
account: PublishAccountIdentity,
|
||||
partition?: string,
|
||||
): Promise<AuthProbeResult> {
|
||||
const handle = partition
|
||||
? createSessionHandleForPartition(account.id, partition)
|
||||
: await findLocalPublishAccountSessionHandle(account)
|
||||
|
||||
if (!handle) {
|
||||
return {
|
||||
verdict: 'expired',
|
||||
reason: 'missing_partition',
|
||||
profile: null,
|
||||
sessionFingerprint: null,
|
||||
}
|
||||
}
|
||||
|
||||
const platformMeta = aiPlatformCatalog.find((item) => item.id === account.platform) ?? null
|
||||
if (!platformMeta) {
|
||||
return {
|
||||
verdict: 'network_error',
|
||||
reason: 'network_error',
|
||||
profile: null,
|
||||
sessionFingerprint: null,
|
||||
evidence: {
|
||||
platform: account.platform,
|
||||
code: 'desktop_ai_platform_not_supported',
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const aiProbeUrl =
|
||||
account.platform === 'doubao'
|
||||
? DOUBAO_AUTH_PROBE_URL
|
||||
: account.platform === 'kimi'
|
||||
? KIMI_AUTH_PROBE_URL
|
||||
: platformMeta.consoleUrl
|
||||
|
||||
const window = createBoundWindow(
|
||||
`${platformMeta.label} AI 静默探针`,
|
||||
aiProbeUrl,
|
||||
handle.session,
|
||||
{ show: false },
|
||||
)
|
||||
|
||||
let mainFrameLoadError:
|
||||
| {
|
||||
code: number
|
||||
description: string
|
||||
url: string
|
||||
}
|
||||
| undefined
|
||||
window.webContents.on(
|
||||
'did-fail-load',
|
||||
(_event, errorCode, errorDescription, validatedURL, isMainFrame) => {
|
||||
if (isMainFrame && errorCode !== -3) {
|
||||
mainFrameLoadError = {
|
||||
code: errorCode,
|
||||
description: errorDescription,
|
||||
url: validatedURL,
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
try {
|
||||
await waitForWindowNavigationSettled(window)
|
||||
if (window.isDestroyed()) {
|
||||
return {
|
||||
verdict: 'network_error',
|
||||
reason: 'network_error',
|
||||
profile: null,
|
||||
sessionFingerprint: null,
|
||||
}
|
||||
}
|
||||
|
||||
const loadError = mainFrameLoadError
|
||||
if (loadError) {
|
||||
return {
|
||||
verdict: 'network_error',
|
||||
reason: 'network_error',
|
||||
profile: null,
|
||||
sessionFingerprint: null,
|
||||
evidence: {
|
||||
errorCode: loadError.code,
|
||||
errorDescription: loadError.description,
|
||||
url: loadError.url,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const currentURL = window.webContents.getURL()
|
||||
if (!currentURL || currentURL.startsWith('data:text/html')) {
|
||||
return {
|
||||
verdict: 'network_error',
|
||||
reason: 'network_error',
|
||||
profile: null,
|
||||
sessionFingerprint: null,
|
||||
evidence: {
|
||||
url: currentURL,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
if (account.platform === 'doubao') {
|
||||
return await probeDoubaoAccountSession({
|
||||
session: handle.session,
|
||||
webContents: window.webContents,
|
||||
urls: new Set([platformMeta.loginUrl, platformMeta.consoleUrl]),
|
||||
currentURL,
|
||||
fallbackDisplayName: account.displayName,
|
||||
expectedPlatformUid: account.platformUid,
|
||||
})
|
||||
}
|
||||
|
||||
if (account.platform === 'kimi') {
|
||||
return await probeKimiAccountSession({
|
||||
session: handle.session,
|
||||
webContents: window.webContents,
|
||||
currentURL,
|
||||
fallbackDisplayName: account.displayName,
|
||||
expectedPlatformUid: account.platformUid,
|
||||
})
|
||||
}
|
||||
|
||||
if (definitionLooksLikeLoginRedirect(currentURL, {
|
||||
id: platformMeta.id,
|
||||
label: platformMeta.label,
|
||||
loginUrl: platformMeta.loginUrl,
|
||||
consoleUrl: platformMeta.consoleUrl,
|
||||
detect: (context: DetectContext) =>
|
||||
detectAIPlatformAccount(platformMeta.id, platformMeta.label, context),
|
||||
})) {
|
||||
return {
|
||||
verdict: 'expired',
|
||||
reason: 'login_redirect',
|
||||
profile: null,
|
||||
sessionFingerprint: null,
|
||||
evidence: {
|
||||
url: currentURL,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const pageState = await readGenericAIPageState(window.webContents)
|
||||
if ((pageState?.challengeSignalCount ?? 0) > 0) {
|
||||
return {
|
||||
verdict: 'challenge_required',
|
||||
reason: 'captcha_gate',
|
||||
profile: null,
|
||||
sessionFingerprint: null,
|
||||
evidence: {
|
||||
url: currentURL,
|
||||
challengeSignalCount: pageState?.challengeSignalCount ?? 0,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const detected = await detectAIPlatformAccount(account.platform, platformMeta.label, {
|
||||
session: handle.session,
|
||||
webContents: window.webContents,
|
||||
})
|
||||
if (detected) {
|
||||
return {
|
||||
verdict: 'active',
|
||||
reason: 'probe_success',
|
||||
profile: toAccountHealthProfile(toPublishAccountProfile(detected)),
|
||||
sessionFingerprint: detected.platformUid,
|
||||
}
|
||||
}
|
||||
|
||||
if (!pageState) {
|
||||
return {
|
||||
verdict: 'network_error',
|
||||
reason: 'network_error',
|
||||
profile: null,
|
||||
sessionFingerprint: null,
|
||||
evidence: {
|
||||
url: currentURL,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
if (pageState.loggedOutSignalCount > 0) {
|
||||
return {
|
||||
verdict: 'expired',
|
||||
reason: 'login_redirect',
|
||||
profile: null,
|
||||
sessionFingerprint: null,
|
||||
evidence: {
|
||||
url: currentURL,
|
||||
loggedOutSignalCount: pageState.loggedOutSignalCount,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
if ((pageState.loginSignalCount ?? 0) > 0 && (pageState.strongAuthSignalCount ?? 0) === 0) {
|
||||
return {
|
||||
verdict: 'expired',
|
||||
reason: 'login_redirect',
|
||||
profile: null,
|
||||
sessionFingerprint: null,
|
||||
evidence: {
|
||||
url: currentURL,
|
||||
loginSignalCount: pageState.loginSignalCount,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
verdict: 'expired',
|
||||
reason: 'login_redirect',
|
||||
profile: null,
|
||||
sessionFingerprint: null,
|
||||
evidence: {
|
||||
url: currentURL,
|
||||
strongAuthSignalCount: pageState.strongAuthSignalCount,
|
||||
loginSignalCount: pageState.loginSignalCount,
|
||||
loggedOutSignalCount: pageState.loggedOutSignalCount,
|
||||
},
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
verdict: 'network_error',
|
||||
reason: 'network_error',
|
||||
profile: null,
|
||||
sessionFingerprint: null,
|
||||
evidence: {
|
||||
message: error instanceof Error ? error.message : String(error),
|
||||
},
|
||||
}
|
||||
} finally {
|
||||
if (!window.isDestroyed()) {
|
||||
window.destroy()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function probePublishAccountSession(
|
||||
account: PublishAccountIdentity,
|
||||
partition?: string,
|
||||
): Promise<AuthProbeResult> {
|
||||
if (isAIPlatformBinding(account.platform)) {
|
||||
return await probeAIAccountSession(account, partition)
|
||||
}
|
||||
|
||||
const handle = partition
|
||||
? createSessionHandleForPartition(account.id, partition)
|
||||
: await findLocalPublishAccountSessionHandle(account)
|
||||
@@ -2298,106 +2741,6 @@ export async function probePublishAccountSession(
|
||||
}
|
||||
}
|
||||
|
||||
if (isAIPlatformBinding(account.platform)) {
|
||||
const pageState = await readGenericAIPageState(window.webContents)
|
||||
if ((pageState?.challengeSignalCount ?? 0) > 0) {
|
||||
return {
|
||||
verdict: 'challenge_required',
|
||||
reason: 'captcha_gate',
|
||||
profile: null,
|
||||
sessionFingerprint: null,
|
||||
evidence: {
|
||||
url: currentURL,
|
||||
challengeSignalCount: pageState?.challengeSignalCount ?? 0,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const detected = await detectAIPlatformAccount(account.platform, definition.label, {
|
||||
session: handle.session,
|
||||
webContents: window.webContents,
|
||||
})
|
||||
if (detected) {
|
||||
return {
|
||||
verdict: 'active',
|
||||
reason: 'probe_success',
|
||||
profile: toAccountHealthProfile(toPublishAccountProfile(detected)),
|
||||
sessionFingerprint: detected.platformUid,
|
||||
}
|
||||
}
|
||||
|
||||
if (!pageState) {
|
||||
return {
|
||||
verdict: 'network_error',
|
||||
reason: 'network_error',
|
||||
profile: null,
|
||||
sessionFingerprint: null,
|
||||
evidence: {
|
||||
url: currentURL,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
pageState.loggedOutSignalCount > 0 ||
|
||||
definitionLooksLikeLoginRedirect(currentURL, definition)
|
||||
) {
|
||||
return {
|
||||
verdict: 'expired',
|
||||
reason: 'login_redirect',
|
||||
profile: null,
|
||||
sessionFingerprint: null,
|
||||
evidence: {
|
||||
url: currentURL,
|
||||
loggedOutSignalCount: pageState.loggedOutSignalCount,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
if ((pageState.loginSignalCount ?? 0) > 0 && (pageState.strongAuthSignalCount ?? 0) === 0) {
|
||||
return {
|
||||
verdict: 'expired',
|
||||
reason: 'login_redirect',
|
||||
profile: null,
|
||||
sessionFingerprint: null,
|
||||
evidence: {
|
||||
url: currentURL,
|
||||
loginSignalCount: pageState.loginSignalCount,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const fingerprint = await buildGenericAISessionFingerprint(
|
||||
account.platform,
|
||||
handle.session,
|
||||
pageState,
|
||||
)
|
||||
if (genericAIPageLooksAuthenticated(currentURL, pageState)) {
|
||||
return {
|
||||
verdict: 'active',
|
||||
reason: 'probe_success',
|
||||
profile: fallbackAccountHealthProfile(account, {
|
||||
displayName: pageState.displayName ?? account.displayName,
|
||||
avatarUrl: pageState.avatarUrl ?? null,
|
||||
}),
|
||||
sessionFingerprint: fingerprint,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
verdict: 'network_error',
|
||||
reason: 'network_error',
|
||||
profile: null,
|
||||
sessionFingerprint: fingerprint,
|
||||
evidence: {
|
||||
url: currentURL,
|
||||
strongAuthSignalCount: pageState.strongAuthSignalCount,
|
||||
loginSignalCount: pageState.loginSignalCount,
|
||||
loggedOutSignalCount: pageState.loggedOutSignalCount,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const detected = await definition.detect({
|
||||
session: handle.session,
|
||||
webContents: window.webContents,
|
||||
@@ -2479,10 +2822,22 @@ export async function probePublishAccountSession(
|
||||
}
|
||||
}
|
||||
|
||||
export async function silentRefreshAIAccountSession(
|
||||
account: PublishAccountIdentity,
|
||||
partition?: string,
|
||||
): Promise<boolean> {
|
||||
const result = await probeAIAccountSession(account, partition).catch(() => null)
|
||||
return result?.verdict === 'active'
|
||||
}
|
||||
|
||||
export async function silentRefreshPublishAccountSession(
|
||||
account: PublishAccountIdentity,
|
||||
partition?: string,
|
||||
): Promise<boolean> {
|
||||
if (isAIPlatformBinding(account.platform)) {
|
||||
return await silentRefreshAIAccountSession(account, partition)
|
||||
}
|
||||
|
||||
const handle = partition
|
||||
? createSessionHandleForPartition(account.id, partition)
|
||||
: await findLocalPublishAccountSessionHandle(account)
|
||||
@@ -4049,7 +4404,10 @@ export async function openPublishAccountConsole(account: PublishAccountIdentity)
|
||||
if (!handle) {
|
||||
throw new Error(`desktop_account_session_expired:${account.platform}`)
|
||||
}
|
||||
const consoleURL = await resolvePublishAccountConsoleURL(definition, handle.session)
|
||||
const consoleURL =
|
||||
account.platform === 'doubao'
|
||||
? DOUBAO_AUTH_PROBE_URL
|
||||
: await resolvePublishAccountConsoleURL(definition, handle.session)
|
||||
if (platformRequiresDetectedAccountForConsoleAccess(account.platform)) {
|
||||
const detected = await definition
|
||||
.detect({
|
||||
|
||||
Reference in New Issue
Block a user