Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
71 changes: 70 additions & 1 deletion src/main/oauth/adapters/qwen-ai.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,21 @@ 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...')

Expand Down Expand Up @@ -85,11 +100,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
65 changes: 64 additions & 1 deletion src/main/oauth/manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -327,6 +327,19 @@ 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) + '...')

Expand Down Expand Up @@ -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 Down
12 changes: 6 additions & 6 deletions src/main/oauth/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export type OAuthStatus = 'idle' | 'pending' | 'success' | 'error' | 'cancelled'
/**
* Token type
*/
export type TokenType = 'jwt' | 'refresh' | 'access' | 'cookie'
export type TokenType = 'jwt' | 'refresh' | 'access' | 'cookie' | 'token'

/**
* OAuth login result
Expand Down Expand Up @@ -129,7 +129,7 @@ export interface ManualTokenConfig {
/**
* Manual input config for each provider
*/
export const MANUAL_TOKEN_CONFIGS: Record<ProviderType, ManualTokenConfig[]> = {
export const MANUAL_TOKEN_CONFIGS: Partial<Record<ProviderType, ManualTokenConfig[]>> = {
deepseek: [
{
providerType: 'deepseek',
Expand Down Expand Up @@ -191,10 +191,10 @@ export const MANUAL_TOKEN_CONFIGS: Record<ProviderType, ManualTokenConfig[]> = {
'qwen-ai': [
{
providerType: 'qwen-ai',
tokenType: 'jwt',
label: 'Auth Token',
placeholder: 'Enter JWT token from chat.qwen.ai',
description: 'JWT token obtained from chat.qwen.ai Local Storage (key: "token")',
tokenType: 'cookie',
label: 'Cookies',
placeholder: 'Paste full Cookie header from chat.qwen.ai browser request',
description: 'Full Cookie header from browser DevTools Network request to chat.qwen.ai',
helpUrl: 'https://chat.qwen.ai',
},
],
Expand Down
15 changes: 8 additions & 7 deletions src/main/providers/builtin/qwen-ai.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,14 @@ export const qwenAiConfig: BuiltinProviderConfig = {
id: 'qwen-ai',
name: 'Qwen AI (International)',
type: 'builtin',
authType: 'jwt',
authType: 'cookie',
apiEndpoint: 'https://chat.qwen.ai',
chatPath: '/api/v2/chat/completions',
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
source: 'web',
Version: '0.2.45',
},
enabled: true,
description: 'Qwen AI international version (chat.qwen.ai)',
Expand Down Expand Up @@ -64,17 +65,17 @@ export const qwenAiConfig: BuiltinProviderConfig = {
name: 'token',
label: 'Auth Token',
type: 'password',
required: true,
required: false,
placeholder: 'Enter JWT token from chat.qwen.ai',
helpText: 'JWT token obtained from chat.qwen.ai Local Storage (key: "token")',
helpText: 'JWT token obtained from chat.qwen.ai Local Storage (key: "token"). Optional if full cookies are provided.',
},
{
name: 'cookies',
label: 'Cookies (Optional)',
label: 'Cookies',
type: 'textarea',
required: false,
placeholder: 'Optional cookies for enhanced compatibility',
helpText: 'Full cookie string from browser DevTools (optional but recommended)',
required: true,
placeholder: 'Paste full Cookie header from chat.qwen.ai browser request',
helpText: 'Full Cookie header from browser DevTools Network request to chat.qwen.ai.',
},
],
}
Expand Down
49 changes: 39 additions & 10 deletions src/main/providers/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ export class ProviderChecker {
case 'qwen':
return this.checkQwenToken(account.credentials.ticket)
case 'qwen-ai':
return this.checkQwenAiToken(account.credentials.token)
return this.checkQwenAiToken(account.credentials)
case 'perplexity':
return this.checkPerplexityToken(account.credentials.sessionToken || account.credentials.token)
case 'mimo':
Expand Down Expand Up @@ -537,34 +537,63 @@ export class ProviderChecker {
}
}

private static async checkQwenAiToken(token: string): Promise<TokenCheckResult> {
private static async checkQwenAiToken(credentials: Record<string, string>): Promise<TokenCheckResult> {
try {
const response = await axios.get(
'https://chat.qwen.ai/api/v2/user',
const cookies = (credentials.cookies || credentials.cookie || '') as unknown
const cookieHeader = typeof cookies === 'string'
? cookies
: cookies && typeof cookies === 'object'
? Object.entries(cookies)
.filter(([, value]) => value)
.map(([key, value]) => `${key}=${value}`)
.join('; ')
: credentials.token
? `token=${credentials.token}`
: ''

if (!cookieHeader) {
return { valid: false, error: 'Cookies are required' }
}

const response = await axios.post(
'https://chat.qwen.ai/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: {
Authorization: `Bearer ${token}`,
Cookie: cookieHeader,
'Content-Type': 'application/json',
Accept: 'application/json',
Accept: 'application/json, text/plain, */*',
Origin: 'https://chat.qwen.ai',
Referer: 'https://chat.qwen.ai/c/new-chat',
source: 'web',
Version: '0.2.45',
},
timeout: CHECK_TIMEOUT,
validateStatus: () => true,
}
)

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

if (response.status === 401) {
return { valid: false, error: 'Token expired or invalid' }
return { valid: false, error: 'Cookies expired or invalid' }
}

return { valid: false, error: `Validation failed: HTTP ${response.status}` }
Expand Down
Loading