现在官方会对ip地址进行限速,所以增加代理服务器功能
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -3,3 +3,4 @@ node_modules/
|
|||||||
.env
|
.env
|
||||||
.DS_Store
|
.DS_Store
|
||||||
*.txt
|
*.txt
|
||||||
|
AGENTS.md
|
||||||
25
README.md
25
README.md
@@ -53,6 +53,7 @@ npm install
|
|||||||
**依赖说明**:
|
**依赖说明**:
|
||||||
- `express` - Web服务器框架
|
- `express` - Web服务器框架
|
||||||
- `node-fetch` - HTTP请求库
|
- `node-fetch` - HTTP请求库
|
||||||
|
- `https-proxy-agent` - 为外部请求提供代理支持
|
||||||
|
|
||||||
> 💡 **首次使用必须执行 `npm install`**,之后只需要 `npm start` 启动服务即可。
|
> 💡 **首次使用必须执行 `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 os from 'os';
|
||||||
import fetch from 'node-fetch';
|
import fetch from 'node-fetch';
|
||||||
import { logDebug, logError, logInfo } from './logger.js';
|
import { logDebug, logError, logInfo } from './logger.js';
|
||||||
|
import { getNextProxyAgent } from './proxy-manager.js';
|
||||||
|
|
||||||
// State management for API key and refresh
|
// State management for API key and refresh
|
||||||
let currentApiKey = null;
|
let currentApiKey = null;
|
||||||
@@ -134,13 +135,20 @@ async function refreshApiKey() {
|
|||||||
formData.append('refresh_token', currentRefreshToken);
|
formData.append('refresh_token', currentRefreshToken);
|
||||||
formData.append('client_id', clientId);
|
formData.append('client_id', clientId);
|
||||||
|
|
||||||
const response = await fetch(REFRESH_URL, {
|
const proxyAgentInfo = getNextProxyAgent(REFRESH_URL);
|
||||||
|
const fetchOptions = {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/x-www-form-urlencoded'
|
'Content-Type': 'application/x-www-form-urlencoded'
|
||||||
},
|
},
|
||||||
body: formData.toString()
|
body: formData.toString()
|
||||||
});
|
};
|
||||||
|
|
||||||
|
if (proxyAgentInfo?.agent) {
|
||||||
|
fetchOptions.agent = proxyAgentInfo.agent;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(REFRESH_URL, fetchOptions);
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
const errorText = await response.text();
|
const errorText = await response.text();
|
||||||
|
|||||||
@@ -68,6 +68,14 @@ export function getUserAgent() {
|
|||||||
return cfg.user_agent || 'factory-cli/0.19.3';
|
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) {
|
export function getRedirectedModelId(modelId) {
|
||||||
const cfg = getConfig();
|
const cfg = getConfig();
|
||||||
if (cfg.model_redirects && cfg.model_redirects[modelId]) {
|
if (cfg.model_redirects && cfg.model_redirects[modelId]) {
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
"base_url": "https://app.factory.ai/api/llm/o/v1/chat/completions"
|
"base_url": "https://app.factory.ai/api/llm/o/v1/chat/completions"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"proxies": [],
|
||||||
"models": [
|
"models": [
|
||||||
{
|
{
|
||||||
"name": "Opus 4.1",
|
"name": "Opus 4.1",
|
||||||
@@ -57,6 +58,6 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"dev_mode": false,
|
"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"
|
"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",
|
"name": "droid2api",
|
||||||
"version": "1.3.1",
|
"version": "1.3.5",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "droid2api",
|
"name": "droid2api",
|
||||||
"version": "1.3.1",
|
"version": "1.3.5",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
|
"https-proxy-agent": "^7.0.2",
|
||||||
"node-fetch": "^3.3.2"
|
"node-fetch": "^3.3.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -26,6 +27,15 @@
|
|||||||
"node": ">= 0.6"
|
"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": {
|
"node_modules/array-flatten": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmmirror.com/array-flatten/-/array-flatten-1.1.1.tgz",
|
"resolved": "https://registry.npmmirror.com/array-flatten/-/array-flatten-1.1.1.tgz",
|
||||||
@@ -456,6 +466,42 @@
|
|||||||
"node": ">= 0.8"
|
"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": {
|
"node_modules/iconv-lite": {
|
||||||
"version": "0.4.24",
|
"version": "0.4.24",
|
||||||
"resolved": "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.4.24.tgz",
|
"resolved": "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.4.24.tgz",
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
|
"https-proxy-agent": "^7.0.2",
|
||||||
"node-fetch": "^3.3.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 { AnthropicResponseTransformer } from './transformers/response-anthropic.js';
|
||||||
import { OpenAIResponseTransformer } from './transformers/response-openai.js';
|
import { OpenAIResponseTransformer } from './transformers/response-openai.js';
|
||||||
import { getApiKey } from './auth.js';
|
import { getApiKey } from './auth.js';
|
||||||
|
import { getNextProxyAgent } from './proxy-manager.js';
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
@@ -144,11 +145,18 @@ async function handleChatCompletions(req, res) {
|
|||||||
|
|
||||||
logRequest('POST', endpoint.base_url, headers, transformedRequest);
|
logRequest('POST', endpoint.base_url, headers, transformedRequest);
|
||||||
|
|
||||||
const response = await fetch(endpoint.base_url, {
|
const proxyAgentInfo = getNextProxyAgent(endpoint.base_url);
|
||||||
|
const fetchOptions = {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers,
|
headers,
|
||||||
body: JSON.stringify(transformedRequest)
|
body: JSON.stringify(transformedRequest)
|
||||||
});
|
};
|
||||||
|
|
||||||
|
if (proxyAgentInfo?.agent) {
|
||||||
|
fetchOptions.agent = proxyAgentInfo.agent;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(endpoint.base_url, fetchOptions);
|
||||||
|
|
||||||
logInfo(`Response status: ${response.status}`);
|
logInfo(`Response status: ${response.status}`);
|
||||||
|
|
||||||
@@ -311,11 +319,18 @@ async function handleDirectResponses(req, res) {
|
|||||||
logRequest('POST', endpoint.base_url, headers, modifiedRequest);
|
logRequest('POST', endpoint.base_url, headers, modifiedRequest);
|
||||||
|
|
||||||
// 转发修改后的请求
|
// 转发修改后的请求
|
||||||
const response = await fetch(endpoint.base_url, {
|
const proxyAgentInfo = getNextProxyAgent(endpoint.base_url);
|
||||||
|
const fetchOptions = {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers,
|
headers,
|
||||||
body: JSON.stringify(modifiedRequest)
|
body: JSON.stringify(modifiedRequest)
|
||||||
});
|
};
|
||||||
|
|
||||||
|
if (proxyAgentInfo?.agent) {
|
||||||
|
fetchOptions.agent = proxyAgentInfo.agent;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(endpoint.base_url, fetchOptions);
|
||||||
|
|
||||||
logInfo(`Response status: ${response.status}`);
|
logInfo(`Response status: ${response.status}`);
|
||||||
|
|
||||||
@@ -458,11 +473,18 @@ async function handleDirectMessages(req, res) {
|
|||||||
logRequest('POST', endpoint.base_url, headers, modifiedRequest);
|
logRequest('POST', endpoint.base_url, headers, modifiedRequest);
|
||||||
|
|
||||||
// 转发修改后的请求
|
// 转发修改后的请求
|
||||||
const response = await fetch(endpoint.base_url, {
|
const proxyAgentInfo = getNextProxyAgent(endpoint.base_url);
|
||||||
|
const fetchOptions = {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers,
|
headers,
|
||||||
body: JSON.stringify(modifiedRequest)
|
body: JSON.stringify(modifiedRequest)
|
||||||
});
|
};
|
||||||
|
|
||||||
|
if (proxyAgentInfo?.agent) {
|
||||||
|
fetchOptions.agent = proxyAgentInfo.agent;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(endpoint.base_url, fetchOptions);
|
||||||
|
|
||||||
logInfo(`Response status: ${response.status}`);
|
logInfo(`Response status: ${response.status}`);
|
||||||
|
|
||||||
@@ -565,11 +587,18 @@ async function handleCountTokens(req, res) {
|
|||||||
logInfo(`Forwarding to count_tokens endpoint: ${countTokensUrl}`);
|
logInfo(`Forwarding to count_tokens endpoint: ${countTokensUrl}`);
|
||||||
logRequest('POST', countTokensUrl, headers, modifiedRequest);
|
logRequest('POST', countTokensUrl, headers, modifiedRequest);
|
||||||
|
|
||||||
const response = await fetch(countTokensUrl, {
|
const proxyAgentInfo = getNextProxyAgent(countTokensUrl);
|
||||||
|
const fetchOptions = {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers,
|
headers,
|
||||||
body: JSON.stringify(modifiedRequest)
|
body: JSON.stringify(modifiedRequest)
|
||||||
});
|
};
|
||||||
|
|
||||||
|
if (proxyAgentInfo?.agent) {
|
||||||
|
fetchOptions.agent = proxyAgentInfo.agent;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(countTokensUrl, fetchOptions);
|
||||||
|
|
||||||
logInfo(`Response status: ${response.status}`);
|
logInfo(`Response status: ${response.status}`);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user