Pool/IP fitness registry (globalThis-backed, provider::model scopes, 5-min cooldown, provider::* wildcard) fed by pool-scoped failures: freebuff limited-IP / model-locked gates and opencode free per-IP limits. chatCore retries a failed pool via another pool without locking the account; new Smart rotation strategy (per-connection + no-auth providers) skips unfit pools. Proxy Fitness dashboard page: active-block table with provider/IP filters, per-record Clear and provider-scoped Clear All; egress column via pool geo enrichment. Unit tests for the registry.
97 lines
3.0 KiB
JavaScript
97 lines
3.0 KiB
JavaScript
import { NextResponse } from "next/server";
|
|
import { createProxyPool, getProviderConnections, getProxyPools } from "@/models";
|
|
import { getPoolGeo } from "open-sse/services/poolGeo.js";
|
|
|
|
function toBoolean(value) {
|
|
if (value === "true") return true;
|
|
if (value === "false") return false;
|
|
return undefined;
|
|
}
|
|
|
|
const VALID_PROXY_TYPES = ["http", "vercel", "cloudflare", "deno"];
|
|
|
|
function normalizeProxyPoolInput(body = {}) {
|
|
const name = typeof body?.name === "string" ? body.name.trim() : "";
|
|
const proxyUrl = typeof body?.proxyUrl === "string" ? body.proxyUrl.trim() : "";
|
|
const noProxy = typeof body?.noProxy === "string" ? body.noProxy.trim() : "";
|
|
const isActive = body?.isActive === undefined ? true : body.isActive === true;
|
|
const strictProxy = body?.strictProxy === true;
|
|
const type = VALID_PROXY_TYPES.includes(body?.type) ? body.type : "http";
|
|
|
|
if (!name) {
|
|
return { error: "Name is required" };
|
|
}
|
|
|
|
if (!proxyUrl) {
|
|
return { error: "Proxy URL is required" };
|
|
}
|
|
|
|
return { name, proxyUrl, noProxy, isActive, strictProxy, type };
|
|
}
|
|
|
|
function buildUsageMap(connections = []) {
|
|
const usageMap = new Map();
|
|
|
|
for (const connection of connections) {
|
|
const proxyPoolId = connection?.providerSpecificData?.proxyPoolId;
|
|
if (!proxyPoolId) continue;
|
|
|
|
usageMap.set(proxyPoolId, (usageMap.get(proxyPoolId) || 0) + 1);
|
|
}
|
|
|
|
return usageMap;
|
|
}
|
|
|
|
// GET /api/proxy-pools - List proxy pools
|
|
export async function GET(request) {
|
|
try {
|
|
const { searchParams } = new URL(request.url);
|
|
const isActive = toBoolean(searchParams.get("isActive"));
|
|
const includeUsage = searchParams.get("includeUsage") === "true";
|
|
|
|
const filter = {};
|
|
if (isActive !== undefined) {
|
|
filter.isActive = isActive;
|
|
}
|
|
|
|
const proxyPools = await getProxyPools(filter);
|
|
|
|
if (!includeUsage) {
|
|
return NextResponse.json({ proxyPools });
|
|
}
|
|
|
|
const connections = await getProviderConnections();
|
|
const usageMap = buildUsageMap(connections);
|
|
|
|
const enrichedProxyPools = proxyPools.map((pool) => ({
|
|
...pool,
|
|
boundConnectionCount: usageMap.get(pool.id) || 0,
|
|
// Egress geo from the background probe cache (null until first probe).
|
|
egress: getPoolGeo(pool.id) || null,
|
|
}));
|
|
|
|
return NextResponse.json({ proxyPools: enrichedProxyPools });
|
|
} catch (error) {
|
|
console.log("Error fetching proxy pools:", error);
|
|
return NextResponse.json({ error: "Failed to fetch proxy pools" }, { status: 500 });
|
|
}
|
|
}
|
|
|
|
// POST /api/proxy-pools - Create proxy pool
|
|
export async function POST(request) {
|
|
try {
|
|
const body = await request.json();
|
|
const normalized = normalizeProxyPoolInput(body);
|
|
|
|
if (normalized.error) {
|
|
return NextResponse.json({ error: normalized.error }, { status: 400 });
|
|
}
|
|
|
|
const proxyPool = await createProxyPool(normalized);
|
|
return NextResponse.json({ proxyPool }, { status: 201 });
|
|
} catch (error) {
|
|
console.log("Error creating proxy pool:", error);
|
|
return NextResponse.json({ error: "Failed to create proxy pool" }, { status: 500 });
|
|
}
|
|
}
|