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:
parent
671db3c34f
commit
19a40c7fae
20 changed files with 1767 additions and 21 deletions
|
|
@ -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
|
||||
```
|
||||
|
||||
**パラメータ説明**:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
```
|
||||
|
||||
**参数说明**:
|
||||
|
|
|
|||
|
|
@ -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**:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -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}`);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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
1049
src/openai/iflow-core.js
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -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'
|
||||
]
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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']
|
||||
}
|
||||
];
|
||||
|
||||
|
|
|
|||
|
|
@ -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 = [];
|
||||
|
|
|
|||
|
|
@ -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]) {
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Reference in a new issue