HouYunFei Codex commited on
Commit
b13761a
·
1 Parent(s): 0b07f5f

fix merged proxy and settings regressions

Browse files

Co-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 = (proxy_config.get().get("url") or "").strip()
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
- proxy_settings = ProxySettingsStore()
 
 
 
 
 
 
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 { Link2, LoaderCircle, Save } from "lucide-react";
 
 
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) => setProxy(event.target.value)}
 
 
 
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
- <div className="flex justify-end">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 />