diff --git a/src/main/oauth/adapters/qwen-ai.ts b/src/main/oauth/adapters/qwen-ai.ts index a4416997..9467acad 100644 --- a/src/main/oauth/adapters/qwen-ai.ts +++ b/src/main/oauth/adapters/qwen-ai.ts @@ -44,12 +44,33 @@ export class QwenAiAdapter extends BaseOAuthAdapter { }) } + private getCookieHeader(credentials: Record): 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 { - 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 = isJwt ? { token } : { cookies: token } + try { - const validation = await this.validateToken({ token }) - + const validation = await this.validateToken(credentials) + if (!validation.valid) { return { success: false, @@ -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) { @@ -85,11 +106,65 @@ export class QwenAiAdapter extends BaseOAuthAdapter { async validateToken(credentials: Record): Promise { 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', } } diff --git a/src/main/oauth/inAppLogin.ts b/src/main/oauth/inAppLogin.ts index 8056d803..163f743e 100644 --- a/src/main/oauth/inAppLogin.ts +++ b/src/main/oauth/inAppLogin.ts @@ -17,6 +17,7 @@ export interface InAppLoginResult { export interface TokenFoundEvent { key: string value: string + allCookies?: Record } export interface InAppLoginOptions { @@ -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 }) }) @@ -255,6 +270,22 @@ export class InAppLoginManager extends EventEmitter { return Date.now() - this.loginStartTime >= MIN_LOGIN_TIME } + private parseCookieHeader(cookieHeader: string): Record { + const cookies: Record = {} + 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 diff --git a/src/main/oauth/manager.ts b/src/main/oauth/manager.ts index 96433b08..a42ba213 100644 --- a/src/main/oauth/manager.ts +++ b/src/main/oauth/manager.ts @@ -52,7 +52,7 @@ 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, @@ -60,19 +60,19 @@ export class OAuthManager extends EventEmitter { 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)! } @@ -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 = { @@ -146,11 +146,11 @@ export class OAuthManager extends EventEmitter { mimoPhToken?: string ): Promise { 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) { @@ -166,7 +166,7 @@ export class OAuthManager extends EventEmitter { user_id: mimoUserId, ph_token: mimoPhToken, }) - + if (!validation.valid) { return { success: false, @@ -175,7 +175,7 @@ export class OAuthManager extends EventEmitter { error: validation.error || 'Token validation failed', } } - + return { success: true, providerId, @@ -188,9 +188,9 @@ export class OAuthManager extends EventEmitter { accountInfo: validation.accountInfo, } } - + const validation = await adapter.validateToken({ token }) - + if (!validation.valid) { return { success: false, @@ -199,7 +199,7 @@ export class OAuthManager extends EventEmitter { error: validation.error || 'Token validation failed', } } - + return { success: true, providerId, @@ -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) + .filter(([, value]) => value) + .map(([key, value]) => `${key}=${value}`) + .join('; ') + } + return '' + } + const tokenFoundHandler = async (event: { key: string; value: string; allCookies?: Record }) => { 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 @@ -388,7 +401,7 @@ export class OAuthManager extends EventEmitter { hasUserId: !!hasUserId, hasPhToken: !!hasPhToken, }) - + // Clear any existing timeout if (validationTimeout) { clearTimeout(validationTimeout) @@ -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 @@ -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, + } + console.log('[OAuthManager] Qwen AI: Final credentials prepared:', Object.keys(finalCredentials)) } else { validationCredentials = { ...collectedTokens } finalCredentials = { ...collectedTokens } @@ -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) diff --git a/src/main/oauth/types.ts b/src/main/oauth/types.ts index 226ac1b6..443ec649 100644 --- a/src/main/oauth/types.ts +++ b/src/main/oauth/types.ts @@ -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 @@ -129,7 +129,7 @@ export interface ManualTokenConfig { /** * Manual input config for each provider */ -export const MANUAL_TOKEN_CONFIGS: Record = { +export const MANUAL_TOKEN_CONFIGS: Partial> = { deepseek: [ { providerType: 'deepseek', @@ -191,10 +191,10 @@ export const MANUAL_TOKEN_CONFIGS: Record = { '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', }, ], diff --git a/src/main/providers/builtin/qwen-ai.ts b/src/main/providers/builtin/qwen-ai.ts index c417e990..1acdfd35 100644 --- a/src/main/providers/builtin/qwen-ai.ts +++ b/src/main/providers/builtin/qwen-ai.ts @@ -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)', @@ -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.', }, ], } diff --git a/src/main/providers/checker.ts b/src/main/providers/checker.ts index bd0ba2f2..24179f7a 100644 --- a/src/main/providers/checker.ts +++ b/src/main/providers/checker.ts @@ -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': @@ -537,34 +537,63 @@ export class ProviderChecker { } } - private static async checkQwenAiToken(token: string): Promise { + private static async checkQwenAiToken(credentials: Record): Promise { 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}` } diff --git a/src/main/proxy/adapters/qwen-ai.ts b/src/main/proxy/adapters/qwen-ai.ts index 631639d8..0f04a455 100644 --- a/src/main/proxy/adapters/qwen-ai.ts +++ b/src/main/proxy/adapters/qwen-ai.ts @@ -17,18 +17,16 @@ const DEFAULT_HEADERS = { 'Accept-Language': 'zh-CN,zh;q=0.9', 'Content-Type': 'application/json', source: 'web', - 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36', - 'sec-ch-ua': '"Not:A-Brand";v="99", "Google Chrome";v="145", "Chromium";v="145"', + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/147.0.0.0 Safari/537.36 Edg/147.0.0.0', + 'sec-ch-ua': '"Microsoft Edge";v="147", "Not.A/Brand";v="8", "Chromium";v="147"', 'sec-ch-ua-mobile': '?0', - 'sec-ch-ua-platform': '"macOS"', + 'sec-ch-ua-platform': '"Windows"', 'Sec-Fetch-Dest': 'empty', 'Sec-Fetch-Mode': 'cors', 'Sec-Fetch-Site': 'same-origin', 'bx-v': '2.5.36', - 'bx-umidtoken': 'T2gAr9z8byN8sNOmfQ3X9j61MNTNmSqDO5L1rs2jMcQCVhOKgZICcBN-UdTuJGig-NM=', - 'bx-ua': '231!lWD36kmUe5E+joKDK5gBZ48FEl2ZWfPwIPF92lBLek2KxVW/XJ2EwruCiDOX5Px4EXNhmh6EfS9eDwQGRwijIK64A4nPqeLysJcDjUACje/H3J4ZgGZpicG6K8AkiGGaEKC830+QSiSUsLRlL/EyhXTmLcJc/5iDkMuOpUhNz0e0Q/nTqjVJ3ko00Q/oyE+jauHhUHfb1GxGHkE+++3+qCS4+ItkaA6tiItCo+romzElfLFD6RIj7oHt9vffs98nLwpHnaqKjufnLFMejSlAUGiQvTofIiGhIvftAMcoFV4mrUHsqyQ/ncQihmJHkbxXjvM57FCb6b9dEIRZl7jgj0+QLNLRs0NZ4azdZ6rzbGTSO8KA5I3Aq/3gBr87X16Mj0oJtaPKmFGaP2zghfOVhxQht8YjRd50lJa+Ue4PAuPSdu2O69DKLH8VOhrsB+psaBIRxnRi5POUQ6w8s8qlb9vxvExjHNOAKWXV1by1Nz+6FPWdyTeAgcmonjCcV0dCtPj/KyeVDkeSrDkKZjnDzHEqeCdfmJ65kve+Vy3YS0vagzyHfVEnzN0ULUZtkGfJXFNm6+bIa55wmGBhUeXbHL0EdlQXMu1YXxmcwBgTaq7tlQcfv7AefanbfjGE8R1IFnNyg2/jXLbnLg5Z6l1oKqgnxZQg0DE9BJuw6s0XjGwTdSxybWxp+WFD/RsXt76uwvCBk7z+YmSFLtFj2UlTsoq+vl0DTmsVItDKf9SZ94NcuJ7mxJYI02S/2kQBfbbHG0d4hXevDrEC0cb86EvzN2ud+v6bAunNRGNFz/RH0KLusoBVeo+puCFKeeIJWEo0t1UicX5YxJwMAoV7+g0gK93y4W9sMQtso8/wY5wsBzis9dwfLvIwXpaAM1g0MZp/YIRq8T/Qc+U/8x99tam4er0IWizvrkjqhIzCWBKpJ4Y4gj3bOmiS3VCMEaoVfKCwUWENwYKuP3H5VI0n+O2vVVRrekUrwvkm6URRhVhN4eEFTCjB9nSQu++qKyDH8HPpkS3YfwF8/OQtrZo7hQXxvNmP2HcH/K7zcweD00BaoOLiYUtXRItGYbl06sVSbm04soRf1Jqpyo3XiRqBWD9rmJfr4w8NOEGVGUCKXLDLsXy+8JC4Iqf0FsIjWxjMVdraTUtCbwXRbYUownQVm6bt7LYD1SNPoWNPqUJgsLMwP33ugrb1UbHCs24roOch6Go5QHIPA8E15SZE9pkr1SkmqrNs/+KRomFJ9HyFnWUYhZIV9MRLqlOAt6XBBTash3WJnCjhx/PZGhXVvdn2jX4+0Pm55LsiNugA8vaAUJQBxD/8a1u/RvTgbj35+b7I7m8tG0hMhClNZF+tpsOmZZhUGuXH9uVbkJMlMuAmMVCHwn3O31GlLeXXzzep2WS3xN2U+p5J0I7GySnuZUkuGs1ZTVqGUvR2g4q+7ljU55Ak78yPZiQXeUeqS74azszvZvCqWxXn2eePj+gcpliOjrYKpglUP19rQrMt8PqLt8L0ghIqVCmMwl3Hgr/VUcqDpXdpPTR=', Timezone: 'Mon Feb 23 2026 22:06:02 GMT+0800', - Version: '0.2.7', + Version: '0.2.45', Origin: 'https://chat.qwen.ai', } @@ -92,26 +90,56 @@ export class QwenAiAdapter { private getCookies(): string { const credentials = this.account.credentials - return credentials.cookies || credentials.cookie || '' + 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('; ') + } + return '' + } + + private getAuthCookie(): string { + const cookies = this.getCookies().trim() + if (cookies) { + return cookies + } + + const token = this.getToken().trim() + return token ? `token=${token}` : '' + } + + private redactHeaders(headers: Record): Record { + return Object.fromEntries( + Object.entries(headers).map(([key, value]) => [ + key, + /authorization|cookie|token|bx-ua|bx-umidtoken/i.test(key) ? '' : value, + ]) + ) } - private getHeaders(chatId?: string): Record { + private getHeaders(context: 'default' | 'new-chat' | 'completion' = 'default', chatId?: string): Record { const headers: Record = { ...DEFAULT_HEADERS, - Authorization: `Bearer ${this.getToken()}`, 'X-Request-Id': uuid(), } - if (chatId) { - headers['Referer'] = `https://chat.qwen.ai/c/${chatId}` + if (context === 'new-chat') { + headers['Accept'] = 'application/json, text/plain, */*' + headers['Referer'] = 'https://chat.qwen.ai/c/new-chat' + } else if (context === 'completion') { + headers['Referer'] = chatId ? `https://chat.qwen.ai/c/${chatId}` : 'https://chat.qwen.ai/c/new-chat' + } else { + headers['Referer'] = 'https://chat.qwen.ai/' } - const cookies = this.getCookies() - if (cookies) { - headers['Cookie'] = cookies - } else { - console.warn('[QwenAI] Warning: No cookies provided. This may cause Bad_Request error.') - console.warn('[QwenAI] Required cookies: cnaui, aui, sca, xlly_s, cna, token, _bl_uid, x-ap') + const authCookie = this.getAuthCookie() + if (authCookie) { + headers['Cookie'] = authCookie } return headers @@ -120,7 +148,7 @@ export class QwenAiAdapter { mapModel(openaiModel: string): string { let model = openaiModel let forceThinking: boolean | undefined - + if (model.endsWith('-thinking')) { forceThinking = true model = model.slice(0, -9) @@ -128,15 +156,15 @@ export class QwenAiAdapter { forceThinking = false model = model.slice(0, -5) } - - ;(this as any)._forceThinking = forceThinking - + + ; (this as any)._forceThinking = forceThinking + const lowerModel = model.toLowerCase() - + if (MODEL_ALIASES[lowerModel]) { return MODEL_ALIASES[lowerModel] } - + if (this.provider.modelMappings) { for (const [key, value] of Object.entries(this.provider.modelMappings)) { if (key.toLowerCase() === lowerModel) { @@ -144,7 +172,7 @@ export class QwenAiAdapter { } } } - + return model } @@ -161,7 +189,7 @@ export class QwenAiAdapter { try { const response = await this.axiosInstance.post(url, payload, { - headers: this.getHeaders(), + headers: this.getHeaders('new-chat'), }) console.log('[QwenAI] Create chat response:', JSON.stringify(response.data, null, 2)) @@ -208,7 +236,7 @@ export class QwenAiAdapter { try { console.log('[QwenAI] Deleting all chats for account') - + const response = await this.axiosInstance.delete(url, { headers: this.getHeaders(), }) @@ -231,13 +259,13 @@ export class QwenAiAdapter { chatId: string parentId: string | null }> { - const token = this.getToken() - if (!token) { - throw new Error('Qwen AI token not configured, please add token in account settings') + const authCookie = this.getAuthCookie() + if (!authCookie) { + throw new Error('Qwen AI cookies not configured, please add browser cookies in account settings') } const modelId = this.mapModel(request.model) - + // Get forced thinking mode setting from originalModel (preserves user's intent before mapping) // If originalModel exists, use it for thinking detection; otherwise fall back to request.model const modelForThinking = request.originalModel || request.model @@ -261,11 +289,11 @@ export class QwenAiAdapter { console.log('[QwenAI] Created new chat:', chatId) const messages = request.messages - + // Extract system message and user message let systemContent = '' let userContent = '' - + // Single-turn mode: extract all messages for (const msg of messages) { if (msg.role === 'system') { @@ -274,7 +302,7 @@ export class QwenAiAdapter { userContent = msg.content } } - + // If system prompt exists, prepend it to user content if (systemContent) { userContent = `${systemContent}\n\nUser: ${userContent}` @@ -289,19 +317,23 @@ export class QwenAiAdapter { // 1. Model name suffix: -thinking (force thinking), -fast (force fast mode) // 2. enable_thinking parameter for explicit control // 3. If neither is specified, thinking mode is disabled by default (fast mode) - const shouldEnableThinking = forceThinking !== undefined - ? forceThinking + const shouldEnableThinking = forceThinking !== undefined + ? forceThinking : request.enable_thinking === true - + const featureConfig: Record = { thinking_enabled: shouldEnableThinking, output_schema: 'phase', research_mode: 'normal', - auto_thinking: shouldEnableThinking, - thinking_format: 'summary', + auto_thinking: false, + thinking_mode: shouldEnableThinking ? 'Thinking' : 'Fast', auto_search: false, // Default to disable auto search } + if (shouldEnableThinking) { + featureConfig.thinking_format = 'summary' + } + if (request.thinking_budget) { featureConfig.thinking_budget = request.thinking_budget } @@ -332,7 +364,7 @@ export class QwenAiAdapter { parent_id: null, }, ], - timestamp: ts + 1, + timestamp: ts, } const url = `${QWEN_AI_BASE}/api/v2/chat/completions?chat_id=${chatId}` @@ -340,11 +372,11 @@ export class QwenAiAdapter { console.log('[QwenAI] Sending request to /api/v2/chat/completions...') console.log('[QwenAI] Request URL:', url) console.log('[QwenAI] Request payload:', JSON.stringify(payload, null, 2)) - console.log('[QwenAI] Request headers:', JSON.stringify(this.getHeaders(chatId), null, 2)) + console.log('[QwenAI] Request headers:', JSON.stringify(this.redactHeaders(this.getHeaders('completion', chatId)), null, 2)) const response = await this.axiosInstance.post(url, payload, { headers: { - ...this.getHeaders(chatId), + ...this.getHeaders('completion', chatId), 'x-accel-buffering': 'no', }, responseType: 'stream', @@ -387,11 +419,11 @@ export class QwenAiStreamHandler { private sendToolCalls(transStream: PassThrough): void { if (this.toolCallsSent) return - + const toolCalls = parseToolUse(this.content) if (toolCalls && toolCalls.length > 0) { this.toolCallsSent = true - + // Send tool_calls delta for (let i = 0; i < toolCalls.length; i++) { const tc = toolCalls[i] @@ -419,7 +451,7 @@ export class QwenAiStreamHandler { })}\n\n` ) } - + // Send finish with tool_calls transStream.write( `data: ${JSON.stringify({ @@ -467,7 +499,7 @@ export class QwenAiStreamHandler { onEvent: (event: any) => { try { console.log('[QwenAI] Parsed event:', event.event, 'data:', event.data?.substring(0, 200)) - + if (event.data === '[DONE]') { console.log('[QwenAI] Received [DONE] signal') return @@ -560,10 +592,10 @@ export class QwenAiStreamHandler { sendInitialChunk() } console.log('[QwenAI] Entering answer branch, content:', content) - + // Accumulate content for tool call detection this.content += content - + if (content) { console.log('[QwenAI] Sending content chunk:', content) const chunk = { @@ -582,7 +614,7 @@ export class QwenAiStreamHandler { } // Accumulate content for tool call detection this.content += content - + const chunk = { id: this.responseId || this.chatId, model: this.model, @@ -600,7 +632,7 @@ export class QwenAiStreamHandler { this.sendToolCalls(transStream) return } - + const finishReason = delta.finish_reason || 'stop' const finalChunk = { id: this.responseId || this.chatId, diff --git a/src/renderer/src/components/providers/AddAccountDialog.tsx b/src/renderer/src/components/providers/AddAccountDialog.tsx index d8abbbd2..58d05a5b 100644 --- a/src/renderer/src/components/providers/AddAccountDialog.tsx +++ b/src/renderer/src/components/providers/AddAccountDialog.tsx @@ -39,11 +39,32 @@ import type { Provider, CredentialField, Account, BuiltinProviderConfig, Provide function mapOAuthCredentials(providerId: string | undefined, credentials: Record): Record { if (!providerId) return credentials + const stringifyCookies = (cookies: unknown): string => { + if (typeof cookies === 'string') { + return cookies + } + if (cookies && typeof cookies === 'object') { + return Object.entries(cookies as Record) + .filter(([, value]) => value) + .map(([key, value]) => `${key}=${value}`) + .join('; ') + } + return '' + } + + if (providerId === 'qwen-ai') { + const cookies = stringifyCookies((credentials as Record).cookies || (credentials as Record).cookie) + const token = credentials.token || credentials['token'] + return { + ...(token ? { token } : {}), + ...(cookies ? { cookies } : {}), + } + } + const credentialKeyMap: Record = { 'glm': 'chatglm_refresh_token', 'deepseek': 'userToken', 'qwen': 'tongyi_sso_ticket', - 'qwen-ai': 'tongyi_sso_ticket', 'zai': 'tongyi_sso_ticket', 'perplexity': '__Secure-next-auth.session-token', 'mimo': 'serviceToken', @@ -53,7 +74,6 @@ function mapOAuthCredentials(providerId: string | undefined, credentials: Record 'glm': 'refresh_token', 'deepseek': 'token', 'qwen': 'ticket', - 'qwen-ai': 'ticket', 'zai': 'ticket', 'perplexity': 'sessionToken', 'mimo': 'service_token', diff --git a/src/renderer/src/components/providers/AddProviderDialog.tsx b/src/renderer/src/components/providers/AddProviderDialog.tsx index 2fe19a63..62db53f2 100644 --- a/src/renderer/src/components/providers/AddProviderDialog.tsx +++ b/src/renderer/src/components/providers/AddProviderDialog.tsx @@ -64,11 +64,32 @@ function mapOAuthCredentials(providerId: string | undefined, credentials: Record return credentials } + const stringifyCookies = (cookies: unknown): string => { + if (typeof cookies === 'string') { + return cookies + } + if (cookies && typeof cookies === 'object') { + return Object.entries(cookies as Record) + .filter(([, value]) => value) + .map(([key, value]) => `${key}=${value}`) + .join('; ') + } + return '' + } + + if (providerId === 'qwen-ai') { + const cookies = stringifyCookies((credentials as Record).cookies || (credentials as Record).cookie) + const token = credentials.token || credentials['token'] + return { + ...(token ? { token } : {}), + ...(cookies ? { cookies } : {}), + } + } + const credentialKeyMap: Record = { 'glm': 'chatglm_refresh_token', 'deepseek': 'userToken', 'qwen': 'tongyi_sso_ticket', - 'qwen-ai': 'tongyi_sso_ticket', 'zai': 'tongyi_sso_ticket', 'perplexity': '__Secure-next-auth.session-token', } @@ -77,7 +98,6 @@ function mapOAuthCredentials(providerId: string | undefined, credentials: Record 'glm': 'refresh_token', 'deepseek': 'token', 'qwen': 'ticket', - 'qwen-ai': 'ticket', 'zai': 'ticket', 'perplexity': 'sessionToken', } diff --git a/src/renderer/src/i18n/locales/en-US.json b/src/renderer/src/i18n/locales/en-US.json index 0899ed62..3c05453e 100644 --- a/src/renderer/src/i18n/locales/en-US.json +++ b/src/renderer/src/i18n/locales/en-US.json @@ -317,10 +317,10 @@ "description": "Qwen AI international version (chat.qwen.ai), supports thinking mode with reasoning output", "token": "Auth Token", "tokenPlaceholder": "Enter JWT token from chat.qwen.ai", - "tokenHelp": "JWT token obtained from chat.qwen.ai Local Storage (key: \"token\")", - "cookies": "Cookies (Optional)", - "cookiesPlaceholder": "Optional cookies for enhanced compatibility", - "cookiesHelp": "Full cookie string from browser DevTools (optional but recommended)", + "tokenHelp": "JWT token obtained from chat.qwen.ai Local Storage (key: \"token\"). Optional if full cookies are provided.", + "cookies": "Cookies", + "cookiesPlaceholder": "Paste full Cookie header from chat.qwen.ai browser request", + "cookiesHelp": "Full Cookie header from browser DevTools Network request to chat.qwen.ai.", "thinkingMode": "Thinking Mode", "thinkingModeDesc": "Enable thinking mode to get reasoning process before answer" }, diff --git a/src/renderer/src/i18n/locales/zh-CN.json b/src/renderer/src/i18n/locales/zh-CN.json index 9a6b390a..58c51d78 100644 --- a/src/renderer/src/i18n/locales/zh-CN.json +++ b/src/renderer/src/i18n/locales/zh-CN.json @@ -317,10 +317,10 @@ "description": "Qwen AI 国际版 (chat.qwen.ai),支持思考模式并输出推理过程", "token": "认证令牌", "tokenPlaceholder": "请输入 chat.qwen.ai 的 JWT Token", - "tokenHelp": "从 chat.qwen.ai 网页版 Local Storage 获取 (key: \"token\")", - "cookies": "Cookies (可选)", - "cookiesPlaceholder": "可选的 Cookies 用于增强兼容性", - "cookiesHelp": "从浏览器 DevTools 获取的完整 Cookie 字符串(可选但推荐)", + "tokenHelp": "从 chat.qwen.ai 网页版 Local Storage 获取 (key: \"token\")。如果已提供完整 Cookie,可留空", + "cookies": "Cookies", + "cookiesPlaceholder": "粘贴 chat.qwen.ai 浏览器请求中的完整 Cookie Header", + "cookiesHelp": "从浏览器 DevTools Network 里复制 chat.qwen.ai 请求的完整 Cookie Header", "thinkingMode": "思考模式", "thinkingModeDesc": "启用思考模式,在回答前输出推理过程" },