mirror of
https://github.com/Nezumi-2711/9router.git
synced 2026-09-22 20:00:47 +00:00
feat(proxy-pools): auto-rotate strategy for no-auth providers (#2409)
Add round-robin/random proxy pool rotation for no-auth free providers (e.g. OpenCode Free) to distribute load across all active pools and avoid per-IP rate limits. Rotation strategy is selectable per provider in NoAuthProxyCard and persisted to settings.providerStrategies.
This commit is contained in:
committed by
decolua
parent
f1f9d27061
commit
e1f3399b73
@@ -1,16 +1,22 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import Card from "./Card";
|
||||
import Select from "./Select";
|
||||
import Badge from "./Badge";
|
||||
|
||||
const NONE_PROXY_POOL_VALUE = "__none__";
|
||||
const STRATEGIES = [
|
||||
{ value: "none", label: "None (single pool)" },
|
||||
{ value: "round-robin", label: "Round-robin" },
|
||||
{ value: "random", label: "Random" },
|
||||
];
|
||||
|
||||
export default function NoAuthProxyCard({ providerId }) {
|
||||
const [proxyPools, setProxyPools] = useState([]);
|
||||
const [proxyPoolId, setProxyPoolId] = useState(NONE_PROXY_POOL_VALUE);
|
||||
const [rotateStrategy, setRotateStrategy] = useState("none");
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [savedFlash, setSavedFlash] = useState(false);
|
||||
|
||||
@@ -24,20 +30,22 @@ export default function NoAuthProxyCard({ providerId }) {
|
||||
setProxyPools(poolData.proxyPools || []);
|
||||
const override = (settingsData.providerStrategies || {})[providerId] || {};
|
||||
setProxyPoolId(override.proxyPoolId || NONE_PROXY_POOL_VALUE);
|
||||
setRotateStrategy(override.rotateStrategy || "none");
|
||||
}).catch(() => {});
|
||||
return () => { cancelled = true; };
|
||||
}, [providerId]);
|
||||
|
||||
const handleChange = async (newValue) => {
|
||||
setProxyPoolId(newValue);
|
||||
const save = useCallback(async (poolId, strategy) => {
|
||||
setSaving(true);
|
||||
try {
|
||||
const res = await fetch("/api/settings", { cache: "no-store" });
|
||||
const data = res.ok ? await res.json() : {};
|
||||
const current = data.providerStrategies || {};
|
||||
const override = { ...(current[providerId] || {}) };
|
||||
if (newValue === NONE_PROXY_POOL_VALUE) delete override.proxyPoolId;
|
||||
else override.proxyPoolId = newValue;
|
||||
if (poolId === NONE_PROXY_POOL_VALUE) delete override.proxyPoolId;
|
||||
else override.proxyPoolId = poolId;
|
||||
if (strategy === "none") delete override.rotateStrategy;
|
||||
else override.rotateStrategy = strategy;
|
||||
const updated = { ...current };
|
||||
if (Object.keys(override).length === 0) delete updated[providerId];
|
||||
else updated[providerId] = override;
|
||||
@@ -49,12 +57,25 @@ export default function NoAuthProxyCard({ providerId }) {
|
||||
setSavedFlash(true);
|
||||
setTimeout(() => setSavedFlash(false), 1500);
|
||||
} catch (e) {
|
||||
console.log("Save proxyPoolId error:", e);
|
||||
console.log("Save proxy config error:", e);
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
}, [providerId]);
|
||||
|
||||
const handlePoolChange = (newPoolId) => {
|
||||
setProxyPoolId(newPoolId);
|
||||
save(newPoolId, rotateStrategy);
|
||||
};
|
||||
|
||||
const handleStrategyChange = (newStrategy) => {
|
||||
setRotateStrategy(newStrategy);
|
||||
save(proxyPoolId, newStrategy);
|
||||
};
|
||||
|
||||
const canRotate = proxyPools.length >= 2;
|
||||
const isRotation = rotateStrategy !== "none";
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
@@ -67,16 +88,43 @@ export default function NoAuthProxyCard({ providerId }) {
|
||||
</div>
|
||||
{savedFlash && <Badge variant="success" size="sm">Saved</Badge>}
|
||||
</div>
|
||||
|
||||
<Select
|
||||
label="Proxy Pool"
|
||||
value={proxyPoolId}
|
||||
onChange={(e) => handleChange(e.target.value)}
|
||||
disabled={saving}
|
||||
onChange={(e) => handlePoolChange(e.target.value)}
|
||||
disabled={saving || isRotation}
|
||||
options={[
|
||||
{ value: NONE_PROXY_POOL_VALUE, label: "None (direct)" },
|
||||
...proxyPools.map((pool) => ({ value: pool.id, label: pool.name })),
|
||||
]}
|
||||
hint={isRotation ? "Pool selector is ignored when rotation is active — all active pools are used." : undefined}
|
||||
/>
|
||||
|
||||
<div className="flex flex-col gap-2 mt-4">
|
||||
<label className="text-sm font-medium text-text-main">Rotation Strategy</label>
|
||||
<select
|
||||
value={rotateStrategy}
|
||||
onChange={(e) => handleStrategyChange(e.target.value)}
|
||||
disabled={saving}
|
||||
className="py-2 px-3 text-sm text-text-main bg-white dark:bg-white/5 border border-black/10 dark:border-white/10 rounded-md focus:ring-1 focus:ring-primary/30 focus:border-primary/50 focus:outline-none transition-all disabled:opacity-50"
|
||||
>
|
||||
{STRATEGIES.map((s) => (
|
||||
<option key={s.value} value={s.value} disabled={s.value !== "none" && !canRotate}>
|
||||
{s.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<p className="text-xs text-text-muted">
|
||||
{!canRotate
|
||||
? `Need at least 2 active proxy pools for rotation.`
|
||||
: isRotation
|
||||
? rotateStrategy === "round-robin"
|
||||
? `Rotating through all ${proxyPools.length} active pools in order. State is in-memory (resets on restart).`
|
||||
: `Picking a random pool from ${proxyPools.length} active pools each request.`
|
||||
: `Uses the selected pool above. Set to Round-robin or Random to rotate across all active pools.`}
|
||||
</p>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user