feat(proxy): add proxy pool and per-connection binding + strictProxy support

- Centralize proxy management with reusable proxy pools
- Per-connection proxy binding with legacy fallback
- Add strictProxy option: fail hard instead of silently falling back to direct
- Resolve alicode-intl conflict: keep alicode-intl support + proxy support

Made-with: Cursor
This commit is contained in:
decolua
2026-03-09 15:46:06 +07:00
parent 4c469291a1
commit 880f4eca91
22 changed files with 1811 additions and 146 deletions

View File

@@ -43,6 +43,7 @@ if (!isCloud && !fs.existsSync(DATA_DIR)) {
const defaultData = {
providerConnections: [],
providerNodes: [],
proxyPools: [],
modelAliases: {},
mitmAlias: {},
combos: [],
@@ -69,6 +70,7 @@ function cloneDefaultData() {
return {
providerConnections: [],
providerNodes: [],
proxyPools: [],
modelAliases: {},
mitmAlias: {},
combos: [],
@@ -308,7 +310,7 @@ export async function deleteProviderNode(id) {
if (!db.data.providerNodes) {
db.data.providerNodes = [];
}
const index = db.data.providerNodes.findIndex((node) => node.id === id);
if (index === -1) return null;
@@ -319,6 +321,104 @@ export async function deleteProviderNode(id) {
return removed;
}
// ============ Proxy Pools ============
/**
* Get proxy pools
*/
export async function getProxyPools(filter = {}) {
const db = await getDb();
let pools = db.data.proxyPools || [];
if (filter.isActive !== undefined) {
pools = pools.filter((pool) => pool.isActive === filter.isActive);
}
if (filter.testStatus) {
pools = pools.filter((pool) => pool.testStatus === filter.testStatus);
}
return pools.sort((a, b) => new Date(b.updatedAt || 0) - new Date(a.updatedAt || 0));
}
/**
* Get proxy pool by ID
*/
export async function getProxyPoolById(id) {
const db = await getDb();
return (db.data.proxyPools || []).find((pool) => pool.id === id) || null;
}
/**
* Create proxy pool
*/
export async function createProxyPool(data) {
const db = await getDb();
if (!db.data.proxyPools) {
db.data.proxyPools = [];
}
const now = new Date().toISOString();
const pool = {
id: data.id || uuidv4(),
name: data.name,
proxyUrl: data.proxyUrl,
noProxy: data.noProxy || "",
isActive: data.isActive !== undefined ? data.isActive : true,
strictProxy: data.strictProxy === true,
testStatus: data.testStatus || "unknown",
lastTestedAt: data.lastTestedAt || null,
lastError: data.lastError || null,
createdAt: now,
updatedAt: now,
};
db.data.proxyPools.push(pool);
await db.write();
return pool;
}
/**
* Update proxy pool
*/
export async function updateProxyPool(id, data) {
const db = await getDb();
if (!db.data.proxyPools) {
db.data.proxyPools = [];
}
const index = db.data.proxyPools.findIndex((pool) => pool.id === id);
if (index === -1) return null;
db.data.proxyPools[index] = {
...db.data.proxyPools[index],
...data,
updatedAt: new Date().toISOString(),
};
await db.write();
return db.data.proxyPools[index];
}
/**
* Delete proxy pool
*/
export async function deleteProxyPool(id) {
const db = await getDb();
if (!db.data.proxyPools) {
db.data.proxyPools = [];
}
const index = db.data.proxyPools.findIndex((pool) => pool.id === id);
if (index === -1) return null;
const [removed] = db.data.proxyPools.splice(index, 1);
await db.write();
return removed;
}
/**
* Delete all provider connections by provider ID
*/

View File

@@ -0,0 +1,58 @@
import { getProxyPoolById } from "@/models";
function normalizeString(value) {
if (value === undefined || value === null) return "";
return String(value).trim();
}
function normalizeLegacyProxy(providerSpecificData = {}) {
const connectionProxyEnabled = providerSpecificData?.connectionProxyEnabled === true;
const connectionProxyUrl = normalizeString(providerSpecificData?.connectionProxyUrl);
const connectionNoProxy = normalizeString(providerSpecificData?.connectionNoProxy);
return {
connectionProxyEnabled,
connectionProxyUrl,
connectionNoProxy,
};
}
export async function resolveConnectionProxyConfig(providerSpecificData = {}) {
const proxyPoolIdRaw = normalizeString(providerSpecificData?.proxyPoolId);
const proxyPoolId = proxyPoolIdRaw === "__none__" ? "" : proxyPoolIdRaw;
const legacy = normalizeLegacyProxy(providerSpecificData);
if (proxyPoolId) {
const proxyPool = await getProxyPoolById(proxyPoolId);
const proxyUrl = normalizeString(proxyPool?.proxyUrl);
const noProxy = normalizeString(proxyPool?.noProxy);
if (proxyPool && proxyPool.isActive === true && proxyUrl) {
return {
source: "pool",
proxyPoolId,
proxyPool,
connectionProxyEnabled: true,
connectionProxyUrl: proxyUrl,
connectionNoProxy: noProxy,
strictProxy: proxyPool.strictProxy === true,
};
}
}
if (legacy.connectionProxyEnabled && legacy.connectionProxyUrl) {
return {
source: "legacy",
proxyPoolId: proxyPoolId || null,
proxyPool: null,
...legacy,
};
}
return {
source: "none",
proxyPoolId: proxyPoolId || null,
proxyPool: null,
...legacy,
};
}