aiclient-2-api / src /auth /iflow-oauth.js
Jaasomn
Initial deployment
ceb3821
import http from 'http';
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<Object>} 返回类似 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
console.log(`[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 `<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>${title}</title>
</head>
<body>
<div class="container">
<h1>${title}</h1>
<p>${message}</p>
</div>
</body>
</html>`;
}
/**
* 生成 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<Object>} 令牌数据
*/
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<Object>} 用户信息
*/
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);
console.log(`${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);
console.log(`${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.Server>} 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) {
console.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) {
console.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) {
console.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;
}
console.log(`${IFLOW_OAUTH_CONFIG.logPrefix} 收到授权回调,正在交换令牌...`);
try {
// 1. 交换授权码获取令牌
const tokenData = await exchangeIFlowCodeForTokens(code, redirectUri);
console.log(`${IFLOW_OAUTH_CONFIG.logPrefix} 令牌交换成功`);
// 2. 获取用户信息(包含 API Key)
const userInfo = await fetchIFlowUserInfo(tokenData.accessToken);
console.log(`${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));
console.log(`${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);
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
res.end(generateResponsePage(true, `授权成功!账户: ${userInfo.email},您可以关闭此页面`));
} catch (tokenError) {
console.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) {
console.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') {
console.error(`${IFLOW_OAUTH_CONFIG.logPrefix} 端口 ${port} 已被占用`);
reject(new Error(`端口 ${port} 已被占用`));
} else {
console.error(`${IFLOW_OAUTH_CONFIG.logPrefix} 服务器错误:`, err);
reject(err);
}
});
const host = '0.0.0.0';
server.listen(port, host, () => {
console.log(`${IFLOW_OAUTH_CONFIG.logPrefix} OAuth 回调服务器已启动于 ${host}:${port}`);
resolve(server);
});
// 10 分钟超时自动关闭
setTimeout(() => {
if (server.listening) {
console.log(`${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<Object>} 返回授权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);
console.log(`${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<Object>} 新的令牌数据
*/
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
};
}