现在官方会对ip地址进行限速,所以增加代理服务器功能
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -3,3 +3,4 @@ node_modules/
|
||||
.env
|
||||
.DS_Store
|
||||
*.txt
|
||||
AGENTS.md
|
||||
25
README.md
25
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 ...`,可用于验证命中情况。
|
||||
- 代理数组留空或所有条目无效时,系统自动回退为直连。
|
||||
|
||||
#### 推理级别配置
|
||||
|
||||
每个模型支持五种推理级别:
|
||||
|
||||
12
auth.js
12
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();
|
||||
|
||||
@@ -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]) {
|
||||
|
||||
@@ -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
50
package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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
56
proxy-manager.js
Normal 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;
|
||||
}
|
||||
|
||||
45
routes.js
45
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}`);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user