From 19a40c7fae92cd955f477e8b545e63650ec3122d Mon Sep 17 00:00:00 2001 From: hex2077 Date: Wed, 7 Jan 2026 21:30:51 +0800 Subject: [PATCH] =?UTF-8?q?feat(iflow):=20=E6=96=B0=E5=A2=9E=20iFlow=20CLI?= =?UTF-8?q?=20=E6=94=AF=E6=8C=81=E5=8F=8A=20OAuth=20=E8=AE=A4=E8=AF=81?= =?UTF-8?q?=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 添加 iFlow API 提供商支持,包括: 1. 新增 MODEL_PROVIDER.IFLOW_API 常量 2. 实现 IFlowApiService 和适配器 3. 添加 OAuth 认证流程及令牌刷新机制 4. 更新相关配置文件、路由和前端界面 5. 扩展多语言支持 6. 修改 Docker 端口映射范围以包含 iFlow 回调端口 --- README-JA.md | 3 +- README-ZH.md | 3 +- README.md | 3 +- configs/provider_pools.json.example | 30 + src/adapter.js | 45 ++ src/claude/claude-kiro.js | 2 +- src/common.js | 1 + src/oauth-handlers.js | 438 +++++++++++ src/openai/iflow-core.js | 1049 +++++++++++++++++++++++++++ src/provider-models.js | 23 + src/provider-utils.js | 11 + src/service-manager.js | 9 +- src/ui-manager.js | 36 +- static/app/file-upload.js | 3 +- static/app/i18n.js | 12 + static/app/modal.js | 5 +- static/app/provider-manager.js | 20 +- static/app/styles.css | 4 +- static/app/utils.js | 18 +- static/index.html | 73 +- 20 files changed, 1767 insertions(+), 21 deletions(-) create mode 100644 src/openai/iflow-core.js diff --git a/README-JA.md b/README-JA.md index a5bb59b..e877336 100644 --- a/README-JA.md +++ b/README-JA.md @@ -34,6 +34,7 @@ >
> クリックして詳細なバージョン履歴を展開 > +> - **2026.01.07** - iFlowプロトコルサポートの追加、OAuth認証方式でQwen、Kimi、DeepSeek、GLMシリーズモデルにアクセス可能、自動トークンリフレッシュ機能をサポート > - **2026.01.03** - テーマ切替機能を追加し、プロバイダープール初期化を最適化、プロバイダーのデフォルト設定を使用するフォールバック戦略を削除 > - **2025.12.30** - メインプロセス管理と自動更新機能を追加 > - **2025.12.25** - 設定ファイル統一管理:すべての設定を `configs/` ディレクトリに集約。Dockerユーザーはマウントパスを `-v "ローカルパス:/app/configs"` に更新が必要 @@ -106,7 +107,7 @@ AIClient-2-APIを使い始める最も推奨される方法は、自動起動ス #### 🐳 Docker クイックスタート (推奨) ```bash -docker run -d -p 3000:3000 -p 8085:8085 -p 8086:8086 -p 19876-19880:19876-19880 --restart=always -v "指定パス:/app/configs" --name aiclient2api justlikemaki/aiclient-2-api +docker run -d -p 3000:3000 -p 8085-8087:8085-8087 -p 19876-19880:19876-19880 --restart=always -v "指定パス:/app/configs" --name aiclient2api justlikemaki/aiclient-2-api ``` **パラメータ説明**: diff --git a/README-ZH.md b/README-ZH.md index 9591bbb..62c1fd8 100644 --- a/README-ZH.md +++ b/README-ZH.md @@ -34,6 +34,7 @@ >
> 点击展开查看详细版本历史 > +> - **2026.01.07** - 新增 iFlow 协议支持,通过 OAuth 认证方式访问 Qwen、Kimi、DeepSeek 和 GLM 系列模型,支持自动 token 刷新功能 > - **2026.01.03** - 新增主题切换功能并优化提供商池初始化,移除使用提供商默认配置的降级策略 > - **2025.12.30** - 添加主进程管理和自动更新功能 > - **2025.12.25** - 配置文件统一管理:所有配置集中到 `configs/` 目录,Docker 用户需更新挂载路径为 `-v "本地路径:/app/configs"` @@ -105,7 +106,7 @@ #### 🐳 Docker 快捷启动 (推荐) ```bash -docker run -d -p 3000:3000 -p 8085:8085 -p 8086:8086 -p 19876-19880:19876-19880 --restart=always -v "指定路径:/app/configs" --name aiclient2api justlikemaki/aiclient-2-api +docker run -d -p 3000:3000 -p 8085-8087:8085-8087 -p 19876-19880:19876-19880 --restart=always -v "指定路径:/app/configs" --name aiclient2api justlikemaki/aiclient-2-api ``` **参数说明**: diff --git a/README.md b/README.md index 827b139..64e97f5 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,7 @@ >
> Click to expand detailed version history > +> - **2026.01.07** - Added iFlow protocol support, enabling access to Qwen, Kimi, DeepSeek, and GLM series models via OAuth authentication with automatic token refresh > - **2026.01.03** - Added theme switching functionality and optimized provider pool initialization, removed the fallback strategy of using provider default configuration > - **2025.12.30** - Added main process management and automatic update functionality > - **2025.12.25** - Unified configuration management: All configs centralized to `configs/` directory. Docker users need to update mount path to `-v "local_path:/app/configs"` @@ -106,7 +107,7 @@ The most recommended way to use AIClient-2-API is to start it through an automat #### 🐳 Docker Quick Start (Recommended) ```bash -docker run -d -p 3000:3000 -p 8085:8085 -p 8086:8086 -p 19876-19880:19876-19880 --restart=always -v "your_path:/app/configs" --name aiclient2api justlikemaki/aiclient-2-api +docker run -d -p 3000:3000 -p 8085-8087:8085-8087 -p 19876-19880:19876-19880 --restart=always -v "your_path:/app/configs" --name aiclient2api justlikemaki/aiclient-2-api ``` **Parameter Description**: diff --git a/configs/provider_pools.json.example b/configs/provider_pools.json.example index 2a5e4dc..6998f60 100644 --- a/configs/provider_pools.json.example +++ b/configs/provider_pools.json.example @@ -179,5 +179,35 @@ "errorCount": 0, "lastErrorTime": null } + ], + "openai-iflow": [ + { + "customName": "iFlow Token节点1", + "IFLOW_TOKEN_FILE_PATH": "./configs/iflow/iflow_token.json", + "IFLOW_BASE_URL": "https://apis.iflow.cn/v1", + "uuid": "11223344-5566-7788-99aa-bbccddeeff00", + "checkModelName": "gpt-4o", + "checkHealth": true, + "isHealthy": true, + "isDisabled": false, + "lastUsed": null, + "usageCount": 0, + "errorCount": 0, + "lastErrorTime": null + }, + { + "customName": "iFlow Token节点2", + "IFLOW_TOKEN_FILE_PATH": "./configs/iflow/iflow_token2.json", + "IFLOW_BASE_URL": "https://apis.iflow.cn/v1", + "uuid": "aabbccdd-eeff-0011-2233-445566778899", + "checkModelName": "gpt-4o", + "checkHealth": true, + "isHealthy": true, + "isDisabled": false, + "lastUsed": null, + "usageCount": 0, + "errorCount": 0, + "lastErrorTime": null + } ] } \ No newline at end of file diff --git a/src/adapter.js b/src/adapter.js index 1d7e0c9..9465b14 100644 --- a/src/adapter.js +++ b/src/adapter.js @@ -5,6 +5,7 @@ import { OpenAIApiService } from './openai/openai-core.js'; // 导入OpenAIApiSe import { ClaudeApiService } from './claude/claude-core.js'; // 导入ClaudeApiService import { KiroApiService } from './claude/claude-kiro.js'; // 导入KiroApiService import { QwenApiService } from './openai/qwen-core.js'; // 导入QwenApiService +import { IFlowApiService } from './openai/iflow-core.js'; // 导入IFlowApiService import { MODEL_PROVIDER } from './common.js'; // 导入 MODEL_PROVIDER // 定义AI服务适配器接口 @@ -357,6 +358,47 @@ export class QwenApiServiceAdapter extends ApiServiceAdapter { } } +// iFlow API 服务适配器 +export class IFlowApiServiceAdapter extends ApiServiceAdapter { + constructor(config) { + super(); + this.iflowApiService = new IFlowApiService(config); + } + + async generateContent(model, requestBody) { + if (!this.iflowApiService.isInitialized) { + console.warn("iflowApiService not initialized, attempting to re-initialize..."); + await this.iflowApiService.initialize(); + } + return this.iflowApiService.generateContent(model, requestBody); + } + + async *generateContentStream(model, requestBody) { + if (!this.iflowApiService.isInitialized) { + console.warn("iflowApiService not initialized, attempting to re-initialize..."); + await this.iflowApiService.initialize(); + } + yield* this.iflowApiService.generateContentStream(model, requestBody); + } + + async listModels() { + if (!this.iflowApiService.isInitialized) { + console.warn("iflowApiService not initialized, attempting to re-initialize..."); + await this.iflowApiService.initialize(); + } + return this.iflowApiService.listModels(); + } + + async refreshToken() { + if (this.iflowApiService.isExpiryDateNear()) { + console.log(`[iFlow] Expiry date is near, refreshing API key...`); + await this.iflowApiService.initializeAuth(true); + } + return Promise.resolve(); + } + +} + // 用于存储服务适配器单例的映射 export const serviceInstances = {}; @@ -389,6 +431,9 @@ export function getServiceAdapter(config) { case MODEL_PROVIDER.QWEN_API: serviceInstances[providerKey] = new QwenApiServiceAdapter(config); break; + case MODEL_PROVIDER.IFLOW_API: + serviceInstances[providerKey] = new IFlowApiServiceAdapter(config); + break; default: throw new Error(`Unsupported model provider: ${provider}`); } diff --git a/src/claude/claude-kiro.js b/src/claude/claude-kiro.js index 6da932e..233c97e 100644 --- a/src/claude/claude-kiro.js +++ b/src/claude/claude-kiro.js @@ -616,7 +616,7 @@ async initializeAuth(forceRefresh = false) { // 上一条是字符串,当前是数组,转换为数组格式 lastMsg.content = [{ type: 'text', text: lastMsg.content }, ...currentMsg.content]; } - console.log(`[Kiro] Merged adjacent ${currentMsg.role} messages`); + // console.log(`[Kiro] Merged adjacent ${currentMsg.role} messages`); } else { mergedMessages.push(currentMsg); } diff --git a/src/common.js b/src/common.js index 9a7a6ff..e5cd676 100644 --- a/src/common.js +++ b/src/common.js @@ -28,6 +28,7 @@ export const MODEL_PROVIDER = { CLAUDE_CUSTOM: 'claude-custom', KIRO_API: 'claude-kiro-oauth', QWEN_API: 'openai-qwen-oauth', + IFLOW_API: 'openai-iflow', } /** diff --git a/src/oauth-handlers.js b/src/oauth-handlers.js index 5bc8d60..6a8f332 100644 --- a/src/oauth-handlers.js +++ b/src/oauth-handlers.js @@ -95,6 +95,36 @@ const KIRO_OAUTH_CONFIG = { logPrefix: '[Kiro Auth]' }; +/** + * 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(); + /** * 活动的 Kiro 回调服务器管理 */ @@ -1046,4 +1076,412 @@ function createKiroHttpCallbackServer(port, codeVerifier, expectedState, options } }, KIRO_OAUTH_CONFIG.authTimeout); }); +} + +/** + * 生成 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 fetch(IFLOW_OAUTH_CONFIG.tokenEndpoint, { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + 'Accept': 'application/json', + 'Authorization': `Basic ${basicAuth}` + }, + body: form.toString() + }); + + 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 fetch(endpoint, { + method: 'GET', + headers: { + 'Accept': 'application/json' + } + }); + + 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 服务器实例 + */ +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 = { + accessToken: tokenData.accessToken, + refreshToken: tokenData.refreshToken, + tokenType: tokenData.tokenType, + scope: tokenData.scope, + expiresAt: tokenData.expiresAt, + apiKey: userInfo.apiKey, + email: userInfo.email, + lastRefresh: new Date().toISOString(), + type: 'iflow' + }; + + // 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} 返回授权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} 新的令牌数据 + */ +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 fetch(IFLOW_OAUTH_CONFIG.tokenEndpoint, { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + 'Accept': 'application/json', + 'Authorization': `Basic ${basicAuth}` + }, + body: form.toString() + }); + + 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 { + accessToken: tokenData.access_token, + refreshToken: tokenData.refresh_token, + tokenType: tokenData.token_type, + scope: tokenData.scope, + expiresAt: new Date(Date.now() + tokenData.expires_in * 1000).toISOString(), + apiKey: userInfo.apiKey, + email: userInfo.email, + lastRefresh: new Date().toISOString(), + type: 'iflow' + }; } \ No newline at end of file diff --git a/src/openai/iflow-core.js b/src/openai/iflow-core.js new file mode 100644 index 0000000..1574352 --- /dev/null +++ b/src/openai/iflow-core.js @@ -0,0 +1,1049 @@ +/** + * iFlow API Service + * + * iFlow 是一个 AI 服务平台,提供 OpenAI 兼容的 API 接口。 + * 使用 Token 文件方式认证 - 从文件读取 API Key + * + * 支持的模型: + * - Qwen 系列: qwen3-max, qwen3-coder-plus, qwen3-vl-plus, qwen3-235b 等 + * - Kimi 系列: kimi-k2, kimi-k2-0905 + * - DeepSeek 系列: deepseek-v3, deepseek-v3.2, deepseek-r1 + * - GLM 系列: glm-4.6 + * + * 支持的特殊模型配置: + * - GLM-4.x: 使用 chat_template_kwargs.enable_thinking + * - Qwen thinking 模型: 内置推理能力 + * - DeepSeek R1: 内置推理能力 + */ + +import axios from 'axios'; +import * as http from 'http'; +import * as https from 'https'; +import { promises as fs } from 'fs'; +import * as path from 'path'; +import * as os from 'os'; +import { configureAxiosProxy } from '../proxy-utils.js'; + +// iFlow API 端点 +const IFLOW_API_BASE_URL = 'https://apis.iflow.cn/v1'; +const IFLOW_USER_AGENT = 'iFlow-Cli'; +const IFLOW_OAUTH_TOKEN_ENDPOINT = 'https://iflow.cn/oauth/token'; +const IFLOW_USER_INFO_ENDPOINT = 'https://iflow.cn/api/oauth/getUserInfo'; +const IFLOW_OAUTH_CLIENT_ID = '10009311001'; +const IFLOW_OAUTH_CLIENT_SECRET = '4Z3YjXycVsQvyGF1etiNlIBB4RsqSDtW'; + +// 默认模型列表 +const IFLOW_MODELS = [ + // iFlow 特有模型 + 'iflow-rome-30ba3b', + // Qwen 模型 + 'qwen3-coder-plus', + 'qwen3-max', + 'qwen3-vl-plus', + 'qwen3-max-preview', + 'qwen3-32b', + 'qwen3-235b-a22b-thinking-2507', + 'qwen3-235b-a22b-instruct', + 'qwen3-235b', + // Kimi 模型 + 'kimi-k2-0905', + 'kimi-k2', + // GLM 模型 + 'glm-4.6', + 'glm-4.7', + // DeepSeek 模型 + 'deepseek-v3.2', + 'deepseek-r1', + 'deepseek-v3' +]; + +// 支持 thinking 的模型前缀 +const THINKING_MODEL_PREFIXES = ['glm-4', 'qwen3-235b-a22b-thinking', 'deepseek-r1']; + +// ==================== Token 管理 ==================== + +/** + * iFlow Token 存储类 + */ +class IFlowTokenStorage { + constructor(data = {}) { + this.accessToken = data.accessToken || data.access_token || ''; + this.refreshToken = data.refreshToken || data.refresh_token || ''; + this.expiryDate = data.expiryDate || data.expiry_date || ''; + this.apiKey = data.apiKey || data.api_key || ''; + this.email = data.email || ''; + this.tokenType = data.tokenType || data.token_type || ''; + this.scope = data.scope || ''; + this.type = data.type || 'iflow'; + } + + /** + * 转换为 JSON 对象 + */ + toJSON() { + return { + access_token: this.accessToken, + refresh_token: this.refreshToken, + expiry_date: this.expiryDate, + token_type: this.tokenType, + scope: this.scope, + apiKey: this.apiKey, + email: this.email, + type: this.type + }; + } + + /** + * 从 JSON 对象创建实例 + */ + static fromJSON(json) { + return new IFlowTokenStorage(json); + } +} + +/** + * 从文件加载 Token + * @param {string} filePath - Token 文件路径 + * @returns {Promise} + */ +async function loadTokenFromFile(filePath) { + try { + const absolutePath = path.isAbsolute(filePath) + ? filePath + : path.join(process.cwd(), filePath); + + const data = await fs.readFile(absolutePath, 'utf-8'); + const json = JSON.parse(data); + + return IFlowTokenStorage.fromJSON(json); + } catch (error) { + if (error.code === 'ENOENT') { + console.warn(`[iFlow] Token file not found: ${filePath}`); + return null; + } + throw new Error(`[iFlow] Failed to load token from file: ${error.message}`); + } +} + +/** + * 保存 Token 到文件 + * @param {string} filePath - Token 文件路径 + * @param {IFlowTokenStorage} tokenStorage - Token 存储对象 + */ +async function saveTokenToFile(filePath, tokenStorage) { + try { + const absolutePath = path.isAbsolute(filePath) + ? filePath + : path.join(process.cwd(), filePath); + + // 确保目录存在 + const dir = path.dirname(absolutePath); + await fs.mkdir(dir, { recursive: true }); + + // 写入文件 + const json = tokenStorage.toJSON(); + await fs.writeFile(absolutePath, JSON.stringify(json, null, 2), 'utf-8'); + + console.log(`[iFlow] Token saved to: ${filePath}`); + } catch (error) { + throw new Error(`[iFlow] Failed to save token to file: ${error.message}`); + } +} + +// ==================== Token 刷新逻辑 ==================== + +/** + * 使用 refresh_token 刷新 OAuth Token + * @param {string} refreshToken - 刷新令牌 + * @param {Object} axiosInstance - axios 实例(可选,用于代理配置) + * @returns {Promise} - 新的 Token 数据 + */ +async function refreshOAuthTokens(refreshToken, axiosInstance = null) { + if (!refreshToken || refreshToken.trim() === '') { + throw new Error('[iFlow] refresh_token is empty'); + } + + console.log('[iFlow] Refreshing OAuth tokens...'); + + // 构建请求参数 + const params = new URLSearchParams(); + params.append('grant_type', 'refresh_token'); + params.append('refresh_token', refreshToken); + params.append('client_id', IFLOW_OAUTH_CLIENT_ID); + params.append('client_secret', IFLOW_OAUTH_CLIENT_SECRET); + + // 构建 Basic Auth header + const basicAuth = Buffer.from(`${IFLOW_OAUTH_CLIENT_ID}:${IFLOW_OAUTH_CLIENT_SECRET}`).toString('base64'); + + const requestConfig = { + method: 'POST', + url: IFLOW_OAUTH_TOKEN_ENDPOINT, + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + 'Accept': 'application/json', + 'Authorization': `Basic ${basicAuth}` + }, + data: params.toString(), + timeout: 30000 + }; + + try { + const response = axiosInstance + ? await axiosInstance.request(requestConfig) + : await axios.request(requestConfig); + + const tokenResp = response.data; + + // console.log('[iFlow] Token response:', JSON.stringify(tokenResp)); + if (!tokenResp.access_token) { + console.error('[iFlow] Token response:', JSON.stringify(tokenResp)); + throw new Error('[iFlow] Missing access_token in response'); + } + + // 计算过期时间(毫秒级时间戳) + const expiresIn = tokenResp.expires_in || 3600; + const expireTimestamp = Date.now() + expiresIn * 1000; + + const tokenData = { + accessToken: tokenResp.access_token, + refreshToken: tokenResp.refresh_token || refreshToken, + tokenType: tokenResp.token_type || 'Bearer', + scope: tokenResp.scope || '', + expiryDate: expireTimestamp // 毫秒级时间戳 + }; + + console.log('[iFlow] OAuth tokens refreshed successfully'); + + // 获取用户信息以获取 API Key + const userInfo = await fetchUserInfo(tokenData.accessToken, axiosInstance); + if (userInfo && userInfo.apiKey) { + tokenData.apiKey = userInfo.apiKey; + tokenData.email = userInfo.email || userInfo.phone || ''; + } + + return tokenData; + } catch (error) { + const status = error.response?.status; + const data = error.response?.data; + console.error(`[iFlow] OAuth token refresh failed (Status: ${status}):`, data || error.message); + throw error; + } +} + +/** + * 获取用户信息(包含 API Key) + * @param {string} accessToken - 访问令牌 + * @param {Object} axiosInstance - axios 实例(可选) + * @returns {Promise} - 用户信息 + */ +async function fetchUserInfo(accessToken, axiosInstance = null) { + if (!accessToken || accessToken.trim() === '') { + throw new Error('[iFlow] access_token is empty'); + } + + const url = `${IFLOW_USER_INFO_ENDPOINT}?accessToken=${encodeURIComponent(accessToken)}`; + + const requestConfig = { + method: 'GET', + url, + headers: { + 'Accept': 'application/json' + }, + timeout: 30000 + }; + + try { + const response = axiosInstance + ? await axiosInstance.request(requestConfig) + : await axios.request(requestConfig); + + const result = response.data; + // console.log('[iFlow] User info response:', JSON.stringify(result)); + if (!result.success) { + throw new Error('[iFlow] User info request not successful'); + } + + if (!result.data || !result.data.apiKey) { + throw new Error('[iFlow] Missing apiKey in user info response'); + } + + return { + apiKey: result.data.apiKey, + email: result.data.email || '', + phone: result.data.phone || '' + }; + } catch (error) { + const status = error.response?.status; + const data = error.response?.data; + console.error(`[iFlow] Fetch user info failed (Status: ${status}):`, data || error.message); + throw error; + } +} + +// ==================== 请求处理工具函数 ==================== + +/** + * 检查模型是否支持 thinking 配置 + * @param {string} model - 模型名称 + * @returns {boolean} + */ +function isThinkingModel(model) { + if (!model) return false; + const lowerModel = model.toLowerCase(); + return THINKING_MODEL_PREFIXES.some(prefix => lowerModel.startsWith(prefix)); +} + +/** + * 应用 iFlow 特定的 thinking 配置 + * 将 reasoning_effort 转换为模型特定的配置 + * + * @param {Object} body - 请求体 + * @param {string} model - 模型名称 + * @returns {Object} - 处理后的请求体 + */ +function applyIFlowThinkingConfig(body, model) { + if (!body || !model) return body; + + const lowerModel = model.toLowerCase(); + const reasoningEffort = body.reasoning_effort; + + // 如果没有 reasoning_effort,直接返回 + if (reasoningEffort === undefined) return body; + + const enableThinking = reasoningEffort !== 'none' && reasoningEffort !== ''; + + // 创建新对象,移除 reasoning_effort 和 thinking + const newBody = { ...body }; + delete newBody.reasoning_effort; + delete newBody.thinking; + + // GLM-4.x: 使用 chat_template_kwargs + if (lowerModel.startsWith('glm-4')) { + newBody.chat_template_kwargs = { + ...(newBody.chat_template_kwargs || {}), + enable_thinking: enableThinking + }; + if (enableThinking) { + newBody.chat_template_kwargs.clear_thinking = false; + } + return newBody; + } + + // Qwen thinking 模型: 保持 thinking 配置 + if (lowerModel.includes('thinking')) { + // Qwen thinking 模型默认启用 thinking,不需要额外配置 + return newBody; + } + + // DeepSeek R1: 推理模型,不需要额外配置 + if (lowerModel.startsWith('deepseek-r1')) { + return newBody; + } + + return newBody; +} + +/** + * 保留消息历史中的 reasoning_content + * 对于支持 thinking 的模型,保留 assistant 消息中的 reasoning_content + * + * @param {Object} body - 请求体 + * @param {string} model - 模型名称 + * @returns {Object} - 处理后的请求体 + */ +function preserveReasoningContentInMessages(body, model) { + if (!body || !model) return body; + + const lowerModel = model.toLowerCase(); + + // 只对支持 thinking 的模型应用 + const needsPreservation = lowerModel.startsWith('glm-4') || + lowerModel.includes('thinking') || + lowerModel.startsWith('deepseek-r1'); + if (!needsPreservation) return body; + + const messages = body.messages; + if (!Array.isArray(messages)) return body; + + // 检查是否有 assistant 消息包含 reasoning_content + const hasReasoningContent = messages.some(msg => + msg.role === 'assistant' && msg.reasoning_content && msg.reasoning_content !== '' + ); + + if (hasReasoningContent) { + console.log(`[iFlow] reasoning_content found in message history for ${model}`); + } + + return body; +} + +/** + * 确保 tools 数组存在(避免某些模型的问题) + * 如果 tools 是空数组,添加一个占位工具 + * + * @param {Object} body - 请求体 + * @returns {Object} - 处理后的请求体 + */ +function ensureToolsArray(body) { + if (!body || !body.tools) return body; + + if (Array.isArray(body.tools) && body.tools.length === 0) { + return { + ...body, + tools: [{ + type: 'function', + function: { + name: 'noop', + description: 'Placeholder tool to stabilise streaming', + parameters: { type: 'object' } + } + }] + }; + } + + return body; +} + +/** + * 预处理请求体 + * @param {Object} body - 原始请求体 + * @param {string} model - 模型名称 + * @returns {Object} - 处理后的请求体 + */ +function preprocessRequestBody(body, model) { + let processedBody = { ...body }; + + // 确保模型名称正确 + processedBody.model = model; + + // 应用 iFlow thinking 配置 + processedBody = applyIFlowThinkingConfig(processedBody, model); + + // 保留 reasoning_content + processedBody = preserveReasoningContentInMessages(processedBody, model); + + // 确保 tools 数组 + processedBody = ensureToolsArray(processedBody); + + return processedBody; +} + +// ==================== API 服务 ==================== + +/** + * iFlow API 服务类 + */ +// 默认 Token 文件路径 +const DEFAULT_TOKEN_FILE_PATH = path.join(os.homedir(), '.iflow', 'oauth_creds.json'); + +export class IFlowApiService { + constructor(config) { + this.config = config; + this.apiKey = null; + this.baseUrl = config.IFLOW_BASE_URL || IFLOW_API_BASE_URL; + this.tokenFilePath = config.IFLOW_TOKEN_FILE_PATH || DEFAULT_TOKEN_FILE_PATH; + this.isInitialized = false; + this.tokenStorage = null; + + // 配置 HTTP/HTTPS agent + const httpAgent = new http.Agent({ + keepAlive: true, + maxSockets: 100, + maxFreeSockets: 5, + timeout: 120000, + }); + const httpsAgent = new https.Agent({ + keepAlive: true, + maxSockets: 100, + maxFreeSockets: 5, + timeout: 120000, + }); + + const axiosConfig = { + baseURL: this.baseUrl, + httpAgent, + httpsAgent, + headers: { + 'Content-Type': 'application/json', + 'User-Agent': IFLOW_USER_AGENT, + }, + }; + + // 配置自定义代理 + configureAxiosProxy(axiosConfig, config, 'openai-iflow'); + + this.axiosInstance = axios.create(axiosConfig); + } + + /** + * 初始化服务 + */ + async initialize() { + if (this.isInitialized) return; + + console.log('[iFlow] Initializing iFlow API Service...'); + await this.initializeAuth(); + + this.isInitialized = true; + console.log('[iFlow] Initialization complete.'); + } + + /** + * 初始化认证 + * @param {boolean} forceRefresh - 是否强制刷新 Token + */ + async initializeAuth(forceRefresh = false) { + // 如果已有 API Key 且不强制刷新,直接返回 + if (this.apiKey && !forceRefresh) return; + + // 从 Token 文件加载 API Key + if (!this.tokenFilePath) { + throw new Error('[iFlow] IFLOW_TOKEN_FILE_PATH is required.'); + } + + try { + this.tokenStorage = await loadTokenFromFile(this.tokenFilePath); + if (this.tokenStorage && this.tokenStorage.apiKey) { + this.apiKey = this.tokenStorage.apiKey; + console.log('[iFlow Auth] Authentication configured successfully from file.'); + + if (forceRefresh) { + console.log('[iFlow Auth] Forcing token refresh...'); + await this._refreshOAuthTokens(); + console.log('[iFlow Auth] Token refreshed and saved successfully.'); + } + } else { + throw new Error('[iFlow] Token file does not contain a valid API key.'); + } + } catch (error) { + console.error('[iFlow Auth] Error initializing authentication:', error.code || error.message); + if (error.code === 'ENOENT') { + console.log(`[iFlow Auth] Credentials file '${this.tokenFilePath}' not found.`); + throw new Error(`[iFlow Auth] Credentials file not found. Please run OAuth flow first.`); + } else { + console.error('[iFlow Auth] Failed to initialize authentication from file:', error.message); + throw new Error(`[iFlow Auth] Failed to load OAuth credentials.`); + } + } + + // 更新 axios 实例的 Authorization header + this.axiosInstance.defaults.headers['Authorization'] = `Bearer ${this.apiKey}`; + } + + /** + * 检查是否需要刷新 Token 并执行刷新 + * @returns {Promise} - 是否执行了刷新 + */ + async _checkAndRefreshTokenIfNeeded() { + if (!this.tokenStorage) { + return false; + } + + // 检查是否有 refresh_token + if (!this.tokenStorage.refreshToken || this.tokenStorage.refreshToken.trim() === '') { + console.log('[iFlow] No refresh_token available, skipping token refresh check'); + return false; + } + + // 使用 isExpiryDateNear 检查过期时间 + if (!this.isExpiryDateNear()) { + console.log('[iFlow] Token is valid, no refresh needed'); + return false; + } + + console.log('[iFlow] Token is expiring soon, attempting refresh...'); + + try { + await this._refreshOAuthTokens(); + return true; + } catch (error) { + console.error('[iFlow] Token refresh failed:', error.message); + // 刷新失败不抛出异常,继续使用现有 Token + return false; + } + } + + /** + * 使用 refresh_token 刷新 OAuth Token + * @returns {Promise} + */ + async _refreshOAuthTokens() { + if (!this.tokenStorage || !this.tokenStorage.refreshToken) { + throw new Error('[iFlow] No refresh_token available'); + } + + const oldAccessToken = this.tokenStorage.accessToken; + if (oldAccessToken) { + console.log(`[iFlow] Refreshing access token, old: ${this._maskToken(oldAccessToken)}`); + } + + // 调用刷新函数 + const tokenData = await refreshOAuthTokens(this.tokenStorage.refreshToken, this.axiosInstance); + + // 更新 tokenStorage + this.tokenStorage.accessToken = tokenData.accessToken; + if (tokenData.refreshToken) { + this.tokenStorage.refreshToken = tokenData.refreshToken; + } + if (tokenData.apiKey) { + this.tokenStorage.apiKey = tokenData.apiKey; + this.apiKey = tokenData.apiKey; + } + this.tokenStorage.expiryDate = tokenData.expiryDate; + this.tokenStorage.tokenType = tokenData.tokenType || 'Bearer'; + this.tokenStorage.scope = tokenData.scope || ''; + if (tokenData.email) { + this.tokenStorage.email = tokenData.email; + } + + // 更新 axios 实例的 Authorization header + this.axiosInstance.defaults.headers['Authorization'] = `Bearer ${this.apiKey}`; + + // 保存到文件 + await saveTokenToFile(this.tokenFilePath, this.tokenStorage); + + console.log(`[iFlow] Token refresh successful, new: ${this._maskToken(tokenData.accessToken)}`); + } + + /** + * 掩码 Token(只显示前后几个字符) + * @param {string} token - Token 字符串 + * @returns {string} - 掩码后的 Token + */ + _maskToken(token) { + if (!token || token.length < 10) { + return '***'; + } + return `${token.substring(0, 4)}...${token.substring(token.length - 4)}`; + } + + /** + * 手动刷新 Token(供外部调用) + * @returns {Promise} - 是否刷新成功 + */ + async refreshToken() { + if (!this.isInitialized) { + await this.initialize(); + } + + try { + await this._refreshOAuthTokens(); + return true; + } catch (error) { + console.error('[iFlow] Manual token refresh failed:', error.message); + return false; + } + } + + /** + * Checks if the given expiry date is within the threshold from now or already expired. + * @returns {boolean} True if the expiry date is within the threshold or already expired, false otherwise. + */ + isExpiryDateNear() { + try { + if (!this.tokenStorage || !this.tokenStorage.expiryDate) { + return false; + } + + const currentTime = Date.now(); + // 默认 10 分钟,可通过配置覆盖 + const cronNearMinutes = this.config.CRON_NEAR_MINUTES || 10; + const cronNearMinutesInMillis = cronNearMinutes * 60 * 1000; + + // 解析过期时间 + let expireTime; + const expireValue = this.tokenStorage.expiryDate; + + // 检查是否为数字(毫秒时间戳) + if (typeof expireValue === 'number') { + expireTime = expireValue; + } else if (typeof expireValue === 'string') { + // 检查是否为纯数字字符串(毫秒时间戳) + if (/^\d+$/.test(expireValue)) { + expireTime = parseInt(expireValue, 10); + } else if (expireValue.includes('T')) { + // ISO 8601 格式 + expireTime = new Date(expireValue).getTime(); + } else { + // 格式:2006-01-02 15:04 + expireTime = new Date(expireValue.replace(' ', 'T') + ':00').getTime(); + } + } else { + console.error(`[iFlow] Invalid expiry date type: ${typeof expireValue}`); + return false; + } + + if (isNaN(expireTime)) { + console.error(`[iFlow] Error parsing expiry date: ${expireValue}`); + return false; + } + + // 计算剩余时间 + const timeRemaining = expireTime - currentTime; + + // 判断是否已过期或接近过期 + // 已过期:timeRemaining <= 0 + // 接近过期:timeRemaining > 0 && timeRemaining <= cronNearMinutesInMillis + const isExpired = timeRemaining <= 0; + const isNear = timeRemaining > 0 && timeRemaining <= cronNearMinutesInMillis; + const needsRefresh = isExpired || isNear; + + const expireDateStr = new Date(expireTime).toISOString(); + const timeRemainingMinutes = Math.floor(timeRemaining / 60000); + const timeRemainingHours = (timeRemaining / 3600000).toFixed(2); + + console.log(`[iFlow] Token expiry check: Expiry=${expireDateStr}, Remaining=${timeRemainingHours}h (${timeRemainingMinutes}min), Threshold=${cronNearMinutes}min, Expired=${isExpired}, Near=${isNear}, NeedsRefresh=${needsRefresh}`); + + return needsRefresh; + } catch (error) { + console.error(`[iFlow] Error checking expiry date: ${error.message}`); + return false; + } + } + + /** + * 获取请求头 + * @param {boolean} stream - 是否为流式请求 + * @returns {Object} - 请求头 + */ + _getHeaders(stream = false) { + const headers = { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${this.apiKey}`, + 'User-Agent': IFLOW_USER_AGENT, + }; + + if (stream) { + headers['Accept'] = 'text/event-stream'; + } else { + headers['Accept'] = 'application/json'; + } + + return headers; + } + + /** + * 调用 API + */ + async callApi(endpoint, body, model, retryCount = 0) { + const maxRetries = this.config.REQUEST_MAX_RETRIES || 3; + const baseDelay = this.config.REQUEST_BASE_DELAY || 1000; + + // 预处理请求体 + const processedBody = preprocessRequestBody(body, model); + + try { + const response = await this.axiosInstance.post(endpoint, processedBody, { + headers: this._getHeaders(false) + }); + return response.data; + } catch (error) { + const status = error.response?.status; + const data = error.response?.data; + const errorCode = error.code; + const errorMessage = error.message || ''; + + // 定义可重试的网络错误标识(可能出现在 code 或 message 中) + const retryableNetworkErrors = [ + 'ECONNRESET', // 连接被重置 + 'ETIMEDOUT', // 连接超时 + 'ECONNREFUSED', // 连接被拒绝 + 'ENOTFOUND', // DNS 解析失败 + 'ENETUNREACH', // 网络不可达 + 'EHOSTUNREACH', // 主机不可达 + 'EPIPE', // 管道破裂 + 'EAI_AGAIN', // DNS 临时失败 + 'ECONNABORTED', // 连接中止 + 'ESOCKETTIMEDOUT', // Socket 超时 + ]; + + // 检查是否为可重试的网络错误(检查 code 和 message) + const isRetryableNetworkError = retryableNetworkErrors.some(errId => + errorCode === errId || errorMessage.includes(errId) + ); + + if (status === 401 || status === 403) { + console.error(`[iFlow] Received ${status}. API Key might be invalid or expired.`); + throw error; + } + + // Handle 429 (Too Many Requests) with exponential backoff + if (status === 429 && retryCount < maxRetries) { + const delay = baseDelay * Math.pow(2, retryCount); + console.log(`[iFlow] Received 429 (Too Many Requests). Retrying in ${delay}ms... (attempt ${retryCount + 1}/${maxRetries})`); + await new Promise(resolve => setTimeout(resolve, delay)); + return this.callApi(endpoint, body, model, retryCount + 1); + } + + // Handle other retryable errors (5xx server errors) + if (status >= 500 && status < 600 && retryCount < maxRetries) { + const delay = baseDelay * Math.pow(2, retryCount); + console.log(`[iFlow] Received ${status} server error. Retrying in ${delay}ms... (attempt ${retryCount + 1}/${maxRetries})`); + await new Promise(resolve => setTimeout(resolve, delay)); + return this.callApi(endpoint, body, model, retryCount + 1); + } + + // Handle network errors (ECONNRESET, ETIMEDOUT, etc.) with exponential backoff + if (isRetryableNetworkError && retryCount < maxRetries) { + const delay = baseDelay * Math.pow(2, retryCount); + const errorIdentifier = errorCode || errorMessage.substring(0, 50); + console.log(`[iFlow] Network error (${errorIdentifier}). Retrying in ${delay}ms... (attempt ${retryCount + 1}/${maxRetries})`); + await new Promise(resolve => setTimeout(resolve, delay)); + return this.callApi(endpoint, body, model, retryCount + 1); + } + + console.error(`[iFlow] Error calling API (Status: ${status}, Code: ${errorCode}):`, data || error.message); + throw error; + } + } + + /** + * 流式调用 API + * + * - 使用大缓冲区处理长行 + * - 逐行处理 SSE 数据 + * - 正确处理 data: 前缀和 [DONE] 标记 + */ + async *streamApi(endpoint, body, model, retryCount = 0) { + const maxRetries = this.config.REQUEST_MAX_RETRIES || 3; + const baseDelay = this.config.REQUEST_BASE_DELAY || 1000; + + // 预处理请求体并设置 stream: true + const processedBody = preprocessRequestBody({ ...body, stream: true }, model); + + try { + const response = await this.axiosInstance.post(endpoint, processedBody, { + responseType: 'stream', + headers: this._getHeaders(true) + }); + + const stream = response.data; + let buffer = ''; + + for await (const chunk of stream) { + // 将 chunk 转换为字符串并追加到缓冲区 + buffer += chunk.toString(); + + // 逐行处理 + let newlineIndex; + while ((newlineIndex = buffer.indexOf('\n')) !== -1) { + // 提取一行(不包含换行符) + const line = buffer.substring(0, newlineIndex); + buffer = buffer.substring(newlineIndex + 1); + + // 去除行首尾空白(处理 \r\n 情况) + const trimmedLine = line.trim(); + + // 跳过空行(SSE 格式中的分隔符) + if (trimmedLine === '') { + continue; + } + + // 处理 SSE data: 前缀 + if (trimmedLine.startsWith('data:')) { + // 提取 data: 后的内容(注意:data: 后可能有空格也可能没有) + let jsonData = trimmedLine.substring(5); + // 去除前导空格 + if (jsonData.startsWith(' ')) { + jsonData = jsonData.substring(1); + } + jsonData = jsonData.trim(); + + // 检查流结束标记 + if (jsonData === '[DONE]') { + return; // 流结束 + } + + // 跳过空数据 + if (jsonData === '') { + continue; + } + + try { + const parsedChunk = JSON.parse(jsonData); + yield parsedChunk; + } catch (e) { + // JSON 解析失败,记录警告但继续处理 + console.warn("[iFlow] Failed to parse stream chunk JSON:", e.message, "Data:", jsonData.substring(0, 200)); + } + } + // 忽略其他 SSE 字段(如 event:, id:, retry: 等) + } + } + + // 处理缓冲区中剩余的数据(如果有的话) + if (buffer.trim() !== '') { + const trimmedLine = buffer.trim(); + if (trimmedLine.startsWith('data:')) { + let jsonData = trimmedLine.substring(5); + if (jsonData.startsWith(' ')) { + jsonData = jsonData.substring(1); + } + jsonData = jsonData.trim(); + + if (jsonData !== '[DONE]' && jsonData !== '') { + try { + const parsedChunk = JSON.parse(jsonData); + yield parsedChunk; + } catch (e) { + console.warn("[iFlow] Failed to parse final stream chunk JSON:", e.message); + } + } + } + } + } catch (error) { + const status = error.response?.status; + const data = error.response?.data; + const errorCode = error.code; + const errorMessage = error.message || ''; + + // 定义可重试的网络错误标识(可能出现在 code 或 message 中) + const retryableNetworkErrors = [ + 'ECONNRESET', // 连接被重置 + 'ETIMEDOUT', // 连接超时 + 'ECONNREFUSED', // 连接被拒绝 + 'ENOTFOUND', // DNS 解析失败 + 'ENETUNREACH', // 网络不可达 + 'EHOSTUNREACH', // 主机不可达 + 'EPIPE', // 管道破裂 + 'EAI_AGAIN', // DNS 临时失败 + 'ECONNABORTED', // 连接中止 + 'ESOCKETTIMEDOUT', // Socket 超时 + ]; + + // 检查是否为可重试的网络错误(检查 code 和 message) + const isRetryableNetworkError = retryableNetworkErrors.some(errId => + errorCode === errId || errorMessage.includes(errId) + ); + + if (status === 401 || status === 403) { + console.error(`[iFlow] Received ${status} during stream. API Key might be invalid or expired.`); + throw error; + } + + // Handle 429 (Too Many Requests) with exponential backoff + if (status === 429 && retryCount < maxRetries) { + const delay = baseDelay * Math.pow(2, retryCount); + console.log(`[iFlow] Received 429 (Too Many Requests) during stream. Retrying in ${delay}ms... (attempt ${retryCount + 1}/${maxRetries})`); + await new Promise(resolve => setTimeout(resolve, delay)); + yield* this.streamApi(endpoint, body, model, retryCount + 1); + return; + } + + // Handle other retryable errors (5xx server errors) + if (status >= 500 && status < 600 && retryCount < maxRetries) { + const delay = baseDelay * Math.pow(2, retryCount); + console.log(`[iFlow] Received ${status} server error during stream. Retrying in ${delay}ms... (attempt ${retryCount + 1}/${maxRetries})`); + await new Promise(resolve => setTimeout(resolve, delay)); + yield* this.streamApi(endpoint, body, model, retryCount + 1); + return; + } + + // Handle network errors (ECONNRESET, ETIMEDOUT, etc.) with exponential backoff + if (isRetryableNetworkError && retryCount < maxRetries) { + const delay = baseDelay * Math.pow(2, retryCount); + const errorIdentifier = errorCode || errorMessage.substring(0, 50); + console.log(`[iFlow] Network error (${errorIdentifier}) during stream. Retrying in ${delay}ms... (attempt ${retryCount + 1}/${maxRetries})`); + await new Promise(resolve => setTimeout(resolve, delay)); + yield* this.streamApi(endpoint, body, model, retryCount + 1); + return; + } + + console.error(`[iFlow] Error calling streaming API (Status: ${status}, Code: ${errorCode}):`, data || error.message); + throw error; + } + } + + /** + * 生成内容 + */ + async generateContent(model, requestBody) { + if (!this.isInitialized) { + await this.initialize(); + } + + // 在 API 调用前检查是否需要刷新 Token + await this._checkAndRefreshTokenIfNeeded(); + + return this.callApi('/chat/completions', requestBody, model); + } + + /** + * 流式生成内容 + */ + async *generateContentStream(model, requestBody) { + if (!this.isInitialized) { + await this.initialize(); + } + + // 在 API 调用前检查是否需要刷新 Token + await this._checkAndRefreshTokenIfNeeded(); + + yield* this.streamApi('/chat/completions', requestBody, model); + } + + /** + * 列出可用模型 + */ + async listModels() { + if (!this.isInitialized) { + await this.initialize(); + } + + try { + const response = await this.axiosInstance.get('/models', { + headers: this._getHeaders(false) + }); + + // 检查返回数据中是否包含 glm-4.7,如果没有则添加 + const modelsData = response.data; + if (modelsData && modelsData.data && Array.isArray(modelsData.data)) { + const hasGlm47 = modelsData.data.some(model => model.id === 'glm-4.7'); + if (!hasGlm47) { + // 添加 glm-4.7 模型到返回列表 + modelsData.data.push({ + id: 'glm-4.7', + object: 'model', + created: Math.floor(Date.now() / 1000), + owned_by: 'iflow' + }); + console.log('[iFlow] Added glm-4.7 to models list'); + } + } + + return modelsData; + } catch (error) { + console.warn('[iFlow] Failed to fetch models from API, using default list:', error.message); + // 返回默认模型列表,确保包含 glm-4.7 + const defaultModels = [...IFLOW_MODELS]; + if (!defaultModels.includes('glm-4.7')) { + defaultModels.push('glm-4.7'); + } + return { + object: 'list', + data: defaultModels.map(id => ({ + id, + object: 'model', + created: Math.floor(Date.now() / 1000), + owned_by: 'iflow' + })) + }; + } + } + +} + +export { + IFLOW_MODELS, + IFLOW_USER_AGENT, + IFlowTokenStorage, + loadTokenFromFile, + saveTokenToFile, + refreshOAuthTokens, + fetchUserInfo, + isThinkingModel, + applyIFlowThinkingConfig, + preserveReasoningContentInMessages, + ensureToolsArray, + preprocessRequestBody, +}; \ No newline at end of file diff --git a/src/provider-models.js b/src/provider-models.js index 48471fb..69d037a 100644 --- a/src/provider-models.js +++ b/src/provider-models.js @@ -38,6 +38,29 @@ export const PROVIDER_MODELS = { 'openai-qwen-oauth': [ 'qwen3-coder-plus', 'qwen3-coder-flash' + ], + 'openai-iflow': [ + // iFlow 特有模型 + 'iflow-rome-30ba3b', + // Qwen 模型 + 'qwen3-coder-plus', + 'qwen3-max', + 'qwen3-vl-plus', + 'qwen3-max-preview', + 'qwen3-32b', + 'qwen3-235b-a22b-thinking-2507', + 'qwen3-235b-a22b-instruct', + 'qwen3-235b', + // Kimi 模型 + 'kimi-k2-0905', + 'kimi-k2', + // GLM 模型 + 'glm-4.6', + 'glm-4.7', + // DeepSeek 模型 + 'deepseek-v3.2', + 'deepseek-r1', + 'deepseek-v3' ] }; diff --git a/src/provider-utils.js b/src/provider-utils.js index b235fe4..0c304fc 100644 --- a/src/provider-utils.js +++ b/src/provider-utils.js @@ -54,6 +54,17 @@ export const PROVIDER_MAPPINGS = [ displayName: 'Gemini Antigravity', needsProjectId: true, urlKeys: ['ANTIGRAVITY_BASE_URL_DAILY', 'ANTIGRAVITY_BASE_URL_AUTOPUSH'] + }, + { + // iFlow 配置 + dirName: 'iflow', + patterns: ['configs/iflow/', '/iflow/'], + providerType: 'openai-iflow', + credPathKey: 'IFLOW_TOKEN_FILE_PATH', + defaultCheckModel: 'gpt-4o', + displayName: 'iFlow API', + needsProjectId: false, + urlKeys: ['IFLOW_BASE_URL'] } ]; diff --git a/src/service-manager.js b/src/service-manager.js index 9446c40..e18405f 100644 --- a/src/service-manager.js +++ b/src/service-manager.js @@ -79,8 +79,10 @@ export async function autoLinkProviderConfigs(config) { for (const [displayName, providers] of Object.entries(allNewProviders)) { console.log(` ${displayName}: ${providers.length} config(s)`); providers.forEach(p => { - // 获取凭据路径键 - const credKey = Object.keys(p).find(k => k.endsWith('_CREDS_FILE_PATH')); + // 获取凭据路径键(支持 _CREDS_FILE_PATH 和 _TOKEN_FILE_PATH 两种格式) + const credKey = Object.keys(p).find(k => + k.endsWith('_CREDS_FILE_PATH') || k.endsWith('_TOKEN_FILE_PATH') + ); if (credKey) { console.log(` - ${p[credKey]}`); } @@ -376,7 +378,8 @@ export async function getProviderStatus(config, options = {}) { 'claude-custom': 'CLAUDE_BASE_URL', 'claude-kiro-oauth': 'KIRO_OAUTH_CREDS_FILE_PATH', 'openai-qwen-oauth': 'QWEN_OAUTH_CREDS_FILE_PATH', - 'gemini-antigravity': 'ANTIGRAVITY_OAUTH_CREDS_FILE_PATH' + 'gemini-antigravity': 'ANTIGRAVITY_OAUTH_CREDS_FILE_PATH', + 'openai-iflow': 'IFLOW_TOKEN_FILE_PATH' }; let providerPoolsSlim = []; let unhealthyProvideIdentifyList = []; diff --git a/src/ui-manager.js b/src/ui-manager.js index c7cf499..82d4a0d 100644 --- a/src/ui-manager.js +++ b/src/ui-manager.js @@ -56,7 +56,7 @@ import { getAllProviderModels, getProviderModels } from './provider-models.js'; import { CONFIG } from './config-manager.js'; import { serviceInstances, getServiceAdapter } from './adapter.js'; import { initApiService } from './service-manager.js'; -import { handleGeminiCliOAuth, handleGeminiAntigravityOAuth, handleQwenOAuth, handleKiroOAuth } from './oauth-handlers.js'; +import { handleGeminiCliOAuth, handleGeminiAntigravityOAuth, handleQwenOAuth, handleKiroOAuth, handleIFlowOAuth } from './oauth-handlers.js'; import { generateUUID, normalizePath, @@ -1442,6 +1442,11 @@ export async function handleUIApiRequests(method, pathParam, req, res, currentCo const result = await handleKiroOAuth(currentConfig, options); authUrl = result.authUrl; authInfo = result.authInfo; + } else if (providerType === 'openai-iflow') { + // iFlow OAuth 授权 + const result = await handleIFlowOAuth(currentConfig, options); + authUrl = result.authUrl; + authInfo = result.authInfo; } else { res.writeHead(400, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ @@ -2229,6 +2234,8 @@ async function scanConfigFiles(currentConfig, providerPoolManager) { addToUsedPaths(usedPaths, currentConfig.GEMINI_OAUTH_CREDS_FILE_PATH); addToUsedPaths(usedPaths, currentConfig.KIRO_OAUTH_CREDS_FILE_PATH); addToUsedPaths(usedPaths, currentConfig.QWEN_OAUTH_CREDS_FILE_PATH); + addToUsedPaths(usedPaths, currentConfig.ANTIGRAVITY_OAUTH_CREDS_FILE_PATH); + addToUsedPaths(usedPaths, currentConfig.IFLOW_TOKEN_FILE_PATH); // 使用最新的提供商池数据 let providerPools = currentConfig.providerPools; @@ -2244,6 +2251,7 @@ async function scanConfigFiles(currentConfig, providerPoolManager) { addToUsedPaths(usedPaths, provider.KIRO_OAUTH_CREDS_FILE_PATH); addToUsedPaths(usedPaths, provider.QWEN_OAUTH_CREDS_FILE_PATH); addToUsedPaths(usedPaths, provider.ANTIGRAVITY_OAUTH_CREDS_FILE_PATH); + addToUsedPaths(usedPaths, provider.IFLOW_TOKEN_FILE_PATH); } } } @@ -2403,6 +2411,17 @@ function getFileUsageInfo(relativePath, fileName, usedPaths, currentConfig) { }); } + if (currentConfig.IFLOW_TOKEN_FILE_PATH && + (pathsEqual(relativePath, currentConfig.IFLOW_TOKEN_FILE_PATH) || + pathsEqual(relativePath, currentConfig.IFLOW_TOKEN_FILE_PATH.replace(/\\/g, '/')))) { + usageInfo.usageType = 'main_config'; + usageInfo.usageDetails.push({ + type: 'Main Config', + location: 'iFlow Token file path', + configKey: 'IFLOW_TOKEN_FILE_PATH' + }); + } + // 检查提供商池中的使用情况 if (currentConfig.providerPools) { // 使用 flatMap 将双重循环优化为单层循环 O(n) @@ -2461,6 +2480,18 @@ function getFileUsageInfo(relativePath, fileName, usedPaths, currentConfig) { configKey: 'ANTIGRAVITY_OAUTH_CREDS_FILE_PATH' }); } + + if (provider.IFLOW_TOKEN_FILE_PATH && + (pathsEqual(relativePath, provider.IFLOW_TOKEN_FILE_PATH) || + pathsEqual(relativePath, provider.IFLOW_TOKEN_FILE_PATH.replace(/\\/g, '/')))) { + providerUsages.push({ + type: 'Provider Pool', + location: `iFlow Token (node ${index + 1})`, + providerType: providerType, + providerIndex: index, + configKey: 'IFLOW_TOKEN_FILE_PATH' + }); + } if (providerUsages.length > 0) { usageInfo.usageType = 'provider_pool'; @@ -2708,7 +2739,8 @@ function getProviderDisplayName(provider, providerType) { 'claude-kiro-oauth': 'KIRO_OAUTH_CREDS_FILE_PATH', 'gemini-cli-oauth': 'GEMINI_OAUTH_CREDS_FILE_PATH', 'gemini-antigravity': 'ANTIGRAVITY_OAUTH_CREDS_FILE_PATH', - 'openai-qwen-oauth': 'QWEN_OAUTH_CREDS_FILE_PATH' + 'openai-qwen-oauth': 'QWEN_OAUTH_CREDS_FILE_PATH', + 'openai-iflow': 'IFLOW_TOKEN_FILE_PATH' }[providerType]; if (credPathKey && provider[credPathKey]) { diff --git a/static/app/file-upload.js b/static/app/file-upload.js index d440869..a6dd610 100644 --- a/static/app/file-upload.js +++ b/static/app/file-upload.js @@ -57,7 +57,8 @@ class FileUploadHandler { 'gemini-cli-oauth': 'gemini', 'gemini-antigravity': 'antigravity', 'claude-kiro-oauth': 'kiro', - 'openai-qwen-oauth': 'qwen' + 'openai-qwen-oauth': 'qwen', + 'openai-iflow': 'iflow' }; return providerMap[provider] || 'gemini'; } diff --git a/static/app/i18n.js b/static/app/i18n.js index 6d5f46d..4b760f5 100644 --- a/static/app/i18n.js +++ b/static/app/i18n.js @@ -3,6 +3,7 @@ const translations = { 'zh-CN': { // Header 'header.title': 'AIClient2API 管理控制台', + 'header.description': 'AIClient2API 管理控制台 - 统一管理 AI 服务提供商', 'header.github': 'GitHub 仓库', 'header.themeToggle': '切换主题', 'header.status.connecting': '连接中...', @@ -83,6 +84,7 @@ const translations = { 'dashboard.routing.nodeName.kiro': 'Claude Kiro OAuth', 'dashboard.routing.nodeName.openai': 'OpenAI Custom', 'dashboard.routing.nodeName.qwen': 'Qwen OAuth', + 'dashboard.routing.nodeName.iflow': 'iFlow OAuth', 'dashboard.contact.title': '联系与赞助', 'dashboard.contact.wechat': '扫码进群,注明来意', 'dashboard.contact.wechatDesc': '添加微信获取更多技术支持和交流', @@ -130,6 +132,10 @@ const translations = { 'oauth.kiro.step2': '使用您的 {method} 账号登录', 'oauth.kiro.step3': '授权完成后页面会自动关闭', 'oauth.kiro.step4': '刷新本页面查看凭据文件', + 'oauth.iflow.step1': '点击下方按钮在浏览器中打开 iFlow 授权页面', + 'oauth.iflow.step2': '使用您的 iFlow 账号登录并授权', + 'oauth.iflow.step3': '授权完成后,系统会自动获取 API Key', + 'oauth.iflow.step4': '凭据文件可在上传配置管理中查看和管理', // Config 'config.title': '配置管理', @@ -425,6 +431,7 @@ const translations = { 'en-US': { // Header 'header.title': 'AIClient2API Management Console', + 'header.description': 'AIClient2API Management Console - Unified management of AI service providers', 'header.github': 'GitHub Repository', 'header.themeToggle': 'Toggle Theme', 'header.status.connecting': 'Connecting...', @@ -505,6 +512,7 @@ const translations = { 'dashboard.routing.nodeName.kiro': 'Claude Kiro OAuth', 'dashboard.routing.nodeName.openai': 'OpenAI Custom', 'dashboard.routing.nodeName.qwen': 'Qwen OAuth', + 'dashboard.routing.nodeName.iflow': 'iFlow OAuth', 'dashboard.contact.title': 'Contact & Support', 'dashboard.contact.wechat': 'Scan to Join Group', 'dashboard.contact.wechatDesc': 'Add WeChat for more technical support and communication', @@ -552,6 +560,10 @@ const translations = { 'oauth.kiro.step2': 'Log in with your {method} account', 'oauth.kiro.step3': 'The page will close automatically after authorization', 'oauth.kiro.step4': 'Refresh this page to view the credentials file', + 'oauth.iflow.step1': 'Click the button below to open the iFlow authorization page', + 'oauth.iflow.step2': 'Log in with your iFlow account and authorize', + 'oauth.iflow.step3': 'After authorization, the system will automatically fetch the API Key', + 'oauth.iflow.step4': 'Credentials files can be viewed and managed in Upload Config', // Config 'config.title': 'Configuration Management', diff --git a/static/app/modal.js b/static/app/modal.js index 9df953a..0def12c 100644 --- a/static/app/modal.js +++ b/static/app/modal.js @@ -676,7 +676,8 @@ function getFieldOrder(provider) { 'gemini-cli-oauth': ['PROJECT_ID', 'GEMINI_OAUTH_CREDS_FILE_PATH', 'GEMINI_BASE_URL'], 'claude-kiro-oauth': ['KIRO_OAUTH_CREDS_FILE_PATH', 'KIRO_BASE_URL', 'KIRO_REFRESH_URL', 'KIRO_REFRESH_IDC_URL'], 'openai-qwen-oauth': ['QWEN_OAUTH_CREDS_FILE_PATH', 'QWEN_BASE_URL', 'QWEN_OAUTH_BASE_URL'], - 'gemini-antigravity': ['PROJECT_ID', 'ANTIGRAVITY_OAUTH_CREDS_FILE_PATH', 'ANTIGRAVITY_BASE_URL_DAILY', 'ANTIGRAVITY_BASE_URL_AUTOPUSH'] + 'gemini-antigravity': ['PROJECT_ID', 'ANTIGRAVITY_OAUTH_CREDS_FILE_PATH', 'ANTIGRAVITY_BASE_URL_DAILY', 'ANTIGRAVITY_BASE_URL_AUTOPUSH'], + 'openai-iflow': ['IFLOW_OAUTH_CREDS_FILE_PATH', 'IFLOW_BASE_URL'] }; // 尝试从全局或当前模态框上下文中推断提供商类型 @@ -694,6 +695,8 @@ function getFieldOrder(provider) { providerType = 'openai-qwen-oauth'; } else if (provider.ANTIGRAVITY_OAUTH_CREDS_FILE_PATH) { providerType = 'gemini-antigravity'; + } else if (provider.IFLOW_OAUTH_CREDS_FILE_PATH) { + providerType = 'openai-iflow'; } } diff --git a/static/app/provider-manager.js b/static/app/provider-manager.js index d8ad44e..e45f5b9 100644 --- a/static/app/provider-manager.js +++ b/static/app/provider-manager.js @@ -213,7 +213,8 @@ function renderProviders(providers) { 'claude-custom', 'claude-kiro-oauth', 'openai-qwen-oauth', - 'openaiResponses-custom' + 'openaiResponses-custom', + 'openai-iflow' ]; // 获取所有提供商类型并按指定顺序排序 @@ -422,7 +423,7 @@ async function openProviderManager(providerType) { */ function generateAuthButton(providerType) { // 只为支持OAuth的提供商显示授权按钮 - const oauthProviders = ['gemini-cli-oauth', 'gemini-antigravity', 'openai-qwen-oauth', 'claude-kiro-oauth']; + const oauthProviders = ['gemini-cli-oauth', 'gemini-antigravity', 'openai-qwen-oauth', 'claude-kiro-oauth', 'openai-iflow']; if (!oauthProviders.includes(providerType)) { return ''; @@ -587,7 +588,8 @@ function getAuthFilePath(provider) { 'gemini-cli-oauth': '~/.gemini/oauth_creds.json', 'gemini-antigravity': '~/.antigravity/oauth_creds.json', 'openai-qwen-oauth': '~/.qwen/oauth_creds.json', - 'claude-kiro-oauth': '~/.aws/sso/cache/kiro-auth-token.json' + 'claude-kiro-oauth': '~/.aws/sso/cache/kiro-auth-token.json', + 'openai-iflow': '~/.iflow/oauth_creds.json' }; return authFilePaths[provider] || (getCurrentLanguage() === 'en-US' ? 'Unknown Path' : '未知路径'); } @@ -637,6 +639,18 @@ function showAuthModal(authUrl, authInfo) { `; + } else if (authInfo.provider === 'openai-iflow') { + instructionsHtml = ` +
+

${t('oauth.modal.steps')}

+
    +
  1. ${t('oauth.iflow.step1')}
  2. +
  3. ${t('oauth.iflow.step2')}
  4. +
  5. ${t('oauth.iflow.step3')}
  6. +
  7. ${t('oauth.iflow.step4')}
  8. +
+
+ `; } else { instructionsHtml = `
diff --git a/static/app/styles.css b/static/app/styles.css index cc12faf..6ecc2b7 100644 --- a/static/app/styles.css +++ b/static/app/styles.css @@ -3172,7 +3172,7 @@ input:checked + .toggle-slider:before { } .delete-confirm-modal.unused .btn-confirm-delete { - background: linear-gradient(135deg, var(--warning-text-dark) 0%, var(--warning-alt) 100%); + /* background: linear-gradient(135deg, var(--warning-text-dark) 0%, var(--warning-alt) 100%); */ color: var(--white); box-shadow: 0 4px 15px var(--warning-30); } @@ -3184,7 +3184,7 @@ input:checked + .toggle-slider:before { } .delete-confirm-modal.unused .btn-confirm-delete:hover { - background: linear-gradient(135deg, var(--warning-text-darker) 0%, var(--warning-text) 100%); + /* background: linear-gradient(135deg, var(--warning-text-darker) 0%, var(--warning-text) 100%); */ transform: translateY(-2px); box-shadow: 0 6px 20px var(--warning-40); } diff --git a/static/app/utils.js b/static/app/utils.js index b26e275..159c8e6 100644 --- a/static/app/utils.js +++ b/static/app/utils.js @@ -81,6 +81,7 @@ function getFieldLabel(key) { 'KIRO_OAUTH_CREDS_FILE_PATH': isEn ? 'OAuth Credentials File Path' : 'OAuth凭据文件路径', 'QWEN_OAUTH_CREDS_FILE_PATH': isEn ? 'OAuth Credentials File Path' : 'OAuth凭据文件路径', 'ANTIGRAVITY_OAUTH_CREDS_FILE_PATH': isEn ? 'OAuth Credentials File Path' : 'OAuth凭据文件路径', + 'IFLOW_OAUTH_CREDS_FILE_PATH': isEn ? 'OAuth Credentials File Path' : 'OAuth凭据文件路径', 'GEMINI_BASE_URL': 'Gemini Base URL', 'KIRO_BASE_URL': 'Base URL', 'KIRO_REFRESH_URL': 'Refresh URL', @@ -88,7 +89,8 @@ function getFieldLabel(key) { 'QWEN_BASE_URL': 'Qwen Base URL', 'QWEN_OAUTH_BASE_URL': 'OAuth Base URL', 'ANTIGRAVITY_BASE_URL_DAILY': 'Daily Base URL', - 'ANTIGRAVITY_BASE_URL_AUTOPUSH': 'Autopush Base URL' + 'ANTIGRAVITY_BASE_URL_AUTOPUSH': 'Autopush Base URL', + 'IFLOW_BASE_URL': 'iFlow Base URL' }; return labelMap[key] || key; @@ -235,6 +237,20 @@ function getProviderTypeFields(providerType) { type: 'text', placeholder: 'https://autopush-cloudcode-pa.sandbox.googleapis.com' } + ], + 'openai-iflow': [ + { + id: 'IFLOW_OAUTH_CREDS_FILE_PATH', + label: isEn ? 'OAuth Credentials File Path' : 'OAuth凭据文件路径', + type: 'text', + placeholder: isEn ? 'e.g.: configs/iflow/oauth_creds.json' : '例如: configs/iflow/oauth_creds.json' + }, + { + id: 'IFLOW_BASE_URL', + label: `iFlow Base URL ${t('config.optional')}`, + type: 'text', + placeholder: 'https://iflow.cn/api' + } ] }; diff --git a/static/index.html b/static/index.html index 819194b..f81c146 100644 --- a/static/index.html +++ b/static/index.html @@ -510,6 +510,59 @@
+
+
+ +

iFlow OAuth

+ 突破限制 +
+
+ +
+ + +
+ + +
+
+ + /openai-iflow/v1/chat/completions +
+
+ +
curl http://localhost:3000/openai-iflow/v1/chat/completions \
+  -H "Content-Type: application/json" \
+  -H "Authorization: Bearer YOUR_API_KEY" \
+  -d '{
+    "model": "qwen3-max",
+    "messages": [{"role": "user", "content": "Hello!"}],
+    "max_tokens": 1000
+  }'
+
+
+ + +
+
+ + /openai-iflow/v1/messages +
+
+ +
curl http://localhost:3000/openai-iflow/v1/messages \
+  -H "Content-Type: application/json" \
+  -H "X-API-Key: YOUR_API_KEY" \
+  -d '{
+    "model": "qwen3-max",
+    "max_tokens": 1000,
+    "messages": [{"role": "user", "content": "Hello!"}]
+  }'
+
+
+
+
+
@@ -579,6 +632,10 @@ OpenAI Responses +
勾选启动时初始化的模型提供商 (必须至少勾选一个) @@ -606,6 +663,14 @@ Gemini Antigravity + + 选择需要通过代理访问的提供商,未选中的提供商将直接连接