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
@@ -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