mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-26 09:52:11 +03:00
Integrated into release/v3.7.8
This commit is contained in:
@@ -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) */
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 "Sync Now" 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">✓</span>
|
||||
) : (
|
||||
<span className="text-red-600">✗</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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
44
src/app/api/settings/oneproxy/rotate/route.ts
Normal file
44
src/app/api/settings/oneproxy/rotate/route.ts
Normal 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");
|
||||
}
|
||||
}
|
||||
126
src/app/api/settings/oneproxy/route.ts
Normal file
126
src/app/api/settings/oneproxy/route.ts
Normal 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");
|
||||
}
|
||||
}
|
||||
24
src/lib/db/migrations/040_oneproxy_proxy_fields.sql
Normal file
24
src/lib/db/migrations/040_oneproxy_proxy_fields.sql
Normal 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
293
src/lib/db/oneproxy.ts
Normal 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;
|
||||
}
|
||||
@@ -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";
|
||||
|
||||
19
src/lib/oneproxyRotator.ts
Normal file
19
src/lib/oneproxyRotator.ts
Normal 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
182
src/lib/oneproxySync.ts
Normal 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;
|
||||
}
|
||||
@@ -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 ============
|
||||
|
||||
14
src/shared/validation/oneproxySchemas.ts
Normal file
14
src/shared/validation/oneproxySchemas.ts
Normal 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(),
|
||||
});
|
||||
Reference in New Issue
Block a user