From 7d037a6e9a53a500da95948327483d9cde8d77c1 Mon Sep 17 00:00:00 2001 From: 1e0n Date: Fri, 24 Oct 2025 12:34:21 +0800 Subject: [PATCH] =?UTF-8?q?=E7=8E=B0=E5=9C=A8=E5=AE=98=E6=96=B9=E4=BC=9A?= =?UTF-8?q?=E5=AF=B9ip=E5=9C=B0=E5=9D=80=E8=BF=9B=E8=A1=8C=E9=99=90?= =?UTF-8?q?=E9=80=9F=EF=BC=8C=E6=89=80=E4=BB=A5=E5=A2=9E=E5=8A=A0=E4=BB=A3?= =?UTF-8?q?=E7=90=86=E6=9C=8D=E5=8A=A1=E5=99=A8=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + README.md | 25 +++++++++++++++++++++ auth.js | 12 ++++++++-- config.js | 8 +++++++ config.json | 3 ++- package-lock.json | 50 ++++++++++++++++++++++++++++++++++++++++-- package.json | 1 + proxy-manager.js | 56 +++++++++++++++++++++++++++++++++++++++++++++++ routes.js | 45 ++++++++++++++++++++++++++++++------- 9 files changed, 188 insertions(+), 13 deletions(-) create mode 100644 proxy-manager.js diff --git a/.gitignore b/.gitignore index 852f2a4..0e7f077 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ node_modules/ .env .DS_Store *.txt +AGENTS.md \ No newline at end of file diff --git a/README.md b/README.md index 6879c33..8ad6254 100644 --- a/README.md +++ b/README.md @@ -53,6 +53,7 @@ npm install **依赖说明**: - `express` - Web服务器框架 - `node-fetch` - HTTP请求库 +- `https-proxy-agent` - 为外部请求提供代理支持 > 💡 **首次使用必须执行 `npm install`**,之后只需要 `npm start` 启动服务即可。 @@ -104,6 +105,30 @@ export DROID_REFRESH_KEY="your_refresh_token_here" } ``` +### 3. 配置网络代理(可选) + +通过 `config.json` 的 `proxies` 数组为所有下游请求配置代理。数组为空表示直连;配置多个代理时会按照数组顺序轮询使用。 + +```json +{ + "proxies": [ + { + "name": "default-proxy", + "url": "http://127.0.0.1:3128" + }, + { + "name": "auth-proxy", + "url": "http://username:password@123.123.123.123:12345" + } + ] +} +``` + +- `url` 支持带用户名和密码的 `http://user:pass@host:port` 或 HTTPS 代理地址,必要时请为特殊字符进行 URL 编码。 +- 每次请求都会调用下一项代理,配置发生变化时索引会自动重置。 +- 当配置合法代理时,日志会输出类似 `[INFO] Using proxy auth-proxy for request to ...`,可用于验证命中情况。 +- 代理数组留空或所有条目无效时,系统自动回退为直连。 + #### 推理级别配置 每个模型支持五种推理级别: diff --git a/auth.js b/auth.js index 57a30a7..4216863 100644 --- a/auth.js +++ b/auth.js @@ -3,6 +3,7 @@ import path from 'path'; import os from 'os'; import fetch from 'node-fetch'; import { logDebug, logError, logInfo } from './logger.js'; +import { getNextProxyAgent } from './proxy-manager.js'; // State management for API key and refresh let currentApiKey = null; @@ -134,13 +135,20 @@ async function refreshApiKey() { formData.append('refresh_token', currentRefreshToken); formData.append('client_id', clientId); - const response = await fetch(REFRESH_URL, { + const proxyAgentInfo = getNextProxyAgent(REFRESH_URL); + const fetchOptions = { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: formData.toString() - }); + }; + + if (proxyAgentInfo?.agent) { + fetchOptions.agent = proxyAgentInfo.agent; + } + + const response = await fetch(REFRESH_URL, fetchOptions); if (!response.ok) { const errorText = await response.text(); diff --git a/config.js b/config.js index 99da137..69d193d 100644 --- a/config.js +++ b/config.js @@ -68,6 +68,14 @@ export function getUserAgent() { return cfg.user_agent || 'factory-cli/0.19.3'; } +export function getProxyConfigs() { + const cfg = getConfig(); + if (!Array.isArray(cfg.proxies)) { + return []; + } + return cfg.proxies.filter(proxy => proxy && typeof proxy === 'object'); +} + export function getRedirectedModelId(modelId) { const cfg = getConfig(); if (cfg.model_redirects && cfg.model_redirects[modelId]) { diff --git a/config.json b/config.json index e353c5a..b221b43 100644 --- a/config.json +++ b/config.json @@ -19,6 +19,7 @@ "base_url": "https://app.factory.ai/api/llm/o/v1/chat/completions" } ], + "proxies": [], "models": [ { "name": "Opus 4.1", @@ -57,6 +58,6 @@ } ], "dev_mode": false, - "user_agent": "factory-cli/0.20.0", + "user_agent": "factory-cli/0.22.2", "system_prompt": "You are Droid, an AI software engineering agent built by Factory.\n\n" } diff --git a/package-lock.json b/package-lock.json index 22d745e..cdfedc5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,15 +1,16 @@ { "name": "droid2api", - "version": "1.3.1", + "version": "1.3.5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "droid2api", - "version": "1.3.1", + "version": "1.3.5", "license": "MIT", "dependencies": { "express": "^4.18.2", + "https-proxy-agent": "^7.0.2", "node-fetch": "^3.3.2" } }, @@ -26,6 +27,15 @@ "node": ">= 0.6" } }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmmirror.com/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, "node_modules/array-flatten": { "version": "1.1.1", "resolved": "https://registry.npmmirror.com/array-flatten/-/array-flatten-1.1.1.tgz", @@ -456,6 +466,42 @@ "node": ">= 0.8" } }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmmirror.com/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/https-proxy-agent/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, "node_modules/iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.4.24.tgz", diff --git a/package.json b/package.json index 583a7c3..cf32c38 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "license": "MIT", "dependencies": { "express": "^4.18.2", + "https-proxy-agent": "^7.0.2", "node-fetch": "^3.3.2" } } diff --git a/proxy-manager.js b/proxy-manager.js new file mode 100644 index 0000000..daa0a4e --- /dev/null +++ b/proxy-manager.js @@ -0,0 +1,56 @@ +import { HttpsProxyAgent } from 'https-proxy-agent'; +import { getProxyConfigs } from './config.js'; +import { logInfo, logError, logDebug } from './logger.js'; + +let proxyIndex = 0; +let lastSnapshot = ''; + +function snapshotConfigs(configs) { + try { + return JSON.stringify(configs); + } catch (error) { + logDebug('Failed to snapshot proxy configs', { error: error.message }); + return ''; + } +} + +export function getNextProxyAgent(targetUrl) { + const proxies = getProxyConfigs(); + + if (!Array.isArray(proxies) || proxies.length === 0) { + return null; + } + + const currentSnapshot = snapshotConfigs(proxies); + if (currentSnapshot !== lastSnapshot) { + proxyIndex = 0; + lastSnapshot = currentSnapshot; + logInfo('Proxy configuration changed, round-robin index reset'); + } + + for (let attempt = 0; attempt < proxies.length; attempt += 1) { + const index = (proxyIndex + attempt) % proxies.length; + const proxy = proxies[index]; + + if (!proxy || typeof proxy.url !== 'string' || proxy.url.trim() === '') { + logError('Invalid proxy configuration encountered', new Error(`Proxy entry at index ${index} is missing a url`)); + continue; + } + + try { + const agent = new HttpsProxyAgent(proxy.url); + proxyIndex = (index + 1) % proxies.length; + + const label = proxy.name || proxy.url; + logInfo(`Using proxy ${label} for request to ${targetUrl}`); + + return { agent, proxy }; + } catch (error) { + logError(`Failed to create proxy agent for ${proxy.url}`, error); + } + } + + logError('All configured proxies failed to initialize', new Error('Proxy initialization failure')); + return null; +} + diff --git a/routes.js b/routes.js index 1f71c11..099fff0 100644 --- a/routes.js +++ b/routes.js @@ -8,6 +8,7 @@ import { transformToCommon, getCommonHeaders } from './transformers/request-comm import { AnthropicResponseTransformer } from './transformers/response-anthropic.js'; import { OpenAIResponseTransformer } from './transformers/response-openai.js'; import { getApiKey } from './auth.js'; +import { getNextProxyAgent } from './proxy-manager.js'; const router = express.Router(); @@ -144,11 +145,18 @@ async function handleChatCompletions(req, res) { logRequest('POST', endpoint.base_url, headers, transformedRequest); - const response = await fetch(endpoint.base_url, { + const proxyAgentInfo = getNextProxyAgent(endpoint.base_url); + const fetchOptions = { method: 'POST', headers, body: JSON.stringify(transformedRequest) - }); + }; + + if (proxyAgentInfo?.agent) { + fetchOptions.agent = proxyAgentInfo.agent; + } + + const response = await fetch(endpoint.base_url, fetchOptions); logInfo(`Response status: ${response.status}`); @@ -311,11 +319,18 @@ async function handleDirectResponses(req, res) { logRequest('POST', endpoint.base_url, headers, modifiedRequest); // 转发修改后的请求 - const response = await fetch(endpoint.base_url, { + const proxyAgentInfo = getNextProxyAgent(endpoint.base_url); + const fetchOptions = { method: 'POST', headers, body: JSON.stringify(modifiedRequest) - }); + }; + + if (proxyAgentInfo?.agent) { + fetchOptions.agent = proxyAgentInfo.agent; + } + + const response = await fetch(endpoint.base_url, fetchOptions); logInfo(`Response status: ${response.status}`); @@ -458,11 +473,18 @@ async function handleDirectMessages(req, res) { logRequest('POST', endpoint.base_url, headers, modifiedRequest); // 转发修改后的请求 - const response = await fetch(endpoint.base_url, { + const proxyAgentInfo = getNextProxyAgent(endpoint.base_url); + const fetchOptions = { method: 'POST', headers, body: JSON.stringify(modifiedRequest) - }); + }; + + if (proxyAgentInfo?.agent) { + fetchOptions.agent = proxyAgentInfo.agent; + } + + const response = await fetch(endpoint.base_url, fetchOptions); logInfo(`Response status: ${response.status}`); @@ -565,11 +587,18 @@ async function handleCountTokens(req, res) { logInfo(`Forwarding to count_tokens endpoint: ${countTokensUrl}`); logRequest('POST', countTokensUrl, headers, modifiedRequest); - const response = await fetch(countTokensUrl, { + const proxyAgentInfo = getNextProxyAgent(countTokensUrl); + const fetchOptions = { method: 'POST', headers, body: JSON.stringify(modifiedRequest) - }); + }; + + if (proxyAgentInfo?.agent) { + fetchOptions.agent = proxyAgentInfo.agent; + } + + const response = await fetch(countTokensUrl, fetchOptions); logInfo(`Response status: ${response.status}`);