fix: Kiro长上下文400错误修复 & 添加自定义代理支持
## Kiro 长上下文修复 - 添加请求体大小限制(默认240KB),避免超过Kiro API限制导致400错误 - 添加 cleanIncompleteToolCalls 方法,清理不完整的工具调用 - 确保截断后第一条消息是user类型 - 新增配置项 KIRO_MAX_REQUEST_SIZE_KB ## 自定义代理支持 - 为各Provider添加独立的代理配置选项 - 支持 USE_SYSTEM_PROXY_* 配置 - UI界面添加代理配置入口 - 新增 proxy-utils.js 代理工具模块
This commit is contained in:
parent
ed634050a9
commit
10146e3cf0
18 changed files with 555 additions and 11 deletions
2
.vscode/settings.json
vendored
Normal file
2
.vscode/settings.json
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
{
|
||||
}
|
||||
|
|
@ -3,6 +3,8 @@
|
|||
"SERVER_PORT": 3000,
|
||||
"HOST": "0.0.0.0",
|
||||
"MODEL_PROVIDER": "gemini-cli-oauth",
|
||||
"PROXY_URL": null,
|
||||
"PROXY_ENABLED_PROVIDERS": [],
|
||||
"OPENAI_API_KEY": "xxx",
|
||||
"OPENAI_BASE_URL": "https://openai/v1",
|
||||
"CLAUDE_API_KEY": "xxx",
|
||||
|
|
@ -13,6 +15,7 @@
|
|||
"ANTIGRAVITY_OAUTH_CREDS_FILE_PATH": null,
|
||||
"KIRO_OAUTH_CREDS_BASE64": null,
|
||||
"KIRO_OAUTH_CREDS_FILE_PATH": null,
|
||||
"KIRO_MAX_CONTEXT_TOKENS": 180000,
|
||||
"QWEN_OAUTH_CREDS_FILE_PATH": null,
|
||||
"SYSTEM_PROMPT_FILE_PATH": "configs/input_system_prompt.txt",
|
||||
"SYSTEM_PROMPT_MODE": "overwrite",
|
||||
|
|
|
|||
67
package-lock.json
generated
67
package-lock.json
generated
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"name": "AIClient2API",
|
||||
"name": "AIClient-2-API",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
|
|
@ -11,9 +11,12 @@
|
|||
"deepmerge": "^4.3.1",
|
||||
"dotenv": "^16.4.5",
|
||||
"google-auth-library": "^10.1.0",
|
||||
"http-proxy-agent": "^7.0.2",
|
||||
"https-proxy-agent": "^7.0.6",
|
||||
"lodash": "^4.17.21",
|
||||
"multer": "^2.0.2",
|
||||
"open": "^10.2.0",
|
||||
"socks-proxy-agent": "^8.0.5",
|
||||
"undici": "^7.12.0",
|
||||
"uuid": "^11.1.0"
|
||||
},
|
||||
|
|
@ -97,6 +100,7 @@
|
|||
"integrity": "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@ampproject/remapping": "^2.2.0",
|
||||
"@babel/code-frame": "^7.27.1",
|
||||
|
|
@ -2955,6 +2959,7 @@
|
|||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"caniuse-lite": "^1.0.30001726",
|
||||
"electron-to-chromium": "^1.5.173",
|
||||
|
|
@ -4060,6 +4065,19 @@
|
|||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/http-proxy-agent": {
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz",
|
||||
"integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"agent-base": "^7.1.0",
|
||||
"debug": "^4.3.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 14"
|
||||
}
|
||||
},
|
||||
"node_modules/https-proxy-agent": {
|
||||
"version": "7.0.6",
|
||||
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
|
||||
|
|
@ -4131,6 +4149,15 @@
|
|||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/ip-address": {
|
||||
"version": "10.1.0",
|
||||
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz",
|
||||
"integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 12"
|
||||
}
|
||||
},
|
||||
"node_modules/is-arrayish": {
|
||||
"version": "0.2.1",
|
||||
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
|
||||
|
|
@ -6028,6 +6055,44 @@
|
|||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/smart-buffer": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz",
|
||||
"integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 6.0.0",
|
||||
"npm": ">= 3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/socks": {
|
||||
"version": "2.8.7",
|
||||
"resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz",
|
||||
"integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ip-address": "^10.0.1",
|
||||
"smart-buffer": "^4.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 10.0.0",
|
||||
"npm": ">= 3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/socks-proxy-agent": {
|
||||
"version": "8.0.5",
|
||||
"resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz",
|
||||
"integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"agent-base": "^7.1.2",
|
||||
"debug": "^4.3.4",
|
||||
"socks": "^2.8.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 14"
|
||||
}
|
||||
},
|
||||
"node_modules/source-map": {
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
||||
|
|
|
|||
|
|
@ -7,9 +7,12 @@
|
|||
"deepmerge": "^4.3.1",
|
||||
"dotenv": "^16.4.5",
|
||||
"google-auth-library": "^10.1.0",
|
||||
"http-proxy-agent": "^7.0.2",
|
||||
"https-proxy-agent": "^7.0.6",
|
||||
"lodash": "^4.17.21",
|
||||
"multer": "^2.0.2",
|
||||
"open": "^10.2.0",
|
||||
"socks-proxy-agent": "^8.0.5",
|
||||
"undici": "^7.12.0",
|
||||
"uuid": "^11.1.0"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import axios from 'axios';
|
||||
import * as http from 'http';
|
||||
import * as https from 'https';
|
||||
import { configureAxiosProxy } from '../proxy-utils.js';
|
||||
|
||||
/**
|
||||
* Claude API Core Service Class.
|
||||
|
|
@ -59,6 +60,9 @@ export class ClaudeApiService {
|
|||
axiosConfig.proxy = false;
|
||||
}
|
||||
|
||||
// 配置自定义代理
|
||||
configureAxiosProxy(axiosConfig, this.config, 'claude-custom');
|
||||
|
||||
return axios.create(axiosConfig);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import * as https from 'https';
|
|||
import { getProviderModels } from '../provider-models.js';
|
||||
import { countTokens } from '@anthropic-ai/tokenizer';
|
||||
import { json } from 'stream/consumers';
|
||||
import { configureAxiosProxy, getProxyConfigForProvider } from '../proxy-utils.js';
|
||||
|
||||
const KIRO_CONSTANTS = {
|
||||
REFRESH_URL: 'https://prod.{{region}}.auth.desktop.kiro.dev/refreshToken',
|
||||
|
|
@ -357,6 +358,9 @@ export class KiroApiService {
|
|||
axiosConfig.proxy = false;
|
||||
}
|
||||
|
||||
// 配置自定义代理
|
||||
configureAxiosProxy(axiosConfig, this.config, 'claude-kiro-oauth');
|
||||
|
||||
this.axiosInstance = axios.create(axiosConfig);
|
||||
this.isInitialized = true;
|
||||
}
|
||||
|
|
@ -530,6 +534,96 @@ async initializeAuth(forceRefresh = false) {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理不完整的工具调用
|
||||
* 确保每个 tool_use 都有对应的 tool_result,否则 Kiro API 会返回 400 错误
|
||||
* @param {Array} messages - 消息数组
|
||||
* @returns {Array} 清理后的消息数组
|
||||
*/
|
||||
cleanIncompleteToolCalls(messages) {
|
||||
if (!messages || messages.length === 0) return messages;
|
||||
|
||||
// 收集所有 tool_use 的 ID
|
||||
const toolUseIds = new Set();
|
||||
// 收集所有 tool_result 对应的 tool_use_id
|
||||
const toolResultIds = new Set();
|
||||
|
||||
for (const msg of messages) {
|
||||
if (!msg.content || !Array.isArray(msg.content)) continue;
|
||||
|
||||
for (const part of msg.content) {
|
||||
if (part.type === 'tool_use' && part.id) {
|
||||
toolUseIds.add(part.id);
|
||||
} else if (part.type === 'tool_result' && part.tool_use_id) {
|
||||
toolResultIds.add(part.tool_use_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 找出没有对应 tool_result 的 tool_use ID
|
||||
const orphanedToolUseIds = new Set();
|
||||
for (const id of toolUseIds) {
|
||||
if (!toolResultIds.has(id)) {
|
||||
orphanedToolUseIds.add(id);
|
||||
}
|
||||
}
|
||||
|
||||
// 找出没有对应 tool_use 的 tool_result ID
|
||||
const orphanedToolResultIds = new Set();
|
||||
for (const id of toolResultIds) {
|
||||
if (!toolUseIds.has(id)) {
|
||||
orphanedToolResultIds.add(id);
|
||||
}
|
||||
}
|
||||
|
||||
if (orphanedToolUseIds.size === 0 && orphanedToolResultIds.size === 0) {
|
||||
return messages; // 没有孤立的工具调用,直接返回
|
||||
}
|
||||
|
||||
console.log(`[Kiro] Cleaning incomplete tool calls: ${orphanedToolUseIds.size} orphaned tool_use, ${orphanedToolResultIds.size} orphaned tool_result`);
|
||||
|
||||
// 过滤掉孤立的工具调用
|
||||
const cleanedMessages = [];
|
||||
for (const msg of messages) {
|
||||
if (!msg.content || !Array.isArray(msg.content)) {
|
||||
cleanedMessages.push(msg);
|
||||
continue;
|
||||
}
|
||||
|
||||
const cleanedContent = msg.content.filter(part => {
|
||||
if (part.type === 'tool_use' && orphanedToolUseIds.has(part.id)) {
|
||||
console.log(`[Kiro] Removing orphaned tool_use: ${part.name} (${part.id})`);
|
||||
return false;
|
||||
}
|
||||
if (part.type === 'tool_result' && orphanedToolResultIds.has(part.tool_use_id)) {
|
||||
console.log(`[Kiro] Removing orphaned tool_result for: ${part.tool_use_id}`);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
// 如果消息内容被清空,检查是否需要保留
|
||||
if (cleanedContent.length === 0) {
|
||||
// 如果是 assistant 消息且内容被完全清空,添加一个占位文本
|
||||
if (msg.role === 'assistant') {
|
||||
cleanedMessages.push({
|
||||
...msg,
|
||||
content: [{ type: 'text', text: '(continued)' }]
|
||||
});
|
||||
}
|
||||
// user 消息如果被清空则跳过
|
||||
continue;
|
||||
}
|
||||
|
||||
cleanedMessages.push({
|
||||
...msg,
|
||||
content: cleanedContent
|
||||
});
|
||||
}
|
||||
|
||||
return cleanedMessages;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract text content from OpenAI message format
|
||||
*/
|
||||
|
|
@ -560,7 +654,70 @@ async initializeAuth(forceRefresh = false) {
|
|||
const conversationId = uuidv4();
|
||||
|
||||
let systemPrompt = this.getContentText(inSystemPrompt);
|
||||
const processedMessages = messages;
|
||||
let processedMessages = messages;
|
||||
|
||||
// 上下文截断:限制请求体大小,避免 400 错误
|
||||
// Kiro API 对请求体大小有硬性限制,需要按字节数截断而不是 token 数
|
||||
const MAX_REQUEST_SIZE_KB = (this.config && this.config.KIRO_MAX_REQUEST_SIZE_KB) || 240; // 默认 240KB
|
||||
const MAX_REQUEST_SIZE = MAX_REQUEST_SIZE_KB * 1024;
|
||||
|
||||
// 估算每条消息的字节大小
|
||||
const estimateMessageSize = (msg) => {
|
||||
let size = 0;
|
||||
if (Array.isArray(msg.content)) {
|
||||
for (const part of msg.content) {
|
||||
if (part.type === 'text' && part.text) {
|
||||
size += part.text.length;
|
||||
} else if (part.type === 'image' && part.source?.data) {
|
||||
size += part.source.data.length; // base64 字符串长度
|
||||
} else if (part.type === 'tool_use' || part.type === 'tool_result') {
|
||||
size += JSON.stringify(part).length;
|
||||
}
|
||||
}
|
||||
} else if (typeof msg.content === 'string') {
|
||||
size += msg.content.length;
|
||||
}
|
||||
return size;
|
||||
};
|
||||
|
||||
let totalSize = systemPrompt ? systemPrompt.length : 0;
|
||||
console.log(`[Kiro] Max request size: ${MAX_REQUEST_SIZE_KB}KB, messages count: ${processedMessages.length}`);
|
||||
|
||||
// 从后往前计算大小,保留最近的消息
|
||||
const messagesToKeep = [];
|
||||
for (let i = processedMessages.length - 1; i >= 0; i--) {
|
||||
const msg = processedMessages[i];
|
||||
const msgSize = estimateMessageSize(msg);
|
||||
|
||||
if (totalSize + msgSize > MAX_REQUEST_SIZE && messagesToKeep.length > 0) {
|
||||
console.log(`[Kiro] Size truncation: keeping ${messagesToKeep.length} messages, dropping ${i + 1} older messages (size would exceed ${MAX_REQUEST_SIZE_KB}KB)`);
|
||||
break;
|
||||
}
|
||||
|
||||
totalSize += msgSize;
|
||||
messagesToKeep.unshift(msg);
|
||||
}
|
||||
|
||||
console.log(`[Kiro] Total estimated size: ${Math.round(totalSize / 1024)}KB`);
|
||||
|
||||
// 确保至少保留最后一条消息
|
||||
if (messagesToKeep.length === 0 && processedMessages.length > 0) {
|
||||
messagesToKeep.push(processedMessages[processedMessages.length - 1]);
|
||||
console.log('[Kiro] Context truncation: keeping only the last message due to size limit');
|
||||
}
|
||||
|
||||
processedMessages = messagesToKeep;
|
||||
|
||||
// 清理不完整的工具调用:确保每个 tool_use 都有对应的 tool_result
|
||||
// Kiro API 要求工具调用必须完整,否则会返回 "Improperly formed request" 错误
|
||||
processedMessages = this.cleanIncompleteToolCalls(processedMessages);
|
||||
|
||||
// 确保第一条消息是 user 类型(Kiro API 要求)
|
||||
// 如果截断后第一条是 assistant,需要移除它
|
||||
while (processedMessages.length > 0 && processedMessages[0].role === 'assistant') {
|
||||
console.log('[Kiro] Removing leading assistant message to ensure first message is user');
|
||||
processedMessages.shift();
|
||||
}
|
||||
|
||||
if (processedMessages.length === 0) {
|
||||
throw new Error('No user messages found');
|
||||
|
|
|
|||
|
|
@ -88,6 +88,8 @@ export async function initializeConfig(args = process.argv.slice(2), configFileP
|
|||
KIRO_BASE_URL: null,
|
||||
SYSTEM_PROMPT_FILE_PATH: INPUT_SYSTEM_PROMPT_FILE, // Default value
|
||||
SYSTEM_PROMPT_MODE: 'append',
|
||||
PROXY_URL: null, // HTTP/HTTPS/SOCKS5 代理地址,如 http://127.0.0.1:7890 或 socks5://127.0.0.1:1080
|
||||
PROXY_ENABLED_PROVIDERS: [], // 启用代理的提供商列表,如 ['gemini-cli-oauth', 'claude-kiro-oauth']
|
||||
PROMPT_LOG_BASE_NAME: "prompt_log",
|
||||
PROMPT_LOG_MODE: "none",
|
||||
REQUEST_MAX_RETRIES: 3,
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import open from 'open';
|
|||
import { formatExpiryTime } from '../common.js';
|
||||
import { getProviderModels } from '../provider-models.js';
|
||||
import { handleGeminiAntigravityOAuth } from '../oauth-handlers.js';
|
||||
import { getProxyConfigForProvider, getGoogleAuthProxyConfig } from '../proxy-utils.js';
|
||||
|
||||
// 配置 HTTP/HTTPS agent 限制连接池大小,避免资源泄漏
|
||||
const httpAgent = new http.Agent({
|
||||
|
|
@ -225,14 +226,25 @@ function ensureRolesInContents(requestBody) {
|
|||
|
||||
export class AntigravityApiService {
|
||||
constructor(config) {
|
||||
// 检查是否需要使用代理
|
||||
const proxyConfig = getGoogleAuthProxyConfig(config, 'gemini-antigravity');
|
||||
|
||||
// 配置 OAuth2Client 使用自定义的 HTTP agent
|
||||
this.authClient = new OAuth2Client({
|
||||
const oauth2Options = {
|
||||
clientId: OAUTH_CLIENT_ID,
|
||||
clientSecret: OAUTH_CLIENT_SECRET,
|
||||
transporterOptions: {
|
||||
};
|
||||
|
||||
if (proxyConfig) {
|
||||
oauth2Options.transporterOptions = proxyConfig;
|
||||
console.log('[Antigravity] Using proxy for OAuth2Client');
|
||||
} else {
|
||||
oauth2Options.transporterOptions = {
|
||||
agent: httpsAgent,
|
||||
},
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
this.authClient = new OAuth2Client(oauth2Options);
|
||||
this.availableModels = [];
|
||||
this.isInitialized = false;
|
||||
|
||||
|
|
@ -252,6 +264,9 @@ export class AntigravityApiService {
|
|||
this.baseUrlAutopush
|
||||
// ANTIGRAVITY_BASE_URL_PROD // 生产环境已注释
|
||||
];
|
||||
|
||||
// 保存代理配置供后续使用
|
||||
this.proxyConfig = getProxyConfigForProvider(config, 'gemini-antigravity');
|
||||
}
|
||||
|
||||
async initialize() {
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import open from 'open';
|
|||
import { API_ACTIONS, formatExpiryTime } from '../common.js';
|
||||
import { getProviderModels } from '../provider-models.js';
|
||||
import { handleGeminiCliOAuth } from '../oauth-handlers.js';
|
||||
import { getProxyConfigForProvider, getGoogleAuthProxyConfig } from '../proxy-utils.js';
|
||||
|
||||
// 配置 HTTP/HTTPS agent 限制连接池大小,避免资源泄漏
|
||||
const httpAgent = new http.Agent({
|
||||
|
|
@ -193,14 +194,25 @@ async function* apply_anti_truncation_to_stream(service, model, requestBody) {
|
|||
|
||||
export class GeminiApiService {
|
||||
constructor(config) {
|
||||
// 检查是否需要使用代理
|
||||
const proxyConfig = getGoogleAuthProxyConfig(config, 'gemini-cli-oauth');
|
||||
|
||||
// 配置 OAuth2Client 使用自定义的 HTTP agent
|
||||
this.authClient = new OAuth2Client({
|
||||
const oauth2Options = {
|
||||
clientId: OAUTH_CLIENT_ID,
|
||||
clientSecret: OAUTH_CLIENT_SECRET,
|
||||
transporterOptions: {
|
||||
};
|
||||
|
||||
if (proxyConfig) {
|
||||
oauth2Options.transporterOptions = proxyConfig;
|
||||
console.log('[Gemini] Using proxy for OAuth2Client');
|
||||
} else {
|
||||
oauth2Options.transporterOptions = {
|
||||
agent: httpsAgent,
|
||||
},
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
this.authClient = new OAuth2Client(oauth2Options);
|
||||
this.availableModels = [];
|
||||
this.isInitialized = false;
|
||||
|
||||
|
|
@ -212,6 +224,9 @@ export class GeminiApiService {
|
|||
|
||||
this.codeAssistEndpoint = config.GEMINI_BASE_URL || DEFAULT_CODE_ASSIST_ENDPOINT;
|
||||
this.apiVersion = DEFAULT_CODE_ASSIST_API_VERSION;
|
||||
|
||||
// 保存代理配置供后续使用
|
||||
this.proxyConfig = getProxyConfigForProvider(config, 'gemini-cli-oauth');
|
||||
}
|
||||
|
||||
async initialize() {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import axios from 'axios';
|
||||
import * as http from 'http';
|
||||
import * as https from 'https';
|
||||
import { configureAxiosProxy } from '../proxy-utils.js';
|
||||
|
||||
// Assumed OpenAI API specification service for interacting with third-party models
|
||||
export class OpenAIApiService {
|
||||
|
|
@ -43,6 +44,9 @@ export class OpenAIApiService {
|
|||
axiosConfig.proxy = false;
|
||||
}
|
||||
|
||||
// 配置自定义代理
|
||||
configureAxiosProxy(axiosConfig, config, 'openai-custom');
|
||||
|
||||
this.axiosInstance = axios.create(axiosConfig);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import axios from 'axios';
|
||||
import * as http from 'http';
|
||||
import * as https from 'https';
|
||||
import { configureAxiosProxy } from '../proxy-utils.js';
|
||||
|
||||
// OpenAI Responses API specification service for interacting with third-party models
|
||||
export class OpenAIResponsesApiService {
|
||||
|
|
@ -42,6 +43,9 @@ export class OpenAIResponsesApiService {
|
|||
if (!this.useSystemProxy) {
|
||||
axiosConfig.proxy = false;
|
||||
}
|
||||
|
||||
// 配置自定义代理 (使用 openai-custom 的代理配置)
|
||||
configureAxiosProxy(axiosConfig, config, 'openai-custom');
|
||||
|
||||
this.axiosInstance = axios.create(axiosConfig);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import { EventEmitter } from 'events';
|
|||
import { randomUUID } from 'node:crypto';
|
||||
import { getProviderModels } from '../provider-models.js';
|
||||
import { handleQwenOAuth } from '../oauth-handlers.js';
|
||||
import { configureAxiosProxy } from '../proxy-utils.js';
|
||||
|
||||
// --- Constants ---
|
||||
const QWEN_DIR = '.qwen';
|
||||
|
|
@ -222,6 +223,9 @@ export class QwenApiService {
|
|||
axiosConfig.proxy = false;
|
||||
}
|
||||
|
||||
// 配置自定义代理
|
||||
configureAxiosProxy(axiosConfig, this.config, 'openai-qwen-oauth');
|
||||
|
||||
this.currentAxiosInstance = axios.create(axiosConfig);
|
||||
|
||||
this.isInitialized = true;
|
||||
|
|
@ -512,6 +516,9 @@ export class QwenApiService {
|
|||
axiosConfig.proxy = false;
|
||||
}
|
||||
|
||||
// 配置自定义代理
|
||||
configureAxiosProxy(axiosConfig, this.config, 'openai-qwen-oauth');
|
||||
|
||||
this.currentAxiosInstance = axios.create(axiosConfig);
|
||||
|
||||
// Process message content before sending the request
|
||||
|
|
|
|||
128
src/proxy-utils.js
Normal file
128
src/proxy-utils.js
Normal file
|
|
@ -0,0 +1,128 @@
|
|||
/**
|
||||
* 代理工具模块
|
||||
* 支持 HTTP、HTTPS 和 SOCKS5 代理
|
||||
*/
|
||||
|
||||
import { HttpsProxyAgent } from 'https-proxy-agent';
|
||||
import { HttpProxyAgent } from 'http-proxy-agent';
|
||||
import { SocksProxyAgent } from 'socks-proxy-agent';
|
||||
|
||||
/**
|
||||
* 解析代理URL并返回相应的代理配置
|
||||
* @param {string} proxyUrl - 代理URL,如 http://127.0.0.1:7890 或 socks5://127.0.0.1:1080
|
||||
* @returns {Object|null} 代理配置对象,包含 httpAgent 和 httpsAgent
|
||||
*/
|
||||
export function parseProxyUrl(proxyUrl) {
|
||||
if (!proxyUrl || typeof proxyUrl !== 'string') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const trimmedUrl = proxyUrl.trim();
|
||||
if (!trimmedUrl) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const url = new URL(trimmedUrl);
|
||||
const protocol = url.protocol.toLowerCase();
|
||||
|
||||
if (protocol === 'socks5:' || protocol === 'socks4:' || protocol === 'socks:') {
|
||||
// SOCKS 代理
|
||||
const socksAgent = new SocksProxyAgent(trimmedUrl);
|
||||
return {
|
||||
httpAgent: socksAgent,
|
||||
httpsAgent: socksAgent,
|
||||
proxyType: 'socks'
|
||||
};
|
||||
} else if (protocol === 'http:' || protocol === 'https:') {
|
||||
// HTTP/HTTPS 代理
|
||||
return {
|
||||
httpAgent: new HttpProxyAgent(trimmedUrl),
|
||||
httpsAgent: new HttpsProxyAgent(trimmedUrl),
|
||||
proxyType: 'http'
|
||||
};
|
||||
} else {
|
||||
console.warn(`[Proxy] Unsupported proxy protocol: ${protocol}`);
|
||||
return null;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`[Proxy] Failed to parse proxy URL: ${error.message}`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查指定的提供商是否启用了代理
|
||||
* @param {Object} config - 配置对象
|
||||
* @param {string} providerType - 提供商类型
|
||||
* @returns {boolean} 是否启用代理
|
||||
*/
|
||||
export function isProxyEnabledForProvider(config, providerType) {
|
||||
if (!config || !config.PROXY_URL || !config.PROXY_ENABLED_PROVIDERS) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const enabledProviders = config.PROXY_ENABLED_PROVIDERS;
|
||||
if (!Array.isArray(enabledProviders)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return enabledProviders.includes(providerType);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定提供商的代理配置
|
||||
* @param {Object} config - 配置对象
|
||||
* @param {string} providerType - 提供商类型
|
||||
* @returns {Object|null} 代理配置对象或 null
|
||||
*/
|
||||
export function getProxyConfigForProvider(config, providerType) {
|
||||
if (!isProxyEnabledForProvider(config, providerType)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const proxyConfig = parseProxyUrl(config.PROXY_URL);
|
||||
if (proxyConfig) {
|
||||
console.log(`[Proxy] Using ${proxyConfig.proxyType} proxy for ${providerType}: ${config.PROXY_URL}`);
|
||||
}
|
||||
return proxyConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* 为 axios 配置代理
|
||||
* @param {Object} axiosConfig - axios 配置对象
|
||||
* @param {Object} config - 应用配置对象
|
||||
* @param {string} providerType - 提供商类型
|
||||
* @returns {Object} 更新后的 axios 配置
|
||||
*/
|
||||
export function configureAxiosProxy(axiosConfig, config, providerType) {
|
||||
const proxyConfig = getProxyConfigForProvider(config, providerType);
|
||||
|
||||
if (proxyConfig) {
|
||||
// 使用代理 agent
|
||||
axiosConfig.httpAgent = proxyConfig.httpAgent;
|
||||
axiosConfig.httpsAgent = proxyConfig.httpsAgent;
|
||||
// 禁用 axios 内置的代理配置,使用我们的 agent
|
||||
axiosConfig.proxy = false;
|
||||
}
|
||||
|
||||
return axiosConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* 为 google-auth-library 配置代理
|
||||
* @param {Object} config - 应用配置对象
|
||||
* @param {string} providerType - 提供商类型
|
||||
* @returns {Object|null} transporter 配置对象或 null
|
||||
*/
|
||||
export function getGoogleAuthProxyConfig(config, providerType) {
|
||||
const proxyConfig = getProxyConfigForProvider(config, providerType);
|
||||
|
||||
if (proxyConfig) {
|
||||
return {
|
||||
agent: proxyConfig.httpsAgent
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
|
@ -721,6 +721,10 @@ export async function handleUIApiRequests(method, pathParam, req, res, currentCo
|
|||
if (newConfig.PROVIDER_POOLS_FILE_PATH !== undefined) currentConfig.PROVIDER_POOLS_FILE_PATH = newConfig.PROVIDER_POOLS_FILE_PATH;
|
||||
if (newConfig.MAX_ERROR_COUNT !== undefined) currentConfig.MAX_ERROR_COUNT = newConfig.MAX_ERROR_COUNT;
|
||||
if (newConfig.providerFallbackChain !== undefined) currentConfig.providerFallbackChain = newConfig.providerFallbackChain;
|
||||
|
||||
// Proxy settings
|
||||
if (newConfig.PROXY_URL !== undefined) currentConfig.PROXY_URL = newConfig.PROXY_URL;
|
||||
if (newConfig.PROXY_ENABLED_PROVIDERS !== undefined) currentConfig.PROXY_ENABLED_PROVIDERS = newConfig.PROXY_ENABLED_PROVIDERS;
|
||||
|
||||
// Handle system prompt update
|
||||
if (newConfig.systemPrompt !== undefined) {
|
||||
|
|
@ -784,7 +788,9 @@ export async function handleUIApiRequests(method, pathParam, req, res, currentCo
|
|||
CRON_REFRESH_TOKEN: currentConfig.CRON_REFRESH_TOKEN,
|
||||
PROVIDER_POOLS_FILE_PATH: currentConfig.PROVIDER_POOLS_FILE_PATH,
|
||||
MAX_ERROR_COUNT: currentConfig.MAX_ERROR_COUNT,
|
||||
providerFallbackChain: currentConfig.providerFallbackChain
|
||||
providerFallbackChain: currentConfig.providerFallbackChain,
|
||||
PROXY_URL: currentConfig.PROXY_URL,
|
||||
PROXY_ENABLED_PROVIDERS: currentConfig.PROXY_ENABLED_PROVIDERS
|
||||
};
|
||||
|
||||
writeFileSync(configPath, JSON.stringify(configToSave, null, 2), 'utf-8');
|
||||
|
|
|
|||
|
|
@ -114,6 +114,17 @@ async function loadConfiguration() {
|
|||
providerFallbackChainEl.value = '';
|
||||
}
|
||||
}
|
||||
|
||||
// 加载代理配置
|
||||
const proxyUrlEl = document.getElementById('proxyUrl');
|
||||
if (proxyUrlEl) proxyUrlEl.value = data.PROXY_URL || '';
|
||||
|
||||
// 加载启用代理的提供商
|
||||
const proxyProviderCheckboxes = document.querySelectorAll('input[name="proxyProvider"]');
|
||||
const enabledProviders = data.PROXY_ENABLED_PROVIDERS || [];
|
||||
proxyProviderCheckboxes.forEach(checkbox => {
|
||||
checkbox.checked = enabledProviders.includes(checkbox.value);
|
||||
});
|
||||
|
||||
// 触发提供商配置显示
|
||||
handleProviderChange();
|
||||
|
|
@ -246,6 +257,13 @@ async function saveConfiguration() {
|
|||
} else {
|
||||
config.providerFallbackChain = {};
|
||||
}
|
||||
|
||||
// 保存代理配置
|
||||
config.PROXY_URL = document.getElementById('proxyUrl')?.value?.trim() || null;
|
||||
|
||||
// 获取启用代理的提供商列表
|
||||
const proxyProviderCheckboxes = document.querySelectorAll('input[name="proxyProvider"]:checked');
|
||||
config.PROXY_ENABLED_PROVIDERS = Array.from(proxyProviderCheckboxes).map(cb => cb.value);
|
||||
|
||||
try {
|
||||
await window.apiClient.post('/config', config);
|
||||
|
|
|
|||
|
|
@ -209,6 +209,12 @@ const translations = {
|
|||
'config.advanced.adminPassword': '后台登录密码',
|
||||
'config.advanced.adminPasswordPlaceholder': '设置后台登录密码(留空则不修改)',
|
||||
'config.advanced.adminPasswordNote': '用于保护管理控制台的访问,修改后需要重新登录',
|
||||
'config.proxy.title': '代理设置',
|
||||
'config.proxy.url': '代理地址',
|
||||
'config.proxy.urlPlaceholder': '例如: http://127.0.0.1:7890 或 socks5://127.0.0.1:1080',
|
||||
'config.proxy.urlNote': '支持 HTTP、HTTPS 和 SOCKS5 代理,留空则不使用代理',
|
||||
'config.proxy.enabledProviders': '启用代理的提供商',
|
||||
'config.proxy.enabledProvidersNote': '选择需要通过代理访问的提供商,未选中的提供商将直接连接',
|
||||
'config.save': '保存配置',
|
||||
'config.reset': '重置',
|
||||
'config.placeholder.nodeName': '例如: 我的节点1',
|
||||
|
|
@ -614,6 +620,12 @@ const translations = {
|
|||
'config.advanced.adminPassword': 'Admin Password',
|
||||
'config.advanced.adminPasswordPlaceholder': 'Set admin password (leave empty to keep unchanged)',
|
||||
'config.advanced.adminPasswordNote': 'Used to protect management console access, requires re-login after modification',
|
||||
'config.proxy.title': 'Proxy Settings',
|
||||
'config.proxy.url': 'Proxy URL',
|
||||
'config.proxy.urlPlaceholder': 'e.g.: http://127.0.0.1:7890 or socks5://127.0.0.1:1080',
|
||||
'config.proxy.urlNote': 'Supports HTTP, HTTPS and SOCKS5 proxies. Leave empty to disable proxy',
|
||||
'config.proxy.enabledProviders': 'Providers Using Proxy',
|
||||
'config.proxy.enabledProvidersNote': 'Select providers that should use the proxy. Unselected providers will connect directly',
|
||||
'config.save': 'Save Configuration',
|
||||
'config.reset': 'Reset',
|
||||
'config.placeholder.nodeName': 'e.g.: My Node 1',
|
||||
|
|
|
|||
|
|
@ -517,6 +517,65 @@ textarea.form-control {
|
|||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
/* 代理配置区域 */
|
||||
.proxy-config-section {
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 0.5rem;
|
||||
padding: 1rem 1.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
background: var(--bg-primary);
|
||||
}
|
||||
|
||||
.proxy-config-section h4 {
|
||||
color: var(--text-primary);
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: 1rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.proxy-config-section h4 i {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
/* 复选框组样式 */
|
||||
.checkbox-group {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.checkbox-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
cursor: pointer;
|
||||
padding: 0.5rem 0.75rem;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 0.375rem;
|
||||
background: var(--bg-secondary);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.checkbox-label:hover {
|
||||
border-color: var(--primary-color);
|
||||
background: var(--bg-hover);
|
||||
}
|
||||
|
||||
.checkbox-label input[type="checkbox"] {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
accent-color: var(--primary-color);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.checkbox-label input[type="checkbox"]:checked + span {
|
||||
color: var(--primary-color);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.config-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
|
|
|
|||
|
|
@ -751,6 +751,46 @@
|
|||
<div class="advanced-config-section">
|
||||
<h3 data-i18n="config.advanced.title"><i class="fas fa-cogs"></i> 高级配置</h3>
|
||||
|
||||
<!-- 代理配置 -->
|
||||
<div class="proxy-config-section">
|
||||
<h4 data-i18n="config.proxy.title"><i class="fas fa-globe"></i> 代理设置</h4>
|
||||
<div class="form-group">
|
||||
<label for="proxyUrl" data-i18n="config.proxy.url">代理地址</label>
|
||||
<input type="text" id="proxyUrl" class="form-control" data-i18n-placeholder="config.proxy.urlPlaceholder" placeholder="例如: http://127.0.0.1:7890 或 socks5://127.0.0.1:1080">
|
||||
<small class="form-text" data-i18n="config.proxy.urlNote">支持 HTTP、HTTPS 和 SOCKS5 代理,留空则不使用代理</small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label data-i18n="config.proxy.enabledProviders">启用代理的提供商</label>
|
||||
<div class="checkbox-group proxy-providers-group">
|
||||
<label class="checkbox-label">
|
||||
<input type="checkbox" name="proxyProvider" value="gemini-cli-oauth">
|
||||
<span>Gemini CLI OAuth</span>
|
||||
</label>
|
||||
<label class="checkbox-label">
|
||||
<input type="checkbox" name="proxyProvider" value="gemini-antigravity">
|
||||
<span>Gemini Antigravity</span>
|
||||
</label>
|
||||
<label class="checkbox-label">
|
||||
<input type="checkbox" name="proxyProvider" value="claude-kiro-oauth">
|
||||
<span>Claude Kiro OAuth</span>
|
||||
</label>
|
||||
<label class="checkbox-label">
|
||||
<input type="checkbox" name="proxyProvider" value="openai-qwen-oauth">
|
||||
<span>Qwen OAuth</span>
|
||||
</label>
|
||||
<label class="checkbox-label">
|
||||
<input type="checkbox" name="proxyProvider" value="openai-custom">
|
||||
<span>OpenAI Custom</span>
|
||||
</label>
|
||||
<label class="checkbox-label">
|
||||
<input type="checkbox" name="proxyProvider" value="claude-custom">
|
||||
<span>Claude Custom</span>
|
||||
</label>
|
||||
</div>
|
||||
<small class="form-text" data-i18n="config.proxy.enabledProvidersNote">选择需要通过代理访问的提供商,未选中的提供商将直接连接</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="config-row">
|
||||
<div class="form-group">
|
||||
<label for="systemPromptFilePath" data-i18n="config.advanced.systemPromptFile">系统提示文件路径</label>
|
||||
|
|
|
|||
Loading…
Reference in a new issue