Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 84 additions & 9 deletions src/main/oauth/adapters/qwen-ai.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,12 +44,33 @@ export class QwenAiAdapter extends BaseOAuthAdapter {
})
}

private getCookieHeader(credentials: Record<string, string>): string {
const cookies = (credentials.cookies || credentials.cookie || '') as unknown
if (typeof cookies === 'string') {
return cookies
}
if (cookies && typeof cookies === 'object') {
return Object.entries(cookies)
.filter(([, value]) => value)
.map(([key, value]) => `${key}=${value}`)
.join('; ')
}
const token = credentials.token
return token ? `token=${token}` : ''
}

async loginWithToken(providerId: string, token: string): Promise<OAuthResult> {
this.emitProgress('pending', 'Validating Token...')

this.emitProgress('pending', 'Validating credentials...')

// 区分凭据类型:JWT token 以 "eyJ" 开头且有三段,否则视为完整 Cookie Header 字符串
// (注意:当前 UI 的 LoginDialog 中没有 qwen-ai 入口,此路径暂不可达,
// 此处修复是为防止未来接入时出现 Cookie 被误存为 token 的问题)
const isJwt = token.startsWith('eyJ') && token.split('.').length === 3
const credentials: Record<string, string> = isJwt ? { token } : { cookies: token }

try {
const validation = await this.validateToken({ token })
const validation = await this.validateToken(credentials)

if (!validation.valid) {
return {
success: false,
Expand All @@ -58,14 +79,14 @@ export class QwenAiAdapter extends BaseOAuthAdapter {
error: validation.error || 'Token validation failed',
}
}
this.emitProgress('success', 'Token validation successful')

this.emitProgress('success', 'Validation successful')

return {
success: true,
providerId,
providerType: 'qwen-ai',
credentials: { token },
credentials,
accountInfo: validation.accountInfo,
}
} catch (error) {
Expand All @@ -85,11 +106,65 @@ export class QwenAiAdapter extends BaseOAuthAdapter {

async validateToken(credentials: Record<string, string>): Promise<TokenValidationResult> {
const token = credentials.token
const rawCookies = credentials.cookies || credentials.cookie
const cookieHeader = this.getCookieHeader(credentials)

if (cookieHeader) {
try {
const response = await axios.post(
`${QWEN_AI_API_BASE}/api/v2/users/status`,
{
typarms: {
typarm1: 'web',
typarm3: 'prod',
typarm4: 'qwen_chat',
typarm5: 'product',
orgid: 'tongyi',
cdn_version: '0.2.45',
domain: 'chat.qwen.ai',
},
},
{
headers: {
Cookie: cookieHeader,
...FAKE_HEADERS,
Accept: 'application/json, text/plain, */*',
Referer: 'https://chat.qwen.ai/c/new-chat',
Version: '0.2.45',
},
timeout: 15000,
validateStatus: () => true,
}
)

if (response.status === 200 && response.data?.success && response.data?.data === true) {
return {
valid: true,
tokenType: 'cookie',
accountInfo: {
name: 'Qwen AI User',
},
}
}

if (rawCookies) {
return {
valid: false,
error: response.data?.errorMsg || `Validation failed: HTTP ${response.status}`,
}
}
} catch (error) {
return {
valid: false,
error: error instanceof Error ? error.message : 'Validation request failed',
}
}
}

if (!token) {
return {
valid: false,
error: 'Token cannot be empty',
error: 'Cookies cannot be empty',
}
}

Expand Down
31 changes: 31 additions & 0 deletions src/main/oauth/inAppLogin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export interface InAppLoginResult {
export interface TokenFoundEvent {
key: string
value: string
allCookies?: Record<string, string>
}

export interface InAppLoginOptions {
Expand Down Expand Up @@ -211,6 +212,20 @@ export class InAppLoginManager extends EventEmitter {
}
}

const cookieHeader = details.requestHeaders['Cookie'] || details.requestHeaders['cookie']
if (cookieHeader && typeof cookieHeader === 'string') {
const allCookiesObj = this.parseCookieHeader(cookieHeader)
for (const source of this.config!.tokenSources) {
if (source.type === 'cookie' && allCookiesObj[source.key] && this.isValidToken(allCookiesObj[source.key])) {
this.emit('tokenFound', {
key: source.key,
value: allCookiesObj[source.key],
allCookies: allCookiesObj,
})
}
}
}

callback({ requestHeaders: details.requestHeaders })
})

Expand Down Expand Up @@ -255,6 +270,22 @@ export class InAppLoginManager extends EventEmitter {
return Date.now() - this.loginStartTime >= MIN_LOGIN_TIME
}

private parseCookieHeader(cookieHeader: string): Record<string, string> {
const cookies: Record<string, string> = {}
for (const part of cookieHeader.split(';')) {
const trimmed = part.trim()
const equalIndex = trimmed.indexOf('=')
if (equalIndex > 0) {
const name = trimmed.substring(0, equalIndex)
const value = trimmed.substring(equalIndex + 1)
if (name && value) {
cookies[name] = value
}
}
}
return cookies
}

private delayedTokenCheck(): void {
const now = Date.now()
if (now - this.lastTokenCheckTime < 2000) return
Expand Down
99 changes: 81 additions & 18 deletions src/main/oauth/manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,27 +52,27 @@ export class OAuthManager extends EventEmitter {
*/
private getAdapter(providerId: string, providerType: ProviderType): BaseOAuthAdapter {
const key = `${providerId}_${providerType}`

if (!this.adapters.has(key)) {
const adapter = createAdapter(providerType, {
providerId,
providerType,
authMethods: [],
callbackPort: DEFAULT_CALLBACK_PORT,
})

if (this.mainWindow) {
adapter.setMainWindow(this.mainWindow)
}

adapter.setProgressCallback((event) => {
this.emit('progress', event)
this.sendProgressToRenderer(event)
})

this.adapters.set(key, adapter)
}

return this.adapters.get(key)!
}

Expand Down Expand Up @@ -100,7 +100,7 @@ export class OAuthManager extends EventEmitter {

return new Promise((resolve, reject) => {
const adapter = this.getAdapter(options.providerId, options.providerType)

const timeout = setTimeout(() => {
this.cancelLogin()
const result: OAuthResult = {
Expand Down Expand Up @@ -146,11 +146,11 @@ export class OAuthManager extends EventEmitter {
mimoPhToken?: string
): Promise<OAuthResult> {
const adapter = this.getAdapter(providerId, providerType)

if ('loginWithToken' in adapter && typeof (adapter as any).loginWithToken === 'function') {
return await (adapter as any).loginWithToken(providerId, token, realUserID, mimoUserId, mimoPhToken)
}

// For Mimo, validate with all three tokens
if (providerType === 'mimo') {
if (!mimoUserId || !mimoPhToken) {
Expand All @@ -166,7 +166,7 @@ export class OAuthManager extends EventEmitter {
user_id: mimoUserId,
ph_token: mimoPhToken,
})

if (!validation.valid) {
return {
success: false,
Expand All @@ -175,7 +175,7 @@ export class OAuthManager extends EventEmitter {
error: validation.error || 'Token validation failed',
}
}

return {
success: true,
providerId,
Expand All @@ -188,9 +188,9 @@ export class OAuthManager extends EventEmitter {
accountInfo: validation.accountInfo,
}
}

const validation = await adapter.validateToken({ token })

if (!validation.valid) {
return {
success: false,
Expand All @@ -199,7 +199,7 @@ export class OAuthManager extends EventEmitter {
error: validation.error || 'Token validation failed',
}
}

return {
success: true,
providerId,
Expand Down Expand Up @@ -327,18 +327,31 @@ export class OAuthManager extends EventEmitter {

let validationTimeout: NodeJS.Timeout | null = null

const stringifyCookies = (cookies: unknown): string => {
if (typeof cookies === 'string') {
return cookies
}
if (cookies && typeof cookies === 'object') {
return Object.entries(cookies as Record<string, string>)
.filter(([, value]) => value)
.map(([key, value]) => `${key}=${value}`)
.join('; ')
}
return ''
}

const tokenFoundHandler = async (event: { key: string; value: string; allCookies?: Record<string, string> }) => {
console.log('[OAuthManager] tokenFoundHandler called, isValidating:', isValidating, 'event:', event.key, event.value.substring(0, 50) + '...')

// Store the token
collectedTokens[event.key] = event.value

// Store all cookies if provided (needed for Cloudflare-protected requests)
if (event.allCookies) {
collectedTokens['cookies'] = event.allCookies as any
console.log('[OAuthManager] Stored all cookies:', Object.keys(event.allCookies).length, 'cookies')
}

console.log('[OAuthManager] Collected tokens:', Object.keys(collectedTokens))

// For MiniMax, we need both token and realUserID before validating
Expand Down Expand Up @@ -388,7 +401,7 @@ export class OAuthManager extends EventEmitter {
hasUserId: !!hasUserId,
hasPhToken: !!hasPhToken,
})

// Clear any existing timeout
if (validationTimeout) {
clearTimeout(validationTimeout)
Expand All @@ -413,8 +426,35 @@ export class OAuthManager extends EventEmitter {
}
}

if (providerType === 'qwen-ai') {
if (!collectedTokens.cookies) {
console.log('[OAuthManager] Qwen AI: got token, waiting for full cookies...')
if (validationTimeout) {
clearTimeout(validationTimeout)
}
validationTimeout = setTimeout(() => {
if (!collectedTokens.cookies) {
console.log('[OAuthManager] Qwen AI: full cookies not collected yet')
} else {
console.log('[OAuthManager] Qwen AI: full cookies collected, validating...')
validateAndComplete()
}
}, 1000)
return
}

if (!isValidating) {
console.log('[OAuthManager] Qwen AI: validating with full cookies...')
if (validationTimeout) {
clearTimeout(validationTimeout)
}
validateAndComplete()
return
}
}

// For non-MiniMax/Mimo providers, validate immediately when we have a token
if (providerType !== 'minimax' && providerType !== 'mimo') {
if (providerType !== 'minimax' && providerType !== 'mimo' && providerType !== 'qwen-ai') {
if (isValidating) {
console.log('[OAuthManager] Already validating, skipping')
return
Expand Down Expand Up @@ -482,6 +522,29 @@ export class OAuthManager extends EventEmitter {
ph_token: phToken,
}
console.log('[OAuthManager] Mimo: Final credentials prepared:', Object.keys(finalCredentials))
} else if (providerType === 'qwen-ai') {
const cookies = stringifyCookies(collectedTokens.cookies)
const token = collectedTokens.token

if (!cookies) {
console.log('[OAuthManager] Qwen AI: Missing full cookies, aborting validation')
this.sendProgressToRenderer({
status: 'pending',
message: 'Waiting for full cookies...',
})
isValidating = false
return
}

validationCredentials = {
...(token ? { token } : {}),
cookies,
}
finalCredentials = {
...(token ? { token } : {}),
cookies,
}
Comment on lines +543 to +546

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Avoid logging Qwen AI session cookies

When Qwen AI in-app login succeeds, finalCredentials now includes the full Cookie header, and the existing success path immediately stringifies finalCredentials to the main-process log. In successful Qwen AI logins this exposes reusable session cookies in logs; keep passing/storing the cookies, but redact the value or log only credential keys.

Useful? React with 👍 / 👎.

console.log('[OAuthManager] Qwen AI: Final credentials prepared:', Object.keys(finalCredentials))
} else {
validationCredentials = { ...collectedTokens }
finalCredentials = { ...collectedTokens }
Expand All @@ -492,7 +555,7 @@ export class OAuthManager extends EventEmitter {
console.log('[OAuthManager] Validation result:', validation)

if (validation.valid) {
console.log('[OAuthManager] Token is valid, completing login with credentials:', JSON.stringify(finalCredentials, null, 2))
console.log('[OAuthManager] Token is valid, completing login with credentials keys:', Object.keys(finalCredentials).join(', '))
inAppLoginManager.completeWithSuccess(finalCredentials)
} else {
console.log('[OAuthManager] Token validation failed:', validation.error)
Expand Down
Loading