feat: 增强TLS sidecar支持并更新模型列表

- 扩展TLS sidecar配置,支持按提供商启用和设置上游代理
- 更新gemini-antigravity提供商模型列表至最新版本
- 修复JSON schema转换中数组type的处理以兼容Google Gemini API
- 为所有主要提供商集成TLS sidecar支持
- 修复CodexConverter中系统消息重复问题
- 改进gemini-core的错误处理和请求头设置
This commit is contained in:
hex2077 2026-03-16 18:26:16 +08:00
parent b37ead525c
commit 602c6be836
20 changed files with 523 additions and 203 deletions

View file

@ -1 +1 @@
2.11.4.2
2.11.5

View file

@ -132,7 +132,13 @@ export class CodexConverter extends BaseConverter {
// 确保 input 数组中的每个项都有 type: "message",并将系统角色转换为开发者角色
if (codexRequest.input && Array.isArray(codexRequest.input)) {
codexRequest.input = codexRequest.input.map(item => {
codexRequest.input = codexRequest.input.filter(item => {
// 如果 instructions 已存在,过滤掉 input 中的 system/developer 消息以避免重复
if (codexRequest.instructions && (item.role === 'system' || item.role === 'developer')) {
return false;
}
return true;
}).map(item => {
// 如果没有 type 或者 type 不是 message则添加 type: "message"
if (!item.type || item.type !== 'message') {
item = { type: "message", ...item };
@ -160,7 +166,7 @@ export class CodexConverter extends BaseConverter {
const codexRequest = {
model: data.model,
instructions: this.buildInstructions(data),
input: this.convertMessages(data.messages || []),
input: this.convertMessages((data.messages || []).filter(m => m.role !== 'system' && m.role !== 'developer')),
stream: true,
store: false,
metadata: data.metadata || {},
@ -193,7 +199,7 @@ export class CodexConverter extends BaseConverter {
if (data.input && Array.isArray(data.input) && codexRequest.input.length === 0) {
// 如果是 OpenAI Responses 格式的 input
for (const item of data.input) {
if (item.type === 'message') {
if (item.type === 'message' && item.role !== 'system' && item.role !== 'developer') {
codexRequest.input.push({
type: 'message',
role: item.role === 'system' ? 'developer' : item.role,

View file

@ -191,6 +191,22 @@ export function cleanJsonSchemaProperties(schema) {
sanitized[key] = cleanProperties;
} else if (key === 'items') {
sanitized[key] = cleanJsonSchemaProperties(value);
} else if (key === 'type') {
// Google Gemini API 不支持数组形式的 type (如 ["string", "null"])
// 必须是单个字符串,且通常需要大写 (STRING, NUMBER, OBJECT, ARRAY, BOOLEAN, INTEGER)
if (Array.isArray(value)) {
// 如果包含 null设置 nullable 为 true
if (value.includes('null')) {
sanitized.nullable = true;
}
// 取第一个非 null 类型
const actualType = value.find(t => t !== 'null');
if (actualType) {
sanitized[key] = actualType.toUpperCase();
}
} else if (typeof value === 'string') {
sanitized[key] = value.toUpperCase();
}
} else {
sanitized[key] = value;
}

View file

@ -86,8 +86,10 @@ export async function initializeConfig(args = process.argv.slice(2), configFileP
LOG_MAX_FILE_SIZE: 10485760,
LOG_MAX_FILES: 10,
TLS_SIDECAR_ENABLED: false, // 启用 Go uTLS sidecar需要编译 tls-sidecar 二进制)
TLS_SIDECAR_ENABLED_PROVIDERS: [], // 启用 TLS Sidecar 的提供商列表
TLS_SIDECAR_PORT: 9090, // sidecar 监听端口
TLS_SIDECAR_BINARY_PATH: null // 自定义二进制路径(默认自动搜索)
TLS_SIDECAR_BINARY_PATH: null, // 自定义二进制路径(默认自动搜索)
TLS_SIDECAR_PROXY_URL: null // TLS Sidecar 专用的上游代理地址
};
let currentConfig = { ...defaultConfig };

View file

@ -2,8 +2,8 @@ import axios from 'axios';
import logger from '../../utils/logger.js';
import * as http from 'http';
import * as https from 'https';
import { configureAxiosProxy } from '../../utils/proxy-utils.js';
import { isRetryableNetworkError } from '../../utils/common.js';
import { configureAxiosProxy, configureTLSSidecar } from '../../utils/proxy-utils.js';
import { isRetryableNetworkError, MODEL_PROVIDER } from '../../utils/common.js';
/**
* Claude API Core Service Class.
@ -63,11 +63,15 @@ export class ClaudeApiService {
}
// 配置自定义代理
configureAxiosProxy(axiosConfig, this.config, 'claude-custom');
configureAxiosProxy(axiosConfig, this.config, MODEL_PROVIDER.CLAUDE_CUSTOM);
return axios.create(axiosConfig);
}
_applySidecar(axiosConfig) {
return configureTLSSidecar(axiosConfig, this.config, MODEL_PROVIDER.CLAUDE_CUSTOM, this.baseUrl);
}
/**
* Generic method to call the Claude API, with retry mechanism.
* @param {string} endpoint - API endpoint, e.g., '/messages'.
@ -81,7 +85,13 @@ export class ClaudeApiService {
const baseDelay = this.config.REQUEST_BASE_DELAY || 1000; // 1 second base delay
try {
const response = await this.client.post(endpoint, body);
const axiosConfig = {
method: 'post',
url: endpoint,
data: body
};
this._applySidecar(axiosConfig);
const response = await this.client.request(axiosConfig);
return response.data;
} catch (error) {
const status = error.response?.status;
@ -140,7 +150,14 @@ export class ClaudeApiService {
const baseDelay = this.config.REQUEST_BASE_DELAY || 1000; // 1 second base delay
try {
const response = await this.client.post(endpoint, { ...body, stream: true }, { responseType: 'stream' });
const axiosConfig = {
method: 'post',
url: endpoint,
data: { ...body, stream: true },
responseType: 'stream'
};
this._applySidecar(axiosConfig);
const response = await this.client.request(axiosConfig);
const reader = response.data;
let buffer = '';

View file

@ -15,7 +15,7 @@ import {
processContent as processContentUtil,
getContentText as getContentTextUtil
} from '../../utils/token-utils.js';
import { configureAxiosProxy } from '../../utils/proxy-utils.js';
import { configureAxiosProxy, configureTLSSidecar } from '../../utils/proxy-utils.js';
import { isRetryableNetworkError, MODEL_PROVIDER, formatExpiryLog } from '../../utils/common.js';
import { getProviderPoolManager } from '../../services/service-manager.js';
@ -487,6 +487,10 @@ export class KiroApiService {
this.isInitialized = true;
}
_applySidecar(axiosConfig) {
return configureTLSSidecar(axiosConfig, this.config, MODEL_PROVIDER.KIRO_API);
}
/**
* 加载凭证信息不执行刷新
*/
@ -684,11 +688,20 @@ async saveCredentialsToFile(filePath, newData) {
let response = null;
// 使用更短的超时时间进行 token 刷新,避免阻塞其他请求
const refreshConfig = { timeout: KIRO_CONSTANTS.TOKEN_REFRESH_TIMEOUT };
const axiosConfig = {
method: 'post',
url: refreshUrl,
data: requestBody,
...refreshConfig
};
this._applySidecar(axiosConfig);
if (this.authMethod === KIRO_CONSTANTS.AUTH_METHOD_SOCIAL) {
response = await this.axiosSocialRefreshInstance.post(refreshUrl, requestBody, refreshConfig);
response = await this.axiosSocialRefreshInstance.request(axiosConfig);
logger.info('[Kiro Auth] Token refresh social response: ok');
} else {
response = await this.axiosInstance.post(refreshUrl, requestBody, refreshConfig);
response = await this.axiosInstance.request(axiosConfig);
logger.info('[Kiro Auth] Token refresh idc response: ok');
}
@ -1484,7 +1497,14 @@ async saveCredentialsToFile(filePath, newData) {
// 当 model 以 kiro-amazonq 开头时,使用 amazonQUrl否则使用 baseUrl
const requestUrl = model.startsWith('amazonq') ? this.amazonQUrl : this.baseUrl;
const response = await this.axiosInstance.post(requestUrl, requestData, { headers });
const axiosConfig = {
method: 'post',
url: requestUrl,
data: requestData,
headers
};
this._applySidecar(axiosConfig);
const response = await this.axiosInstance.request(axiosConfig);
return response;
} catch (error) {
const status = error.response?.status;
@ -1980,10 +2000,15 @@ async saveCredentialsToFile(filePath, newData) {
let stream = null;
try {
const response = await this.axiosInstance.post(requestUrl, requestData, {
const axiosConfig = {
method: 'post',
url: requestUrl,
data: requestData,
headers,
responseType: 'stream'
});
};
this._applySidecar(axiosConfig);
const response = await this.axiosInstance.request(axiosConfig);
stream = response.data;
let buffer = '';
@ -2961,8 +2986,15 @@ async saveCredentialsToFile(filePath, newData) {
'Connection': 'close'
};
const axiosConfig = {
method: 'get',
url: fullUrl,
headers
};
this._applySidecar(axiosConfig);
try {
const response = await this.axiosInstance.get(fullUrl, { headers });
const response = await this.axiosInstance.request(axiosConfig);
logger.info('[Kiro] Usage limits fetched successfully');
return response.data;
} catch (error) {

View file

@ -2,8 +2,8 @@ import axios from 'axios';
import logger from '../../utils/logger.js';
import * as http from 'http';
import * as https from 'https';
import { configureAxiosProxy } from '../../utils/proxy-utils.js';
import { isRetryableNetworkError } from '../../utils/common.js';
import { configureAxiosProxy, configureTLSSidecar } from '../../utils/proxy-utils.js';
import { isRetryableNetworkError, MODEL_PROVIDER } from '../../utils/common.js';
/**
* ForwardApiService - A provider that forwards requests to a specified API endpoint.
@ -56,17 +56,27 @@ export class ForwardApiService {
axiosConfig.proxy = false;
}
configureAxiosProxy(axiosConfig, config, 'forward-custom');
configureAxiosProxy(axiosConfig, config, MODEL_PROVIDER.FORWARD_API);
this.axiosInstance = axios.create(axiosConfig);
}
_applySidecar(axiosConfig) {
return configureTLSSidecar(axiosConfig, this.config, MODEL_PROVIDER.FORWARD_API, this.baseUrl);
}
async callApi(endpoint, body, isRetry = false, retryCount = 0) {
const maxRetries = this.config.REQUEST_MAX_RETRIES || 3;
const baseDelay = this.config.REQUEST_BASE_DELAY || 1000;
try {
const response = await this.axiosInstance.post(endpoint, body);
const axiosConfig = {
method: 'post',
url: endpoint,
data: body
};
this._applySidecar(axiosConfig);
const response = await this.axiosInstance.request(axiosConfig);
return response.data;
} catch (error) {
const status = error.response?.status;
@ -97,9 +107,14 @@ export class ForwardApiService {
const baseDelay = this.config.REQUEST_BASE_DELAY || 1000;
try {
const response = await this.axiosInstance.post(endpoint, body, {
const axiosConfig = {
method: 'post',
url: endpoint,
data: body,
responseType: 'stream'
});
};
this._applySidecar(axiosConfig);
const response = await this.axiosInstance.request(axiosConfig);
const stream = response.data;
let buffer = '';
@ -176,7 +191,12 @@ export class ForwardApiService {
async listModels() {
try {
const response = await this.axiosInstance.get('/models');
const axiosConfig = {
method: 'get',
url: '/models'
};
this._applySidecar(axiosConfig);
const response = await this.axiosInstance.request(axiosConfig);
return response.data;
} catch (error) {
logger.error(`Error listing Forward models:`, error.message);
@ -184,4 +204,3 @@ export class ForwardApiService {
}
}
}

View file

@ -10,6 +10,7 @@ import * as os from 'os';
import * as readline from 'readline';
import { v4 as uuidv4 } from 'uuid';
import open from 'open';
import { configureTLSSidecar } from '../../utils/proxy-utils.js';
import { formatExpiryTime, isRetryableNetworkError, formatExpiryLog } from '../../utils/common.js';
import { getProviderModels } from '../provider-models.js';
import { handleGeminiAntigravityOAuth } from '../../auth/oauth-handlers.js';
@ -18,20 +19,6 @@ import { cleanJsonSchemaProperties } from '../../converters/utils.js';
import { getProviderPoolManager } from '../../services/service-manager.js';
import { MODEL_PROVIDER } from '../../utils/common.js';
// 配置 HTTP/HTTPS agent 限制连接池大小,避免资源泄漏
const httpAgent = new http.Agent({
keepAlive: true,
maxSockets: 100,
maxFreeSockets: 5,
timeout: 120000,
});
const httpsAgent = new https.Agent({
keepAlive: true,
maxSockets: 100,
maxFreeSockets: 5,
timeout: 120000,
});
// --- Constants ---
const CREDENTIALS_DIR = '.antigravity';
const CREDENTIALS_FILE = 'oauth_creds.json';
@ -57,53 +44,6 @@ const DEFAULT_THINKING_MAX = 100000;
// 获取 Antigravity 模型列表
const ANTIGRAVITY_MODELS = getProviderModels(MODEL_PROVIDER.ANTIGRAVITY);
// 模型别名映射 - 别名 -> 真实模型名
const MODEL_ALIAS_MAP = {
'gemini-2.5-computer-use-preview-10-2025': 'rev19-uic3-1p',
'gemini-3-pro-image-preview': 'gemini-3-pro-image',
'gemini-3-pro-preview': 'gemini-3-pro-high',
'gemini-3.1-pro-preview': 'gemini-3.1-pro-high',
'gemini-3.1-flash-lite-preview': 'gemini-3.1-flash-lite-preview',
'gemini-3-flash-preview': 'gemini-3-flash',
'gemini-2.5-flash-preview': 'gemini-2.5-flash',
'gemini-claude-sonnet-4-5': 'claude-sonnet-4-5',
'gemini-claude-sonnet-4-5-thinking': 'claude-sonnet-4-5-thinking',
'gemini-claude-opus-4-5-thinking': 'claude-opus-4-5-thinking',
'gemini-claude-opus-4-6-thinking': 'claude-opus-4-6-thinking'
};
// 真实模型名 -> 别名
const MODEL_NAME_MAP = {
'rev19-uic3-1p': 'gemini-2.5-computer-use-preview-10-2025',
'gemini-3-pro-image': 'gemini-3-pro-image-preview',
'gemini-3-pro-high': 'gemini-3-pro-preview',
'gemini-3.1-pro-high': 'gemini-3.1-pro-preview',
'gemini-3.1-flash-lite-preview': 'gemini-3.1-flash-lite-preview',
'gemini-3-flash': 'gemini-3-flash-preview',
'gemini-2.5-flash': 'gemini-2.5-flash-preview',
'claude-sonnet-4-5': 'gemini-claude-sonnet-4-5',
'claude-sonnet-4-5-thinking': 'gemini-claude-sonnet-4-5-thinking',
'claude-opus-4-5-thinking': 'gemini-claude-opus-4-5-thinking',
'claude-opus-4-6-thinking': 'gemini-claude-opus-4-6-thinking'
};
/**
* 将别名转换为真实模型名
* @param {string} modelName - 模型别名
* @returns {string} 真实模型名
*/
function alias2ModelName(modelName) {
return MODEL_ALIAS_MAP[modelName];
}
/**
* 将真实模型名转换为别名
* @param {string} modelName - 真实模型名
* @returns {string|null} 模型别名如果不支持则返回 null
*/
function modelName2Alias(modelName) {
return MODEL_NAME_MAP[modelName];
}
/**
* 检查模型是否为 Claude 模型
@ -145,6 +85,14 @@ function generateRequestID() {
return 'agent-' + uuidv4();
}
/**
* 生成随机图像生成请求ID
* @returns {string}
*/
function generateImageGenRequestID() {
return `image_gen/${Date.now()}/${uuidv4()}/12`;
}
/**
* 生成随机会话ID
* @returns {string}
@ -165,7 +113,7 @@ function generateStableSessionID(payload) {
const contents = payload?.request?.contents;
if (Array.isArray(contents)) {
for (const content of contents) {
if (content.role === 'user') {
if (content && content.role === 'user' && Array.isArray(content.parts)) {
const text = content.parts?.[0]?.text;
if (text) {
const hash = crypto.createHash('sha256').update(text).digest();
@ -272,33 +220,44 @@ function geminiToAntigravity(modelName, payload, projectId) {
let template = JSON.parse(JSON.stringify(payload));
const isClaudeModel = isClaude(modelName);
const isImgModel = isImageModel(modelName);
// 设置基本字段
template.model = modelName;
template.userAgent = 'antigravity';
template.requestType = 'agent';
// 设置请求类型
template.requestType = isImgModel ? 'image_gen' : 'agent';
template.project = projectId || generateProjectID();
template.requestId = generateRequestID();
// 确保 request 对象存在
if (!template.request) {
template.request = {};
// 设置请求ID和会话ID
if (isImgModel) {
template.requestId = generateImageGenRequestID();
} else {
template.requestId = generateRequestID();
// 确保 request 对象存在
if (!template.request) {
template.request = {};
}
// 设置会话ID - 使用稳定的会话ID
template.request.sessionId = generateStableSessionID(template);
}
// 设置会话ID - 使用稳定的会话ID
template.request.sessionId = generateStableSessionID(template);
// 删除安全设置
if (template.request.safetySettings) {
delete template.request.safetySettings;
}
// 设置工具配置
// 如果根部有 toolConfig且 request 内部没有,则移动进去
if (template.request.toolConfig) {
if (!template.request.toolConfig.functionCallingConfig) {
template.request.toolConfig.functionCallingConfig = {};
}
template.request.toolConfig.functionCallingConfig.mode = 'VALIDATED';
if (isClaudeModel) {
template.request.toolConfig.functionCallingConfig.mode = 'VALIDATED';
}
}
// 当模型是 Claude 时,禁止使用 tools
@ -331,22 +290,22 @@ function geminiToAntigravity(modelName, payload, projectId) {
}
// 清理所有工具声明中的 JSON Schema 属性(移除 Google API 不支持的属性如 exclusiveMinimum 等)
if (template.request.tools && Array.isArray(template.request.tools)) {
if (template.request.tools && Array.isArray(template.request.tools)) {
template.request.tools.forEach((tool) => {
if (tool.functionDeclarations && Array.isArray(tool.functionDeclarations)) {
if (tool.functionDeclarations && Array.isArray(tool.functionDeclarations)) {
tool.functionDeclarations.forEach((funcDecl) => {
// 对于 Claude 模型,处理 parametersJsonSchema
if (isClaudeModel && funcDecl.parametersJsonSchema) {
funcDecl.parameters = cleanJsonSchemaProperties(funcDecl.parametersJsonSchema);
delete funcDecl.parameters.$schema;
delete funcDecl.parametersJsonSchema;
delete funcDecl.parameters.$schema;
delete funcDecl.parametersJsonSchema;
} else if (funcDecl.parameters) {
funcDecl.parameters = cleanJsonSchemaProperties(funcDecl.parameters);
}
});
}
});
}
}
});
}
});
}
// 如果是图像模型,增加参数 "generationConfig.imageConfig.imageSize": "4K"
if (isImageModel(modelName)) {
@ -712,6 +671,20 @@ function ensureRolesInContents(requestBody, modelName) {
export class AntigravityApiService {
constructor(config) {
// 配置 HTTP/HTTPS agent 限制连接池大小,避免资源泄漏
this.httpAgent = new http.Agent({
keepAlive: true,
maxSockets: 100,
maxFreeSockets: 5,
timeout: 120000,
});
this.httpsAgent = new https.Agent({
keepAlive: true,
maxSockets: 100,
maxFreeSockets: 5,
timeout: 120000,
});
// 检查是否需要使用代理
const proxyConfig = getGoogleAuthProxyConfig(config, 'gemini-antigravity');
@ -726,7 +699,7 @@ export class AntigravityApiService {
logger.info('[Antigravity] Using proxy for OAuth2Client');
} else {
oauth2Options.transporterOptions = {
agent: httpsAgent,
agent: this.httpsAgent,
};
}
@ -748,6 +721,10 @@ export class AntigravityApiService {
this.proxyConfig = getProxyConfigForProvider(config, 'gemini-antigravity');
}
_applySidecar(requestOptions) {
return configureTLSSidecar(requestOptions, this.config, MODEL_PROVIDER.ANTIGRAVITY);
}
/**
* 获取 Base URL 降级顺序
* @param {Object} config - 配置对象
@ -1025,9 +1002,9 @@ export class AntigravityApiService {
if (res.data && res.data.models) {
const models = Object.keys(res.data.models);
this.availableModels = models
.map(modelName2Alias)
.filter(alias => alias !== undefined && alias !== '' && alias !== null)
.filter(alias => ANTIGRAVITY_MODELS.includes(alias));
.filter(alias => ANTIGRAVITY_MODELS.includes(alias) || alias.startsWith('claude-'))
.map(alias => alias.startsWith('claude-') ? `gemini-${alias}` : alias);
logger.info(`[Antigravity] Available models: [${this.availableModels.join(', ')}]`);
return;
@ -1101,6 +1078,7 @@ export class AntigravityApiService {
body: JSON.stringify(body)
};
this._applySidecar(requestOptions);
const res = await this.authClient.request(requestOptions);
return res.data;
} catch (error) {
@ -1194,6 +1172,7 @@ export class AntigravityApiService {
body: JSON.stringify(body)
};
this._applySidecar(requestOptions);
const res = await this.authClient.request(requestOptions);
if (res.status !== 200) {
@ -1337,7 +1316,9 @@ export class AntigravityApiService {
selectedModel = this.availableModels[0];
}
const actualModelName = alias2ModelName(selectedModel);
// 移除 gemini- 前缀以获取实际模型名称(针对 claude 模型)
const actualModelName = selectedModel.startsWith('gemini-claude-') ? selectedModel.replace('gemini-claude-', 'claude-') : selectedModel;
logger.info(`[Antigravity] Selected model: ${actualModelName}`);
// 深拷贝请求体
const processedRequestBody = ensureRolesInContents(JSON.parse(JSON.stringify(requestBody)), actualModelName);
const isClaudeModel = isClaude(actualModelName);
@ -1413,7 +1394,9 @@ export class AntigravityApiService {
selectedModel = this.availableModels[0];
}
const actualModelName = alias2ModelName(selectedModel);
// 移除 gemini- 前缀以获取实际模型名称(针对 claude 模型)
const actualModelName = selectedModel.startsWith('gemini-claude-') ? selectedModel.replace('gemini-claude-', 'claude-') : selectedModel;
logger.info(`[Antigravity] Selected model: ${actualModelName}`);
// 深拷贝请求体
const processedRequestBody = ensureRolesInContents(JSON.parse(JSON.stringify(requestBody)), actualModelName);
@ -1491,6 +1474,7 @@ export class AntigravityApiService {
body: JSON.stringify({ project: this.projectId })
};
this._applySidecar(requestOptions);
const res = await this.authClient.request(requestOptions);
// logger.info(`[Antigravity] fetchAvailableModels success: ${JSON.stringify(res.data)}`);
if (res.data) {

View file

@ -7,6 +7,7 @@ import * as path from 'path';
import * as os from 'os';
import * as readline from 'readline';
import open from 'open';
import { configureTLSSidecar } from '../../utils/proxy-utils.js';
import { API_ACTIONS, formatExpiryTime, isRetryableNetworkError, formatExpiryLog } from '../../utils/common.js';
import { getProviderModels } from '../provider-models.js';
import { handleGeminiCliOAuth } from '../../auth/oauth-handlers.js';
@ -14,20 +15,6 @@ import { getProxyConfigForProvider, getGoogleAuthProxyConfig } from '../../utils
import { getProviderPoolManager } from '../../services/service-manager.js';
import { MODEL_PROVIDER } from '../../utils/common.js';
// 配置 HTTP/HTTPS agent 限制连接池大小,避免资源泄漏
const httpAgent = new http.Agent({
keepAlive: true,
maxSockets: 100,
maxFreeSockets: 5,
timeout: 120000,
});
const httpsAgent = new https.Agent({
keepAlive: true,
maxSockets: 100,
maxFreeSockets: 5,
timeout: 120000,
});
// --- Constants ---
const AUTH_REDIRECT_PORT = 8085;
const CREDENTIALS_DIR = '.gemini';
@ -38,6 +25,67 @@ const OAUTH_CLIENT_ID = '681255809395-oo8ft2oprdrnp9e3aqf6av3hmdib135j.apps.goog
const OAUTH_CLIENT_SECRET = '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';
const GEMINI_MODELS = getProviderModels(MODEL_PROVIDER.GEMINI_CLI);
const ANTI_TRUNCATION_MODELS = GEMINI_MODELS.map(model => `anti-${model}`);
const GEMINI_CLI_VERSION = '0.31.0';
const GEMINI_CLI_API_CLIENT_HEADER = 'google-genai-sdk/1.41.0 gl-node/v22.19.0';
/**
* 设置 Gemini CLI 所需的特定请求头
* @param {Object} headers - 请求头对象
* @param {string} model - 模型名称
*/
function applyGeminiCLIHeaders(headers, model) {
const platform = os.platform();
let arch = os.arch();
if (arch === 'ia32') arch = 'x86';
const modelName = model || 'unknown';
if (model !== 'load-code-assist' && model !== 'onboard-user') {
headers['User-Agent'] = `GeminiCLI/${GEMINI_CLI_VERSION}/${modelName} (${platform}; ${arch})`;
}
headers['X-Goog-Api-Client'] = GEMINI_CLI_API_CLIENT_HEADER;
}
/**
* Google API 429 错误响应中提取重试延迟
* @param {Object|string} errorBody - 错误响应体
* @returns {number|null} 延迟毫秒数
*/
function parseRetryDelay(errorBody) {
try {
const data = typeof errorBody === 'string' ? JSON.parse(errorBody) : errorBody;
const details = data?.error?.details;
if (Array.isArray(details)) {
for (const detail of details) {
if (detail['@type'] === 'type.googleapis.com/google.rpc.RetryInfo') {
const retryDelay = detail.retryDelay;
if (retryDelay) {
const match = retryDelay.match(/^([\d.]+)s$/);
if (match) return parseFloat(match[1]) * 1000;
}
}
}
for (const detail of details) {
if (detail['@type'] === 'type.googleapis.com/google.rpc.ErrorInfo') {
const quotaResetDelay = detail.metadata?.quotaResetDelay;
if (quotaResetDelay) {
const match = quotaResetDelay.match(/^([\d.]+)(ms|s)$/);
if (match) {
let ms = parseFloat(match[1]);
if (match[2] === 's') ms *= 1000;
return ms;
}
}
}
}
}
const message = data?.error?.message;
if (message) {
const match = message.match(/after\s+(\d+)s\.?/);
if (match) return parseInt(match[1]) * 1000;
}
} catch (e) {}
return null;
}
function is_anti_truncation_model(model) {
return ANTI_TRUNCATION_MODELS.some(antiModel => model.includes(antiModel));
@ -134,7 +182,7 @@ async function* apply_anti_truncation_to_stream(service, model, requestBody) {
project: service.projectId,
request: currentRequest
};
const stream = service.streamApi(API_ACTIONS.STREAM_GENERATE_CONTENT, apiRequest);
const stream = service.streamApi(API_ACTIONS.STREAM_GENERATE_CONTENT, apiRequest, false, 0, model);
let lastChunk = null;
let hasContent = false;
@ -155,9 +203,9 @@ async function* apply_anti_truncation_to_stream(service, model, requestBody) {
lastChunk.candidates[0].finishReason === 'MAX_TOKENS') {
// 提取已生成的文本内容
if (lastChunk.candidates[0].content && lastChunk.candidates[0].content.parts) {
if (lastChunk.candidates[0].content && Array.isArray(lastChunk.candidates[0].content.parts)) {
const generatedParts = lastChunk.candidates[0].content.parts
.filter(part => part.text)
.filter(part => part?.text)
.map(part => part.text);
if (generatedParts.length > 0) {
@ -197,6 +245,20 @@ async function* apply_anti_truncation_to_stream(service, model, requestBody) {
export class GeminiApiService {
constructor(config) {
// 配置 HTTP/HTTPS agent 限制连接池大小,避免资源泄漏
this.httpAgent = new http.Agent({
keepAlive: true,
maxSockets: 100,
maxFreeSockets: 5,
timeout: 120000,
});
this.httpsAgent = new https.Agent({
keepAlive: true,
maxSockets: 100,
maxFreeSockets: 5,
timeout: 120000,
});
// 检查是否需要使用代理
const proxyConfig = getGoogleAuthProxyConfig(config, 'gemini-cli-oauth');
@ -211,7 +273,7 @@ export class GeminiApiService {
logger.info('[Gemini] Using proxy for OAuth2Client');
} else {
oauth2Options.transporterOptions = {
agent: httpsAgent,
agent: this.httpsAgent,
};
}
@ -253,6 +315,10 @@ export class GeminiApiService {
logger.info(`[Gemini] Initialization complete. Project ID: ${this.projectId}`);
}
_applySidecar(requestOptions) {
return configureTLSSidecar(requestOptions, this.config, MODEL_PROVIDER.GEMINI_CLI);
}
/**
* 加载凭证信息不执行刷新
*/
@ -412,7 +478,7 @@ export class GeminiApiService {
metadata: clientMetadata,
}
const loadResponse = await this.callApi('loadCodeAssist', loadRequest);
const loadResponse = await this.callApi('loadCodeAssist', loadRequest, false, 0, 'load-code-assist');
// Check if we already have a project ID from the response
if (loadResponse.cloudaicompanionProject) {
@ -429,7 +495,7 @@ export class GeminiApiService {
metadata: clientMetadata,
};
let lroResponse = await this.callApi('onboardUser', onboardRequest);
let lroResponse = await this.callApi('onboardUser', onboardRequest, false, 0, 'onboard-user');
// Poll until operation is complete with timeout protection
const MAX_RETRIES = 30; // Maximum number of retries (60 seconds total)
@ -437,7 +503,7 @@ export class GeminiApiService {
while (!lroResponse.done && retryCount < MAX_RETRIES) {
await new Promise(resolve => setTimeout(resolve, 2000));
lroResponse = await this.callApi('onboardUser', onboardRequest);
lroResponse = await this.callApi('onboardUser', onboardRequest, false, 0, 'onboard-user');
retryCount++;
}
@ -467,18 +533,22 @@ export class GeminiApiService {
return { models: formattedModels };
}
async callApi(method, body, isRetry = false, retryCount = 0) {
async callApi(method, body, isRetry = false, retryCount = 0, model = 'unknown') {
const maxRetries = this.config.REQUEST_MAX_RETRIES || 3;
const baseDelay = this.config.REQUEST_BASE_DELAY || 1000; // 1 second base delay
try {
const headers = { "Content-Type": "application/json" };
applyGeminiCLIHeaders(headers, model);
const requestOptions = {
url: `${this.codeAssistEndpoint}/${this.apiVersion}:${method}`,
method: "POST",
headers: { "Content-Type": "application/json" },
headers: headers,
responseType: "json",
body: JSON.stringify(body),
};
this._applySidecar(requestOptions);
const res = await this.authClient.request(requestOptions);
return res.data;
} catch (error) {
@ -513,10 +583,10 @@ export class GeminiApiService {
// Handle 429 (Too Many Requests) with exponential backoff
if (status === 429 && retryCount < maxRetries) {
const delay = baseDelay * Math.pow(2, retryCount);
const delay = parseRetryDelay(error.response?.data) || (baseDelay * Math.pow(2, retryCount));
logger.info(`[Gemini API] Received 429 (Too Many Requests). Retrying in ${delay}ms... (attempt ${retryCount + 1}/${maxRetries})`);
await new Promise(resolve => setTimeout(resolve, delay));
return this.callApi(method, body, isRetry, retryCount + 1);
return this.callApi(method, body, isRetry, retryCount + 1, model);
}
// Handle other retryable errors (5xx server errors)
@ -524,7 +594,7 @@ export class GeminiApiService {
const delay = baseDelay * Math.pow(2, retryCount);
logger.info(`[Gemini API] Received ${status} server error. Retrying in ${delay}ms... (attempt ${retryCount + 1}/${maxRetries})`);
await new Promise(resolve => setTimeout(resolve, delay));
return this.callApi(method, body, isRetry, retryCount + 1);
return this.callApi(method, body, isRetry, retryCount + 1, model);
}
// Handle network errors (ECONNRESET, ETIMEDOUT, etc.) with exponential backoff
@ -533,26 +603,30 @@ export class GeminiApiService {
const errorIdentifier = errorCode || errorMessage.substring(0, 50);
logger.info(`[Gemini API] Network error (${errorIdentifier}). Retrying in ${delay}ms... (attempt ${retryCount + 1}/${maxRetries})`);
await new Promise(resolve => setTimeout(resolve, delay));
return this.callApi(method, body, isRetry, retryCount + 1);
return this.callApi(method, body, isRetry, retryCount + 1, model);
}
throw error;
}
}
async * streamApi(method, body, isRetry = false, retryCount = 0) {
async * streamApi(method, body, isRetry = false, retryCount = 0, model = 'unknown') {
const maxRetries = this.config.REQUEST_MAX_RETRIES || 3;
const baseDelay = this.config.REQUEST_BASE_DELAY || 1000; // 1 second base delay
try {
const headers = { "Content-Type": "application/json" };
applyGeminiCLIHeaders(headers, model);
const requestOptions = {
url: `${this.codeAssistEndpoint}/${this.apiVersion}:${method}`,
method: "POST",
params: { alt: "sse" },
headers: { "Content-Type": "application/json" },
headers: headers,
responseType: "stream",
body: JSON.stringify(body),
};
this._applySidecar(requestOptions);
const res = await this.authClient.request(requestOptions);
if (res.status !== 200) {
let errorBody = '';
@ -592,10 +666,10 @@ export class GeminiApiService {
// Handle 429 (Too Many Requests) with exponential backoff
if (status === 429 && retryCount < maxRetries) {
const delay = baseDelay * Math.pow(2, retryCount);
const delay = parseRetryDelay(error.response?.data) || (baseDelay * Math.pow(2, retryCount));
logger.info(`[Gemini API] Received 429 (Too Many Requests) during stream. Retrying in ${delay}ms... (attempt ${retryCount + 1}/${maxRetries})`);
await new Promise(resolve => setTimeout(resolve, delay));
yield* this.streamApi(method, body, isRetry, retryCount + 1);
yield* this.streamApi(method, body, isRetry, retryCount + 1, model);
return;
}
@ -604,7 +678,7 @@ export class GeminiApiService {
const delay = baseDelay * Math.pow(2, retryCount);
logger.info(`[Gemini API] Received ${status} server error during stream. Retrying in ${delay}ms... (attempt ${retryCount + 1}/${maxRetries})`);
await new Promise(resolve => setTimeout(resolve, delay));
yield* this.streamApi(method, body, isRetry, retryCount + 1);
yield* this.streamApi(method, body, isRetry, retryCount + 1, model);
return;
}
@ -614,7 +688,7 @@ export class GeminiApiService {
const errorIdentifier = errorCode || errorMessage.substring(0, 50);
logger.info(`[Gemini API] Network error (${errorIdentifier}) during stream. Retrying in ${delay}ms... (attempt ${retryCount + 1}/${maxRetries})`);
await new Promise(resolve => setTimeout(resolve, delay));
yield* this.streamApi(method, body, isRetry, retryCount + 1);
yield* this.streamApi(method, body, isRetry, retryCount + 1, model);
return;
}
@ -660,14 +734,16 @@ export class GeminiApiService {
}
}
let selectedModel = model;
let baseModel = model;
if (!GEMINI_MODELS.includes(model)) {
logger.warn(`[Gemini] Model '${model}' not found. Using default model: '${GEMINI_MODELS[0]}'`);
selectedModel = GEMINI_MODELS[0];
baseModel = GEMINI_MODELS[0];
}
const processedRequestBody = ensureRolesInContents(requestBody);
const apiRequest = { model: selectedModel, project: this.projectId, request: processedRequestBody };
const response = await this.callApi(API_ACTIONS.GENERATE_CONTENT, apiRequest);
const processedRequestBody = ensureRolesInContents({ ...requestBody });
const apiRequest = { model: baseModel, project: this.projectId, request: processedRequestBody };
const response = await this.callApi(API_ACTIONS.GENERATE_CONTENT, apiRequest, false, 0, baseModel);
return toGeminiApiResponse(response.response);
}
@ -699,21 +775,23 @@ export class GeminiApiService {
// 从防截断模型名中提取实际模型名
const actualModel = extract_model_from_anti_model(model);
// 使用防截断流处理
const processedRequestBody = ensureRolesInContents(requestBody);
const processedRequestBody = ensureRolesInContents({ ...requestBody });
yield* apply_anti_truncation_to_stream(this, actualModel, processedRequestBody);
} else {
// 正常流处理
let selectedModel = model;
if (!GEMINI_MODELS.includes(model)) {
logger.warn(`[Gemini] Model '${model}' not found. Using default model: '${GEMINI_MODELS[0]}'`);
selectedModel = GEMINI_MODELS[0];
}
const processedRequestBody = ensureRolesInContents(requestBody);
const apiRequest = { model: selectedModel, project: this.projectId, request: processedRequestBody };
const stream = this.streamApi(API_ACTIONS.STREAM_GENERATE_CONTENT, apiRequest);
for await (const chunk of stream) {
yield toGeminiApiResponse(chunk.response);
}
return;
}
let baseModel = model;
if (!GEMINI_MODELS.includes(model)) {
logger.warn(`[Gemini] Model '${model}' not found. Using default model: '${GEMINI_MODELS[0]}'`);
baseModel = GEMINI_MODELS[0];
}
const processedRequestBody = ensureRolesInContents({ ...requestBody });
const apiRequest = { model: baseModel, project: this.projectId, request: processedRequestBody };
const stream = this.streamApi(API_ACTIONS.STREAM_GENERATE_CONTENT, apiRequest, false, 0, baseModel);
for await (const chunk of stream) {
yield toGeminiApiResponse(chunk.response);
}
}
@ -799,6 +877,7 @@ export class GeminiApiService {
body: JSON.stringify(requestBody)
};
this._applySidecar(requestOptions);
const res = await this.authClient.request(requestOptions);
// logger.info(`[Gemini] retrieveUserQuota success`, JSON.stringify(res.data));
if (res.data && res.data.buckets) {

View file

@ -5,8 +5,7 @@ import * as https from 'https';
import { v4 as uuidv4 } from 'uuid';
import { MODEL_PROTOCOL_PREFIX } from '../../utils/common.js';
import { getProviderModels } from '../provider-models.js';
import { configureAxiosProxy } from '../../utils/proxy-utils.js';
import { getTLSSidecar } from '../../utils/tls-sidecar.js';
import { configureAxiosProxy, configureTLSSidecar } from '../../utils/proxy-utils.js';
import { MODEL_PROVIDER } from '../../utils/common.js';
import { ConverterFactory } from '../../converters/ConverterFactory.js';
import * as readline from 'readline';
@ -145,12 +144,7 @@ export class GrokApiService {
}
_applySidecar(axiosConfig) {
const sidecar = getTLSSidecar();
if (sidecar.isReady()) {
const proxyUrl = this.config.PROXY_URL && this.config.PROXY_ENABLED_PROVIDERS?.includes(MODEL_PROVIDER.GROK_CUSTOM) ? this.config.PROXY_URL : null;
sidecar.wrapAxiosConfig(axiosConfig, proxyUrl);
}
return axiosConfig;
return configureTLSSidecar(axiosConfig, this.config, MODEL_PROVIDER.GROK_CUSTOM);
}
async initialize() {

View file

@ -6,6 +6,7 @@ import path from 'path';
import os from 'os';
import { refreshCodexTokensWithRetry } from '../../auth/oauth-handlers.js';
import { getProviderPoolManager } from '../../services/service-manager.js';
import { configureTLSSidecar } from '../../utils/proxy-utils.js';
import { MODEL_PROVIDER, formatExpiryLog } from '../../utils/common.js';
import { getProxyConfigForProvider } from '../../utils/proxy-utils.js';
import { getProviderModels } from '../provider-models.js';
@ -38,6 +39,10 @@ export class CodexApiService {
this.startCacheCleanup();
}
_applySidecar(axiosConfig) {
return configureTLSSidecar(axiosConfig, this.config, MODEL_PROVIDER.CODEX_API, this.baseUrl);
}
/**
* 初始化服务加载凭据
*/
@ -196,7 +201,15 @@ export class CodexApiService {
config.httpsAgent = proxyConfig.httpsAgent;
}
const response = await axios.post(url, body, config);
const axiosRequestConfig = {
method: 'post',
url,
data: body,
...config
};
this._applySidecar(axiosRequestConfig);
const response = await axios.request(axiosRequestConfig);
return this.parseNonStreamResponse(response.data);
} catch (error) {
@ -265,7 +278,15 @@ export class CodexApiService {
config.httpsAgent = proxyConfig.httpsAgent;
}
const response = await axios.post(url, body, config);
const axiosRequestConfig = {
method: 'post',
url,
data: body,
...config
};
this._applySidecar(axiosRequestConfig);
const response = await axios.request(axiosRequestConfig);
yield* this.parseSSEStream(response.data);
} catch (error) {
@ -673,7 +694,14 @@ export class CodexApiService {
config.httpsAgent = proxyConfig.httpsAgent;
}
const response = await axios.get(url, config);
const axiosRequestConfig = {
method: 'get',
url,
...config
};
this._applySidecar(axiosRequestConfig);
const response = await axios.request(axiosRequestConfig);
// 解析响应数据并转换为通用格式
const data = response.data;

View file

@ -2,8 +2,8 @@ import axios from 'axios';
import logger from '../../utils/logger.js';
import * as http from 'http';
import * as https from 'https';
import { configureAxiosProxy } from '../../utils/proxy-utils.js';
import { isRetryableNetworkError } from '../../utils/common.js';
import { configureAxiosProxy, configureTLSSidecar } from '../../utils/proxy-utils.js';
import { isRetryableNetworkError, MODEL_PROVIDER } from '../../utils/common.js';
// Assumed OpenAI API specification service for interacting with third-party models
export class OpenAIApiService {
@ -47,17 +47,27 @@ export class OpenAIApiService {
}
// 配置自定义代理
configureAxiosProxy(axiosConfig, config, 'openai-custom');
configureAxiosProxy(axiosConfig, config, MODEL_PROVIDER.OPENAI_CUSTOM);
this.axiosInstance = axios.create(axiosConfig);
}
_applySidecar(axiosConfig) {
return configureTLSSidecar(axiosConfig, this.config, MODEL_PROVIDER.OPENAI_CUSTOM, this.baseUrl);
}
async callApi(endpoint, body, isRetry = false, retryCount = 0) {
const maxRetries = this.config.REQUEST_MAX_RETRIES || 3;
const baseDelay = this.config.REQUEST_BASE_DELAY || 1000; // 1 second base delay
try {
const response = await this.axiosInstance.post(endpoint, body);
const axiosConfig = {
method: 'post',
url: endpoint,
data: body
};
this._applySidecar(axiosConfig);
const response = await this.axiosInstance.request(axiosConfig);
return response.data;
} catch (error) {
const status = error.response?.status;
@ -111,9 +121,14 @@ export class OpenAIApiService {
const streamRequestBody = { ...body, stream: true };
try {
const response = await this.axiosInstance.post(endpoint, streamRequestBody, {
const axiosConfig = {
method: 'post',
url: endpoint,
data: streamRequestBody,
responseType: 'stream'
});
};
this._applySidecar(axiosConfig);
const response = await this.axiosInstance.request(axiosConfig);
const stream = response.data;
let buffer = '';

View file

@ -2,7 +2,8 @@ import axios from 'axios';
import logger from '../../utils/logger.js';
import * as http from 'http';
import * as https from 'https';
import { configureAxiosProxy } from '../../utils/proxy-utils.js';
import { configureAxiosProxy, configureTLSSidecar } from '../../utils/proxy-utils.js';
import { MODEL_PROVIDER } from '../../utils/common.js';
// OpenAI Responses API specification service for interacting with third-party models
export class OpenAIResponsesApiService {
@ -46,17 +47,27 @@ export class OpenAIResponsesApiService {
}
// 配置自定义代理 (使用 openai-custom 的代理配置)
configureAxiosProxy(axiosConfig, config, 'openai-custom');
configureAxiosProxy(axiosConfig, config, MODEL_PROVIDER.OPENAI_CUSTOM_RESPONSES);
this.axiosInstance = axios.create(axiosConfig);
}
_applySidecar(axiosConfig) {
return configureTLSSidecar(axiosConfig, this.config, MODEL_PROVIDER.OPENAI_CUSTOM_RESPONSES, this.baseUrl);
}
async callApi(endpoint, body, isRetry = false, retryCount = 0) {
const maxRetries = this.config.REQUEST_MAX_RETRIES || 3;
const baseDelay = this.config.REQUEST_BASE_DELAY || 1000; // 1 second base delay
try {
const response = await this.axiosInstance.post(endpoint, body);
const axiosConfig = {
method: 'post',
url: endpoint,
data: body
};
this._applySidecar(axiosConfig);
const response = await this.axiosInstance.request(axiosConfig);
return response.data;
} catch (error) {
const status = error.response?.status;
@ -95,9 +106,14 @@ export class OpenAIResponsesApiService {
const streamRequestBody = { ...body, stream: true };
try {
const response = await this.axiosInstance.post(endpoint, streamRequestBody, {
const axiosConfig = {
method: 'post',
url: endpoint,
data: streamRequestBody,
responseType: 'stream'
});
};
this._applySidecar(axiosConfig);
const response = await this.axiosInstance.request(axiosConfig);
const stream = response.data;
let buffer = '';
@ -184,7 +200,12 @@ export class OpenAIResponsesApiService {
async listModels() {
try {
const response = await this.axiosInstance.get('/models');
const axiosConfig = {
method: 'get',
url: '/models'
};
this._applySidecar(axiosConfig);
const response = await this.axiosInstance.request(axiosConfig);
return response.data;
} catch (error) {
const status = error.response?.status;

View file

@ -11,7 +11,7 @@ import { EventEmitter } from 'events';
import { randomUUID } from 'node:crypto';
import { getProviderModels } from '../provider-models.js';
import { handleQwenOAuth } from '../../auth/oauth-handlers.js';
import { configureAxiosProxy } from '../../utils/proxy-utils.js';
import { configureAxiosProxy, configureTLSSidecar } from '../../utils/proxy-utils.js';
import { isRetryableNetworkError, MODEL_PROVIDER, formatExpiryLog } from '../../utils/common.js';
import { getProviderPoolManager } from '../../services/service-manager.js';
@ -238,6 +238,10 @@ export class QwenApiService {
logger.info('[Qwen] Initialization complete.');
}
_applySidecar(axiosConfig) {
return configureTLSSidecar(axiosConfig, this.config, MODEL_PROVIDER.QWEN_API, this.baseUrl);
}
/**
* 加载凭证信息不执行刷新
*/
@ -597,8 +601,16 @@ export class QwenApiService {
const mergedTools = processedBody.tools ? [...defaultTools, ...processedBody.tools] : defaultTools;
const requestBody = isStream ? { ...processedBody, stream: true, tools: mergedTools } : { ...processedBody, tools: mergedTools };
const options = isStream ? { responseType: 'stream' } : {};
const response = await this.currentAxiosInstance.post(endpoint, requestBody, options);
const axiosRequestConfig = {
method: 'post',
url: endpoint,
data: requestBody,
...(isStream ? { responseType: 'stream' } : {})
};
this._applySidecar(axiosRequestConfig);
const response = await this.currentAxiosInstance.request(axiosRequestConfig);
return response.data;
} catch (error) {

View file

@ -16,17 +16,13 @@ export const PROVIDER_MODELS = {
'gemini-3.1-flash-lite-preview',
],
'gemini-antigravity': [
'gemini-2.5-computer-use-preview-10-2025',
'gemini-3-pro-image-preview',
'gemini-3.1-pro-preview',
'gemini-3.1-flash-lite-preview',
'gemini-3-pro-preview',
'gemini-3-flash-preview',
'gemini-2.5-flash-preview',
'gemini-claude-sonnet-4-5',
'gemini-claude-sonnet-4-5-thinking',
'gemini-claude-opus-4-5-thinking',
'gemini-claude-opus-4-6-thinking'
'gemini-3-flash',
'gemini-3.1-pro-high',
'gemini-3.1-pro-low',
'gemini-3.1-flash-image',
'gemini-3-flash-agent',
'gemini-claude-sonnet-4-6',
'gemini-claude-opus-4-6-thinking',
],
'claude-custom': [],
'claude-kiro-oauth': [

View file

@ -101,7 +101,9 @@ export async function handleUpdateConfig(req, res, currentConfig) {
// TLS Sidecar settings
if (newConfig.TLS_SIDECAR_ENABLED !== undefined) currentConfig.TLS_SIDECAR_ENABLED = newConfig.TLS_SIDECAR_ENABLED;
if (newConfig.TLS_SIDECAR_ENABLED_PROVIDERS !== undefined) currentConfig.TLS_SIDECAR_ENABLED_PROVIDERS = newConfig.TLS_SIDECAR_ENABLED_PROVIDERS;
if (newConfig.TLS_SIDECAR_PORT !== undefined) currentConfig.TLS_SIDECAR_PORT = newConfig.TLS_SIDECAR_PORT;
if (newConfig.TLS_SIDECAR_PROXY_URL !== undefined) currentConfig.TLS_SIDECAR_PROXY_URL = newConfig.TLS_SIDECAR_PROXY_URL;
// Log settings
if (newConfig.LOG_ENABLED !== undefined) currentConfig.LOG_ENABLED = newConfig.LOG_ENABLED;
@ -171,7 +173,9 @@ export async function handleUpdateConfig(req, res, currentConfig) {
LOG_MAX_FILE_SIZE: currentConfig.LOG_MAX_FILE_SIZE,
LOG_MAX_FILES: currentConfig.LOG_MAX_FILES,
TLS_SIDECAR_ENABLED: currentConfig.TLS_SIDECAR_ENABLED,
TLS_SIDECAR_PORT: currentConfig.TLS_SIDECAR_PORT
TLS_SIDECAR_ENABLED_PROVIDERS: currentConfig.TLS_SIDECAR_ENABLED_PROVIDERS,
TLS_SIDECAR_PORT: currentConfig.TLS_SIDECAR_PORT,
TLS_SIDECAR_PROXY_URL: currentConfig.TLS_SIDECAR_PROXY_URL
};
writeFileSync(configPath, JSON.stringify(configToSave, null, 2), 'utf-8');

View file

@ -7,6 +7,7 @@ import { HttpsProxyAgent } from 'https-proxy-agent';
import logger from './logger.js';
import { HttpProxyAgent } from 'http-proxy-agent';
import { SocksProxyAgent } from 'socks-proxy-agent';
import { getTLSSidecar } from './tls-sidecar.js';
/**
* 解析代理URL并返回相应的代理配置
@ -110,6 +111,52 @@ export function configureAxiosProxy(axiosConfig, config, providerType) {
return axiosConfig;
}
/**
* 检查指定的提供商是否启用了 TLS Sidecar
* @param {Object} config - 配置对象
* @param {string} providerType - 提供商类型
* @returns {boolean} 是否启用 TLS Sidecar
*/
export function isTLSSidecarEnabledForProvider(config, providerType) {
if (!config || !config.TLS_SIDECAR_ENABLED || !config.TLS_SIDECAR_ENABLED_PROVIDERS) {
return false;
}
const enabledProviders = config.TLS_SIDECAR_ENABLED_PROVIDERS;
if (!Array.isArray(enabledProviders)) {
return false;
}
return enabledProviders.includes(providerType);
}
/**
* axios 配置 TLS Sidecar
* @param {Object} axiosConfig - axios 配置对象
* @param {Object} config - 应用配置对象
* @param {string} providerType - 提供商类型
* @param {string} [defaultBaseUrl] - 默认基础 URL用于处理相对路径
* @returns {Object} 更新后的 axios 配置
*/
export function configureTLSSidecar(axiosConfig, config, providerType, defaultBaseUrl = null) {
const sidecar = getTLSSidecar();
if (sidecar.isReady() && isTLSSidecarEnabledForProvider(config, providerType)) {
const proxyUrl = config.TLS_SIDECAR_PROXY_URL || null;
// 处理相对路径
if (axiosConfig.url && !axiosConfig.url.startsWith('http')) {
const baseUrl = (axiosConfig.baseURL || defaultBaseUrl || '').replace(/\/$/, '');
if (baseUrl) {
const path = axiosConfig.url.startsWith('/') ? axiosConfig.url : '/' + axiosConfig.url;
axiosConfig.url = baseUrl + path;
}
}
sidecar.wrapAxiosConfig(axiosConfig, proxyUrl);
}
return axiosConfig;
}
/**
* google-auth-library 配置代理
* @param {Object} config - 应用配置对象

View file

@ -26,6 +26,12 @@ function updateConfigProviderConfigs(configs) {
if (proxyProvidersEl) {
renderProviderTags(proxyProvidersEl, configs, false);
}
// 渲染 TLS Sidecar 设置中的提供商选择
const tlsSidecarProvidersEl = document.getElementById('tlsSidecarProviders');
if (tlsSidecarProvidersEl) {
renderProviderTags(tlsSidecarProvidersEl, configs, false);
}
// 重新加载当前配置以恢复选中状态
loadConfiguration();
@ -210,8 +216,25 @@ async function loadConfiguration() {
// TLS Sidecar 配置
const tlsSidecarEnabledEl = document.getElementById('tlsSidecarEnabled');
const tlsSidecarPortEl = document.getElementById('tlsSidecarPort');
const tlsSidecarProxyUrlEl = document.getElementById('tlsSidecarProxyUrl');
const tlsSidecarProvidersEl = document.getElementById('tlsSidecarProviders');
if (tlsSidecarEnabledEl) tlsSidecarEnabledEl.checked = data.TLS_SIDECAR_ENABLED || false;
if (tlsSidecarPortEl) tlsSidecarPortEl.value = data.TLS_SIDECAR_PORT || 9090;
if (tlsSidecarProxyUrlEl) tlsSidecarProxyUrlEl.value = data.TLS_SIDECAR_PROXY_URL || '';
if (tlsSidecarProvidersEl) {
const enabledProviders = data.TLS_SIDECAR_ENABLED_PROVIDERS || [];
const tags = tlsSidecarProvidersEl.querySelectorAll('.provider-tag');
tags.forEach(tag => {
const value = tag.getAttribute('data-value');
if (enabledProviders.includes(value)) {
tag.classList.add('selected');
} else {
tag.classList.remove('selected');
}
});
}
} catch (error) {
console.error('Failed to load configuration:', error);
@ -314,6 +337,15 @@ async function saveConfiguration() {
// TLS Sidecar 配置
config.TLS_SIDECAR_ENABLED = document.getElementById('tlsSidecarEnabled')?.checked || false;
config.TLS_SIDECAR_PORT = parseInt(document.getElementById('tlsSidecarPort')?.value || 9090);
config.TLS_SIDECAR_PROXY_URL = document.getElementById('tlsSidecarProxyUrl')?.value?.trim() || null;
const tlsSidecarProvidersEl = document.getElementById('tlsSidecarProviders');
if (tlsSidecarProvidersEl) {
config.TLS_SIDECAR_ENABLED_PROVIDERS = Array.from(tlsSidecarProvidersEl.querySelectorAll('.provider-tag.selected'))
.map(tag => tag.getAttribute('data-value'));
} else {
config.TLS_SIDECAR_ENABLED_PROVIDERS = [];
}
try {
await window.apiClient.post('/config', config);

View file

@ -340,7 +340,9 @@ const translations = {
'config.proxy.enabledProvidersNote': '选择需要通过代理访问的提供商,未选中的提供商将直接连接',
'config.proxy.tlsSidecarEnabled': 'TLS 指纹伪装 (uTLS Sidecar)',
'config.proxy.tlsSidecarPort': 'Sidecar 端口',
'config.proxy.tlsSidecarNote': '启用后 Grok 请求将通过 Go uTLS sidecar 转发,完美模拟 Chrome TLS/H2 指纹绕过 Cloudflare需重启服务',
'config.proxy.tlsSidecarProxyUrl': 'Sidecar 上游代理',
'config.proxy.tlsSidecarEnabledProviders': '启用 TLS Sidecar 的提供商',
'config.proxy.tlsSidecarNote': '启用后选中的提供商请求将通过 Go uTLS sidecar 转发,完美模拟 Chrome TLS/H2 指纹绕过 Cloudflare需重启服务',
'config.log.title': '日志设置',
'config.log.enabled': '启用日志',
'config.log.outputMode': '日志输出模式',
@ -1183,7 +1185,9 @@ const translations = {
'config.proxy.enabledProvidersNote': 'Select providers that should use the proxy. Unselected providers will connect directly',
'config.proxy.tlsSidecarEnabled': 'TLS Fingerprint Spoofing (uTLS Sidecar)',
'config.proxy.tlsSidecarPort': 'Sidecar Port',
'config.proxy.tlsSidecarNote': 'When enabled, Grok requests are routed through Go uTLS sidecar for perfect Chrome TLS/H2 fingerprint to bypass Cloudflare (requires restart)',
'config.proxy.tlsSidecarProxyUrl': 'Sidecar Upstream Proxy',
'config.proxy.tlsSidecarEnabledProviders': 'Providers Using TLS Sidecar',
'config.proxy.tlsSidecarNote': 'When enabled, requests for selected providers are routed through Go uTLS sidecar for perfect Chrome TLS/H2 fingerprint to bypass Cloudflare (requires restart)',
'config.log.title': 'Log Settings',
'config.log.enabled': 'Enable Logging',
'config.log.outputMode': 'Log Output Mode',

View file

@ -147,7 +147,19 @@
<input type="number" id="tlsSidecarPort" class="form-control" min="1024" max="65535" value="9090">
</div>
</div>
<small class="form-text" data-i18n="config.proxy.tlsSidecarNote">启用后 Grok 请求将通过 Go uTLS sidecar 转发,完美模拟 Chrome TLS/H2 指纹绕过 Cloudflare需重启服务</small>
<div class="form-group">
<label for="tlsSidecarProxyUrl" data-i18n="config.proxy.tlsSidecarProxyUrl">Sidecar 上游代理</label>
<input type="text" id="tlsSidecarProxyUrl" class="form-control" data-i18n-placeholder="config.proxy.urlPlaceholder" placeholder="例如: http://127.0.0.1:7890">
<small class="form-text" data-i18n="config.proxy.urlNote">TLS Sidecar 专用上游代理,留空则不使用代理</small>
</div>
<div class="form-group pool-section">
<label data-i18n="config.proxy.tlsSidecarEnabledProviders">启用 TLS Sidecar 的提供商</label>
<div id="tlsSidecarProviders" class="provider-tags">
<!-- 动态渲染 -->
</div>
<small class="form-text" data-i18n="config.proxy.enabledProvidersNote">点击选择需要通过 TLS Sidecar 访问的提供商</small>
</div>
<small class="form-text" data-i18n="config.proxy.tlsSidecarNote">启用后选中的提供商请求将通过 Go uTLS sidecar 转发,完美模拟 Chrome TLS/H2 指纹绕过 Cloudflare需重启服务</small>
</div>
<!-- 服务治理与高可用 -->