feat: 优化OAuth授权流程并更新UI样式

- 在OAuth授权成功页面添加倒计时自动关闭功能,提升用户体验
- 改进授权弹窗通信机制,支持postMessage方式主动关闭窗口
- 更新Gemini OAuth回调页面,添加提供商标识和跨窗口通信
- 重构Grok API错误处理和重试逻辑,增强网络稳定性
- 修改头部组件购买链接为AI账号购买,并更新对应样式
This commit is contained in:
hex2077 2026-03-20 22:39:52 +08:00
parent 2a3312df15
commit 46038a5459
8 changed files with 322 additions and 139 deletions

View file

@ -1 +1 @@
2.11.6
2.11.7

View file

@ -42,10 +42,42 @@ const activeServers = new Map();
* 生成 HTML 响应页面
* @param {boolean} isSuccess - 是否成功
* @param {string} message - 显示消息
* @param {string|null} provider - 提供商标识
* @returns {string} HTML 内容
*/
function generateResponsePage(isSuccess, message) {
function generateResponsePage(isSuccess, message, provider = null) {
const title = isSuccess ? '授权成功!' : '授权失败';
const countdownHtml = isSuccess ? `
<p>此窗口将在 <span id="countdown" style="font-weight: bold; color: #2196f3;">10</span> </p>
<script>
const notifyOpener = () => {
try {
if (window.opener && !window.opener.closed) {
window.opener.postMessage({
type: 'oauth-popup-complete',
provider: ${JSON.stringify(provider)},
success: true
}, window.location.origin);
}
} catch (e) {}
};
notifyOpener();
setTimeout(() => {
try {
window.close();
} catch (e) {}
}, 300);
let countdown = 10;
const timer = setInterval(() => {
countdown--;
const el = document.getElementById('countdown');
if (el) el.textContent = countdown;
if (countdown <= 0) {
clearInterval(timer);
window.close();
}
}, 1000);
</script>` : '';
return `<!DOCTYPE html>
<html lang="zh-CN">
@ -53,11 +85,34 @@ function generateResponsePage(isSuccess, message) {
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>${title}</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
margin: 0;
background-color: #f5f5f5;
}
.container {
text-align: center;
padding: 2rem;
background: white;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
max-width: 400px;
width: 90%;
}
h1 { color: ${isSuccess ? '#4caf50' : '#f44336'}; margin-top: 0; }
p { color: #666; line-height: 1.6; }
</style>
</head>
<body>
<div class="container">
<h1>${title}</h1>
<h1>${isSuccess ? '✅' : '❌'} ${title}</h1>
<p>${message}</p>
${countdownHtml}
</div>
</body>
</html>`;
@ -181,11 +236,19 @@ async function createOAuthCallbackServer(config, redirectUri, authClient, credPa
});
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
res.end(generateResponsePage(true, '您可以关闭此页面'));
res.end(generateResponsePage(true, '您可以关闭此页面', provider));
} catch (tokenError) {
logger.error(`${config.logPrefix} 获取令牌失败:`, tokenError);
// 广播授权失败事件
broadcastEvent('oauth_error', {
provider: provider,
error: tokenError.message,
timestamp: new Date().toISOString()
});
res.writeHead(500, { 'Content-Type': 'text/html; charset=utf-8' });
res.end(generateResponsePage(false, `获取令牌失败: ${tokenError.message}`));
res.end(generateResponsePage(false, `获取令牌失败: ${tokenError.message}`, provider));
} finally {
server.close(() => {
activeServers.delete(provider);
@ -196,8 +259,15 @@ async function createOAuthCallbackServer(config, redirectUri, authClient, credPa
const errorMessage = `授权失败。Google 返回错误: ${errorParam}`;
logger.error(`${config.logPrefix}`, errorMessage);
// 广播授权失败事件
broadcastEvent('oauth_error', {
provider: provider,
error: errorMessage,
timestamp: new Date().toISOString()
});
res.writeHead(400, { 'Content-Type': 'text/html; charset=utf-8' });
res.end(generateResponsePage(false, errorMessage));
res.end(generateResponsePage(false, errorMessage, provider));
server.close(() => {
activeServers.delete(provider);
});
@ -210,7 +280,7 @@ async function createOAuthCallbackServer(config, redirectUri, authClient, credPa
clearPollTimer();
logger.error(`${config.logPrefix} 处理回调时出错:`, error);
res.writeHead(500, { 'Content-Type': 'text/html; charset=utf-8' });
res.end(generateResponsePage(false, `服务器错误: ${error.message}`));
res.end(generateResponsePage(false, `服务器错误: ${error.message}`, provider));
if (server.listening) {
server.close(() => {

View file

@ -110,6 +110,20 @@ async function fetchWithProxy(url, options = {}, providerType) {
*/
function generateResponsePage(isSuccess, message) {
const title = isSuccess ? '授权成功!' : '授权失败';
const countdownHtml = isSuccess ? `
<p>此窗口将在 <span id="countdown" style="font-weight: bold; color: #2196f3;">10</span> </p>
<script>
let countdown = 10;
const timer = setInterval(() => {
countdown--;
const el = document.getElementById('countdown');
if (el) el.textContent = countdown;
if (countdown <= 0) {
clearInterval(timer);
window.close();
}
}, 1000);
</script>` : '';
return `<!DOCTYPE html>
<html lang="zh-CN">
@ -117,11 +131,34 @@ function generateResponsePage(isSuccess, message) {
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>${title}</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
margin: 0;
background-color: #f5f5f5;
}
.container {
text-align: center;
padding: 2rem;
background: white;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
max-width: 400px;
width: 90%;
}
h1 { color: ${isSuccess ? '#4caf50' : '#f44336'}; margin-top: 0; }
p { color: #666; line-height: 1.6; }
</style>
</head>
<body>
<div class="container">
<h1>${title}</h1>
<h1>${isSuccess ? '✅' : '❌'} ${title}</h1>
<p>${message}</p>
${countdownHtml}
</div>
</body>
</html>`;

View file

@ -128,6 +128,20 @@ async function fetchWithProxy(url, options = {}, providerType) {
*/
function generateResponsePage(isSuccess, message) {
const title = isSuccess ? '授权成功!' : '授权失败';
const countdownHtml = isSuccess ? `
<p>此窗口将在 <span id="countdown" style="font-weight: bold; color: #2196f3;">10</span> </p>
<script>
let countdown = 10;
const timer = setInterval(() => {
countdown--;
const el = document.getElementById('countdown');
if (el) el.textContent = countdown;
if (countdown <= 0) {
clearInterval(timer);
window.close();
}
}, 1000);
</script>` : '';
return `<!DOCTYPE html>
<html lang="zh-CN">
@ -135,11 +149,34 @@ function generateResponsePage(isSuccess, message) {
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>${title}</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
margin: 0;
background-color: #f5f5f5;
}
.container {
text-align: center;
padding: 2rem;
background: white;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
max-width: 400px;
width: 90%;
}
h1 { color: ${isSuccess ? '#4caf50' : '#f44336'}; margin-top: 0; }
p { color: #666; line-height: 1.6; }
</style>
</head>
<body>
<div class="container">
<h1>${title}</h1>
<h1>${isSuccess ? '✅' : '❌'} ${title}</h1>
<p>${message}</p>
${countdownHtml}
</div>
</body>
</html>`;

View file

@ -3,9 +3,10 @@ import logger from '../../utils/logger.js';
import * as http from 'http';
import * as https from 'https';
import { v4 as uuidv4 } from 'uuid';
import { MODEL_PROTOCOL_PREFIX, MODEL_PROVIDER, isRetryableNetworkError } from '../../utils/common.js';
import { MODEL_PROTOCOL_PREFIX, isRetryableNetworkError } from '../../utils/common.js';
import { getProviderModels } from '../provider-models.js';
import { configureAxiosProxy, configureTLSSidecar } from '../../utils/proxy-utils.js';
import { MODEL_PROVIDER } from '../../utils/common.js';
import { ConverterFactory } from '../../converters/ConverterFactory.js';
import * as readline from 'readline';
import { getProviderPoolManager } from '../../services/service-manager.js';
@ -84,6 +85,34 @@ export class GrokApiService {
this.lastSyncAt = null;
}
getMaxRequestRetries() {
const requestMaxRetries = Number.parseInt(this.config.REQUEST_MAX_RETRIES, 10);
if (Number.isFinite(requestMaxRetries) && requestMaxRetries > 0) {
return requestMaxRetries;
}
return 3;
}
classifyApiError(error) {
const status = error.response?.status;
const errorCode = error.code;
const errorMessage = error.message || '';
const isNetworkError = isRetryableNetworkError(error);
if (status === 401 || status === 403) {
error.shouldSwitchCredential = true;
error.message = 'Grok authentication failed (SSO token invalid or expired)';
} else if (isNetworkError) {
// Network jitter or request timeout should not immediately degrade account health.
// Let the upper retry layer switch credential without incrementing the provider error count.
error.shouldSwitchCredential = true;
error.skipErrorCount = true;
}
return { status, errorCode, errorMessage, isNetworkError };
}
async setupNsfw() {
if (this.nsfwSetupDone) return;
try {
@ -100,13 +129,15 @@ export class GrokApiService {
async acceptTos() {
const axiosConfig = { method: 'post', url: `${this.baseUrl}/rest/app-chat/accept-tos`, headers: this.buildHeaders(), data: {}, httpAgent, httpsAgent, timeout: 15000 };
configureAxiosProxy(axiosConfig, this.config, MODEL_PROVIDER.GROK_CUSTOM);
try { await this.callApi(axiosConfig); } catch (e) { logger.debug(`[Grok TOS] ${e.message}`); }
this._applySidecar(axiosConfig);
try { await axios(axiosConfig); } catch (e) { logger.debug(`[Grok TOS] ${e.message}`); }
}
async setBirthDate() {
const axiosConfig = { method: 'post', url: `${this.baseUrl}/rest/app-chat/set-birth-date`, headers: this.buildHeaders(), data: { "birthDate": "1990-01-01" }, httpAgent, httpsAgent, timeout: 15000 };
configureAxiosProxy(axiosConfig, this.config, MODEL_PROVIDER.GROK_CUSTOM);
try { await this.callApi(axiosConfig); } catch (e) { logger.debug(`[Grok Birth] ${e.message}`); }
this._applySidecar(axiosConfig);
try { await axios(axiosConfig); } catch (e) { logger.debug(`[Grok Birth] ${e.message}`); }
}
async enableNsfwAccount() {
@ -125,105 +156,25 @@ export class GrokApiService {
headers['x-user-agent'] = 'connect-es/2.1.1';
headers['referer'] = `${this.baseUrl}/?_s=data`;
const axiosConfig = {
method: 'post',
url: `${this.baseUrl}/auth_mgmt.AuthManagement/UpdateUserFeatureControls`,
headers,
data: payload,
httpAgent,
httpsAgent,
const axiosConfig = {
method: 'post',
url: `${this.baseUrl}/auth_mgmt.AuthManagement/UpdateUserFeatureControls`,
headers,
data: payload,
httpAgent,
httpsAgent,
timeout: 15000,
responseType: 'arraybuffer'
responseType: 'arraybuffer'
};
configureAxiosProxy(axiosConfig, this.config, MODEL_PROVIDER.GROK_CUSTOM);
try { await this.callApi(axiosConfig); } catch (e) { throw e; }
this._applySidecar(axiosConfig);
try { await axios(axiosConfig); } catch (e) { throw e; }
}
_applySidecar(axiosConfig) {
return configureTLSSidecar(axiosConfig, this.config, MODEL_PROVIDER.GROK_CUSTOM);
}
async callApi(axiosConfig, retryCount = 0) {
const maxRetries = this.config.REQUEST_MAX_RETRIES || 3;
const baseDelay = this.config.REQUEST_BASE_DELAY || 1000;
try {
this._applySidecar(axiosConfig);
const response = await axios(axiosConfig);
return response.data;
} catch (error) {
const status = error.response?.status;
const isNetworkError = isRetryableNetworkError(error);
if (status === 401 || status === 403) {
this.handleApiError(error);
}
if ((status === 429 || (status >= 500 && status < 600) || isNetworkError) && retryCount < maxRetries) {
const delay = baseDelay * Math.pow(2, retryCount);
const reason = status ? `Status ${status}` : (error.code || 'Network error');
logger.info(`[Grok API] ${reason}. Retrying in ${delay}ms... (${retryCount + 1}/${maxRetries})`);
await new Promise(resolve => setTimeout(resolve, delay));
return this.callApi(axiosConfig, retryCount + 1);
}
this.handleApiError(error);
}
}
async * _streamChat(axiosConfig, reqBaseUrl, payload, retryCount = 0) {
const maxRetries = this.config.REQUEST_MAX_RETRIES || 3;
const baseDelay = this.config.REQUEST_BASE_DELAY || 1000;
try {
this._applySidecar(axiosConfig);
const response = await axios(axiosConfig);
const rl = readline.createInterface({ input: response.data, terminal: false });
let lastResponseId = payload.responseMetadata?.requestModelDetails?.modelId || "final";
for await (const line of rl) {
const trimmed = line.trim();
if (!trimmed) continue;
let dataStr = trimmed.startsWith('data: ') ? trimmed.slice(6).trim() : trimmed;
if (dataStr === '[DONE]') break;
try {
const json = JSON.parse(dataStr);
if (json.result?.response) {
const resp = json.result.response;
resp._requestBaseUrl = reqBaseUrl;
resp._uuid = this.uuid;
if (resp.responseId) lastResponseId = resp.responseId;
if (resp.streamingVideoGenerationResponse) {
const vid = resp.streamingVideoGenerationResponse;
if (vid.progress === 100 && vid.videoUrl && (payload.responseMetadata?.modelConfigOverride?.modelMap?.videoGenModelConfig?.resolutionName === "720p")) {
const hdUrl = await this.upscaleVideo(vid.videoUrl);
if (hdUrl) vid.videoUrl = hdUrl;
}
}
}
yield json;
} catch (e) {}
}
yield { result: { response: { isDone: true, responseId: lastResponseId, _requestBaseUrl: reqBaseUrl, _uuid: this.uuid } } };
} catch (error) {
const status = error.response?.status;
const isNetworkError = isRetryableNetworkError(error);
if (status === 401 || status === 403) {
this.handleApiError(error);
}
if ((status === 429 || (status >= 500 && status < 600) || isNetworkError) && retryCount < maxRetries) {
const delay = baseDelay * Math.pow(2, retryCount);
const reason = status ? `Status ${status}` : (error.code || 'Network error');
logger.info(`[Grok API] ${reason} during stream. Retrying in ${delay}ms... (${retryCount + 1}/${maxRetries})`);
await new Promise(resolve => setTimeout(resolve, delay));
yield* this._streamChat(axiosConfig, reqBaseUrl, payload, retryCount + 1);
return;
}
this.handleApiError(error);
}
}
async initialize() {
if (this.isInitialized) return;
this.isInitialized = true;
@ -242,8 +193,10 @@ export class GrokApiService {
const payload = { "requestKind": "DEFAULT", "modelName": "grok-3" };
const axiosConfig = { method: 'post', url: `${this.baseUrl}/rest/rate-limits`, headers, data: payload, httpAgent, httpsAgent, timeout: 30000 };
configureAxiosProxy(axiosConfig, this.config, MODEL_PROVIDER.GROK_CUSTOM);
this._applySidecar(axiosConfig);
try {
const data = await this.callApi(axiosConfig);
const response = await axios(axiosConfig);
const data = response.data;
let remaining = data.remainingTokens !== undefined ? data.remainingTokens : (data.remainingQueries !== undefined ? data.remainingQueries : data.totalQueries);
if (data.totalQueries > 0) {
data.totalLimit = data.totalQueries;
@ -320,9 +273,10 @@ export class GrokApiService {
const axiosConfig = { method: 'post', url: `${this.baseUrl}/rest/media/post/create`, headers, data: payload, httpAgent, httpsAgent, timeout: 30000 };
configureAxiosProxy(axiosConfig, this.config, MODEL_PROVIDER.GROK_CUSTOM);
this._applySidecar(axiosConfig);
try {
const data = await this.callApi(axiosConfig);
const postId = data?.post?.id;
const response = await axios(axiosConfig);
const postId = response.data?.post?.id;
if (postId) logger.info(`[Grok Post] Media post created: ${postId} (type=${mediaType})`);
return postId;
} catch (error) {
@ -339,9 +293,10 @@ export class GrokApiService {
const videoId = idMatch[1];
const axiosConfig = { method: 'post', url: `${this.baseUrl}/rest/media/video/upscale`, headers: this.buildHeaders(), data: { videoId }, httpAgent, httpsAgent, timeout: 30000 };
configureAxiosProxy(axiosConfig, this.config, MODEL_PROVIDER.GROK_CUSTOM);
this._applySidecar(axiosConfig);
try {
const data = await this.callApi(axiosConfig);
return data?.hdMediaUrl || videoUrl;
const response = await axios(axiosConfig);
return response.data?.hdMediaUrl || videoUrl;
} catch (error) { return videoUrl; }
}
@ -365,9 +320,10 @@ export class GrokApiService {
timeout: 15000
};
configureAxiosProxy(axiosConfig, this.config, MODEL_PROVIDER.GROK_CUSTOM);
this._applySidecar(axiosConfig);
try {
const data = await this.callApi(axiosConfig);
const shareLink = data?.shareLink;
const response = await axios(axiosConfig);
const shareLink = response.data?.shareLink;
if (shareLink) {
// 从 shareLink 中提取 ID (通常与输入的 postId 一致)
const idMatch = shareLink.match(/\/post\/([0-9a-fA-F-]{36}|[0-9a-fA-F]{32})/);
@ -549,10 +505,15 @@ export class GrokApiService {
if (!b64) return null;
const axiosConfig = { method: 'post', url: `${this.baseUrl}/rest/app-chat/upload-file`, headers: this.buildHeaders(), data: { fileName: `file.${mime.split("/")[1] || "bin"}`, fileMimeType: mime, content: b64 }, httpAgent, httpsAgent, timeout: 30000 };
configureAxiosProxy(axiosConfig, this.config, MODEL_PROVIDER.GROK_CUSTOM);
try { return await this.callApi(axiosConfig); } catch (error) { return null; }
this._applySidecar(axiosConfig);
try { return (await axios(axiosConfig)).data; } catch (error) { return null; }
}
async * generateContentStream(model, requestBody) {
async * generateContentStream(model, requestBody, retryCount = 0) {
const maxRetries = this.getMaxRequestRetries();
const baseDelay = this.config.REQUEST_BASE_DELAY || 1000;
let hasYieldedData = false;
if (this.converter) {
if (this.uuid) this.converter.setUuid(this.uuid);
if (requestBody._requestBaseUrl) this.converter.setRequestBaseUrl(requestBody._requestBaseUrl);
@ -642,21 +603,69 @@ export class GrokApiService {
const payload = this.buildPayload(model, requestBody);
const axiosConfig = { method: 'post', url: this.chatApi, headers: this.buildHeaders(), data: payload, responseType: 'stream', httpAgent, httpsAgent, timeout: 60000, maxRedirects: 0 };
configureAxiosProxy(axiosConfig, this.config, MODEL_PROVIDER.GROK_CUSTOM);
this._applySidecar(axiosConfig);
yield* this._streamChat(axiosConfig, reqBaseUrl, payload);
}
try {
const response = await axios(axiosConfig);
const rl = readline.createInterface({ input: response.data, terminal: false });
let lastResponseId = payload.responseMetadata?.requestModelDetails?.modelId || "final";
handleApiError(error) {
const status = error.response?.status;
if (status === 401 || status === 403) {
error.shouldSwitchCredential = true;
error.message = 'Grok authentication failed (SSO token invalid or expired)';
} else if (isRetryableNetworkError(error)) {
// 网络超时等可重试错误,触发凭证切换而不立即增加错误计数(避免因偶发波动导致节点下线)
error.shouldSwitchCredential = true;
error.skipErrorCount = true;
for await (const line of rl) {
const trimmed = line.trim();
if (!trimmed) continue;
let dataStr = trimmed.startsWith('data: ') ? trimmed.slice(6).trim() : trimmed;
if (dataStr === '[DONE]') break;
try {
const json = JSON.parse(dataStr);
if (json.result?.response) {
const resp = json.result.response;
resp._requestBaseUrl = reqBaseUrl;
resp._uuid = this.uuid;
if (resp.responseId) lastResponseId = resp.responseId;
if (resp.streamingVideoGenerationResponse) {
const vid = resp.streamingVideoGenerationResponse;
if (vid.progress === 100 && vid.videoUrl && (requestBody.videoGenModelConfig?.resolutionName === "720p")) {
const hdUrl = await this.upscaleVideo(vid.videoUrl);
if (hdUrl) vid.videoUrl = hdUrl;
}
}
}
hasYieldedData = true;
yield json;
} catch (e) {}
}
yield { result: { response: { isDone: true, responseId: lastResponseId, _requestBaseUrl: reqBaseUrl, _uuid: this.uuid } } };
} catch (error) {
const { status, errorCode, errorMessage, isNetworkError } = this.classifyApiError(error);
const canRetryInRequest = !hasYieldedData && retryCount < maxRetries;
if (status === 429 && canRetryInRequest) {
const delay = baseDelay * Math.pow(2, retryCount);
logger.info(`[Grok API] Received 429 during stream. Retrying in ${delay}ms... (attempt ${retryCount + 1}/${maxRetries})`);
await new Promise(resolve => setTimeout(resolve, delay));
yield* this.generateContentStream(model, requestBody, retryCount + 1);
return;
}
if (status >= 500 && status < 600 && canRetryInRequest) {
const delay = baseDelay * Math.pow(2, retryCount);
logger.info(`[Grok API] Received ${status} server error during stream. Retrying in ${delay}ms... (attempt ${retryCount + 1}/${maxRetries})`);
await new Promise(resolve => setTimeout(resolve, delay));
yield* this.generateContentStream(model, requestBody, retryCount + 1);
return;
}
if (isNetworkError && canRetryInRequest) {
const delay = baseDelay * Math.pow(2, retryCount);
const errorIdentifier = errorCode || errorMessage.substring(0, 50);
logger.info(`[Grok API] Network error (${errorIdentifier}) during stream. Retrying in ${delay}ms... (attempt ${retryCount + 1}/${maxRetries})`);
await new Promise(resolve => setTimeout(resolve, delay));
yield* this.generateContentStream(model, requestBody, retryCount + 1);
return;
}
throw error;
}
throw error;
}
async listModels() {

View file

@ -2759,20 +2759,50 @@ function showAuthModal(authUrl, authInfo) {
'OAuthAuthWindow',
`width=${width},height=${height},left=${left},top=${top},status=no,resizable=yes,scrollbars=yes`
);
let pollTimer = null;
const cleanupAuthListeners = () => {
if (pollTimer) {
clearInterval(pollTimer);
pollTimer = null;
}
window.removeEventListener('oauth_success_event', handleOAuthSuccess);
window.removeEventListener('message', handlePopupMessage);
};
// 监听 OAuth 成功事件,自动关闭窗口和模态框
const handleOAuthSuccess = () => {
if (authWindow && !authWindow.closed) {
authWindow.close();
}
modal.remove();
window.removeEventListener('oauth_success_event', handleOAuthSuccess);
cleanupAuthListeners();
// 授权成功后刷新配置和提供商列表
loadProviders();
loadConfigList();
};
// 回调页主动 postMessage 时,优先使用父页面关闭子窗口
const handlePopupMessage = (event) => {
if (event.origin !== window.location.origin) {
return;
}
const data = event.data;
if (!data || data.type !== 'oauth-popup-complete') {
return;
}
if (data.provider && data.provider !== authInfo.provider) {
return;
}
handleOAuthSuccess();
};
window.addEventListener('oauth_success_event', handleOAuthSuccess);
window.addEventListener('message', handlePopupMessage);
if (authWindow) {
showToast(t('common.info'), t('oauth.window.opened'), 'info');
@ -2806,7 +2836,10 @@ function showAuthModal(authUrl, authInfo) {
const url = new URL(cleanUrlStr);
if (url.searchParams.has('code') || url.searchParams.has('token')) {
clearInterval(pollTimer);
if (pollTimer) {
clearInterval(pollTimer);
pollTimer = null;
}
// 构造本地可处理的 URL只修改 hostname保持原始 URL 的端口号不变
const localUrl = new URL(url.href);
localUrl.hostname = window.location.hostname;
@ -2816,10 +2849,6 @@ function showAuthModal(authUrl, authInfo) {
// 如果是手动输入,直接通过 fetch 请求处理,然后关闭子窗口
if (isManualInput) {
// 关闭子窗口
if (authWindow && !authWindow.closed) {
authWindow.close();
}
// 通过服务端API处理手动输入的回调URL
window.apiClient.post('/oauth/manual-callback', {
provider: authInfo.provider,
@ -2829,6 +2858,7 @@ function showAuthModal(authUrl, authInfo) {
.then(response => {
if (response.success) {
console.log('OAuth 回调处理成功');
handleOAuthSuccess();
showToast(t('common.success'), t('oauth.success.msg'), 'success');
} else {
console.error('OAuth 回调处理失败:', response.error);
@ -2874,10 +2904,10 @@ function showAuthModal(authUrl, authInfo) {
});
// 启动定时器轮询子窗口 URL
const pollTimer = setInterval(() => {
pollTimer = setInterval(() => {
try {
if (authWindow.closed) {
clearInterval(pollTimer);
cleanupAuthListeners();
return;
}
// 如果能读到说明回到了同域
@ -3103,4 +3133,4 @@ export {
handleGenerateAuthUrl,
checkUpdate,
performUpdate
};
};

View file

@ -131,20 +131,20 @@
align-items: center;
gap: 0.5rem;
padding: 0.5rem 1rem;
background: linear-gradient(135deg, var(--warning-color) 0%, #f97316 100%);
background: linear-gradient(135deg, #6366f1 0%, #4f46e5 100%);
color: var(--white);
text-decoration: none;
border-radius: 9999px;
font-size: 0.875rem;
font-weight: 600;
transition: var(--transition);
box-shadow: 0 2px 8px var(--warning-30);
box-shadow: 0 2px 8px rgba(99, 102, 241, 0.3);
}
.kiro-buy-link:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px var(--warning-40);
background: linear-gradient(135deg, #f97316 0%, var(--warning-color) 100%);
box-shadow: 0 4px 12px rgba(99, 102, 241, 0.4);
background: linear-gradient(135deg, #4f46e5 0%, #6366f1 100%);
}
.kiro-buy-link:active {

View file

@ -7,8 +7,8 @@
<i class="fas fa-bars"></i>
</button>
<div class="header-controls" id="headerControls">
<a href="https://pay.ldxp.cn/shop/N0IK02WR" target="_blank" rel="noopener noreferrer" class="kiro-buy-link" title="多渠道账号购买">
<i class="fas fa-shopping-cart"></i> <span>多渠道账号购买</span>
<a href="https://a001.hubtoday.app/" target="_blank" class="kiro-buy-link" title="AI账号购买">
<i class="fas fa-shopping-cart"></i> <span>AI账号购买</span>
</a>
<span class="status-badge" id="serverStatus">
<i class="fas fa-circle"></i> <span class="status-text" data-i18n="header.status.connecting">连接中...</span>