HouYunFei Codex commited on
Commit ·
b13761a
1
Parent(s): 0b07f5f
fix merged proxy and settings regressions
Browse filesCo-Authored-By: Codex <noreply@openai.com>
services/api.py
CHANGED
|
@@ -13,6 +13,7 @@ from services.account_service import account_service
|
|
| 13 |
from services.chatgpt_service import ChatGPTService
|
| 14 |
from services.config import config
|
| 15 |
from services.cpa_service import cpa_config, cpa_import_service, list_remote_files
|
|
|
|
| 16 |
from services.sub2api_service import (
|
| 17 |
list_remote_accounts as sub2api_list_remote_accounts,
|
| 18 |
list_remote_groups as sub2api_list_remote_groups,
|
|
@@ -593,20 +594,6 @@ def create_app() -> FastAPI:
|
|
| 593 |
|
| 594 |
# ── Upstream proxy endpoints ─────────────────────────────────────
|
| 595 |
|
| 596 |
-
@router.get("/api/proxy")
|
| 597 |
-
async def get_proxy(authorization: str | None = Header(default=None)):
|
| 598 |
-
require_auth_key(authorization)
|
| 599 |
-
return {"proxy": proxy_config.get_public()}
|
| 600 |
-
|
| 601 |
-
@router.post("/api/proxy")
|
| 602 |
-
async def update_proxy(body: ProxyUpdateRequest, authorization: str | None = Header(default=None)):
|
| 603 |
-
require_auth_key(authorization)
|
| 604 |
-
try:
|
| 605 |
-
proxy_config.update(enabled=body.enabled, url=body.url)
|
| 606 |
-
except ValueError as exc:
|
| 607 |
-
raise HTTPException(status_code=400, detail={"error": str(exc)}) from exc
|
| 608 |
-
return {"proxy": proxy_config.get_public()}
|
| 609 |
-
|
| 610 |
@router.post("/api/proxy/test")
|
| 611 |
async def test_proxy_endpoint(
|
| 612 |
body: ProxyTestRequest,
|
|
@@ -615,7 +602,7 @@ def create_app() -> FastAPI:
|
|
| 615 |
require_auth_key(authorization)
|
| 616 |
candidate = (body.url or "").strip()
|
| 617 |
if not candidate:
|
| 618 |
-
candidate =
|
| 619 |
if not candidate:
|
| 620 |
raise HTTPException(status_code=400, detail={"error": "proxy url is required"})
|
| 621 |
result = await run_in_threadpool(test_proxy, candidate)
|
|
|
|
| 13 |
from services.chatgpt_service import ChatGPTService
|
| 14 |
from services.config import config
|
| 15 |
from services.cpa_service import cpa_config, cpa_import_service, list_remote_files
|
| 16 |
+
from services.proxy_service import test_proxy
|
| 17 |
from services.sub2api_service import (
|
| 18 |
list_remote_accounts as sub2api_list_remote_accounts,
|
| 19 |
list_remote_groups as sub2api_list_remote_groups,
|
|
|
|
| 594 |
|
| 595 |
# ── Upstream proxy endpoints ─────────────────────────────────────
|
| 596 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 597 |
@router.post("/api/proxy/test")
|
| 598 |
async def test_proxy_endpoint(
|
| 599 |
body: ProxyTestRequest,
|
|
|
|
| 602 |
require_auth_key(authorization)
|
| 603 |
candidate = (body.url or "").strip()
|
| 604 |
if not candidate:
|
| 605 |
+
candidate = config.get_proxy_settings()
|
| 606 |
if not candidate:
|
| 607 |
raise HTTPException(status_code=400, detail={"error": "proxy url is required"})
|
| 608 |
result = await run_in_threadpool(test_proxy, candidate)
|
services/proxy_service.py
CHANGED
|
@@ -2,11 +2,15 @@
|
|
| 2 |
|
| 3 |
from __future__ import annotations
|
| 4 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5 |
from services.config import config
|
| 6 |
|
| 7 |
|
| 8 |
class ProxySettingsStore:
|
| 9 |
-
|
| 10 |
def build_session_kwargs(self, **session_kwargs) -> dict[str, object]:
|
| 11 |
proxy = config.get_proxy_settings()
|
| 12 |
if proxy:
|
|
@@ -14,19 +18,22 @@ class ProxySettingsStore:
|
|
| 14 |
return session_kwargs
|
| 15 |
|
| 16 |
|
| 17 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
|
| 19 |
|
| 20 |
def test_proxy(url: str, *, timeout: float = 15.0) -> dict:
|
| 21 |
-
"""Probe chatgpt.com through the given proxy URL. Returns {ok, status, latency_ms, error}."""
|
| 22 |
candidate = _clean(url)
|
| 23 |
if not candidate:
|
| 24 |
return {"ok": False, "status": 0, "latency_ms": 0, "error": "proxy url is required"}
|
| 25 |
if not _is_valid_proxy_url(candidate):
|
| 26 |
return {"ok": False, "status": 0, "latency_ms": 0, "error": "invalid proxy url"}
|
| 27 |
-
|
| 28 |
-
session = Session(impersonate="edge101", verify=True)
|
| 29 |
-
session.proxies.update({"http": candidate, "https": candidate})
|
| 30 |
started = time.perf_counter()
|
| 31 |
try:
|
| 32 |
response = session.get(
|
|
@@ -51,3 +58,6 @@ def test_proxy(url: str, *, timeout: float = 15.0) -> dict:
|
|
| 51 |
}
|
| 52 |
finally:
|
| 53 |
session.close()
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
|
| 3 |
from __future__ import annotations
|
| 4 |
|
| 5 |
+
import time
|
| 6 |
+
from urllib.parse import urlparse
|
| 7 |
+
|
| 8 |
+
from curl_cffi.requests import Session
|
| 9 |
+
|
| 10 |
from services.config import config
|
| 11 |
|
| 12 |
|
| 13 |
class ProxySettingsStore:
|
|
|
|
| 14 |
def build_session_kwargs(self, **session_kwargs) -> dict[str, object]:
|
| 15 |
proxy = config.get_proxy_settings()
|
| 16 |
if proxy:
|
|
|
|
| 18 |
return session_kwargs
|
| 19 |
|
| 20 |
|
| 21 |
+
def _clean(value: object) -> str:
|
| 22 |
+
return str(value or "").strip()
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
def _is_valid_proxy_url(url: str) -> bool:
|
| 26 |
+
parsed = urlparse(url)
|
| 27 |
+
return parsed.scheme in {"http", "https", "socks5", "socks5h"} and bool(parsed.netloc)
|
| 28 |
|
| 29 |
|
| 30 |
def test_proxy(url: str, *, timeout: float = 15.0) -> dict:
|
|
|
|
| 31 |
candidate = _clean(url)
|
| 32 |
if not candidate:
|
| 33 |
return {"ok": False, "status": 0, "latency_ms": 0, "error": "proxy url is required"}
|
| 34 |
if not _is_valid_proxy_url(candidate):
|
| 35 |
return {"ok": False, "status": 0, "latency_ms": 0, "error": "invalid proxy url"}
|
| 36 |
+
session = Session(impersonate="edge101", verify=True, proxy=candidate)
|
|
|
|
|
|
|
| 37 |
started = time.perf_counter()
|
| 38 |
try:
|
| 39 |
response = session.get(
|
|
|
|
| 58 |
}
|
| 59 |
finally:
|
| 60 |
session.close()
|
| 61 |
+
|
| 62 |
+
proxy_settings = ProxySettingsStore()
|
| 63 |
+
|
web/src/app/settings/components/proxy-settings-card.tsx
CHANGED
|
@@ -1,15 +1,20 @@
|
|
| 1 |
"use client";
|
| 2 |
|
| 3 |
-
import {
|
|
|
|
|
|
|
| 4 |
|
| 5 |
import { Badge } from "@/components/ui/badge";
|
| 6 |
import { Button } from "@/components/ui/button";
|
| 7 |
import { Card, CardContent } from "@/components/ui/card";
|
| 8 |
import { Input } from "@/components/ui/input";
|
|
|
|
| 9 |
|
| 10 |
import { useSettingsStore } from "../store";
|
| 11 |
|
| 12 |
export function ProxySettingsCard() {
|
|
|
|
|
|
|
| 13 |
const config = useSettingsStore((state) => state.config);
|
| 14 |
const isLoadingConfig = useSettingsStore((state) => state.isLoadingConfig);
|
| 15 |
const isSavingConfig = useSettingsStore((state) => state.isSavingConfig);
|
|
@@ -18,6 +23,29 @@ export function ProxySettingsCard() {
|
|
| 18 |
|
| 19 |
const proxy = config?.proxy ?? "";
|
| 20 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
return (
|
| 22 |
<Card className="rounded-2xl border-white/80 bg-white/90 shadow-sm">
|
| 23 |
<CardContent className="space-y-6 p-6">
|
|
@@ -46,7 +74,10 @@ export function ProxySettingsCard() {
|
|
| 46 |
<label className="text-sm font-medium text-stone-700">代理地址</label>
|
| 47 |
<Input
|
| 48 |
value={proxy}
|
| 49 |
-
onChange={(event) =>
|
|
|
|
|
|
|
|
|
|
| 50 |
placeholder="http://user:pass@127.0.0.1:7890"
|
| 51 |
className="h-11 rounded-xl border-stone-200 bg-white"
|
| 52 |
/>
|
|
@@ -55,7 +86,30 @@ export function ProxySettingsCard() {
|
|
| 55 |
</p>
|
| 56 |
</div>
|
| 57 |
|
| 58 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 59 |
<Button
|
| 60 |
className="h-10 rounded-xl bg-stone-950 px-5 text-white hover:bg-stone-800"
|
| 61 |
onClick={() => void saveConfig()}
|
|
|
|
| 1 |
"use client";
|
| 2 |
|
| 3 |
+
import { useState } from "react";
|
| 4 |
+
import { Link2, LoaderCircle, PlugZap, Save } from "lucide-react";
|
| 5 |
+
import { toast } from "sonner";
|
| 6 |
|
| 7 |
import { Badge } from "@/components/ui/badge";
|
| 8 |
import { Button } from "@/components/ui/button";
|
| 9 |
import { Card, CardContent } from "@/components/ui/card";
|
| 10 |
import { Input } from "@/components/ui/input";
|
| 11 |
+
import { testProxy, type ProxyTestResult } from "@/lib/api";
|
| 12 |
|
| 13 |
import { useSettingsStore } from "../store";
|
| 14 |
|
| 15 |
export function ProxySettingsCard() {
|
| 16 |
+
const [isTesting, setIsTesting] = useState(false);
|
| 17 |
+
const [testResult, setTestResult] = useState<ProxyTestResult | null>(null);
|
| 18 |
const config = useSettingsStore((state) => state.config);
|
| 19 |
const isLoadingConfig = useSettingsStore((state) => state.isLoadingConfig);
|
| 20 |
const isSavingConfig = useSettingsStore((state) => state.isSavingConfig);
|
|
|
|
| 23 |
|
| 24 |
const proxy = config?.proxy ?? "";
|
| 25 |
|
| 26 |
+
const handleTest = async () => {
|
| 27 |
+
const candidate = proxy.trim();
|
| 28 |
+
if (!candidate) {
|
| 29 |
+
toast.error("请先填写代理地址");
|
| 30 |
+
return;
|
| 31 |
+
}
|
| 32 |
+
setIsTesting(true);
|
| 33 |
+
setTestResult(null);
|
| 34 |
+
try {
|
| 35 |
+
const data = await testProxy(candidate);
|
| 36 |
+
setTestResult(data.result);
|
| 37 |
+
if (data.result.ok) {
|
| 38 |
+
toast.success(`代理可用(${data.result.latency_ms} ms,HTTP ${data.result.status})`);
|
| 39 |
+
} else {
|
| 40 |
+
toast.error(`代理不可用:${data.result.error ?? "未知错误"}`);
|
| 41 |
+
}
|
| 42 |
+
} catch (error) {
|
| 43 |
+
toast.error(error instanceof Error ? error.message : "测试代理失败");
|
| 44 |
+
} finally {
|
| 45 |
+
setIsTesting(false);
|
| 46 |
+
}
|
| 47 |
+
};
|
| 48 |
+
|
| 49 |
return (
|
| 50 |
<Card className="rounded-2xl border-white/80 bg-white/90 shadow-sm">
|
| 51 |
<CardContent className="space-y-6 p-6">
|
|
|
|
| 74 |
<label className="text-sm font-medium text-stone-700">代理地址</label>
|
| 75 |
<Input
|
| 76 |
value={proxy}
|
| 77 |
+
onChange={(event) => {
|
| 78 |
+
setProxy(event.target.value);
|
| 79 |
+
setTestResult(null);
|
| 80 |
+
}}
|
| 81 |
placeholder="http://user:pass@127.0.0.1:7890"
|
| 82 |
className="h-11 rounded-xl border-stone-200 bg-white"
|
| 83 |
/>
|
|
|
|
| 86 |
</p>
|
| 87 |
</div>
|
| 88 |
|
| 89 |
+
{testResult ? (
|
| 90 |
+
<div
|
| 91 |
+
className={`rounded-xl border px-4 py-3 text-sm leading-6 ${
|
| 92 |
+
testResult.ok
|
| 93 |
+
? "border-emerald-200 bg-emerald-50 text-emerald-800"
|
| 94 |
+
: "border-rose-200 bg-rose-50 text-rose-800"
|
| 95 |
+
}`}
|
| 96 |
+
>
|
| 97 |
+
{testResult.ok
|
| 98 |
+
? `代理可用:HTTP ${testResult.status},用时 ${testResult.latency_ms} ms`
|
| 99 |
+
: `代理不可用:${testResult.error ?? "未知错误"}(用时 ${testResult.latency_ms} ms)`}
|
| 100 |
+
</div>
|
| 101 |
+
) : null}
|
| 102 |
+
|
| 103 |
+
<div className="flex justify-end gap-2">
|
| 104 |
+
<Button
|
| 105 |
+
variant="outline"
|
| 106 |
+
className="h-10 rounded-xl border-stone-200 bg-white px-5 text-stone-700"
|
| 107 |
+
onClick={() => void handleTest()}
|
| 108 |
+
disabled={isTesting || isLoadingConfig}
|
| 109 |
+
>
|
| 110 |
+
{isTesting ? <LoaderCircle className="size-4 animate-spin" /> : <PlugZap className="size-4" />}
|
| 111 |
+
测试代理
|
| 112 |
+
</Button>
|
| 113 |
<Button
|
| 114 |
className="h-10 rounded-xl bg-stone-950 px-5 text-white hover:bg-stone-800"
|
| 115 |
onClick={() => void saveConfig()}
|
web/src/app/settings/page.tsx
CHANGED
|
@@ -7,6 +7,7 @@ import { CPAPoolsCard } from "./components/cpa-pools-card";
|
|
| 7 |
import { ImportBrowserDialog } from "./components/import-browser-dialog";
|
| 8 |
import { ProxySettingsCard } from "./components/proxy-settings-card";
|
| 9 |
import { SettingsHeader } from "./components/settings-header";
|
|
|
|
| 10 |
import { useSettingsStore } from "./store";
|
| 11 |
|
| 12 |
function SettingsDataController() {
|
|
@@ -49,6 +50,7 @@ export default function SettingsPage() {
|
|
| 49 |
<section className="space-y-6">
|
| 50 |
<ProxySettingsCard />
|
| 51 |
<CPAPoolsCard />
|
|
|
|
| 52 |
</section>
|
| 53 |
<CPAPoolDialog />
|
| 54 |
<ImportBrowserDialog />
|
|
|
|
| 7 |
import { ImportBrowserDialog } from "./components/import-browser-dialog";
|
| 8 |
import { ProxySettingsCard } from "./components/proxy-settings-card";
|
| 9 |
import { SettingsHeader } from "./components/settings-header";
|
| 10 |
+
import { Sub2APIConnections } from "./components/sub2api-connections";
|
| 11 |
import { useSettingsStore } from "./store";
|
| 12 |
|
| 13 |
function SettingsDataController() {
|
|
|
|
| 50 |
<section className="space-y-6">
|
| 51 |
<ProxySettingsCard />
|
| 52 |
<CPAPoolsCard />
|
| 53 |
+
<Sub2APIConnections />
|
| 54 |
</section>
|
| 55 |
<CPAPoolDialog />
|
| 56 |
<ImportBrowserDialog />
|