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) <noreply@anthropic.com>
This commit is contained in:
@@ -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¶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()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user