fix(auth): 修复OAuth服务器端口占用和会话管理问题
- 添加全局活动服务器管理,防止端口重复绑定 - 增加轮询超时时间至10分钟,避免过早超时 - 统一错误事件广播格式,添加时间戳字段 - 清理会话时不再重复关闭服务器 - 排除字段列表中添加_lastSelectionSeq字段
This commit is contained in:
parent
18c04d5a04
commit
be64166f37
2 changed files with 50 additions and 27 deletions
|
|
@ -23,6 +23,41 @@ const CODEX_OAUTH_CONFIG = {
|
||||||
logPrefix: '[Codex Auth]'
|
logPrefix: '[Codex Auth]'
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 活动的服务器实例管理(与 gemini-oauth 一致)
|
||||||
|
*/
|
||||||
|
const activeServers = new Map();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 关闭指定端口的活动服务器
|
||||||
|
*/
|
||||||
|
async function closeActiveServer(provider, port = null) {
|
||||||
|
const existing = activeServers.get(provider);
|
||||||
|
if (existing) {
|
||||||
|
await new Promise((resolve) => {
|
||||||
|
existing.server.close(() => {
|
||||||
|
activeServers.delete(provider);
|
||||||
|
logger.info(`[Codex Auth] 已关闭提供商 ${provider} 在端口 ${existing.port} 上的旧服务器`);
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (port) {
|
||||||
|
for (const [p, info] of activeServers.entries()) {
|
||||||
|
if (info.port === port) {
|
||||||
|
await new Promise((resolve) => {
|
||||||
|
info.server.close(() => {
|
||||||
|
activeServers.delete(p);
|
||||||
|
logger.info(`[Codex Auth] 已关闭端口 ${port} 上被占用(提供商: ${p})的旧服务器`);
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Codex OAuth 认证类
|
* Codex OAuth 认证类
|
||||||
* 实现 OAuth2 + PKCE 流程
|
* 实现 OAuth2 + PKCE 流程
|
||||||
|
|
@ -71,17 +106,6 @@ class CodexAuth {
|
||||||
|
|
||||||
logger.info(`${CODEX_OAUTH_CONFIG.logPrefix} Generating auth URL...`);
|
logger.info(`${CODEX_OAUTH_CONFIG.logPrefix} Generating auth URL...`);
|
||||||
|
|
||||||
// 如果已有服务器在运行,先关闭
|
|
||||||
if (this.server) {
|
|
||||||
logger.info(`${CODEX_OAUTH_CONFIG.logPrefix} Closing existing callback server...`);
|
|
||||||
try {
|
|
||||||
this.server.close();
|
|
||||||
this.server = null;
|
|
||||||
} catch (error) {
|
|
||||||
logger.warn(`${CODEX_OAUTH_CONFIG.logPrefix} Failed to close existing server:`, error.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 启动本地回调服务器
|
// 启动本地回调服务器
|
||||||
const server = await this.startCallbackServer();
|
const server = await this.startCallbackServer();
|
||||||
this.server = server;
|
this.server = server;
|
||||||
|
|
@ -231,6 +255,9 @@ class CodexAuth {
|
||||||
* @returns {Promise<http.Server>}
|
* @returns {Promise<http.Server>}
|
||||||
*/
|
*/
|
||||||
async startCallbackServer() {
|
async startCallbackServer() {
|
||||||
|
// 先清理该提供商或该端口的旧服务器
|
||||||
|
await closeActiveServer('openai-codex-oauth', CODEX_OAUTH_CONFIG.port);
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const server = http.createServer();
|
const server = http.createServer();
|
||||||
|
|
||||||
|
|
@ -304,6 +331,7 @@ class CodexAuth {
|
||||||
|
|
||||||
server.listen(CODEX_OAUTH_CONFIG.port, () => {
|
server.listen(CODEX_OAUTH_CONFIG.port, () => {
|
||||||
logger.info(`${CODEX_OAUTH_CONFIG.logPrefix} Callback server listening on port ${CODEX_OAUTH_CONFIG.port}`);
|
logger.info(`${CODEX_OAUTH_CONFIG.logPrefix} Callback server listening on port ${CODEX_OAUTH_CONFIG.port}`);
|
||||||
|
activeServers.set('openai-codex-oauth', { server, port: CODEX_OAUTH_CONFIG.port });
|
||||||
resolve(server);
|
resolve(server);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -618,10 +646,7 @@ export async function handleCodexOAuth(currentConfig, options = {}) {
|
||||||
if (session.pollTimer) {
|
if (session.pollTimer) {
|
||||||
clearInterval(session.pollTimer);
|
clearInterval(session.pollTimer);
|
||||||
}
|
}
|
||||||
// 关闭服务器
|
// 不在这里显式关闭 server,由 startCallbackServer 中的 closeActiveServer 处理
|
||||||
if (session.server) {
|
|
||||||
session.server.close();
|
|
||||||
}
|
|
||||||
global.codexOAuthSessions.delete(sessionId);
|
global.codexOAuthSessions.delete(sessionId);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.warn(`[Codex Auth] Failed to clean up session ${sessionId}:`, error.message);
|
logger.warn(`[Codex Auth] Failed to clean up session ${sessionId}:`, error.message);
|
||||||
|
|
@ -643,7 +668,7 @@ export async function handleCodexOAuth(currentConfig, options = {}) {
|
||||||
|
|
||||||
// 轮询计数器
|
// 轮询计数器
|
||||||
let pollCount = 0;
|
let pollCount = 0;
|
||||||
const maxPollCount = 30; // 最多轮询次数(可随意更改)
|
const maxPollCount = 200; // 增加到约 10 分钟 (200 * 3s = 600s)
|
||||||
const pollInterval = 3000; // 轮询间隔(毫秒)
|
const pollInterval = 3000; // 轮询间隔(毫秒)
|
||||||
let pollTimer = null;
|
let pollTimer = null;
|
||||||
let isCompleted = false;
|
let isCompleted = false;
|
||||||
|
|
@ -672,12 +697,8 @@ export async function handleCodexOAuth(currentConfig, options = {}) {
|
||||||
const totalSeconds = (maxPollCount * pollInterval) / 1000;
|
const totalSeconds = (maxPollCount * pollInterval) / 1000;
|
||||||
logger.info(`[Codex Auth] Polling timeout (${totalSeconds}s), releasing session for next authorization`);
|
logger.info(`[Codex Auth] Polling timeout (${totalSeconds}s), releasing session for next authorization`);
|
||||||
|
|
||||||
// 清理会话和服务器
|
// 清理会话
|
||||||
if (global.codexOAuthSessions.has(sessionId)) {
|
if (global.codexOAuthSessions.has(sessionId)) {
|
||||||
const session = global.codexOAuthSessions.get(sessionId);
|
|
||||||
if (session.server) {
|
|
||||||
session.server.close();
|
|
||||||
}
|
|
||||||
global.codexOAuthSessions.delete(sessionId);
|
global.codexOAuthSessions.delete(sessionId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -728,7 +749,8 @@ export async function handleCodexOAuth(currentConfig, options = {}) {
|
||||||
// 广播认证失败事件
|
// 广播认证失败事件
|
||||||
broadcastEvent('oauth_error', {
|
broadcastEvent('oauth_error', {
|
||||||
provider: 'openai-codex-oauth',
|
provider: 'openai-codex-oauth',
|
||||||
error: error.message
|
error: error.message,
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -745,7 +767,8 @@ export async function handleCodexOAuth(currentConfig, options = {}) {
|
||||||
|
|
||||||
broadcastEvent('oauth_error', {
|
broadcastEvent('oauth_error', {
|
||||||
provider: 'openai-codex-oauth',
|
provider: 'openai-codex-oauth',
|
||||||
error: error.message
|
error: error.message,
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -838,10 +861,10 @@ export async function handleCodexOAuthCallback(code, state) {
|
||||||
logger.error('[Codex Auth] OAuth callback failed:', error.message);
|
logger.error('[Codex Auth] OAuth callback failed:', error.message);
|
||||||
|
|
||||||
// 广播认证失败事件
|
// 广播认证失败事件
|
||||||
broadcastEvent({
|
broadcastEvent('oauth_error', {
|
||||||
type: 'oauth-error',
|
|
||||||
provider: 'openai-codex-oauth',
|
provider: 'openai-codex-oauth',
|
||||||
error: error.message
|
error: error.message,
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
|
|
@ -674,7 +674,7 @@ function getFieldOrder(provider) {
|
||||||
const excludedFields = [
|
const excludedFields = [
|
||||||
'isHealthy', 'lastUsed', 'usageCount', 'errorCount', 'lastErrorTime',
|
'isHealthy', 'lastUsed', 'usageCount', 'errorCount', 'lastErrorTime',
|
||||||
'uuid', 'isDisabled', 'lastHealthCheckTime', 'lastHealthCheckModel', 'lastErrorMessage',
|
'uuid', 'isDisabled', 'lastHealthCheckTime', 'lastHealthCheckModel', 'lastErrorMessage',
|
||||||
'notSupportedModels', 'refreshCount', 'needsRefresh'
|
'notSupportedModels', 'refreshCount', 'needsRefresh', '_lastSelectionSeq'
|
||||||
];
|
];
|
||||||
|
|
||||||
// 从 getProviderTypeFields 获取字段顺序映射
|
// 从 getProviderTypeFields 获取字段顺序映射
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue