现在官方会对ip地址进行限速,所以增加代理服务器功能

This commit is contained in:
1e0n
2025-10-24 12:34:21 +08:00
parent c60a12064c
commit 7d037a6e9a
9 changed files with 188 additions and 13 deletions

1
.gitignore vendored
View File

@@ -3,3 +3,4 @@ node_modules/
.env
.DS_Store
*.txt
AGENTS.md

View File

@@ -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 ...`,可用于验证命中情况。
- 代理数组留空或所有条目无效时,系统自动回退为直连。
#### 推理级别配置
每个模型支持五种推理级别:

12
auth.js
View File

@@ -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();

View File

@@ -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]) {

View File

@@ -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"
}

50
package-lock.json generated
View File

@@ -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",

View File

@@ -13,6 +13,7 @@
"license": "MIT",
"dependencies": {
"express": "^4.18.2",
"https-proxy-agent": "^7.0.2",
"node-fetch": "^3.3.2"
}
}

56
proxy-manager.js Normal file
View File

@@ -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;
}

View File

@@ -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}`);