import type { Cookie, Session, WebContents } from 'electron/main' import { normalizeRemoteUrl } from '../common' import type { AccountHealthProfile, AuthProbeResult } from '../../auth-types' import type { GenericAIPageState } from '../../generic-ai-auth' // Qwen binding rules are deliberately isolated here. // Qwen can keep guest chats and cached sidebar sessions before/without a real // login. Geo Rankly must require the real signed-in Qwen account state before // binding. Do not move these DOM selectors, window state probes, or cookie names // into generic AI auth helpers, and do not infer the account name from recent // conversation titles. export const QWEN_AUTH_PROBE_URL = 'https://www.qianwen.com/' const QWEN_LOGGED_IN_COOKIE_NAMES = new Set(['tongyi_sso_ticket', 'tongyi_sso_ticket_hash']) const QWEN_LOGIN_BRIDGE_HOST = 'passport.qianwen.com' const QWEN_LOGIN_BRIDGE_PATH = '/havanaone/window_page_end_bridge.htm' const QWEN_ACCOUNT_TEXT_STOP_WORDS = /^(千问|通义千问|Qwen|Qwen3\.5-千问|新建对话|我的空间|智能体|对话分组|新分组|最近对话|更多|设置|登录|注册|下载电脑端|API 服务|你好!初次对话)$/i function normalizeText(value: unknown): string | null { if (typeof value !== 'string') { return null } const trimmed = value.trim().replace(/\s+/g, ' ') return trimmed ? trimmed : null } function hasCredentialValue(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 } function hashText(value: string): string { let hash = 0 for (let index = 0; index < value.length; index += 1) { hash = ((hash << 5) - hash + value.charCodeAt(index)) | 0 } return Math.abs(hash).toString(16).padStart(8, '0') } function boundedIdentity(prefix: string, raw: string): string { const normalizedPrefix = prefix.trim().replace(/:+$/, '') const normalizedRaw = raw.trim() if (!normalizedRaw) { return `${normalizedPrefix}:${hashText(prefix)}` } const candidate = `${normalizedPrefix}:${normalizedRaw}` if (candidate.length <= 120) { return candidate } return `${normalizedPrefix}:${hashText(normalizedRaw)}` } function delay(ms: number): Promise { return new Promise((resolve) => setTimeout(resolve, ms)) } function decodeQwenBridgeParams(value: string): Record | null { const normalized = value.trim() if (!normalized) { return null } const decodeAs = (encoding: BufferEncoding) => { try { return JSON.parse(Buffer.from(normalized, encoding).toString('utf8')) as Record< string, unknown > } catch { return null } } return decodeAs('base64url') ?? decodeAs('base64') } function isQwenWebURL(value: unknown): value is string { if (typeof value !== 'string') { return false } try { const parsed = new URL(value) return parsed.protocol === 'https:' && parsed.hostname === 'www.qianwen.com' } catch { return false } } export function resolveQwenLoginBridgeReturnURL(currentURL: string): string | null { try { const parsed = new URL(currentURL) if ( parsed.protocol !== 'https:' || parsed.hostname !== QWEN_LOGIN_BRIDGE_HOST || parsed.pathname !== QWEN_LOGIN_BRIDGE_PATH ) { return null } const params = decodeQwenBridgeParams(parsed.searchParams.get('params') ?? '') const loginResult = typeof params?.loginResult === 'string' ? params.loginResult : null const status = typeof params?.st === 'string' ? params.st : null if (loginResult !== 'success' && status !== 'success') { return null } return isQwenWebURL(params?.returnUrl) ? params.returnUrl : QWEN_AUTH_PROBE_URL } catch { return null } } export async function redirectQwenSuccessfulLoginBridge( webContents: WebContents | null | undefined, ): Promise<{ currentURL: string; returnURL: string } | null> { if (!webContents || webContents.isDestroyed()) { return null } const currentURL = webContents.getURL() const returnURL = resolveQwenLoginBridgeReturnURL(currentURL) if (!returnURL) { return null } await webContents.loadURL(returnURL) return { currentURL, returnURL } } function normalizeQwenPageStateSnapshot(state: unknown): GenericAIPageState | null { if (!state || typeof state !== 'object') { return null } const typed = state as Partial return { displayName: normalizeQwenDisplayName(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, } } export function normalizeQwenDisplayName(value: unknown): string | null { const normalized = normalizeText(value) if (!normalized) { return null } if (/^(?:通义千问|千问|Qwen)\s*(?:会话|账号|account|session)$/i.test(normalized)) { return null } if (normalized.length < 2 || normalized.length > 48) { return null } if (QWEN_ACCOUNT_TEXT_STOP_WORDS.test(normalized)) { return null } return normalized } export function qwenHasBoundAccountPageSignal( pageState: GenericAIPageState | null | undefined, ): boolean { return Boolean( pageState && pageState.strongAuthSignalCount >= 2 && pageState.credentialFingerprint, ) } export function isQwenLoggedInCredentialCookieName(name: string): boolean { return QWEN_LOGGED_IN_COOKIE_NAMES.has(name.trim().toLowerCase()) } export function isQwenLoggedInCredentialCookie( cookie: Pick, pageState: GenericAIPageState | null | undefined, ): boolean { return ( qwenHasBoundAccountPageSignal(pageState) && isQwenLoggedInCredentialCookieName(cookie.name) && hasCredentialValue(cookie.value) ) } export async function buildQwenSessionCredentialEvidence( session: Session, pageState: GenericAIPageState | null | undefined, ): Promise { if (!qwenHasBoundAccountPageSignal(pageState)) { return null } const parts: string[] = [] const credentialFingerprint = normalizeText(pageState?.credentialFingerprint) if (credentialFingerprint) { parts.push(`page:${credentialFingerprint}`) } const cookies = await session.cookies.get({ url: QWEN_AUTH_PROBE_URL }).catch(() => []) const authCookies = cookies.filter((cookie) => isQwenLoggedInCredentialCookie(cookie, pageState)) if (authCookies.length === 0) { return null } const serialized = authCookies .map((cookie) => `${cookie.domain}|${cookie.name}=${cookie.value}`) .sort() .join(';') parts.push(`cookies:${serialized}`) return parts.length > 0 ? parts.join('||') : null } export async function readQwenAuthPageState( webContents: WebContents | null | undefined, ): Promise { if (!webContents || webContents.isDestroyed()) { return null } 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|phone|captcha)/i.test(descriptor)) { return true; } current = current.parentElement; } return false; }; const stopWords = /^(千问|通义千问|Qwen|Qwen3\\.5-千问|新建对话|我的空间|智能体|对话分组|新分组|最近对话|更多|设置|登录|注册|下载电脑端|API 服务|你好!初次对话)$/i; const normalizeName = (value) => { const text = normalize(value || ""); if (!text || /^(?:通义千问|千问|Qwen)\\s*(?:会话|账号|account|session)$/i.test(text)) return null; if (text.length < 2 || text.length > 48 || stopWords.test(text)) return null; return text; }; const pickUserObject = () => { const raw = window._USER_; if (!raw || typeof raw !== 'object') return null; const userId = normalize(String(raw.userId || raw.aliyunUid || raw.uid || raw.id || "")); const displayName = normalizeName(raw.showName || raw.nickName || raw.nickname || raw.name || raw.displayName || ""); const avatarUrl = normalize(raw.avatarUrl || raw.avatar || raw.headUrl || raw.picture || ""); if (!userId) return null; return { userId, displayName, avatarUrl }; }; const isLoginState = () => { try { return window.__qianwenChatAPI?.sharedValues?.chatAPI?.state?.isLogin === true; } catch { return false; } }; const findSidebar = () => { const direct = document.querySelector('#new-nav-tab-wrapper'); if (direct instanceof HTMLElement && isVisible(direct)) return direct; for (const node of Array.from(document.querySelectorAll('aside'))) { if (node instanceof HTMLElement && isVisible(node)) return node; } return null; }; const pickSidebarAccount = () => { const sidebar = findSidebar(); if (!sidebar) return null; const sidebarRect = sidebar.getBoundingClientRect(); const lowerBandTop = sidebarRect.top + sidebarRect.height * 0.86; const candidates = []; const triggers = Array.from( sidebar.querySelectorAll('[aria-haspopup="menu"], [type="button"], button, [role="button"], .tongyiDI-view-container, [class*="desktop-no-drag"]'), ); for (const node of triggers) { if (!(node instanceof HTMLElement) || !isVisible(node) || isInsideAuthSurface(node)) continue; const rect = node.getBoundingClientRect(); if (rect.top < lowerBandTop || rect.left > sidebarRect.right || rect.width > sidebarRect.width * 0.9) { continue; } const image = node.querySelector('img'); const avatarUrl = image instanceof HTMLImageElement ? normalize(image.currentSrc || image.src || "") : null; const leafNames = Array.from(node.querySelectorAll('div, span, p')) .filter((child) => child instanceof HTMLElement && isVisible(child) && child.children.length === 0) .map((child) => normalizeName(child.textContent || "")) .filter(Boolean); const displayName = leafNames[0] || normalizeName(node.textContent || ""); if (!displayName && !avatarUrl) continue; candidates.push({ displayName, avatarUrl, top: rect.top, hasMenu: node.getAttribute('aria-haspopup') === 'menu' }); } candidates.sort((left, right) => Number(right.hasMenu) - Number(left.hasMenu) || right.top - left.top); const candidate = candidates[0]; return candidate ? { displayName: candidate.displayName || null, avatarUrl: candidate.avatarUrl || null } : null; }; const bodyText = normalize(document.body?.innerText || "") || ""; const user = pickUserObject(); const loggedInState = isLoginState(); const sidebarAccount = pickSidebarAccount(); const credentialParts = []; if (user?.userId) credentialParts.push('user_id=' + user.userId); if (loggedInState) credentialParts.push('is_login=true'); const strongSignals = []; if (user?.userId) strongSignals.push('qwenUserObject'); if (loggedInState) strongSignals.push('qwenIsLogin'); if (sidebarAccount?.displayName) strongSignals.push('sidebarDisplayName'); if (sidebarAccount?.avatarUrl || user?.avatarUrl) strongSignals.push('avatar'); const loginSignalCount = /(登录|注册|扫码登录|手机号登录|请先登录|立即登录|登录可同步历史对话)/.test(bodyText) ? 1 : 0; const loggedOutSignalCount = /(未登录|登录后继续|请先登录|扫码登录|手机号登录|登录可同步历史对话)/.test(bodyText) ? 1 : 0; const challengeSignalCount = /(验证码|滑块|人机验证|安全验证|短信验证|captcha|verification)/i.test(bodyText) ? 1 : 0; return { displayName: sidebarAccount?.displayName || user?.displayName || null, avatarUrl: sidebarAccount?.avatarUrl || user?.avatarUrl || null, fingerprint: null, credentialFingerprint: credentialParts.length ? credentialParts.join('||') : null, currentPath: normalize(window.location.pathname || ''), strongAuthSignalCount: strongSignals.length, loginSignalCount, loggedOutSignalCount: loggedOutSignalCount && !user?.userId && !loggedInState ? loggedOutSignalCount : 0, challengeSignalCount, }; } catch { return null; } })();`, true, ) .catch(() => null) return normalizeQwenPageStateSnapshot(state) } export async function detectQwenAccountFromSession( session: Session, pageState: GenericAIPageState | null, hints: { displayName?: string | null avatarUrl?: string | null } = {}, ): Promise<{ platformUid: string; displayName: string; avatarUrl: string | null } | null> { const credentialEvidence = await buildQwenSessionCredentialEvidence(session, pageState) if (!credentialEvidence) { return null } const displayName = normalizeQwenDisplayName(hints.displayName) ?? normalizeQwenDisplayName(pageState?.displayName) if (!displayName) { return null } return { platformUid: boundedIdentity('qwen-session', credentialEvidence), displayName, avatarUrl: hints.avatarUrl ?? pageState?.avatarUrl ?? null, } } export async function probeQwenAccountSession(input: { session: Session webContents: WebContents | null | undefined currentURL: string fallbackDisplayName: string expectedPlatformUid?: string | null }): Promise { const startedAt = Date.now() let pageState: GenericAIPageState | null = null let credentialEvidence: string | null = null while (Date.now() - startedAt < 8_000) { pageState = await readQwenAuthPageState(input.webContents).catch(() => null) if ((pageState?.challengeSignalCount ?? 0) > 0) { break } if ((pageState?.loggedOutSignalCount ?? 0) > 0) { break } if (qwenHasBoundAccountPageSignal(pageState)) { const nextCredentialEvidence = await buildQwenSessionCredentialEvidence( input.session, pageState, ) if (nextCredentialEvidence) { credentialEvidence = nextCredentialEvidence } if (credentialEvidence && normalizeQwenDisplayName(pageState?.displayName)) { break } } await delay(250) } if ((pageState?.challengeSignalCount ?? 0) > 0) { return { verdict: 'challenge_required', reason: 'captcha_gate', profile: null, sessionFingerprint: null, evidence: { url: input.currentURL, challengeSignalCount: pageState?.challengeSignalCount ?? 0, }, } } if ((pageState?.loggedOutSignalCount ?? 0) > 0) { return { verdict: 'expired', reason: 'login_redirect', profile: null, sessionFingerprint: null, evidence: { url: input.currentURL, loggedOutSignalCount: pageState?.loggedOutSignalCount ?? 0, }, } } if (!qwenHasBoundAccountPageSignal(pageState)) { return { verdict: 'expired', reason: 'login_redirect', profile: null, sessionFingerprint: null, evidence: { url: input.currentURL, code: 'qwen_account_state_missing', strongAuthSignalCount: pageState?.strongAuthSignalCount ?? 0, loginSignalCount: pageState?.loginSignalCount ?? 0, loggedOutSignalCount: pageState?.loggedOutSignalCount ?? 0, }, } } if (!credentialEvidence) { return { verdict: 'expired', reason: 'login_redirect', profile: null, sessionFingerprint: null, evidence: { url: input.currentURL, code: 'qwen_login_cookie_missing', }, } } const sessionFingerprint = boundedIdentity('qwen-session', credentialEvidence) const profile: AccountHealthProfile = { subjectUid: sessionFingerprint, displayName: normalizeQwenDisplayName(pageState?.displayName) ?? normalizeQwenDisplayName(input.fallbackDisplayName) ?? null, avatarUrl: pageState?.avatarUrl ?? null, } return { verdict: 'active', reason: 'probe_success', profile, sessionFingerprint, } }