import http from 'http'; import logger from '../utils/logger.js'; import fs from 'fs'; import path from 'path'; import os from 'os'; import crypto from 'crypto'; import { broadcastEvent } from '../services/ui-manager.js'; import { autoLinkProviderConfigs } from '../services/service-manager.js'; import { CONFIG } from '../core/config-manager.js'; import { getProxyConfigForProvider } from '../utils/proxy-utils.js'; /** * iFlow OAuth 配置 */ const IFLOW_OAUTH_CONFIG = { // OAuth 端点 tokenEndpoint: 'https://iflow.cn/oauth/token', authorizeEndpoint: 'https://iflow.cn/oauth', userInfoEndpoint: 'https://iflow.cn/api/oauth/getUserInfo', successRedirectURL: 'https://iflow.cn/oauth/success', // 客户端凭据 clientId: '10009311001', clientSecret: '4Z3YjXycVsQvyGF1etiNlIBB4RsqSDtW', // 本地回调端口 callbackPort: 8087, // 凭据存储 credentialsDir: '.iflow', credentialsFile: 'oauth_creds.json', // 日志前缀 logPrefix: '[iFlow Auth]' }; /** * 活动的 iFlow 回调服务器管理 */ const activeIFlowServers = new Map(); /** * 创建带代理支持的 fetch 请求 * 使用 axios 替代原生 fetch,以正确支持代理配置 * @param {string} url - 请求 URL * @param {Object} options - fetch 选项(兼容 fetch API 格式) * @param {string} providerType - 提供商类型,用于获取代理配置 * @returns {Promise} 返回类似 fetch Response 的对象 */ async function fetchWithProxy(url, options = {}, providerType) { const proxyConfig = getProxyConfigForProvider(CONFIG, providerType); // 构建 axios 配置 const axiosConfig = { url, method: options.method || 'GET', headers: options.headers || {}, timeout: 30000, // 30 秒超时 }; // 处理请求体 if (options.body) { axiosConfig.data = options.body; } // 配置代理 if (proxyConfig) { axiosConfig.httpAgent = proxyConfig.httpAgent; axiosConfig.httpsAgent = proxyConfig.httpsAgent; axiosConfig.proxy = false; // 禁用 axios 内置代理,使用我们的 agent logger.info(`[OAuth] Using proxy for ${providerType}: ${CONFIG.PROXY_URL}`); } try { const axios = (await import('axios')).default; const response = await axios(axiosConfig); // 返回类似 fetch Response 的对象 return { ok: response.status >= 200 && response.status < 300, status: response.status, statusText: response.statusText, headers: response.headers, json: async () => response.data, text: async () => typeof response.data === 'string' ? response.data : JSON.stringify(response.data), }; } catch (error) { // 处理 axios 错误,转换为类似 fetch 的响应格式 if (error.response) { // 服务器返回了错误状态码 return { ok: false, status: error.response.status, statusText: error.response.statusText, headers: error.response.headers, json: async () => error.response.data, text: async () => typeof error.response.data === 'string' ? error.response.data : JSON.stringify(error.response.data), }; } // 网络错误或其他错误 throw error; } } /** * 生成 HTML 响应页面 * @param {boolean} isSuccess - 是否成功 * @param {string} message - 显示消息 * @returns {string} HTML 内容 */ function generateResponsePage(isSuccess, message) { const title = isSuccess ? '授权成功!' : '授权失败'; return ` ${title}

${title}

${message}

`; } /** * 生成 iFlow 授权链接 * @param {string} state - 状态参数 * @param {number} port - 回调端口 * @returns {Object} 包含 authUrl 和 redirectUri */ function generateIFlowAuthorizationURL(state, port) { const redirectUri = `http://localhost:${port}/oauth2callback`; const params = new URLSearchParams({ loginMethod: 'phone', type: 'phone', redirect: redirectUri, state: state, client_id: IFLOW_OAUTH_CONFIG.clientId }); const authUrl = `${IFLOW_OAUTH_CONFIG.authorizeEndpoint}?${params.toString()}`; return { authUrl, redirectUri }; } /** * 交换授权码获取 iFlow 令牌 * @param {string} code - 授权码 * @param {string} redirectUri - 重定向 URI * @returns {Promise} 令牌数据 */ async function exchangeIFlowCodeForTokens(code, redirectUri) { const form = new URLSearchParams({ grant_type: 'authorization_code', code: code, redirect_uri: redirectUri, client_id: IFLOW_OAUTH_CONFIG.clientId, client_secret: IFLOW_OAUTH_CONFIG.clientSecret }); // 生成 Basic Auth 头 const basicAuth = Buffer.from(`${IFLOW_OAUTH_CONFIG.clientId}:${IFLOW_OAUTH_CONFIG.clientSecret}`).toString('base64'); const response = await fetchWithProxy(IFLOW_OAUTH_CONFIG.tokenEndpoint, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'Accept': 'application/json', 'Authorization': `Basic ${basicAuth}` }, body: form.toString() }, 'openai-iflow'); if (!response.ok) { const errorText = await response.text(); throw new Error(`iFlow token exchange failed: ${response.status} ${errorText}`); } const tokenData = await response.json(); if (!tokenData.access_token) { throw new Error('iFlow token: missing access token in response'); } return { accessToken: tokenData.access_token, refreshToken: tokenData.refresh_token, tokenType: tokenData.token_type, scope: tokenData.scope, expiresIn: tokenData.expires_in, expiresAt: new Date(Date.now() + tokenData.expires_in * 1000).toISOString() }; } /** * 获取 iFlow 用户信息(包含 API Key) * @param {string} accessToken - 访问令牌 * @returns {Promise} 用户信息 */ async function fetchIFlowUserInfo(accessToken) { if (!accessToken || accessToken.trim() === '') { throw new Error('iFlow api key: access token is empty'); } const endpoint = `${IFLOW_OAUTH_CONFIG.userInfoEndpoint}?accessToken=${encodeURIComponent(accessToken)}`; const response = await fetchWithProxy(endpoint, { method: 'GET', headers: { 'Accept': 'application/json' } }, 'openai-iflow'); if (!response.ok) { const errorText = await response.text(); throw new Error(`iFlow user info failed: ${response.status} ${errorText}`); } const result = await response.json(); if (!result.success) { throw new Error('iFlow api key: request not successful'); } if (!result.data || !result.data.apiKey) { throw new Error('iFlow api key: missing api key in response'); } // 获取邮箱或手机号作为账户标识 let email = (result.data.email || '').trim(); if (!email) { email = (result.data.phone || '').trim(); } if (!email) { throw new Error('iFlow token: missing account email/phone in user info'); } return { apiKey: result.data.apiKey, email: email, phone: result.data.phone || '' }; } /** * 关闭 iFlow 服务器 * @param {string} provider - 提供商标识 * @param {number} port - 端口号(可选) */ async function closeIFlowServer(provider, port = null) { const existing = activeIFlowServers.get(provider); if (existing) { await new Promise((resolve) => { existing.server.close(() => { activeIFlowServers.delete(provider); logger.info(`${IFLOW_OAUTH_CONFIG.logPrefix} 已关闭提供商 ${provider} 在端口 ${existing.port} 上的旧服务器`); resolve(); }); }); } if (port) { for (const [p, info] of activeIFlowServers.entries()) { if (info.port === port) { await new Promise((resolve) => { info.server.close(() => { activeIFlowServers.delete(p); logger.info(`${IFLOW_OAUTH_CONFIG.logPrefix} 已关闭端口 ${port} 上的旧服务器`); resolve(); }); }); } } } } /** * 创建 iFlow OAuth 回调服务器 * @param {number} port - 端口号 * @param {string} redirectUri - 重定向 URI * @param {string} expectedState - 预期的 state 参数 * @param {Object} options - 额外选项 * @returns {Promise} HTTP 服务器实例 */ function createIFlowCallbackServer(port, redirectUri, expectedState, options = {}) { return new Promise((resolve, reject) => { const server = http.createServer(async (req, res) => { try { const url = new URL(req.url, `http://localhost:${port}`); if (url.pathname === '/oauth2callback') { const code = url.searchParams.get('code'); const state = url.searchParams.get('state'); const errorParam = url.searchParams.get('error'); if (errorParam) { logger.error(`${IFLOW_OAUTH_CONFIG.logPrefix} 授权失败: ${errorParam}`); res.writeHead(400, { 'Content-Type': 'text/html; charset=utf-8' }); res.end(generateResponsePage(false, `授权失败: ${errorParam}`)); server.close(() => { activeIFlowServers.delete('openai-iflow'); }); return; } if (state !== expectedState) { logger.error(`${IFLOW_OAUTH_CONFIG.logPrefix} State 验证失败`); res.writeHead(400, { 'Content-Type': 'text/html; charset=utf-8' }); res.end(generateResponsePage(false, 'State 验证失败')); server.close(() => { activeIFlowServers.delete('openai-iflow'); }); return; } if (!code) { logger.error(`${IFLOW_OAUTH_CONFIG.logPrefix} 缺少授权码`); res.writeHead(400, { 'Content-Type': 'text/html; charset=utf-8' }); res.end(generateResponsePage(false, '缺少授权码')); server.close(() => { activeIFlowServers.delete('openai-iflow'); }); return; } logger.info(`${IFLOW_OAUTH_CONFIG.logPrefix} 收到授权回调,正在交换令牌...`); try { // 1. 交换授权码获取令牌 const tokenData = await exchangeIFlowCodeForTokens(code, redirectUri); logger.info(`${IFLOW_OAUTH_CONFIG.logPrefix} 令牌交换成功`); // 2. 获取用户信息(包含 API Key) const userInfo = await fetchIFlowUserInfo(tokenData.accessToken); logger.info(`${IFLOW_OAUTH_CONFIG.logPrefix} 用户信息获取成功: ${userInfo.email}`); // 3. 组合完整的凭据数据 const credentialsData = { access_token: tokenData.accessToken, refresh_token: tokenData.refreshToken, expiry_date: new Date(tokenData.expiresAt).getTime(), token_type: tokenData.tokenType, scope: tokenData.scope, apiKey: userInfo.apiKey }; // 4. 保存凭据 let credPath = path.join(os.homedir(), IFLOW_OAUTH_CONFIG.credentialsDir, IFLOW_OAUTH_CONFIG.credentialsFile); if (options.saveToConfigs) { const providerDir = options.providerDir || 'iflow'; const targetDir = path.join(process.cwd(), 'configs', providerDir); await fs.promises.mkdir(targetDir, { recursive: true }); const timestamp = Date.now(); const filename = `${timestamp}_oauth_creds.json`; credPath = path.join(targetDir, filename); } await fs.promises.mkdir(path.dirname(credPath), { recursive: true }); await fs.promises.writeFile(credPath, JSON.stringify(credentialsData, null, 2)); logger.info(`${IFLOW_OAUTH_CONFIG.logPrefix} 凭据已保存: ${credPath}`); const relativePath = path.relative(process.cwd(), credPath); // 5. 广播授权成功事件 broadcastEvent('oauth_success', { provider: 'openai-iflow', credPath: credPath, relativePath: relativePath, email: userInfo.email, timestamp: new Date().toISOString() }); // 6. 自动关联新生成的凭据到 Pools await autoLinkProviderConfigs(CONFIG, { onlyCurrentCred: true, credPath: relativePath }); res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' }); res.end(generateResponsePage(true, `授权成功!账户: ${userInfo.email},您可以关闭此页面`)); } catch (tokenError) { logger.error(`${IFLOW_OAUTH_CONFIG.logPrefix} 令牌处理失败:`, tokenError); res.writeHead(500, { 'Content-Type': 'text/html; charset=utf-8' }); res.end(generateResponsePage(false, `令牌处理失败: ${tokenError.message}`)); } finally { server.close(() => { activeIFlowServers.delete('openai-iflow'); }); } } else { // 忽略其他请求 res.writeHead(204); res.end(); } } catch (error) { logger.error(`${IFLOW_OAUTH_CONFIG.logPrefix} 处理回调出错:`, error); res.writeHead(500, { 'Content-Type': 'text/html; charset=utf-8' }); res.end(generateResponsePage(false, `服务器错误: ${error.message}`)); if (server.listening) { server.close(() => { activeIFlowServers.delete('openai-iflow'); }); } } }); server.on('error', (err) => { if (err.code === 'EADDRINUSE') { logger.error(`${IFLOW_OAUTH_CONFIG.logPrefix} 端口 ${port} 已被占用`); reject(new Error(`端口 ${port} 已被占用`)); } else { logger.error(`${IFLOW_OAUTH_CONFIG.logPrefix} 服务器错误:`, err); reject(err); } }); const host = '0.0.0.0'; server.listen(port, host, () => { logger.info(`${IFLOW_OAUTH_CONFIG.logPrefix} OAuth 回调服务器已启动于 ${host}:${port}`); resolve(server); }); // 10 分钟超时自动关闭 setTimeout(() => { if (server.listening) { logger.info(`${IFLOW_OAUTH_CONFIG.logPrefix} 回调服务器超时,自动关闭`); server.close(() => { activeIFlowServers.delete('openai-iflow'); }); } }, 10 * 60 * 1000); }); } /** * 处理 iFlow OAuth 授权 * @param {Object} currentConfig - 当前配置对象 * @param {Object} options - 额外选项 * - port: 自定义端口号 * - saveToConfigs: 是否保存到 configs 目录 * - providerDir: 提供商目录名 * @returns {Promise} 返回授权URL和相关信息 */ export async function handleIFlowOAuth(currentConfig, options = {}) { const port = parseInt(options.port) || IFLOW_OAUTH_CONFIG.callbackPort; const providerKey = 'openai-iflow'; // 生成 state 参数 const state = crypto.randomBytes(16).toString('base64url'); // 生成授权链接 const { authUrl, redirectUri } = generateIFlowAuthorizationURL(state, port); logger.info(`${IFLOW_OAUTH_CONFIG.logPrefix} 生成授权链接: ${authUrl}`); // 关闭之前可能存在的服务器 await closeIFlowServer(providerKey, port); // 启动回调服务器 try { const server = await createIFlowCallbackServer(port, redirectUri, state, options); activeIFlowServers.set(providerKey, { server, port }); } catch (error) { throw new Error(`启动 iFlow 回调服务器失败: ${error.message}`); } return { authUrl, authInfo: { provider: 'openai-iflow', redirectUri: redirectUri, callbackPort: port, state: state, ...options } }; } /** * 使用 refresh_token 刷新 iFlow 令牌 * @param {string} refreshToken - 刷新令牌 * @returns {Promise} 新的令牌数据 */ export async function refreshIFlowTokens(refreshToken) { const form = new URLSearchParams({ grant_type: 'refresh_token', refresh_token: refreshToken, client_id: IFLOW_OAUTH_CONFIG.clientId, client_secret: IFLOW_OAUTH_CONFIG.clientSecret }); // 生成 Basic Auth 头 const basicAuth = Buffer.from(`${IFLOW_OAUTH_CONFIG.clientId}:${IFLOW_OAUTH_CONFIG.clientSecret}`).toString('base64'); const response = await fetchWithProxy(IFLOW_OAUTH_CONFIG.tokenEndpoint, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'Accept': 'application/json', 'Authorization': `Basic ${basicAuth}` }, body: form.toString() }, 'openai-iflow'); if (!response.ok) { const errorText = await response.text(); throw new Error(`iFlow token refresh failed: ${response.status} ${errorText}`); } const tokenData = await response.json(); if (!tokenData.access_token) { throw new Error('iFlow token refresh: missing access token in response'); } // 获取用户信息以更新 API Key const userInfo = await fetchIFlowUserInfo(tokenData.access_token); return { access_token: tokenData.access_token, refresh_token: tokenData.refresh_token, expiry_date: Date.now() + tokenData.expires_in * 1000, token_type: tokenData.token_type, scope: tokenData.scope, apiKey: userInfo.apiKey }; }