feat(provider): 添加自定义名称字段并优化OAuth处理流程

添加自定义名称字段以允许用户为节点设置个性化名称
重构OAuth处理逻辑,统一各提供商的授权流程
增加超时处理并优化错误提示
调整UI显示顺序和字段布局
This commit is contained in:
hex2077 2025-12-17 13:45:02 +08:00
parent 3887243528
commit d5417d9890
8 changed files with 211 additions and 230 deletions

View file

@ -1,6 +1,7 @@
{
"openai-custom": [
{
"customName": "OpenAI节点1",
"OPENAI_API_KEY": "sk-openai-key1",
"OPENAI_BASE_URL": "https://api.openai.com/v1",
"checkModelName": null,
@ -15,6 +16,7 @@
"lastErrorTime": null
},
{
"customName": "OpenAI节点2",
"OPENAI_API_KEY": "sk-openai-key2",
"OPENAI_BASE_URL": "https://api.openai.com/v1",
"checkModelName": null,
@ -31,6 +33,7 @@
],
"openaiResponses-custom": [
{
"customName": "OpenAI Responses节点",
"OPENAI_API_KEY": "sk-openai-key",
"OPENAI_BASE_URL": "https://api.openai.com/v1",
"checkModelName": null,
@ -46,6 +49,7 @@
],
"gemini-cli-oauth": [
{
"customName": "Gemini OAuth节点1",
"GEMINI_OAUTH_CREDS_FILE_PATH": "./credentials1.json",
"PROJECT_ID": "your-project-id-1",
"checkModelName": null,
@ -59,6 +63,7 @@
"lastErrorTime": null
},
{
"customName": "Gemini OAuth节点2",
"GEMINI_OAUTH_CREDS_FILE_PATH": "./credentials2.json",
"PROJECT_ID": "your-project-id-2",
"checkModelName": null,
@ -74,6 +79,7 @@
],
"claude-custom": [
{
"customName": "Claude节点1",
"CLAUDE_API_KEY": "sk-claude-key1",
"CLAUDE_BASE_URL": "https://api.anthropic.com",
"checkModelName": null,
@ -87,6 +93,7 @@
"lastErrorTime": null
},
{
"customName": "Claude节点2",
"CLAUDE_API_KEY": "sk-claude-key2",
"CLAUDE_BASE_URL": "https://api.anthropic.com",
"checkModelName": null,
@ -102,6 +109,7 @@
],
"claude-kiro-oauth": [
{
"customName": "Kiro OAuth节点1",
"KIRO_OAUTH_CREDS_FILE_PATH": "./kiro_creds1.json",
"uuid": "2c69d0ac-b86f-43d8-9d17-0d300afc5cfd",
"checkModelName": null,
@ -114,6 +122,7 @@
"lastErrorTime": null
},
{
"customName": "Kiro OAuth节点2",
"KIRO_OAUTH_CREDS_FILE_PATH": "./kiro_creds2.json",
"uuid": "7482abe6-8083-4288-bb7d-d8ecb7c461e2",
"checkModelName": null,
@ -128,6 +137,7 @@
],
"openai-qwen-oauth": [
{
"customName": "Qwen OAuth节点",
"QWEN_OAUTH_CREDS_FILE_PATH": "./qwen_creds.json",
"uuid": "658a2114-c4c9-d713-b8d4-ceabf0e0bf18",
"checkModelName": null,
@ -142,6 +152,7 @@
],
"gemini-antigravity": [
{
"customName": "Antigravity节点1",
"ANTIGRAVITY_OAUTH_CREDS_FILE_PATH": "./antigravity_creds1.json",
"PROJECT_ID": "antigravity-project-1",
"uuid": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
@ -155,6 +166,7 @@
"lastErrorTime": null
},
{
"customName": "Antigravity节点2",
"ANTIGRAVITY_OAUTH_CREDS_FILE_PATH": "./antigravity_creds2.json",
"PROJECT_ID": "antigravity-project-2",
"uuid": "f0e9d8c7-b6a5-4321-fedc-ba9876543210",

View file

@ -17,7 +17,7 @@ const KIRO_CONSTANTS = {
AMAZON_Q_URL: 'https://codewhisperer.{{region}}.amazonaws.com/SendMessageStreaming',
USAGE_LIMITS_URL: 'https://q.{{region}}.amazonaws.com/getUsageLimits',
DEFAULT_MODEL_NAME: 'claude-opus-4-5',
AXIOS_TIMEOUT: 120000, // 2 minutes timeout
AXIOS_TIMEOUT: 300000, // 5 minutes timeout (increased from 2 minutes)
USER_AGENT: 'KiroIDE',
KIRO_VERSION: '0.7.5',
CONTENT_TYPE_JSON: 'application/json',
@ -312,13 +312,13 @@ export class KiroApiService {
keepAlive: true,
maxSockets: 100, // 每个主机最多 10 个连接
maxFreeSockets: 5, // 最多保留 5 个空闲连接
timeout: 120000, // 空闲连接 60 秒后关闭
timeout: KIRO_CONSTANTS.AXIOS_TIMEOUT,
});
const httpsAgent = new https.Agent({
keepAlive: true,
maxSockets: 100,
maxFreeSockets: 5,
timeout: 120000,
timeout: KIRO_CONSTANTS.AXIOS_TIMEOUT,
});
const axiosConfig = {

View file

@ -2,8 +2,7 @@ import { promises as fs } from 'fs';
import * as path from 'path';
import * as http from 'http'; // Add http for IncomingMessage and ServerResponse types
import * as crypto from 'crypto'; // Import crypto for MD5 hashing
import { ApiServiceAdapter } from './adapter.js'; // Import ApiServiceAdapter
import { convertData, getOpenAIStreamChunkStop, getOpenAIResponsesStreamChunkBegin, getOpenAIResponsesStreamChunkEnd } from './convert.js';
import { convertData, getOpenAIStreamChunkStop } from './convert.js';
import { ProviderStrategyFactory } from './provider-strategies.js';
export const API_ACTIONS = {
@ -317,7 +316,6 @@ export async function handleUnaryRequest(res, service, model, requestBody, fromP
* and sends the JSON response.
* @param {http.IncomingMessage} req The HTTP request object.
* @param {http.ServerResponse} res The HTTP response object.
* @param {ApiServiceAdapter} service The API service adapter.
* @param {string} endpointType The type of endpoint being called (e.g., OPENAI_MODEL_LIST).
* @param {Object} CONFIG - The server configuration object.
*/
@ -368,7 +366,6 @@ export async function handleModelListRequest(req, res, service, endpointType, CO
* logging, and dispatching to the appropriate stream or unary handler.
* @param {http.IncomingMessage} req The HTTP request object.
* @param {http.ServerResponse} res The HTTP response object.
* @param {ApiServiceAdapter} service The API service adapter.
* @param {string} endpointType The type of endpoint being called (e.g., OPENAI_CHAT).
* @param {Object} CONFIG - The server configuration object.
* @param {string} PROMPT_LOG_FILENAME - The prompt log filename.

View file

@ -9,6 +9,7 @@ import { v4 as uuidv4 } from 'uuid';
import open from 'open';
import { formatExpiryTime } from '../common.js';
import { getProviderModels } from '../provider-models.js';
import { handleGeminiAntigravityOAuth } from '../oauth-handlers.js';
// 配置 HTTP/HTTPS agent 限制连接池大小,避免资源泄漏
const httpAgent = new http.Agent({
@ -305,85 +306,51 @@ export class AntigravityApiService {
}
async getNewToken(credPath) {
let host = this.host;
if (!host || host === 'undefined') {
host = '127.0.0.1';
}
const redirectUri = `http://${host}:${AUTH_REDIRECT_PORT}`;
this.authClient.redirectUri = redirectUri;
// 使用统一的 OAuth 处理方法
const { authUrl, authInfo } = await handleGeminiAntigravityOAuth(this.config);
console.log('\n[Antigravity Auth] 正在自动打开浏览器进行授权...');
console.log('[Antigravity Auth] 授权链接:', authUrl, '\n');
return new Promise(async (resolve, reject) => {
const authUrl = this.authClient.generateAuthUrl({
access_type: 'offline',
scope: ['https://www.googleapis.com/auth/cloud-platform']
});
// 自动打开浏览器
const showFallbackMessage = () => {
console.log('[Antigravity Auth] 无法自动打开浏览器,请手动复制上面的链接到浏览器中打开');
};
console.log('\n[Antigravity Auth] 正在自动打开浏览器进行授权...');
console.log('[Antigravity Auth] 授权链接:', authUrl, '\n');
// 自动打开浏览器
const showFallbackMessage = () => {
console.log('[Antigravity Auth] 无法自动打开浏览器,请手动复制上面的链接到浏览器中打开');
};
if (this.config) {
try {
const childProcess = await open(authUrl);
if (childProcess) {
childProcess.on('error', () => showFallbackMessage());
}
} catch (_err) {
showFallbackMessage();
if (this.config) {
try {
const childProcess = await open(authUrl);
if (childProcess) {
childProcess.on('error', () => showFallbackMessage());
}
} else {
} catch (_err) {
showFallbackMessage();
}
} else {
showFallbackMessage();
}
const server = http.createServer(async (req, res) => {
// 等待 OAuth 回调完成并读取保存的凭据
return new Promise((resolve, reject) => {
const checkInterval = setInterval(async () => {
try {
const url = new URL(req.url, redirectUri);
const code = url.searchParams.get('code');
const errorParam = url.searchParams.get('error');
if (code) {
console.log(`[Antigravity Auth] Received successful callback from Google: ${req.url}`);
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end('Authentication successful! You can close this browser tab.');
server.close();
const { tokens } = await this.authClient.getToken(code);
await fs.mkdir(path.dirname(credPath), { recursive: true });
await fs.writeFile(credPath, JSON.stringify(tokens, null, 2));
console.log('[Antigravity Auth] New token received and saved to file.');
resolve(tokens);
} else if (errorParam) {
const errorMessage = `Authentication failed. Google returned an error: ${errorParam}.`;
res.writeHead(400, { 'Content-Type': 'text/plain' });
res.end(errorMessage);
server.close();
reject(new Error(errorMessage));
} else {
console.log(`[Antigravity Auth] Ignoring irrelevant request: ${req.url}`);
res.writeHead(204);
res.end();
const data = await fs.readFile(credPath, 'utf8');
const credentials = JSON.parse(data);
if (credentials.access_token) {
clearInterval(checkInterval);
console.log('[Antigravity Auth] New token obtained successfully.');
resolve(credentials);
}
} catch (e) {
if (server.listening) server.close();
reject(e);
} catch (error) {
// 文件尚未创建或无效,继续等待
}
});
}, 1000);
server.on('error', (err) => {
if (err.code === 'EADDRINUSE') {
const errorMessage = `[Antigravity Auth] Port ${AUTH_REDIRECT_PORT} on ${host} is already in use.`;
console.error(errorMessage);
reject(new Error(errorMessage));
} else {
reject(err);
}
});
server.listen(AUTH_REDIRECT_PORT, host);
// 设置超时5分钟
setTimeout(() => {
clearInterval(checkInterval);
reject(new Error('[Antigravity Auth] OAuth 授权超时'));
}, 5 * 60 * 1000);
});
}

View file

@ -8,6 +8,7 @@ import * as readline from 'readline';
import open from 'open';
import { API_ACTIONS, formatExpiryTime } from '../common.js';
import { getProviderModels } from '../provider-models.js';
import { handleGeminiCliOAuth } from '../oauth-handlers.js';
// 配置 HTTP/HTTPS agent 限制连接池大小,避免资源泄漏
const httpAgent = new http.Agent({
@ -274,75 +275,51 @@ export class GeminiApiService {
}
async getNewToken(credPath) {
let host = this.host;
if (!host || host === 'undefined') {
host = '127.0.0.1';
}
const redirectUri = `http://${host}:${AUTH_REDIRECT_PORT}`;
this.authClient.redirectUri = redirectUri;
return new Promise(async (resolve, reject) => {
const authUrl = this.authClient.generateAuthUrl({ access_type: 'offline', scope: ['https://www.googleapis.com/auth/cloud-platform'] });
console.log('\n[Gemini Auth] 正在自动打开浏览器进行授权...');
// 自动打开浏览器
const showFallbackMessage = () => {
console.log('[Gemini Auth] 无法自动打开浏览器,请手动复制上面的链接到浏览器中打开');
};
if (this.config) {
try {
const childProcess = await open(authUrl);
if (childProcess) {
childProcess.on('error', () => showFallbackMessage());
}
} catch (_err) {
showFallbackMessage();
// 使用统一的 OAuth 处理方法
const { authUrl, authInfo } = await handleGeminiCliOAuth(this.config);
console.log('\n[Gemini Auth] 正在自动打开浏览器进行授权...');
console.log('[Gemini Auth] 授权链接:', authUrl, '\n');
// 自动打开浏览器
const showFallbackMessage = () => {
console.log('[Gemini Auth] 无法自动打开浏览器,请手动复制上面的链接到浏览器中打开');
};
if (this.config) {
try {
const childProcess = await open(authUrl);
if (childProcess) {
childProcess.on('error', () => showFallbackMessage());
}
} else {
} catch (_err) {
showFallbackMessage();
}
console.log('[Gemini Auth] 授权链接:', authUrl, '\n');
const server = http.createServer(async (req, res) => {
} else {
showFallbackMessage();
}
// 等待 OAuth 回调完成并读取保存的凭据
return new Promise((resolve, reject) => {
const checkInterval = setInterval(async () => {
try {
const url = new URL(req.url, redirectUri);
const code = url.searchParams.get('code');
const errorParam = url.searchParams.get('error');
if (code) {
console.log(`[Gemini Auth] Received successful callback from Google: ${req.url}`);
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end('Authentication successful! You can close this browser tab.');
server.close();
const { tokens } = await this.authClient.getToken(code);
await fs.mkdir(path.dirname(credPath), { recursive: true });
await fs.writeFile(credPath, JSON.stringify(tokens, null, 2));
console.log('[Gemini Auth] New token received and saved to file.');
resolve(tokens);
} else if (errorParam) {
const errorMessage = `Authentication failed. Google returned an error: ${errorParam}.`;
res.writeHead(400, { 'Content-Type': 'text/plain' });
res.end(errorMessage);
server.close();
reject(new Error(errorMessage));
} else {
console.log(`[Gemini Auth] Ignoring irrelevant request: ${req.url}`);
res.writeHead(204);
res.end();
const data = await fs.readFile(credPath, 'utf8');
const credentials = JSON.parse(data);
if (credentials.access_token) {
clearInterval(checkInterval);
console.log('[Gemini Auth] New token obtained successfully.');
resolve(credentials);
}
} catch (e) {
if (server.listening) server.close();
reject(e);
} catch (error) {
// 文件尚未创建或无效,继续等待
}
});
server.on('error', (err) => {
if (err.code === 'EADDRINUSE') {
const errorMessage = `[Gemini Auth] Port ${AUTH_REDIRECT_PORT} on ${this.host} is already in use.`;
console.error(errorMessage);
reject(new Error(errorMessage));
} else {
reject(err);
}
});
server.listen(AUTH_REDIRECT_PORT, this.host);
}, 1000);
// 设置超时5分钟
setTimeout(() => {
clearInterval(checkInterval);
reject(new Error('[Gemini Auth] OAuth 授权超时'));
}, 5 * 60 * 1000);
});
}

View file

@ -9,6 +9,7 @@ import open from 'open';
import { EventEmitter } from 'events';
import { randomUUID } from 'node:crypto';
import { getProviderModels } from '../provider-models.js';
import { handleQwenOAuth } from '../oauth-handlers.js';
// --- Constants ---
const QWEN_DIR = '.qwen';
@ -284,39 +285,30 @@ export class QwenApiService {
}
async _authWithQwenDeviceFlow(client, config) {
let isCancelled = false;
const cancelHandler = () => { isCancelled = true; };
const sigintHandler = () => {
isCancelled = true;
qwenOAuth2Events.emit(QwenOAuth2Event.AuthCancel);
};
qwenOAuth2Events.once(QwenOAuth2Event.AuthCancel, cancelHandler);
process.once('SIGINT', sigintHandler);
try {
const { code_verifier, code_challenge } = generatePKCEPair();
const deviceAuth = await client.requestDeviceAuthorization({
scope: QWEN_OAUTH_SCOPE,
code_challenge,
code_challenge_method: 'S256',
// 使用统一的 OAuth 处理方法
const { authUrl, authInfo } = await handleQwenOAuth(config);
// 发送授权 URI 事件
qwenOAuth2Events.emit(QwenOAuth2Event.AuthUri, {
verification_uri_complete: authUrl,
user_code: authInfo.userCode,
verification_uri: authInfo.verificationUri,
device_code: authInfo.deviceCode,
expires_in: authInfo.expiresIn,
interval: authInfo.interval
});
if (!isDeviceAuthorizationSuccess(deviceAuth)) {
throw new Error(`Device authorization failed: ${deviceAuth?.error || 'Unknown error'} - ${deviceAuth?.error_description || 'No details'}`);
}
qwenOAuth2Events.emit(QwenOAuth2Event.AuthUri, deviceAuth);
const showFallbackMessage = () => {
console.log('\n=== Qwen OAuth Device Authorization ===');
console.log('Please visit the following URL in your browser to authorize:');
console.log(`\n${deviceAuth.verification_uri_complete}\n`);
console.log(`\n${authUrl}\n`);
console.log('Waiting for authorization to complete...\n');
};
if (config) {
try {
const childProcess = await open(deviceAuth.verification_uri_complete);
const childProcess = await open(authUrl);
if (childProcess) {
childProcess.on('error', () => showFallbackMessage());
}
@ -330,70 +322,37 @@ export class QwenApiService {
qwenOAuth2Events.emit(QwenOAuth2Event.AuthProgress, 'polling', 'Waiting for authorization...');
console.debug('Waiting for authorization...\n');
let pollInterval = 2000;
const maxAttempts = Math.ceil(deviceAuth.expires_in / (pollInterval / 1000));
for (let attempt = 0; attempt < maxAttempts; attempt++) {
if (isCancelled) {
qwenOAuth2Events.emit(QwenOAuth2Event.AuthProgress, 'error', 'Authentication cancelled by user.');
return { success: false, reason: 'cancelled' };
}
try {
const tokenResponse = await client.pollDeviceToken({ device_code: deviceAuth.device_code, code_verifier });
if (isDeviceTokenSuccess(tokenResponse)) {
const credentials = {
access_token: tokenResponse.access_token,
refresh_token: tokenResponse.refresh_token || undefined,
token_type: tokenResponse.token_type,
resource_url: tokenResponse.resource_url,
expiry_date: tokenResponse.expires_in ? Date.now() + tokenResponse.expires_in * 1000 : undefined,
};
client.setCredentials(credentials);
await this._cacheQwenCredentials(credentials);
qwenOAuth2Events.emit(QwenOAuth2Event.AuthProgress, 'success', 'Authentication successful! Access token obtained.');
return { success: true };
}
if (isDeviceTokenPending(tokenResponse)) {
if (tokenResponse.slowDown) {
pollInterval = Math.min(pollInterval * 1.5, 10000);
} else {
pollInterval = 2000;
// 等待 OAuth 回调完成并读取保存的凭据
const credPath = this._getQwenCachedCredentialPath();
const credentials = await new Promise((resolve, reject) => {
const checkInterval = setInterval(async () => {
try {
const data = await fs.readFile(credPath, 'utf8');
const creds = JSON.parse(data);
if (creds.access_token) {
clearInterval(checkInterval);
console.log('[Qwen Auth] New token obtained successfully.');
resolve(creds);
}
// Fall through to wait and continue
} else if (isErrorResponse(tokenResponse)) {
console.warn(`Token polling failed with error: ${tokenResponse?.error || 'Unknown error'}`);
// Fall through to wait and continue
} catch (error) {
// 文件尚未创建或无效,继续等待
}
} catch (error) {
console.warn(`Token polling threw an exception: ${error.message}`);
// Fall through to wait for the next attempt
}
// Wait for the polling interval before the next attempt
await new Promise(resolve => {
const timeoutId = setTimeout(resolve, pollInterval);
// If cancelled during wait, clear timeout and resolve immediately
if (isCancelled) {
clearTimeout(timeoutId);
resolve();
}
});
// Check again after waiting
if (isCancelled) {
qwenOAuth2Events.emit(QwenOAuth2Event.AuthProgress, 'error', 'Authentication cancelled by user.');
return { success: false, reason: 'cancelled' };
}
}
return { success: false, reason: 'timeout' };
}, 1000);
// 设置超时5分钟
setTimeout(() => {
clearInterval(checkInterval);
reject(new Error('[Qwen Auth] OAuth 授权超时'));
}, 5 * 60 * 1000);
});
client.setCredentials(credentials);
qwenOAuth2Events.emit(QwenOAuth2Event.AuthProgress, 'success', 'Authentication successful! Access token obtained.');
return { success: true };
} catch (error) {
console.error('Device authorization flow failed:', error.message);
qwenOAuth2Events.emit(QwenOAuth2Event.AuthProgress, 'error', error.message);
return { success: false, reason: 'error' };
} finally {
qwenOAuth2Events.off(QwenOAuth2Event.AuthCancel, cancelHandler);
process.off('SIGINT', sigintHandler);
}
}

View file

@ -381,7 +381,7 @@ function renderProviderList(providers) {
<div class="provider-item-detail ${healthClass} ${disabledClass}" data-uuid="${provider.uuid}">
<div class="provider-item-header" onclick="window.toggleProviderDetails('${provider.uuid}')">
<div class="provider-info">
<div class="provider-name">${provider.uuid}</div>
<div class="provider-name">${provider.customName || provider.uuid}</div>
<div class="provider-meta">
<span class="health-status">
<i class="${healthIcon}"></i>
@ -438,17 +438,29 @@ function renderProviderConfig(provider) {
// 获取字段映射,确保顺序一致
const fieldOrder = getFieldOrder(provider);
// 先渲染基础配置字段checkModelName 和 checkHealth
// 先渲染基础配置字段customName、checkModelName 和 checkHealth
let html = '<div class="form-grid">';
const baseFields = ['checkModelName', 'checkHealth'];
const baseFields = ['customName', 'checkModelName', 'checkHealth'];
baseFields.forEach(fieldKey => {
const displayLabel = getFieldLabel(fieldKey);
const value = provider[fieldKey];
const displayValue = value || '';
// 如果是 checkHealth 字段,使用下拉选择框
if (fieldKey === 'checkHealth') {
// 如果是 customName 字段,使用普通文本输入框
if (fieldKey === 'customName') {
html += `
<div class="config-item">
<label>${displayLabel}</label>
<input type="text"
value="${displayValue}"
readonly
data-config-key="${fieldKey}"
data-config-value="${value || ''}"
placeholder="节点自定义名称">
</div>
`;
} else if (fieldKey === 'checkHealth') {
// 如果没有值,默认为 false
const actualValue = value !== undefined ? value : false;
const isEnabled = actualValue === true || actualValue === 'true';
@ -626,22 +638,71 @@ function renderProviderConfig(provider) {
* @returns {Array} 字段键数组
*/
function getFieldOrder(provider) {
const orderedFields = ['checkModelName', 'checkHealth'];
const orderedFields = ['customName', 'checkModelName', 'checkHealth'];
// 需要排除的内部状态字段
const excludedFields = [
'isHealthy', 'lastUsed', 'usageCount', 'errorCount', 'lastErrorTime',
'uuid', 'isDisabled', 'lastHealthCheckTime', 'lastHealthCheckModel', 'lastErrorMessage'
'uuid', 'isDisabled', 'lastHealthCheckTime', 'lastHealthCheckModel', 'lastErrorMessage',
'notSupportedModels'
];
// 从 getProviderTypeFields 获取字段顺序映射
const fieldOrderMap = {
'openai-custom': ['OPENAI_API_KEY', 'OPENAI_BASE_URL'],
'openaiResponses-custom': ['OPENAI_API_KEY', 'OPENAI_BASE_URL'],
'claude-custom': ['CLAUDE_API_KEY', 'CLAUDE_BASE_URL'],
'gemini-cli-oauth': ['PROJECT_ID', 'GEMINI_OAUTH_CREDS_FILE_PATH'],
'claude-kiro-oauth': ['KIRO_OAUTH_CREDS_FILE_PATH'],
'openai-qwen-oauth': ['QWEN_OAUTH_CREDS_FILE_PATH'],
'gemini-antigravity': ['PROJECT_ID', 'ANTIGRAVITY_OAUTH_CREDS_FILE_PATH']
};
// 获取所有其他配置项
const otherFields = Object.keys(provider).filter(key =>
!excludedFields.includes(key) && !orderedFields.includes(key)
);
// 按字母顺序排序其他字段
otherFields.sort();
// 尝试从 provider 中推断提供商类型
let providerType = null;
if (provider.OPENAI_API_KEY && provider.OPENAI_BASE_URL) {
providerType = 'openai-custom'; // 或 openaiResponses-custom顺序相同
} else if (provider.CLAUDE_API_KEY && provider.CLAUDE_BASE_URL) {
providerType = 'claude-custom';
} else if (provider.GEMINI_OAUTH_CREDS_FILE_PATH) {
providerType = 'gemini-cli-oauth';
} else if (provider.KIRO_OAUTH_CREDS_FILE_PATH) {
providerType = 'claude-kiro-oauth';
} else if (provider.QWEN_OAUTH_CREDS_FILE_PATH) {
providerType = 'openai-qwen-oauth';
} else if (provider.ANTIGRAVITY_OAUTH_CREDS_FILE_PATH) {
providerType = 'gemini-antigravity';
}
// 如果能识别提供商类型,使用预定义的顺序
if (providerType && fieldOrderMap[providerType]) {
const predefinedOrder = fieldOrderMap[providerType];
const orderedOtherFields = [];
const remainingFields = [...otherFields];
// 先按预定义顺序添加字段
predefinedOrder.forEach(fieldKey => {
const index = remainingFields.indexOf(fieldKey);
if (index !== -1) {
orderedOtherFields.push(fieldKey);
remainingFields.splice(index, 1);
}
});
// 剩余字段按字母顺序添加
remainingFields.sort();
orderedOtherFields.push(...remainingFields);
return [...orderedFields, ...orderedOtherFields].filter(key => provider.hasOwnProperty(key));
}
// 如果无法识别提供商类型,按字母顺序排序
otherFields.sort();
return [...orderedFields, ...otherFields].filter(key => provider.hasOwnProperty(key));
}
@ -962,6 +1023,10 @@ function showAddProviderForm(providerType) {
form.innerHTML = `
<h4><i class="fas fa-plus"></i> </h4>
<div class="form-grid">
<div class="form-group">
<label>自定义名称 <span class="optional-mark">(选填)</span></label>
<input type="text" id="newCustomName" placeholder="例如: 我的节点1">
</div>
<div class="form-group">
<label>检查模型名称 <span class="optional-mark">(选填)</span></label>
<input type="text" id="newCheckModelName" placeholder="例如: gpt-3.5-turbo">
@ -1140,10 +1205,12 @@ function bindAddFormPasswordToggleListeners(form) {
* @param {string} providerType - 提供商类型
*/
async function addProvider(providerType) {
const customName = document.getElementById('newCustomName')?.value;
const checkModelName = document.getElementById('newCheckModelName')?.value;
const checkHealth = document.getElementById('newCheckHealth')?.value === 'true';
const providerConfig = {
customName: customName || '', // 允许为空
checkModelName: checkModelName || '', // 允许为空
checkHealth
};

View file

@ -54,6 +54,7 @@ function showToast(message, type = 'info') {
*/
function getFieldLabel(key) {
const labelMap = {
'customName': '自定义名称 (选填)',
'checkModelName': '检查模型名称 (选填)',
'checkHealth': '健康检查',
'OPENAI_API_KEY': 'OpenAI API Key',
@ -63,7 +64,8 @@ function getFieldLabel(key) {
'PROJECT_ID': '项目ID',
'GEMINI_OAUTH_CREDS_FILE_PATH': 'OAuth凭据文件路径',
'KIRO_OAUTH_CREDS_FILE_PATH': 'OAuth凭据文件路径',
'QWEN_OAUTH_CREDS_FILE_PATH': 'OAuth凭据文件路径'
'QWEN_OAUTH_CREDS_FILE_PATH': 'OAuth凭据文件路径',
'ANTIGRAVITY_OAUTH_CREDS_FILE_PATH': 'OAuth凭据文件路径'
};
return labelMap[key] || key;