From 8bd2d88d88ef8fedff045a070bd8fc90e409fd57 Mon Sep 17 00:00:00 2001 From: Xu Liang Date: Sat, 9 May 2026 09:28:20 +0800 Subject: [PATCH] fix(qwen): redirect HavanaOne login bridge to qianwen home on success Detect the passport.qianwen.com bridge page after a successful SSO login and navigate the webContents back to the original returnUrl so account binding can proceed instead of stalling on the bridge. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../desktop-client/src/main/account-binder.ts | 20 +++++ .../src/main/adapters/qwen/auth-rules.test.ts | 50 ++++++++++++ .../src/main/adapters/qwen/auth-rules.ts | 76 +++++++++++++++++++ 3 files changed, 146 insertions(+) diff --git a/apps/desktop-client/src/main/account-binder.ts b/apps/desktop-client/src/main/account-binder.ts index 1afc95e..a6ff022 100644 --- a/apps/desktop-client/src/main/account-binder.ts +++ b/apps/desktop-client/src/main/account-binder.ts @@ -44,6 +44,7 @@ import { probeQwenAccountSession, QWEN_AUTH_PROBE_URL, readQwenAuthPageState, + redirectQwenSuccessfulLoginBridge, } from './adapters/qwen/auth-rules' import { readWenxinAccountIdentity } from './adapters/wenxin/account-identity' import { readYuanbaoAccountIdentity } from './adapters/yuanbao/account-identity' @@ -1573,6 +1574,25 @@ async function detectQwenPlatform( } const currentURL = context.webContents.getURL() + const bridgeRedirect = await redirectQwenSuccessfulLoginBridge(context.webContents).catch( + (error) => { + console.warn('[desktop-bind] qwen bridge redirect failed', { + url: currentURL, + message: error instanceof Error ? error.message : String(error), + }) + return null + }, + ) + if (bridgeRedirect) { + appendAccountBindDiagnostic({ + action: 'qwen_login_bridge_redirect', + platform: platformId, + url: diagnosticUrl(bridgeRedirect.currentURL), + returnUrl: diagnosticUrl(bridgeRedirect.returnURL), + }) + return null + } + const platformMeta = aiPlatformCatalog.find((item) => item.id === platformId) ?? null if (!platformMeta) { return null diff --git a/apps/desktop-client/src/main/adapters/qwen/auth-rules.test.ts b/apps/desktop-client/src/main/adapters/qwen/auth-rules.test.ts index 719e925..8661ae7 100644 --- a/apps/desktop-client/src/main/adapters/qwen/auth-rules.test.ts +++ b/apps/desktop-client/src/main/adapters/qwen/auth-rules.test.ts @@ -6,6 +6,7 @@ import { isQwenLoggedInCredentialCookieName, normalizeQwenDisplayName, qwenHasBoundAccountPageSignal, + resolveQwenLoginBridgeReturnURL, } from './auth-rules' function pageState(overrides: Partial = {}): GenericAIPageState { @@ -95,4 +96,53 @@ describe('qwen auth rules', () => { expect(isQwenLoggedInCredentialCookieName('_qk_bx_ck_v1')).toBe(false) expect(isQwenLoggedInCredentialCookieName('XSRF-TOKEN')).toBe(false) }) + + it('recovers the Qwen return URL from a successful HavanaOne bridge page', () => { + const params = Buffer.from( + JSON.stringify({ + loginResult: 'success', + st: 'success', + action: 'redirect', + redirectType: 'redirect', + returnUrl: 'https://www.qianwen.com/', + tongyi_sso_ticket: '4_demo_ticket', + }), + 'utf8', + ).toString('base64url') + + expect( + resolveQwenLoginBridgeReturnURL( + `https://passport.qianwen.com/havanaone/window_page_end_bridge.htm?sg=demo¶ms=${params}&st=null`, + ), + ).toBe('https://www.qianwen.com/') + }) + + it('does not redirect from unsuccessful or offsite Qwen bridge URLs', () => { + const failedParams = Buffer.from( + JSON.stringify({ + loginResult: 'failed', + returnUrl: 'https://www.qianwen.com/', + }), + 'utf8', + ).toString('base64url') + const offsiteParams = Buffer.from( + JSON.stringify({ + loginResult: 'success', + returnUrl: 'https://example.com/', + }), + 'utf8', + ).toString('base64url') + + expect( + resolveQwenLoginBridgeReturnURL( + `https://passport.qianwen.com/havanaone/window_page_end_bridge.htm?params=${failedParams}`, + ), + ).toBeNull() + expect( + resolveQwenLoginBridgeReturnURL( + `https://passport.qianwen.com/havanaone/window_page_end_bridge.htm?params=${offsiteParams}`, + ), + ).toBe('https://www.qianwen.com/') + expect(resolveQwenLoginBridgeReturnURL('https://www.qianwen.com/')).toBeNull() + }) }) diff --git a/apps/desktop-client/src/main/adapters/qwen/auth-rules.ts b/apps/desktop-client/src/main/adapters/qwen/auth-rules.ts index e293055..61b383c 100644 --- a/apps/desktop-client/src/main/adapters/qwen/auth-rules.ts +++ b/apps/desktop-client/src/main/adapters/qwen/auth-rules.ts @@ -13,6 +13,8 @@ import type { GenericAIPageState } from '../../generic-ai-auth' 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 @@ -65,6 +67,80 @@ 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