feat(iflow): 新增 iFlow CLI 支持及 OAuth 认证功能

添加 iFlow API 提供商支持,包括:
1. 新增 MODEL_PROVIDER.IFLOW_API 常量
2. 实现 IFlowApiService 和适配器
3. 添加 OAuth 认证流程及令牌刷新机制
4. 更新相关配置文件、路由和前端界面
5. 扩展多语言支持
6. 修改 Docker 端口映射范围以包含 iFlow 回调端口
This commit is contained in:
hex2077 2026-01-07 21:30:51 +08:00
parent 671db3c34f
commit 19a40c7fae
20 changed files with 1767 additions and 21 deletions

View file

@ -34,6 +34,7 @@
> <details>
> <summary>クリックして詳細なバージョン履歴を展開</summary>
>
> - **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
```
**パラメータ説明**

View file

@ -34,6 +34,7 @@
> <details>
> <summary>点击展开查看详细版本历史</summary>
>
> - **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
```
**参数说明**

View file

@ -34,6 +34,7 @@
> <details>
> <summary>Click to expand detailed version history</summary>
>
> - **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**:

View file

@ -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
}
]
}

View file

@ -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}`);
}

View file

@ -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);
}

View file

@ -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',
}
/**

View file

@ -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<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 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<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 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.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 = {
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<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 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'
};
}

1049
src/openai/iflow-core.js Normal file

File diff suppressed because it is too large Load diff

View file

@ -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'
]
};

View file

@ -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']
}
];

View file

@ -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 = [];

View file

@ -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]) {

View file

@ -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';
}

View file

@ -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',

View file

@ -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';
}
}

View file

@ -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) {
</ol>
</div>
`;
} else if (authInfo.provider === 'openai-iflow') {
instructionsHtml = `
<div class="auth-instructions">
<h4 data-i18n="oauth.modal.steps">${t('oauth.modal.steps')}</h4>
<ol>
<li data-i18n="oauth.iflow.step1">${t('oauth.iflow.step1')}</li>
<li data-i18n="oauth.iflow.step2">${t('oauth.iflow.step2')}</li>
<li data-i18n="oauth.iflow.step3">${t('oauth.iflow.step3')}</li>
<li data-i18n="oauth.iflow.step4">${t('oauth.iflow.step4')}</li>
</ol>
</div>
`;
} else {
instructionsHtml = `
<div class="auth-instructions">

View file

@ -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);
}

View file

@ -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 <span class="optional-tag">${t('config.optional')}</span>`,
type: 'text',
placeholder: 'https://iflow.cn/api'
}
]
};

View file

@ -510,6 +510,59 @@
</div>
</div>
<div class="routing-example-card" data-provider="openai-iflow-card">
<div class="routing-card-header">
<i class="fas fa-wind"></i>
<h4 data-i18n="dashboard.routing.nodeName.iflow">iFlow OAuth</h4>
<span class="provider-badge oauth" data-i18n="dashboard.routing.oauth">突破限制</span>
</div>
<div class="routing-card-content">
<!-- 协议标签切换 -->
<div class="protocol-tabs">
<button class="protocol-tab" data-protocol="openai" data-i18n="dashboard.routing.openai">OpenAI协议</button>
<button class="protocol-tab active" data-protocol="claude" data-i18n="dashboard.routing.claude">Claude协议</button>
</div>
<!-- OpenAI协议示例 -->
<div class="protocol-content" data-protocol="openai">
<div class="endpoint-info">
<label data-i18n="dashboard.routing.endpoint">端点路径:</label>
<code class="endpoint-path">/openai-iflow/v1/chat/completions</code>
</div>
<div class="usage-example">
<label data-i18n="dashboard.routing.exampleOpenAI">使用示例 (OpenAI格式):</label>
<pre><code>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
}'</code></pre>
</div>
</div>
<!-- Claude协议示例 -->
<div class="protocol-content active" data-protocol="claude">
<div class="endpoint-info">
<label data-i18n="dashboard.routing.endpoint">端点路径:</label>
<code class="endpoint-path">/openai-iflow/v1/messages</code>
</div>
<div class="usage-example">
<label data-i18n="dashboard.routing.exampleClaude">使用示例 (Claude格式):</label>
<pre><code>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!"}]
}'</code></pre>
</div>
</div>
</div>
</div>
</div>
<div class="routing-tips">
@ -579,6 +632,10 @@
<input type="checkbox" value="openaiResponses-custom">
<span>OpenAI Responses</span>
</label>
<label class="checkbox-item">
<input type="checkbox" value="openai-iflow">
<span>iFlow OAuth</span>
</label>
</div>
<small class="form-text" data-i18n="config.modelProviderHelp">勾选启动时初始化的模型提供商 (必须至少勾选一个)</small>
</div>
@ -606,6 +663,14 @@
<input type="checkbox" name="proxyProvider" value="gemini-antigravity">
<span>Gemini Antigravity</span>
</label>
<label class="checkbox-item">
<input type="checkbox" name="proxyProvider" value="openai-custom">
<span>OpenAI Custom</span>
</label>
<label class="checkbox-item">
<input type="checkbox" name="proxyProvider" value="claude-custom">
<span>Claude Custom</span>
</label>
<label class="checkbox-item">
<input type="checkbox" name="proxyProvider" value="claude-kiro-oauth">
<span>Claude Kiro OAuth</span>
@ -615,12 +680,12 @@
<span>Qwen OAuth</span>
</label>
<label class="checkbox-item">
<input type="checkbox" name="proxyProvider" value="openai-custom">
<span>OpenAI Custom</span>
<input type="checkbox" name="proxyProvider" value="openaiResponses-custom">
<span>OpenAI Responses</span>
</label>
<label class="checkbox-item">
<input type="checkbox" name="proxyProvider" value="claude-custom">
<span>Claude Custom</span>
<input type="checkbox" name="proxyProvider" value="openai-iflow">
<span>iFlow OAuth</span>
</label>
</div>
<small class="form-text" data-i18n="config.proxy.enabledProvidersNote">选择需要通过代理访问的提供商,未选中的提供商将直接连接</small>