feat: integrate 1proxy free proxy marketplace (closes #1788) (#1847)

Integrated into release/v3.7.8
This commit is contained in:
Paijo
2026-05-01 18:36:07 +07:00
committed by GitHub
parent ea9d9e09ba
commit f3226b7f8d
14 changed files with 1324 additions and 0 deletions

View File

@@ -1072,6 +1072,115 @@ export const compressionConfigureTool: McpToolDefinition<
sourceEndpoints: ["/api/compression/configure"],
};
// ============ 1proxy Tools ============
export const oneproxyFetchInput = z.object({
protocol: z.string().optional().describe("Filter by protocol: http, https, socks4, socks5"),
countryCode: z.string().optional().describe("Filter by country code (e.g. US, DE)"),
minQuality: z.number().optional().describe("Minimum quality score (0-100)"),
limit: z.number().optional().describe("Maximum number of proxies to return"),
});
export const oneproxyFetchOutput = z.object({
items: z.array(
z.object({
id: z.string(),
host: z.string(),
port: z.number(),
type: z.string(),
countryCode: z.string().nullable(),
qualityScore: z.number().nullable(),
latencyMs: z.number().nullable(),
anonymity: z.string().nullable(),
googleAccess: z.boolean(),
status: z.string(),
})
),
total: z.number(),
});
export const oneproxyFetchTool: McpToolDefinition<
typeof oneproxyFetchInput,
typeof oneproxyFetchOutput
> = {
name: "omniroute_oneproxy_fetch",
description:
"Fetch free proxies from the 1proxy marketplace with optional filters for protocol, country, and quality. Returns validated proxies with quality scores.",
inputSchema: oneproxyFetchInput,
outputSchema: oneproxyFetchOutput,
scopes: ["read:proxies"],
auditLevel: "basic",
phase: 2,
sourceEndpoints: ["/api/settings/oneproxy"],
};
export const oneproxyRotateInput = z.object({
strategy: z
.enum(["random", "quality", "sequential"])
.optional()
.describe("Rotation strategy: quality (best first), random, or sequential"),
});
export const oneproxyRotateOutput = z.object({
id: z.string(),
host: z.string(),
port: z.number(),
type: z.string(),
countryCode: z.string().nullable(),
qualityScore: z.number().nullable(),
latencyMs: z.number().nullable(),
});
export const oneproxyRotateTool: McpToolDefinition<
typeof oneproxyRotateInput,
typeof oneproxyRotateOutput
> = {
name: "omniroute_oneproxy_rotate",
description:
"Get the next available free proxy from the 1proxy pool using the specified rotation strategy.",
inputSchema: oneproxyRotateInput,
outputSchema: oneproxyRotateOutput,
scopes: ["read:proxies"],
auditLevel: "basic",
phase: 2,
sourceEndpoints: ["/api/settings/oneproxy/rotate"],
};
export const oneproxyStatsInput = z.object({}).describe("No parameters required");
export const oneproxyStatsOutput = z.object({
stats: z.object({
total: z.number(),
active: z.number(),
avgQuality: z.number().nullable(),
lastValidated: z.string().nullable(),
byProtocol: z.array(z.object({ protocol: z.string(), count: z.number() })),
byCountry: z.array(z.object({ countryCode: z.string(), count: z.number() })),
}),
status: z.object({
lastSyncSuccess: z.boolean(),
lastSyncError: z.string().nullable(),
lastSyncAt: z.string().nullable(),
lastSyncCount: z.number(),
consecutiveFailures: z.number(),
}),
});
export const oneproxyStatsTool: McpToolDefinition<
typeof oneproxyStatsInput,
typeof oneproxyStatsOutput
> = {
name: "omniroute_oneproxy_stats",
description:
"Returns 1proxy sync status and statistics: total proxies, average quality, sync history, and distribution by protocol and country.",
inputSchema: oneproxyStatsInput,
outputSchema: oneproxyStatsOutput,
scopes: ["read:proxies"],
auditLevel: "basic",
phase: 2,
sourceEndpoints: ["/api/settings/oneproxy"],
};
// ============ Tool Registry ============
/** All MCP tool definitions, ordered by phase then name */
@@ -1100,6 +1209,9 @@ export const MCP_TOOLS = [
cacheFlushTool,
compressionStatusTool,
compressionConfigureTool,
oneproxyFetchTool,
oneproxyRotateTool,
oneproxyStatsTool,
] as const;
/** Essential tools only (Phase 1) */

View File

@@ -42,6 +42,9 @@ import {
syncPricingInput,
cacheStatsInput,
cacheFlushInput,
oneproxyFetchInput,
oneproxyRotateInput,
oneproxyStatsInput,
} from "./schemas/tools.ts";
import { startMcpHeartbeat } from "./runtimeHeartbeat.ts";
@@ -66,6 +69,9 @@ import {
handleSyncPricing,
handleCacheStats,
handleCacheFlush,
handleOneproxyFetch,
handleOneproxyRotate,
handleOneproxyStats,
} from "./tools/advancedTools.ts";
import { memoryTools } from "./tools/memoryTools.ts";
import { skillTools } from "./tools/skillTools.ts";
@@ -832,6 +838,44 @@ export function createMcpServer(): McpServer {
)
);
// ── 1proxy Tools ──────────────────────────────
server.registerTool(
"omniroute_oneproxy_fetch",
{
description:
"Fetch free proxies from the 1proxy marketplace with optional filters for protocol, country, and quality. Returns validated proxies with quality scores.",
inputSchema: oneproxyFetchInput,
},
withScopeEnforcement("omniroute_oneproxy_fetch", (args) =>
handleOneproxyFetch(oneproxyFetchInput.parse(args))
)
);
server.registerTool(
"omniroute_oneproxy_rotate",
{
description:
"Get the next available free proxy from the 1proxy pool using the specified rotation strategy.",
inputSchema: oneproxyRotateInput,
},
withScopeEnforcement("omniroute_oneproxy_rotate", (args) =>
handleOneproxyRotate(oneproxyRotateInput.parse(args))
)
);
server.registerTool(
"omniroute_oneproxy_stats",
{
description:
"Returns 1proxy sync status and statistics: total proxies, average quality, sync history, and distribution by protocol and country.",
inputSchema: oneproxyStatsInput,
},
withScopeEnforcement("omniroute_oneproxy_stats", (args) =>
handleOneproxyStats(oneproxyStatsInput.parse(args))
)
);
// ── Memory Tools ──────────────────────────────
Object.values(memoryTools).forEach((toolDef) => {
server.registerTool(

View File

@@ -1000,3 +1000,119 @@ export async function handleCacheFlush(args: { signature?: string; model?: strin
return { content: [{ type: "text" as const, text: `Error: ${msg}` }], isError: true };
}
}
// ============ 1proxy Tools ============
export async function handleOneproxyFetch(
args: { protocol?: string; countryCode?: string; minQuality?: number; limit?: number } = {}
) {
const start = Date.now();
try {
const params = new URLSearchParams();
if (args.protocol) params.set("protocol", args.protocol);
if (args.countryCode) params.set("countryCode", args.countryCode);
if (args.minQuality) params.set("minQuality", String(args.minQuality));
if (args.limit) params.set("limit", String(args.limit));
const query = params.toString();
const path = query ? `/api/settings/oneproxy?${query}` : "/api/settings/oneproxy";
const raw = toRecord(await apiFetch(path));
const items = toArrayOfRecords(raw.items).map((r) => ({
id: toString(r.id, ""),
host: toString(r.host, ""),
port: toNumber(r.port, 0),
type: toString(r.type, "http"),
countryCode: typeof r.country_code === "string" ? r.country_code : null,
qualityScore: r.quality_score != null ? toNumber(r.quality_score) : null,
latencyMs: r.latency_ms != null ? toNumber(r.latency_ms) : null,
anonymity: typeof r.anonymity === "string" ? r.anonymity : null,
googleAccess: r.google_access === 1 || r.google_access === true,
status: toString(r.status, "active"),
}));
const result = { items, total: toNumber(raw.total, items.length) };
await logToolCall("omniroute_oneproxy_fetch", args, result, Date.now() - start, true);
return { content: [{ type: "text" as const, text: JSON.stringify(result, null, 2) }] };
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
await logToolCall("omniroute_oneproxy_fetch", args, null, Date.now() - start, false, msg);
return { content: [{ type: "text" as const, text: `Error: ${msg}` }], isError: true };
}
}
export async function handleOneproxyRotate(
args: { strategy?: "random" | "quality" | "sequential" } = {}
) {
const start = Date.now();
try {
const body: Record<string, unknown> = {};
if (args.strategy) body.strategy = args.strategy;
const raw = toRecord(
await apiFetch("/api/settings/oneproxy/rotate", {
method: "POST",
body: JSON.stringify(body),
})
);
const result = {
id: toString(raw.id, ""),
host: toString(raw.host, ""),
port: toNumber(raw.port, 0),
type: toString(raw.type, "http"),
countryCode: typeof raw.country_code === "string" ? raw.country_code : null,
qualityScore: raw.quality_score != null ? toNumber(raw.quality_score) : null,
latencyMs: raw.latency_ms != null ? toNumber(raw.latency_ms) : null,
};
await logToolCall("omniroute_oneproxy_rotate", args, result, Date.now() - start, true);
return { content: [{ type: "text" as const, text: JSON.stringify(result, null, 2) }] };
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
await logToolCall("omniroute_oneproxy_rotate", args, null, Date.now() - start, false, msg);
return { content: [{ type: "text" as const, text: `Error: ${msg}` }], isError: true };
}
}
export async function handleOneproxyStats(args: Record<string, never> = {}) {
const start = Date.now();
try {
const raw = toRecord(await apiFetch("/api/settings/oneproxy?action=stats"));
const statsRaw = toRecord(raw.stats);
const statusRaw = toRecord(raw.status);
const stats = {
total: toNumber(statsRaw.total, 0),
active: toNumber(statsRaw.active, 0),
avgQuality: statsRaw.avg_quality != null ? toNumber(statsRaw.avg_quality) : null,
lastValidated: typeof statsRaw.last_validated === "string" ? statsRaw.last_validated : null,
byProtocol: toArrayOfRecords(statsRaw.by_protocol || statsRaw.byProtocol).map((r) => ({
protocol: toString(r.protocol, ""),
count: toNumber(r.count, 0),
})),
byCountry: toArrayOfRecords(statsRaw.by_country || statsRaw.byCountry).map((r) => ({
countryCode: toString(r.countryCode || r.country_code, ""),
count: toNumber(r.count, 0),
})),
};
const status = {
lastSyncSuccess: toBoolean(statusRaw.last_sync_success, false),
lastSyncError:
typeof statusRaw.last_sync_error === "string" ? statusRaw.last_sync_error : null,
lastSyncAt: typeof statusRaw.last_sync_at === "string" ? statusRaw.last_sync_at : null,
lastSyncCount: toNumber(statusRaw.last_sync_count, 0),
consecutiveFailures: toNumber(statusRaw.consecutive_failures, 0),
};
const result = { stats, status };
await logToolCall("omniroute_oneproxy_stats", args, result, Date.now() - start, true);
return { content: [{ type: "text" as const, text: JSON.stringify(result, null, 2) }] };
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
await logToolCall("omniroute_oneproxy_stats", args, null, Date.now() - start, false, msg);
return { content: [{ type: "text" as const, text: `Error: ${msg}` }], isError: true };
}
}

View File

@@ -0,0 +1,330 @@
"use client";
import { useState, useEffect, useCallback } from "react";
import { useTranslations } from "next-intl";
import { Button, Card } from "@/shared/components";
type OneproxyItem = {
id: string;
name: string;
host: string;
port: number;
type: string;
countryCode: string | null;
qualityScore: number | null;
latencyMs: number | null;
anonymity: string | null;
googleAccess: boolean;
status: string;
lastValidated: string | null;
};
type OneproxyStats = {
total: number;
active: number;
avgQuality: number | null;
lastValidated: string | null;
byProtocol: Array<{ protocol: string; count: number }>;
byCountry: Array<{ countryCode: string; count: number }>;
};
type SyncStatus = {
lastSyncSuccess: boolean;
lastSyncError: string | null;
lastSyncAt: string | null;
lastSyncCount: number;
consecutiveFailures: number;
};
export default function OneproxyTab() {
const t = useTranslations("settings");
const [proxies, setProxies] = useState<OneproxyItem[]>([]);
const [stats, setStats] = useState<OneproxyStats | null>(null);
const [status, setStatus] = useState<SyncStatus | null>(null);
const [loading, setLoading] = useState(true);
const [syncing, setSyncing] = useState(false);
const [syncResult, setSyncResult] = useState<string | null>(null);
const [filterProtocol, setFilterProtocol] = useState("");
const [filterCountry, setFilterCountry] = useState("");
const [minQuality, setMinQuality] = useState("");
const loadData = useCallback(async () => {
setLoading(true);
try {
const params = new URLSearchParams();
if (filterProtocol) params.set("protocol", filterProtocol);
if (filterCountry) params.set("countryCode", filterCountry);
if (minQuality) params.set("minQuality", minQuality);
const [proxiesRes, statsRes] = await Promise.all([
fetch(`/api/settings/oneproxy?${params.toString()}`),
fetch("/api/settings/oneproxy?action=stats"),
]);
if (proxiesRes.ok) {
const data = await proxiesRes.json();
setProxies(data.items || []);
}
if (statsRes.ok) {
const data = await statsRes.json();
setStats(data.stats);
setStatus(data.status);
}
} catch {
} finally {
setLoading(false);
}
}, [filterProtocol, filterCountry, minQuality]);
useEffect(() => {
loadData();
}, [loadData]);
const handleSync = async () => {
setSyncing(true);
setSyncResult(null);
try {
const res = await fetch("/api/settings/oneproxy", { method: "POST" });
const data = await res.json();
if (data.success) {
setSyncResult(`Synced ${data.total} proxies (${data.added} new, ${data.updated} updated)`);
} else {
setSyncResult(`Sync failed: ${data.error}`);
}
await loadData();
} catch (err) {
setSyncResult(`Sync failed: ${err}`);
} finally {
setSyncing(false);
}
};
const handleClearAll = async () => {
if (!confirm("Clear all 1proxy proxies?")) return;
try {
await fetch("/api/settings/oneproxy?clearAll=1", { method: "DELETE" });
await loadData();
} catch {
// ignore
}
};
const handleDelete = async (id: string) => {
try {
await fetch(`/api/settings/oneproxy?id=${id}`, { method: "DELETE" });
setProxies((prev) => prev.filter((p) => p.id !== id));
if (stats) setStats({ ...stats, total: stats.total - 1, active: stats.active - 1 });
} catch {
// ignore
}
};
const qualityColor = (score: number | null) => {
if (score == null) return "bg-gray-500";
if (score >= 80) return "bg-green-500";
if (score >= 50) return "bg-yellow-500";
return "bg-red-500";
};
const protocolBadge = (type: string) => {
const colors: Record<string, string> = {
http: "bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200",
https: "bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200",
socks4: "bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-200",
socks5: "bg-orange-100 text-orange-800 dark:bg-orange-900 dark:text-orange-200",
};
return colors[type] || "bg-gray-100 text-gray-800";
};
return (
<div className="space-y-6">
<div className="flex items-center justify-between">
<div>
<h2 className="text-lg font-semibold text-text-main">1proxy Free Proxy Marketplace</h2>
<p className="text-sm text-text-muted mt-1">
Fetch and rotate free validated proxies from the 1proxy community platform
</p>
</div>
<div className="flex gap-2">
<Button onClick={handleSync} disabled={syncing} variant="primary">
{syncing ? "Syncing..." : "Sync Now"}
</Button>
{proxies.length > 0 && (
<Button onClick={handleClearAll} variant="danger">
Clear All
</Button>
)}
</div>
</div>
{syncResult && (
<div
className={`p-3 rounded-lg text-sm ${
syncResult.startsWith("Synced")
? "bg-green-50 text-green-800 dark:bg-green-900/30 dark:text-green-300"
: "bg-red-50 text-red-800 dark:bg-red-900/30 dark:text-red-300"
}`}
>
{syncResult}
</div>
)}
{stats && (
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<Card className="p-4">
<div className="text-2xl font-bold text-text-main">{stats.total}</div>
<div className="text-sm text-text-muted">Total Proxies</div>
</Card>
<Card className="p-4">
<div className="text-2xl font-bold text-green-600">{stats.active}</div>
<div className="text-sm text-text-muted">Active</div>
</Card>
<Card className="p-4">
<div className="text-2xl font-bold text-text-main">
{stats.avgQuality != null ? `${stats.avgQuality}` : "—"}
</div>
<div className="text-sm text-text-muted">Avg Quality</div>
</Card>
<Card className="p-4">
<div className="text-2xl font-bold text-text-main">
{status?.lastSyncAt ? new Date(status.lastSyncAt).toLocaleTimeString() : "Never"}
</div>
<div className="text-sm text-text-muted">Last Sync</div>
</Card>
</div>
)}
<Card className="p-4">
<div className="flex flex-wrap gap-3">
<select
value={filterProtocol}
onChange={(e) => setFilterProtocol(e.target.value)}
className="px-3 py-2 rounded-lg bg-black/5 dark:bg-white/5 text-text-main text-sm border border-border"
>
<option value="">All Protocols</option>
<option value="http">HTTP</option>
<option value="https">HTTPS</option>
<option value="socks4">SOCKS4</option>
<option value="socks5">SOCKS5</option>
</select>
<input
type="text"
placeholder="Country code (e.g. US)"
value={filterCountry}
onChange={(e) => setFilterCountry(e.target.value)}
className="px-3 py-2 rounded-lg bg-black/5 dark:bg-white/5 text-text-main text-sm border border-border w-40"
/>
<input
type="number"
placeholder="Min quality"
value={minQuality}
onChange={(e) => setMinQuality(e.target.value)}
className="px-3 py-2 rounded-lg bg-black/5 dark:bg-white/5 text-text-main text-sm border border-border w-32"
/>
</div>
</Card>
<Card className="p-4">
{loading ? (
<div className="text-center py-8 text-text-muted">Loading proxies...</div>
) : proxies.length === 0 ? (
<div className="text-center py-8 text-text-muted">
No 1proxy proxies found. Click &quot;Sync Now&quot; to fetch free proxies.
</div>
) : (
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="border-b border-border">
<th className="text-left py-2 px-3 text-text-muted font-medium">Host</th>
<th className="text-left py-2 px-3 text-text-muted font-medium">Protocol</th>
<th className="text-left py-2 px-3 text-text-muted font-medium">Country</th>
<th className="text-left py-2 px-3 text-text-muted font-medium">Quality</th>
<th className="text-left py-2 px-3 text-text-muted font-medium">Latency</th>
<th className="text-left py-2 px-3 text-text-muted font-medium">Anonymity</th>
<th className="text-left py-2 px-3 text-text-muted font-medium">Google</th>
<th className="text-left py-2 px-3 text-text-muted font-medium">Actions</th>
</tr>
</thead>
<tbody>
{proxies.map((proxy) => (
<tr
key={proxy.id}
className="border-b border-border/50 hover:bg-black/5 dark:hover:bg-white/5"
>
<td className="py-2 px-3 font-mono text-text-main">
{proxy.host}:{proxy.port}
</td>
<td className="py-2 px-3">
<span
className={`px-2 py-0.5 rounded text-xs font-medium ${protocolBadge(proxy.type)}`}
>
{proxy.type.toUpperCase()}
</span>
</td>
<td className="py-2 px-3 text-text-main">{proxy.countryCode || "—"}</td>
<td className="py-2 px-3">
<div className="flex items-center gap-2">
<div
className={`w-3 h-3 rounded-full ${qualityColor(proxy.qualityScore)}`}
/>
<span className="text-text-main">{proxy.qualityScore ?? "—"}</span>
</div>
</td>
<td className="py-2 px-3 text-text-main">
{proxy.latencyMs != null ? `${proxy.latencyMs}ms` : "—"}
</td>
<td className="py-2 px-3 text-text-main">{proxy.anonymity || "—"}</td>
<td className="py-2 px-3">
{proxy.googleAccess ? (
<span className="text-green-600">&#10003;</span>
) : (
<span className="text-red-600">&#10007;</span>
)}
</td>
<td className="py-2 px-3">
<button
onClick={() => handleDelete(proxy.id)}
className="text-red-500 hover:text-red-700 text-xs"
>
Delete
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</Card>
{status && (
<Card className="p-4">
<h3 className="text-sm font-semibold text-text-main mb-2">Sync Status</h3>
<div className="grid grid-cols-2 md:grid-cols-3 gap-4 text-sm">
<div>
<span className="text-text-muted">Last sync: </span>
<span className={status.lastSyncSuccess ? "text-green-600" : "text-red-600"}>
{status.lastSyncSuccess ? "Success" : "Failed"}
</span>
</div>
<div>
<span className="text-text-muted">Proxies fetched: </span>
<span className="text-text-main">{status.lastSyncCount}</span>
</div>
<div>
<span className="text-text-muted">Consecutive failures: </span>
<span className="text-text-main">{status.consecutiveFailures}</span>
</div>
{status.lastSyncError && (
<div className="col-span-full">
<span className="text-text-muted">Error: </span>
<span className="text-red-600 text-xs">{status.lastSyncError}</span>
</div>
)}
</div>
</Card>
)}
</div>
);
}

View File

@@ -4,6 +4,7 @@ import { useState, useEffect, useRef } from "react";
import { Card, Button, ProxyConfigModal, Toggle } from "@/shared/components";
import { useTranslations } from "next-intl";
import ProxyRegistryManager from "./ProxyRegistryManager";
import OneproxyTab from "./OneproxyTab";
export default function ProxyTab() {
const [proxyModalOpen, setProxyModalOpen] = useState(false);
@@ -137,6 +138,7 @@ export default function ProxyTab() {
</Card>
<ProxyRegistryManager />
<OneproxyTab />
<Card className="p-6 mt-4">
<div className="flex items-center justify-between">
<div>

View File

@@ -0,0 +1,44 @@
import { requireManagementAuth } from "@/lib/api/requireManagementAuth";
import { createErrorResponse, createErrorResponseFromUnknown } from "@/lib/api/errorResponse";
import { isValidationFailure, validateBody } from "@/shared/validation/helpers";
import { rotateOneproxyProxy } from "@/lib/oneproxyRotator";
import { oneproxyRotateSchema } from "@/shared/validation/oneproxySchemas";
export async function POST(request: Request) {
const authError = await requireManagementAuth(request);
if (authError) return authError;
let rawBody: unknown;
try {
rawBody = await request.json();
} catch {
rawBody = {};
}
try {
const validation = validateBody(oneproxyRotateSchema, rawBody);
if (isValidationFailure(validation)) {
return createErrorResponse({
status: 400,
message: validation.error.message,
type: "invalid_request",
});
}
const proxy = await rotateOneproxyProxy({
strategy: validation.data.strategy,
});
if (!proxy) {
return createErrorResponse({
status: 404,
message: "No active 1proxy proxies available",
type: "not_found",
});
}
return Response.json(proxy);
} catch (error) {
return createErrorResponseFromUnknown(error, "Failed to rotate 1proxy proxy");
}
}

View File

@@ -0,0 +1,126 @@
import { requireManagementAuth } from "@/lib/api/requireManagementAuth";
import { createErrorResponse, createErrorResponseFromUnknown } from "@/lib/api/errorResponse";
import { isValidationFailure, validateBody } from "@/shared/validation/helpers";
import {
listOneproxyProxies,
getOneproxyStats,
deleteOneproxyProxy,
clearAllOneproxyProxies,
} from "@/lib/localDb";
import {
syncOneproxyProxies,
getOneproxySyncStatus,
resetOneproxyCircuitBreaker,
} from "@/lib/oneproxySync";
import { oneproxyFilterSchema, oneproxySyncSchema } from "@/shared/validation/oneproxySchemas";
export async function GET(request: Request) {
const authError = await requireManagementAuth(request);
if (authError) return authError;
try {
const { searchParams } = new URL(request.url);
const action = searchParams.get("action");
if (action === "stats") {
const stats = await getOneproxyStats();
const status = getOneproxySyncStatus();
return Response.json({ stats, status });
}
if (action === "status") {
return Response.json(getOneproxySyncStatus());
}
const filterValidation = validateBody(oneproxyFilterSchema, {
protocol: searchParams.get("protocol") || undefined,
countryCode: searchParams.get("countryCode") || undefined,
minQuality: searchParams.get("minQuality") || undefined,
limit: searchParams.get("limit") || undefined,
});
if (isValidationFailure(filterValidation)) {
return createErrorResponse({
status: 400,
message: filterValidation.error.message,
type: "invalid_request",
});
}
const proxies = await listOneproxyProxies({
protocol: filterValidation.data.protocol,
countryCode: filterValidation.data.countryCode,
minQuality: filterValidation.data.minQuality,
limit: filterValidation.data.limit,
});
return Response.json({ items: proxies, total: proxies.length });
} catch (error) {
return createErrorResponseFromUnknown(error, "Failed to load 1proxy proxies");
}
}
export async function POST(request: Request) {
const authError = await requireManagementAuth(request);
if (authError) return authError;
let rawBody: unknown;
try {
rawBody = await request.json();
} catch {
return createErrorResponse({
status: 400,
message: "Invalid JSON body",
type: "invalid_request",
});
}
try {
const validation = validateBody(oneproxySyncSchema, rawBody);
if (isValidationFailure(validation)) {
return createErrorResponse({
status: 400,
message: validation.error.message,
type: "invalid_request",
});
}
const result = await syncOneproxyProxies();
return Response.json(result);
} catch (error) {
return createErrorResponseFromUnknown(error, "Failed to sync 1proxy proxies");
}
}
export async function DELETE(request: Request) {
const authError = await requireManagementAuth(request);
if (authError) return authError;
try {
const { searchParams } = new URL(request.url);
const id = searchParams.get("id");
const clearAll = searchParams.get("clearAll") === "1";
if (clearAll) {
const count = await clearAllOneproxyProxies();
return Response.json({ success: true, deleted: count });
}
if (!id) {
return createErrorResponse({
status: 400,
message: "id or clearAll is required",
type: "invalid_request",
});
}
const deleted = await deleteOneproxyProxy(id);
if (!deleted) {
return createErrorResponse({ status: 404, message: "Proxy not found", type: "not_found" });
}
return Response.json({ success: true });
} catch (error) {
return createErrorResponseFromUnknown(error, "Failed to delete 1proxy proxy");
}
}

View File

@@ -0,0 +1,24 @@
-- 040_oneproxy_proxy_fields.sql
-- Add 1proxy-specific columns to proxy_registry to support free proxy
-- marketplace integration (Issue #1788).
--
-- New columns:
-- source — 'manual' or 'oneproxy' (distinguishes origin)
-- quality_score — 0-100 quality rating from 1proxy validation
-- latency_ms — measured latency in milliseconds
-- anonymity — transparent, anonymous, or elite
-- google_access — whether proxy can access Google (0/1)
-- last_validated — ISO timestamp of last validation
-- country_code — two-letter ISO country code
ALTER TABLE proxy_registry ADD COLUMN source TEXT NOT NULL DEFAULT 'manual';
ALTER TABLE proxy_registry ADD COLUMN quality_score INTEGER;
ALTER TABLE proxy_registry ADD COLUMN latency_ms INTEGER;
ALTER TABLE proxy_registry ADD COLUMN anonymity TEXT;
ALTER TABLE proxy_registry ADD COLUMN google_access INTEGER DEFAULT 0;
ALTER TABLE proxy_registry ADD COLUMN last_validated TEXT;
ALTER TABLE proxy_registry ADD COLUMN country_code TEXT;
CREATE INDEX IF NOT EXISTS idx_proxy_registry_source ON proxy_registry(source);
CREATE INDEX IF NOT EXISTS idx_proxy_registry_quality ON proxy_registry(quality_score DESC);
CREATE INDEX IF NOT EXISTS idx_proxy_registry_country ON proxy_registry(country_code);

293
src/lib/db/oneproxy.ts Normal file
View File

@@ -0,0 +1,293 @@
import { randomUUID } from "crypto";
import { getDbInstance } from "./core";
import { backupDbFile } from "./backup";
type JsonRecord = Record<string, unknown>;
export interface OneproxyProxyRecord {
id: string;
name: string;
type: string;
host: string;
port: number;
region: string | null;
notes: string | null;
status: string;
source: string;
qualityScore: number | null;
latencyMs: number | null;
anonymity: string | null;
googleAccess: boolean;
lastValidated: string | null;
countryCode: string | null;
createdAt: string;
updatedAt: string;
}
export interface OneproxyStats {
total: number;
active: number;
avgQuality: number | null;
lastValidated: string | null;
byProtocol: Array<{ protocol: string; count: number }>;
byCountry: Array<{ countryCode: string; count: number }>;
}
interface OneproxyUpsertInput {
ip: string;
port: number;
protocol: string;
country?: string | null;
countryCode?: string | null;
anonymity?: string | null;
qualityScore?: number | null;
latencyMs?: number | null;
googleAccess?: boolean;
lastValidated?: string | null;
}
function toRecord(value: unknown): JsonRecord {
return value && typeof value === "object" ? (value as JsonRecord) : {};
}
function mapProxyRow(row: unknown): OneproxyProxyRecord {
const r = toRecord(row);
return {
id: typeof r.id === "string" ? r.id : "",
name: typeof r.name === "string" ? r.name : "",
type: typeof r.type === "string" ? r.type : "http",
host: typeof r.host === "string" ? r.host : "",
port: Number(r.port) || 0,
region: typeof r.region === "string" ? r.region : null,
notes: typeof r.notes === "string" ? r.notes : null,
status: typeof r.status === "string" ? r.status : "active",
source: typeof r.source === "string" ? r.source : "oneproxy",
qualityScore: typeof r.quality_score === "number" ? r.quality_score : null,
latencyMs: typeof r.latency_ms === "number" ? r.latency_ms : null,
anonymity: typeof r.anonymity === "string" ? r.anonymity : null,
googleAccess: r.google_access === 1 || r.google_access === true,
lastValidated: typeof r.last_validated === "string" ? r.last_validated : null,
countryCode: typeof r.country_code === "string" ? r.country_code : null,
createdAt: typeof r.created_at === "string" ? r.created_at : "",
updatedAt: typeof r.updated_at === "string" ? r.updated_at : "",
};
}
function mapStatsRow(row: unknown) {
const r = toRecord(row);
return {
total: Number(r.total) || 0,
active: Number(r.active) || 0,
avgQuality:
r.avg_quality !== null && r.avg_quality !== undefined
? Math.round(Number(r.avg_quality) * 100) / 100
: null,
lastValidated: typeof r.last_validated === "string" ? r.last_validated : null,
};
}
export async function listOneproxyProxies(options?: {
protocol?: string;
countryCode?: string;
minQuality?: number;
limit?: number;
}): Promise<OneproxyProxyRecord[]> {
const db = getDbInstance();
let sql = "SELECT * FROM proxy_registry WHERE source = 'oneproxy' AND status = 'active'";
const params: unknown[] = [];
if (options?.protocol) {
sql += " AND type = ?";
params.push(options.protocol);
}
if (options?.countryCode) {
sql += " AND country_code = ?";
params.push(options.countryCode);
}
if (options?.minQuality != null) {
sql += " AND quality_score >= ?";
params.push(options.minQuality);
}
sql += " ORDER BY quality_score DESC, last_validated DESC";
if (options?.limit) {
sql += " LIMIT ?";
params.push(options.limit);
}
const rows = db.prepare(sql).all(...params);
return rows.map(mapProxyRow);
}
export async function getOneproxyStats(): Promise<OneproxyStats> {
const db = getDbInstance();
const statsRow = db
.prepare(
`SELECT
COUNT(*) as total,
SUM(CASE WHEN status = 'active' THEN 1 ELSE 0 END) as active,
AVG(quality_score) as avg_quality,
MAX(last_validated) as last_validated
FROM proxy_registry WHERE source = 'oneproxy'`
)
.get();
const stats = mapStatsRow(statsRow);
const byProtocol = db
.prepare(
"SELECT type as protocol, COUNT(*) as count FROM proxy_registry WHERE source = 'oneproxy' GROUP BY type ORDER BY count DESC"
)
.all() as Array<JsonRecord>;
const byCountry = db
.prepare(
"SELECT country_code as countryCode, COUNT(*) as count FROM proxy_registry WHERE source = 'oneproxy' AND country_code IS NOT NULL GROUP BY country_code ORDER BY count DESC LIMIT 20"
)
.all() as Array<JsonRecord>;
return {
...stats,
byProtocol: byProtocol.map((r) => ({
protocol: String(r.protocol || "unknown"),
count: Number(r.count) || 0,
})),
byCountry: byCountry.map((r) => ({
countryCode: String(r.countryCode || "unknown"),
count: Number(r.count) || 0,
})),
};
}
export async function upsertOneproxyProxy(
input: OneproxyUpsertInput
): Promise<{ proxy: OneproxyProxyRecord | null; action: "created" | "updated" }> {
const db = getDbInstance();
const now = new Date().toISOString();
const name = `${input.protocol?.toUpperCase() || "HTTP"} - ${input.countryCode || "Unknown"} - ${input.ip}`;
const existing = db
.prepare("SELECT id FROM proxy_registry WHERE host = ? AND port = ? AND source = 'oneproxy'")
.get(input.ip, input.port) as { id?: string } | undefined;
if (existing?.id) {
db.prepare(
`UPDATE proxy_registry
SET status = ?, quality_score = ?, latency_ms = ?, anonymity = ?,
google_access = ?, last_validated = ?, country_code = ?, updated_at = ?
WHERE id = ?`
).run(
"active",
input.qualityScore ?? null,
input.latencyMs ?? null,
input.anonymity ?? null,
input.googleAccess ? 1 : 0,
input.lastValidated ?? now,
input.countryCode ?? null,
now,
existing.id
);
backupDbFile("pre-write");
const proxy = await getOneproxyProxyById(existing.id);
return { proxy, action: "updated" };
}
const id = randomUUID();
db.prepare(
`INSERT INTO proxy_registry
(id, name, type, host, port, region, notes, status, source,
quality_score, latency_ms, anonymity, google_access, last_validated, country_code,
created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
).run(
id,
name,
input.protocol || "http",
input.ip,
input.port,
input.countryCode ?? null,
null,
"active",
"oneproxy",
input.qualityScore ?? null,
input.latencyMs ?? null,
input.anonymity ?? null,
input.googleAccess ? 1 : 0,
input.lastValidated ?? now,
input.countryCode ?? null,
now,
now
);
backupDbFile("pre-write");
const proxy = await getOneproxyProxyById(id);
return { proxy, action: "created" };
}
export async function getOneproxyProxyById(id: string): Promise<OneproxyProxyRecord | null> {
const db = getDbInstance();
const row = db
.prepare("SELECT * FROM proxy_registry WHERE id = ? AND source = 'oneproxy'")
.get(id);
if (!row) return null;
return mapProxyRow(row);
}
export async function deleteOneproxyProxy(id: string): Promise<boolean> {
const db = getDbInstance();
const result = db
.prepare("DELETE FROM proxy_registry WHERE id = ? AND source = 'oneproxy'")
.run(id);
backupDbFile("pre-write");
return result.changes > 0;
}
export async function clearAllOneproxyProxies(): Promise<number> {
const db = getDbInstance();
const result = db.prepare("DELETE FROM proxy_registry WHERE source = 'oneproxy'").run();
backupDbFile("pre-write");
return result.changes;
}
export async function getOneproxyProxyForRotation(options?: {
strategy?: "random" | "quality" | "sequential";
}): Promise<OneproxyProxyRecord | null> {
const db = getDbInstance();
const strategy = options?.strategy || "quality";
let sql = "SELECT * FROM proxy_registry WHERE source = 'oneproxy' AND status = 'active'";
switch (strategy) {
case "quality":
sql += " ORDER BY quality_score DESC, latency_ms ASC LIMIT 1";
break;
case "random":
sql += " ORDER BY RANDOM() LIMIT 1";
break;
case "sequential":
sql += " ORDER BY last_validated ASC LIMIT 1";
break;
}
const row = db.prepare(sql).get();
if (!row) return null;
return mapProxyRow(row);
}
export async function markOneproxyProxyFailed(host: string, port: number): Promise<boolean> {
const db = getDbInstance();
const result = db
.prepare(
`UPDATE proxy_registry
SET quality_score = MAX(0, COALESCE(quality_score, 50) - 10),
status = CASE WHEN COALESCE(quality_score, 50) <= 10 THEN 'inactive' ELSE status END,
updated_at = datetime('now')
WHERE host = ? AND port = ? AND source = 'oneproxy'`
)
.run(host, port);
backupDbFile("pre-write");
return result.changes > 0;
}

View File

@@ -347,3 +347,17 @@ export {
} from "./db/reasoningCache";
export type { ReasoningCacheEntry, ReasoningCacheStats } from "./db/reasoningCache";
export {
// 1proxy Integration (#1788)
listOneproxyProxies,
getOneproxyStats,
upsertOneproxyProxy,
getOneproxyProxyById,
deleteOneproxyProxy,
clearAllOneproxyProxies,
getOneproxyProxyForRotation,
markOneproxyProxyFailed,
} from "./db/oneproxy";
export type { OneproxyProxyRecord, OneproxyStats } from "./db/oneproxy";

View File

@@ -0,0 +1,19 @@
import { getOneproxyProxyForRotation, markOneproxyProxyFailed } from "./db/oneproxy";
import type { OneproxyProxyRecord } from "./db/oneproxy";
let sequentialIndex = 0;
export async function rotateOneproxyProxy(options?: {
strategy?: "random" | "quality" | "sequential";
}): Promise<OneproxyProxyRecord | null> {
const strategy = options?.strategy || "quality";
return getOneproxyProxyForRotation({ strategy });
}
export async function failOneproxyProxy(host: string, port: number): Promise<boolean> {
return markOneproxyProxyFailed(host, port);
}
export function resetSequentialIndex(): void {
sequentialIndex = 0;
}

182
src/lib/oneproxySync.ts Normal file
View File

@@ -0,0 +1,182 @@
import { upsertOneproxyProxy } from "./db/oneproxy";
import { getSettings } from "./db/settings";
type OneProxyApiResponse = {
total: number;
count: number;
offset: number;
limit: number;
proxies: Array<{
id: number;
url: string;
protocol: string;
ip: string;
port: number;
country_code: string;
country_name: string;
latency_ms: number;
anonymity: string;
proxy_type: string;
can_access_google: boolean;
quality_score: number;
is_working: boolean;
validation_status: string;
last_validated: string;
}>;
};
const DEFAULT_API_URL = "https://1proxy-api.aitradepulse.com";
const DEFAULT_MAX_PROXIES = 500;
const DEFAULT_MIN_QUALITY = 50;
const DEFAULT_PAGE_SIZE = 100;
let lastSyncSuccess = false;
let lastSyncError: string | null = null;
let lastSyncAt: string | null = null;
let lastSyncCount = 0;
let consecutiveFailures = 0;
const MAX_CONSECUTIVE_FAILURES = 5;
export async function syncOneproxyProxies(options?: {
apiUrl?: string;
maxProxies?: number;
minQuality?: number;
}): Promise<{
success: boolean;
added: number;
updated: number;
failed: number;
total: number;
error: string | null;
}> {
const settings = await getSettings();
const enabled = process.env.ONEPROXY_ENABLED !== "false";
if (!enabled) {
return {
success: false,
added: 0,
updated: 0,
failed: 0,
total: 0,
error: "1proxy integration disabled",
};
}
const apiUrl = options?.apiUrl || process.env.ONEPROXY_API_URL || DEFAULT_API_URL;
const maxProxies =
(options?.maxProxies ?? parseInt(process.env.ONEPROXY_MAX_PROXIES || "", 10)) ||
DEFAULT_MAX_PROXIES;
const minQuality =
(options?.minQuality ?? parseInt(process.env.ONEPROXY_MIN_QUALITY_THRESHOLD || "", 10)) ||
DEFAULT_MIN_QUALITY;
if (consecutiveFailures >= MAX_CONSECUTIVE_FAILURES) {
return {
success: false,
added: 0,
updated: 0,
failed: 0,
total: 0,
error: `Circuit breaker: ${consecutiveFailures} consecutive failures. Reset required.`,
};
}
let added = 0;
let updated = 0;
let failed = 0;
let offset = 0;
try {
while (true) {
const url = `${apiUrl}/api/v1/proxies/advanced?limit=${DEFAULT_PAGE_SIZE}&offset=${offset}&min_quality=${minQuality}&is_working=true`;
const response = await fetch(url, {
signal: AbortSignal.timeout(30000),
});
if (!response.ok) {
throw new Error(`1proxy API returned ${response.status}`);
}
const data = (await response.json()) as OneProxyApiResponse;
if (!data.proxies || data.proxies.length === 0) break;
for (const proxy of data.proxies) {
if (added + updated >= maxProxies) break;
try {
const result = await upsertOneproxyProxy({
ip: proxy.ip,
port: proxy.port,
protocol: proxy.protocol,
countryCode: proxy.country_code,
anonymity: proxy.anonymity,
qualityScore: proxy.quality_score,
latencyMs: proxy.latency_ms,
googleAccess: proxy.can_access_google,
lastValidated: proxy.last_validated,
});
if (result.action === "created") added++;
else updated++;
} catch {
failed++;
}
}
if (added + updated >= maxProxies) break;
if (data.proxies.length < DEFAULT_PAGE_SIZE) break;
offset += DEFAULT_PAGE_SIZE;
}
consecutiveFailures = 0;
lastSyncSuccess = true;
lastSyncError = null;
lastSyncAt = new Date().toISOString();
lastSyncCount = added + updated;
return {
success: true,
added,
updated,
failed,
total: added + updated,
error: null,
};
} catch (err) {
consecutiveFailures++;
lastSyncSuccess = false;
lastSyncError = err instanceof Error ? err.message : String(err);
lastSyncAt = new Date().toISOString();
return {
success: false,
added: 0,
updated: 0,
failed: 0,
total: 0,
error: lastSyncError,
};
}
}
export function getOneproxySyncStatus(): {
lastSyncSuccess: boolean;
lastSyncError: string | null;
lastSyncAt: string | null;
lastSyncCount: number;
consecutiveFailures: number;
} {
return {
lastSyncSuccess,
lastSyncError,
lastSyncAt,
lastSyncCount,
consecutiveFailures,
};
}
export function resetOneproxyCircuitBreaker(): void {
consecutiveFailures = 0;
lastSyncError = null;
}

View File

@@ -24,6 +24,7 @@ export const MCP_SCOPE_LIST = [
"write:cache",
"read:compression",
"write:compression",
"read:proxies",
] as const;
export type McpScope = (typeof MCP_SCOPE_LIST)[number];
@@ -58,6 +59,9 @@ export const MCP_TOOL_SCOPES: Record<string, readonly McpScope[]> = {
omniroute_cache_flush: ["write:cache"],
omniroute_compression_status: ["read:compression"],
omniroute_compression_configure: ["write:compression"],
omniroute_oneproxy_fetch: ["read:proxies"],
omniroute_oneproxy_rotate: ["read:proxies"],
omniroute_oneproxy_stats: ["read:proxies"],
} as const;
// ============ Scope Groups ============

View File

@@ -0,0 +1,14 @@
import { z } from "zod";
export const oneproxyFilterSchema = z.object({
protocol: z.enum(["http", "https", "socks4", "socks5"]).optional(),
countryCode: z.string().max(2).optional(),
minQuality: z.coerce.number().int().min(0).max(100).optional(),
maxProxies: z.coerce.number().int().min(1).max(1000).optional(),
});
export const oneproxySyncSchema = z.object({}).strict();
export const oneproxyRotateSchema = z.object({
strategy: z.enum(["random", "quality", "sequential"]).optional(),
});