fix(qwen): redirect HavanaOne login bridge to qianwen home on success
Desktop Client Build / Resolve Build Metadata (push) Successful in 27s
Desktop Client Build / Build Desktop Client (push) Successful in 23m56s
Desktop Client Build / Publish Client Artifacts to NAS (push) Successful in 51s

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) <noreply@anthropic.com>
This commit is contained in:
Xu Liang
2026-05-09 09:28:20 +08:00
parent 3102e261ae
commit 8bd2d88d88
3 changed files with 146 additions and 0 deletions
@@ -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
@@ -6,6 +6,7 @@ import {
isQwenLoggedInCredentialCookieName,
normalizeQwenDisplayName,
qwenHasBoundAccountPageSignal,
resolveQwenLoginBridgeReturnURL,
} from './auth-rules'
function pageState(overrides: Partial<GenericAIPageState> = {}): 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&params=${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()
})
})
@@ -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<void> {
return new Promise((resolve) => setTimeout(resolve, ms))
}
function decodeQwenBridgeParams(value: string): Record<string, unknown> | 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