优化代理配置

This commit is contained in:
yml2213
2026-06-22 13:59:03 +08:00
parent 63c1ffa046
commit b08f0a647d
10 changed files with 577 additions and 197 deletions
+1 -1
View File
@@ -115,7 +115,7 @@ async def ws_login_logs(websocket: WebSocket, batch_id: str):
await websocket.accept()
log_queue = asyncio.Queue()
loop = asyncio.get_event_loop()
loop = asyncio.get_running_loop()
# 查找已运行的批次,或等待新批次
# 简化:直接把 log_queue 注册到全局,前端创建批次后连 ws
+221 -76
View File
@@ -1,17 +1,22 @@
"""代理 & 白名单配置路由"""
"""代理 & 白名单配置路由 + WebSocket 实时日志"""
import re
import time
from fastapi import APIRouter, Depends, HTTPException
import asyncio
import threading
import uuid
from fastapi import APIRouter, Depends, HTTPException, WebSocket, WebSocketDisconnect
from sqlalchemy.orm import Session
from ..database import get_db
from ..models import User, ProxyConfig as ProxyConfigModel, AuditLog
from ..schemas import ProxyConfigOut, ProxyConfigUpdate, MessageResponse
from ..schemas import ProxyConfigOut, ProxyConfigUpdate
from ..deps import require_permission
from core.douyu.proxy import resolve_working_proxy, verify_proxy_url, parse_proxy_response
router = APIRouter(prefix="/api/proxy", tags=["代理与白名单"])
# 运行中的测试: test_id -> {log_queue, loop, result}
_active_tests: dict[str, dict] = {}
def _get_or_create(db: Session) -> ProxyConfigModel:
cfg = db.query(ProxyConfigModel).first()
@@ -54,93 +59,233 @@ def update_proxy_config(
return cfg
# ---- WebSocket 日志推送 ----
@router.websocket("/ws/test/{test_id}")
async def ws_test_logs(websocket: WebSocket, test_id: str):
"""WebSocket 推送代理/白名单测试实时日志。"""
await websocket.accept()
test = _active_tests.get(test_id)
if not test:
await websocket.send_json({"level": "error", "message": "测试任务不存在"})
await websocket.close()
return
log_queue: asyncio.Queue = test["log_queue"]
try:
while True:
try:
msg = await asyncio.wait_for(log_queue.get(), timeout=30)
await websocket.send_json(msg)
# 收到 result 消息表示测试结束
if msg.get("level") == "result":
await asyncio.sleep(0.1)
break
except asyncio.TimeoutError:
await websocket.send_json({"level": "heartbeat", "message": ""})
except WebSocketDisconnect:
pass
finally:
_active_tests.pop(test_id, None)
# ---- 异步测试执行 ----
def _run_proxy_test(
cfg: ProxyConfigModel,
log_queue: asyncio.Queue,
loop: asyncio.AbstractEventLoop,
):
"""在线程中执行代理测试。"""
def push(level, message):
asyncio.run_coroutine_threadsafe(
log_queue.put({"level": level, "message": message}),
loop,
)
try:
if not cfg.enabled:
push("error", "代理未启用")
push("result", "")
return
# 静态代理
if cfg.http or cfg.https:
proxy_url = cfg.http or cfg.https
push("info", f"验证静态代理: {proxy_url}")
ok, msg = verify_proxy_url(proxy_url)
push("success" if ok else "error", msg)
push("result", "")
return
# API代理
if cfg.api_url:
whitelist_uid = cfg.whitelist_uid if cfg.whitelist_enabled else ""
whitelist_ukey = cfg.whitelist_ukey if cfg.whitelist_enabled else ""
proxy_url, msg = resolve_working_proxy(
api_url=cfg.api_url,
whitelist_uid=whitelist_uid,
whitelist_ukey=whitelist_ukey,
max_attempts=3,
log_func=push,
)
if proxy_url:
push("success", f"代理可用: {proxy_url}")
else:
push("error", msg)
push("result", "")
return
push("error", "未配置代理地址或API")
push("result", "")
except Exception as e:
push("error", f"测试异常: {e}")
push("result", "")
def _run_whitelist_test(
cfg: ProxyConfigModel,
log_queue: asyncio.Queue,
loop: asyncio.AbstractEventLoop,
):
"""在线程中执行白名单测试。"""
import requests as req_lib
import re
from core.douyu.whitelist import WhitelistManager, get_exit_ip_via_proxy
def push(level, message):
asyncio.run_coroutine_threadsafe(
log_queue.put({"level": level, "message": message}),
loop,
)
try:
if not cfg.whitelist_enabled:
push("error", "白名单未启用")
push("result", "")
return
if not cfg.whitelist_uid or not cfg.whitelist_ukey:
push("error", "未配置白名单UID/UKEY")
push("result", "")
return
manager = WhitelistManager(cfg.whitelist_uid, cfg.whitelist_ukey)
# 1. 测试API连接
push("info", "测试白名单API连接...")
ok, msg = manager.test_connection()
push("info" if ok else "error", f"白名单API: {msg}")
if not ok:
push("result", "")
return
# 2. 获取本机公网IP(白名单需要的是本机IP,不是代理出口IP)
push("info", "正在获取本机公网IP...")
# 优先:从代理API响应中提取(代理API返回"请先添加白名单:xxx"时,xxx就是本机IP
local_ip = None
if cfg.api_url:
try:
resp = req_lib.get(cfg.api_url, timeout=10)
text = resp.text.strip()
push("info", f"代理API响应: {text[:80]}")
_, whitelist_ip = parse_proxy_response(text)
if whitelist_ip:
local_ip = whitelist_ip
push("info", f"从代理API获取到本机IP: {local_ip}")
except Exception as e:
push("warning", f"代理API请求失败: {e}")
# 备用:直接访问IP检测服务获取本机公网IP
if not local_ip:
push("info", "通过IP检测服务获取本机公网IP...")
for url in [
'https://qifu-api.baidubce.com/ip/local/geo/v1/district',
'https://myip.ipip.net',
'https://4.ipw.cn',
]:
try:
resp = req_lib.get(url, timeout=6, headers={"User-Agent": "Mozilla/5.0"})
match = re.search(r'(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})', resp.text)
if match:
local_ip = match.group(1)
break
except Exception:
continue
if not local_ip:
push("error", "无法获取本机公网IP")
push("result", "")
return
push("info", f"本机公网IP: {local_ip}")
# 3. 检查并同步白名单
records = manager.get_whitelist_json()
in_list = any(r.get('IP') == local_ip for r in records)
push("info", f"白名单共 {len(records)} 条记录")
if in_list:
record = next((r for r in records if r.get('IP') == local_ip), {})
memo = record.get('MEMO', '')
if memo == manager.memo:
push("success", f"本机IP {local_ip} 已在白名单中 (备注正确)")
else:
push("warning", f'本机IP {local_ip} 备注不匹配 (当前: "{memo}"),更新中...')
sync_ok, sync_msg = manager.sync_ip(local_ip)
push("success" if sync_ok else "error", f"白名单更新: {sync_msg}")
else:
push("info", f"正在将 {local_ip} 添加到白名单...")
sync_ok, sync_msg = manager.sync_ip(local_ip)
push("success" if sync_ok else "error", f"白名单同步: {sync_msg}")
push("result", "")
except Exception as e:
push("error", f"测试异常: {e}")
push("result", "")
# ---- API 端点 ----
@router.post("/test")
def test_proxy(
async def test_proxy(
db: Session = Depends(get_db),
current: User = Depends(require_permission("proxy:manage")),
):
"""测试代理连通性"""
import requests as req_lib
"""启动代理测试(异步执行,通过 WebSocket 推送日志)"""
cfg = _get_or_create(db)
if not cfg.enabled:
return {"success": False, "message": "代理未启用"}
proxy_url = cfg.http or cfg.https
if cfg.api_url and not proxy_url:
try:
resp = req_lib.get(cfg.api_url, timeout=10)
match = re.search(r'(\d+\.\d+\.\d+\.\d+):(\d+)', resp.text)
if match:
proxy_url = f"http://{match.group(1)}:{match.group(2)}"
except Exception as e:
return {"success": False, "message": f"代理API请求失败: {e}"}
test_id = uuid.uuid4().hex[:12]
log_queue = asyncio.Queue()
loop = asyncio.get_running_loop()
if not proxy_url:
return {"success": False, "message": "无可用代理地址"}
_active_tests[test_id] = {"log_queue": log_queue, "loop": loop}
try:
resp = req_lib.get(
"https://qifu-api.baidubce.com/ip/local/geo/v1/district",
proxies={"http": proxy_url, "https": proxy_url},
timeout=(4, 6),
headers={"User-Agent": "Mozilla/5.0"},
)
resp.raise_for_status()
return {"success": True, "message": f"代理可用,响应: {resp.text[:100]}"}
except Exception as e:
return {"success": False, "message": f"代理验证失败: {e}"}
thread = threading.Thread(target=_run_proxy_test, args=(cfg, log_queue, loop), daemon=True)
thread.start()
return {"test_id": test_id, "success": True}
@router.post("/whitelist/test")
def test_whitelist(
async def test_whitelist(
db: Session = Depends(get_db),
current: User = Depends(require_permission("whitelist:test")),
):
"""测试白名单连接并自动同步出口IP"""
from core.douyu.whitelist import WhitelistManager, get_exit_ip_via_proxy
import requests as req_lib
"""启动白名单测试(异步执行,通过 WebSocket 推送日志)"""
cfg = _get_or_create(db)
if not cfg.whitelist_enabled:
return {"success": False, "message": "白名单未启用"}
if not cfg.whitelist_uid or not cfg.whitelist_ukey:
return {"success": False, "message": "未配置白名单UID/UKEY"}
manager = WhitelistManager(cfg.whitelist_uid, cfg.whitelist_ukey)
test_id = uuid.uuid4().hex[:12]
log_queue = asyncio.Queue()
loop = asyncio.get_running_loop()
# 测试API连接
ok, msg = manager.test_connection()
if not ok:
return {"success": False, "message": msg}
_active_tests[test_id] = {"log_queue": log_queue, "loop": loop}
# 获取出口IP
proxy_url = cfg.http or cfg.https
if cfg.api_url and not proxy_url:
try:
resp = req_lib.get(cfg.api_url, timeout=10)
match = re.search(r'(\d+\.\d+\.\d+\.\d+):(\d+)', resp.text)
if match:
proxy_url = f"http://{match.group(1)}:{match.group(2)}"
except Exception:
pass
thread = threading.Thread(target=_run_whitelist_test, args=(cfg, log_queue, loop), daemon=True)
thread.start()
exit_ip = None
if proxy_url:
exit_ip = get_exit_ip_via_proxy(proxy_url)
if not exit_ip:
try:
resp = req_lib.get("https://4.ipw.cn", timeout=6, headers={"User-Agent": "Mozilla/5.0"})
match = re.search(r'(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})', resp.text)
if match:
exit_ip = match.group(1)
except Exception:
pass
if not exit_ip:
return {"success": False, "message": "API连接正常但无法获取出口IP"}
# 同步白名单
sync_ok, sync_msg = manager.sync_ip(exit_ip)
if sync_ok:
return {"success": True, "message": f"出口IP {exit_ip} 已同步: {sync_msg}"}
return {"success": False, "message": f"同步失败: {sync_msg}"}
return {"test_id": test_id, "success": True}
+53 -15
View File
@@ -1,8 +1,7 @@
"""登录服务:复用 douyu/ 核心模块,在线程池中执行登录并推送日志。"""
"""登录服务:复用 core/ 核心模块,在线程池中执行登录并推送日志。"""
import asyncio
import threading
import time
import uuid
from datetime import datetime
from typing import Optional
@@ -11,6 +10,7 @@ from sqlalchemy.orm import Session
from core.douyu import DouyuLogin
from core.models import Account, ProxyConfig as DouyuProxyConfig
from core.douyu.proxy import resolve_working_proxy
from ..models import Account as AccountModel, LoginTask, ProxyConfig as ProxyConfigModel
from ..permissions import has_permission
@@ -50,6 +50,42 @@ class LoginBatchRunner:
self.loop,
)
def _resolve_proxy(self) -> tuple[Optional[dict], str]:
"""
解析代理配置,返回 (proxy_dict, message)。
- 静态代理:直接返回 dict
- API代理:调用 resolve_working_proxy 预检,自动同步白名单
- 无代理:返回 (None, '')
"""
if not self.proxy_config or not self.proxy_config.enabled:
return None, ''
# 静态代理
if self.proxy_config.http or self.proxy_config.https:
proxy_url = self.proxy_config.http or self.proxy_config.https
return {'http': proxy_url, 'https': proxy_url}, f'使用静态代理: {proxy_url}'
# API代理:预检获取可用代理
if self.proxy_config.api_url:
whitelist_uid = ''
whitelist_ukey = ''
if self.proxy_config.whitelist_enabled:
whitelist_uid = self.proxy_config.whitelist_uid or ''
whitelist_ukey = self.proxy_config.whitelist_ukey or ''
proxy_url, msg = resolve_working_proxy(
api_url=self.proxy_config.api_url,
whitelist_uid=whitelist_uid,
whitelist_ukey=whitelist_ukey,
log_func=self._push_log,
)
if proxy_url:
return {'http': proxy_url, 'https': proxy_url}, msg
return None, msg
return None, ''
def run(self):
"""在线程中执行批量登录。"""
batch_id = self.batch_id
@@ -78,17 +114,20 @@ class LoginBatchRunner:
self.db.commit()
# 构建代理配置
proxy_url = None
proxy_api_url = None
if self.proxy_config and self.proxy_config.enabled:
if self.proxy_config.http or self.proxy_config.https:
proxy_url = {
'http': self.proxy_config.http or self.proxy_config.https,
'https': self.proxy_config.https or self.proxy_config.http,
}
elif self.proxy_config.api_url:
proxy_api_url = self.proxy_config.api_url
# 代理预检
proxy_dict, proxy_msg = self._resolve_proxy()
if proxy_msg:
self._push_log("info", proxy_msg)
# 如果启用了代理但预检失败,终止任务
if self.proxy_config and self.proxy_config.enabled and not proxy_dict:
self._push_log("error", f"代理不可用,任务终止: {proxy_msg}")
for task, _ in tasks:
task.status = "error"
task.message = f"代理不可用: {proxy_msg}"
task.finished_at = datetime.utcnow()
self.db.commit()
return
for i, (task, acc) in enumerate(tasks):
if self._stop.is_set():
@@ -112,8 +151,7 @@ class LoginBatchRunner:
loginer = DouyuLogin(
account,
proxy=proxy_url,
proxy_api_url=proxy_api_url,
proxy=proxy_dict,
max_geetest_retries=self.max_geetest_retries,
)
result = loginer.login()
+129 -50
View File
@@ -1,26 +1,64 @@
import { useEffect, useState } from 'react';
import { Form, Input, Switch, Button, Card, message, Divider, Space } from 'antd';
import { useEffect, useState, useRef } from 'react';
import { Form, Input, Switch, Button, Card, message, Row, Col } from 'antd';
import { proxyApi } from '../api/modules';
const WS_BASE = `ws://${window.location.hostname}:8000`;
export default function ProxyPage() {
const [form] = Form.useForm();
const [loading, setLoading] = useState(false);
const [testing, setTesting] = useState(false);
const [testingWl, setTestingWl] = useState(false);
const [configLoaded, setConfigLoaded] = useState(false);
const [logs, setLogs] = useState<{ level: string; message: string }[]>([]);
const wsRef = useRef<WebSocket | null>(null);
const loadConfig = async () => {
try {
const data = await proxyApi.get();
form.setFieldsValue(data);
form.setFieldsValue({
enabled: data.enabled ?? false,
api_url: data.api_url ?? '',
http: data.http ?? '',
https: data.https ?? '',
whitelist_enabled: data.whitelist_enabled ?? false,
whitelist_uid: data.whitelist_uid ?? '',
whitelist_ukey: data.whitelist_ukey ?? '',
});
} catch (e: any) {
message.error(e.message);
} finally {
setConfigLoaded(true);
}
};
useEffect(() => {
loadConfig();
return () => {
wsRef.current?.close();
};
}, []);
const appendLog = (level: string, msg: string) => {
setLogs((prev) => [...prev, { level, message: msg }]);
};
const connectWs = (testId: string) => {
wsRef.current?.close();
setLogs([]);
const ws = new WebSocket(`${WS_BASE}/api/proxy/ws/test/${testId}`);
wsRef.current = ws;
ws.onmessage = (event) => {
const msg = JSON.parse(event.data);
if (msg.level === 'heartbeat') return;
if (msg.level === 'result') return;
appendLog(msg.level, msg.message);
};
ws.onclose = () => {
wsRef.current = null;
};
};
const handleSave = async () => {
setLoading(true);
try {
@@ -36,74 +74,115 @@ export default function ProxyPage() {
const handleTestProxy = async () => {
setTesting(true);
setLogs([]);
try {
const result = await proxyApi.test();
if (result.success) {
message.success(result.message);
} else {
message.warning(result.message);
}
if (result.test_id) connectWs(result.test_id);
} catch (e: any) {
message.error(e.message);
} finally {
setTesting(false);
}
};
const handleTestWhitelist = async () => {
setTestingWl(true);
setLogs([]);
try {
const result = await proxyApi.testWhitelist();
if (result.success) {
message.success(result.message);
} else {
message.warning(result.message);
}
if (result.test_id) connectWs(result.test_id);
} catch (e: any) {
message.error(e.message);
} finally {
setTestingWl(false);
}
};
// WebSocket 关闭后恢复按钮
useEffect(() => {
if (!wsRef.current) {
setTesting(false);
setTestingWl(false);
}
}, [logs.length === 0]);
const logColors: Record<string, string> = {
error: '#ff4d4f',
success: '#52c41a',
warning: '#faad14',
info: '#333',
};
return (
<div>
<h2></h2>
<Form form={form} layout="vertical" style={{ maxWidth: 600 }}>
<Card title="代理设置" size="small" style={{ marginBottom: 16 }}>
<Form.Item name="enabled" label="启用代理" valuePropName="checked">
<Switch />
</Form.Item>
<Form.Item name="api_url" label="代理API地址">
<Input placeholder="http://op.xiequ.cn/...?act=get" />
</Form.Item>
<Form.Item name="http" label="静态HTTP代理">
<Input placeholder="http://ip:port" />
</Form.Item>
<Form.Item name="https" label="静态HTTPS代理">
<Input placeholder="http://ip:port" />
</Form.Item>
<Button onClick={handleTestProxy} loading={testing}></Button>
</Card>
<div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 16 }}>
<h2 style={{ margin: 0 }}></h2>
<Button type="primary" onClick={handleSave} loading={loading}></Button>
</div>
<Card title="白名单管理" size="small">
<Form.Item name="whitelist_enabled" label="启用白名单自动管理" valuePropName="checked">
<Switch />
</Form.Item>
<Form.Item name="whitelist_uid" label="协固UID">
<Input placeholder="如: 99769" />
</Form.Item>
<Form.Item name="whitelist_ukey" label="协固UKEY">
<Input placeholder="如: C99371082B965B70F46DCAA87A04618B" />
</Form.Item>
<Button onClick={handleTestWhitelist} loading={testingWl}></Button>
</Card>
<Divider />
<Space>
<Button type="primary" onClick={handleSave} loading={loading}></Button>
</Space>
<Form
form={form}
layout="vertical"
disabled={!configLoaded}
initialValues={{
enabled: false,
whitelist_enabled: false,
api_url: '',
http: '',
https: '',
whitelist_uid: '',
whitelist_ukey: '',
}}
>
<Row gutter={16}>
<Col span={12}>
<Card title="代理设置" size="small" style={{ marginBottom: 16 }}>
<Form.Item name="enabled" label="启用代理" valuePropName="checked">
<Switch />
</Form.Item>
<Form.Item name="api_url" label="代理API地址">
<Input placeholder="http://op.xiequ.cn/...?act=get" />
</Form.Item>
<Form.Item name="http" label="静态HTTP代理">
<Input placeholder="http://ip:port" />
</Form.Item>
<Form.Item name="https" label="静态HTTPS代理">
<Input placeholder="http://ip:port" />
</Form.Item>
<Button onClick={handleTestProxy} loading={testing}></Button>
</Card>
</Col>
<Col span={12}>
<Card title="白名单管理" size="small" style={{ marginBottom: 16 }}>
<Form.Item name="whitelist_enabled" label="启用白名单自动管理" valuePropName="checked">
<Switch />
</Form.Item>
<Form.Item name="whitelist_uid" label="协固UID">
<Input placeholder="如: 99769" />
</Form.Item>
<Form.Item name="whitelist_ukey" label="协固UKEY">
<Input placeholder="如: C99371082B965B70F46DCAA87A04618B" />
</Form.Item>
<Button onClick={handleTestWhitelist} loading={testingWl}></Button>
</Card>
</Col>
</Row>
</Form>
<Card
title="实时日志"
size="small"
style={{ flex: 1, minHeight: 200, overflow: 'auto' }}
styles={{ body: { maxHeight: 350, overflow: 'auto', fontFamily: 'monospace', fontSize: 12, padding: '8px 16px' } }}
>
{logs.length === 0 ? (
<span style={{ color: '#999' }}>"测试代理""测试白名单"</span>
) : (
logs.map((log, i) => (
<div key={i} style={{ color: logColors[log.level] || '#333', lineHeight: '20px' }}>
{log.message}
</div>
))
)}
</Card>
</div>
);
}