refactor(desktop): extract qwen auth rules into adapter module

Move the Qwen page-state probe, session detection, and silent probe
helpers out of account-binder into apps/desktop-client/src/main/adapters/qwen,
mirroring the Kimi adapter layout. Generic AI auth no longer special-cases
qwen since the platform now owns its own credential rules.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-06 21:33:40 +08:00
parent 14991d7801
commit ddce8b3d8d
5 changed files with 589 additions and 178 deletions
+24 -172
View File
@@ -30,6 +30,12 @@ import {
probeKimiAccountSession,
readKimiAuthPageState,
} from './adapters/kimi/auth-rules'
import {
detectQwenAccountFromSession,
probeQwenAccountSession,
QWEN_AUTH_PROBE_URL,
readQwenAuthPageState,
} from './adapters/qwen/auth-rules'
import type { AccountHealthProfile, AuthProbeResult } from './auth-types'
import {
attachWindowDiagnostics,
@@ -1263,134 +1269,6 @@ async function readGenericAIPageState(
return normalizeGenericAIPageStateSnapshot(state)
}
async function readQwenPageState(
webContents: WebContents | null | undefined,
): Promise<GenericAIPageState | null> {
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)/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 {
return aiPlatformCatalog.some((item) => item.id === platformId)
}
@@ -1525,33 +1403,8 @@ async function detectQwenFromSession(
avatarUrl?: string | null
} = {},
): Promise<DetectedAccount | null> {
const cookies = await session.cookies.get({ url: 'https://www.qianwen.com/' }).catch(() => [])
if (!cookies.length) {
return null
}
const cookieMap = new Map<string, string>()
for (const cookie of cookies) {
if (!cookieMap.has(cookie.name)) {
cookieMap.set(cookie.name, cookie.value)
}
}
const ticketHash = normalizeText(cookieMap.get('tongyi_sso_ticket_hash'))
const ticket = normalizeText(cookieMap.get('tongyi_sso_ticket'))
const platformUid = ticketHash ?? ticket
if (!platformUid) {
return null
}
if (!genericAIHasVisibleAccountSignal(pageState)) {
return null
}
return sanitizeDetectedAccount({
platformUid,
displayName: hints.displayName ?? '通义千问账号',
avatarUrl: hints.avatarUrl ?? null,
})
const detected = await detectQwenAccountFromSession(session, pageState, hints)
return detected ? sanitizeDetectedAccount(detected) : null
}
async function detectKimiFromSession(
@@ -1585,7 +1438,7 @@ async function detectQwenPlatform(
return null
}
const pageState = await readQwenPageState(context.webContents).catch(() => null)
const pageState = await readQwenAuthPageState(context.webContents).catch(() => null)
if ((pageState?.challengeSignalCount ?? 0) > 0) {
return null
}
@@ -1594,27 +1447,14 @@ async function detectQwenPlatform(
}
const fromSession = await detectQwenFromSession(context.session, pageState, {
displayName: pageState?.displayName ?? `${label} 会话`,
displayName: pageState?.displayName ?? null,
avatarUrl: pageState?.avatarUrl ?? null,
})
if (fromSession) {
return fromSession
}
if (!genericAIHasVisibleAccountSignal(pageState)) {
return null
}
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 detectDoubaoPlatform(
@@ -2330,7 +2170,9 @@ export async function probeAIAccountSession(
? DOUBAO_AUTH_PROBE_URL
: account.platform === 'kimi'
? KIMI_AUTH_PROBE_URL
: platformMeta.consoleUrl
: account.platform === 'qwen'
? QWEN_AUTH_PROBE_URL
: platformMeta.consoleUrl
const window = createBoundWindow(
`${platformMeta.label} AI 静默探针`,
@@ -2419,6 +2261,16 @@ export async function probeAIAccountSession(
})
}
if (account.platform === 'qwen') {
return await probeQwenAccountSession({
session: handle.session,
webContents: window.webContents,
currentURL,
fallbackDisplayName: account.displayName,
expectedPlatformUid: account.platformUid,
})
}
if (definitionLooksLikeLoginRedirect(currentURL, {
id: platformMeta.id,
label: platformMeta.label,