Skip to content

✨feat: 增加对了proxy_manger工具类,支持了socks代理支持,并将代理功能封装进工具类 #1400

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 13 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions astrbot/core/config/default.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,8 +92,9 @@
"t2i_word_threshold": 150,
"t2i_strategy": "remote",
"t2i_endpoint": "",
"t2i_use_file_service": False,
"proxy": "",
"http_proxy": "",
"t2i_use_file_service": False,
"dashboard": {
"enable": True,
"username": "astrbot",
Expand Down Expand Up @@ -1630,7 +1631,13 @@
"http_proxy": {
"description": "HTTP 代理",
"type": "string",
"hint": "启用后,会以添加环境变量的方式设置代理。格式为 `http://ip:port`",
"obvious_hint": True,
"hint": "该配置将在下版本弃用,请配置`网络代理(proxy)`。启用后,会以添加环境变量的方式设置代理。格式为 `http://ip:port`",
},
"proxy": {
"description": "网络代理",
"type": "string",
"hint": "启用后,会以添加环境变量的方式设置代理。支持HTTP和SOCKS代理,格式为 `http://ip:port` 或 `socks5://ip:port`",
},
"timezone": {
"description": "时区",
Expand Down
21 changes: 16 additions & 5 deletions astrbot/core/core_lifecycle.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import time
import threading
import os
from astrbot.core.utils.proxy_manager import ProxyManager
from .event_bus import EventBus
from . import astrbot_config
from asyncio import Queue
Expand Down Expand Up @@ -45,11 +46,21 @@ def __init__(self, log_broker: LogBroker, db: BaseDatabase):
self.log_broker = log_broker # 初始化日志代理
self.astrbot_config = astrbot_config # 初始化配置
self.db = db # 初始化数据库

# 根据环境变量设置代理
os.environ["https_proxy"] = self.astrbot_config["http_proxy"]
os.environ["http_proxy"] = self.astrbot_config["http_proxy"]
os.environ["no_proxy"] = "localhost"

# 初始化代理管理器
self.proxy_manager = ProxyManager()

# 从配置中获取代理设置并应用,优先使用新的 proxy 字段,如果为空则尝试使用旧的 http_proxy 字段
proxy_url = self.astrbot_config.get("proxy", "")
if not proxy_url:
# 兼容旧版本的 http_proxy 配置
proxy_url = self.astrbot_config.get("http_proxy", "")
if proxy_url:
logger.warning(f"检测到旧版本代理配置(http_proxy),已自动兼容。建议更新配置文件,重新设置网络代理(proxy)以适应新版本。")
self.proxy_manager.setup_proxy(proxy_url)

# 设置不使用代理的本地地址
self.proxy_manager.setup_no_proxy_hosts()

async def initialize(self):
"""
Expand Down
166 changes: 166 additions & 0 deletions astrbot/core/utils/proxy_manager.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
import os
import socket
from typing import Optional
from astrbot.core import logger

try:
import socks # PySocks提供的SOCKS代理功能
except ImportError:
socks = None # 如果未安装PySocks,则设为None

class ProxyManager:
"""代理管理类,负责处理HTTP和SOCKS代理的设置和清除"""

def __init__(self):
# 保存原始socket类,用于本地连接检测和恢复
self.original_socket = socket.socket
# 记录当前代理状态
self.current_proxy = None
self.is_socks_proxy = False

def setup_proxy(self, proxy_url: Optional[str]) -> bool:
"""
根据提供的代理URL设置代理

Args:
proxy_url: 代理URL,格式为 'http://host:port' 或 'socks5://host:port'
如果为None或空字符串,则清除代理

Returns:
bool: 代理设置是否成功
"""
# 首先清除所有现有代理设置
self.clear_proxy()

# 如果没有提供代理URL,直接返回True
if not proxy_url:
logger.info("未配置代理,使用直接连接")
return True

self.current_proxy = proxy_url

# 检查是否是SOCKS代理
if proxy_url.lower().startswith('socks'):
return self._setup_socks_proxy(proxy_url)
else:
return self._setup_http_proxy(proxy_url)

def _setup_socks_proxy(self, proxy_url: str) -> bool:
"""设置SOCKS代理"""
if socks is None:
logger.warning("检测到SOCKS代理配置,但未正确安装PySocks。请使用 pip install pysocks")
return False

try:
proxy_parts = proxy_url.split('://')
if len(proxy_parts) != 2:
logger.error(f"代理URL格式错误: {proxy_url}")
return False

proxy_type_str, proxy_addr = proxy_parts
# 确定代理类型
if proxy_type_str == 'socks5':
proxy_type = socks.SOCKS5
elif proxy_type_str == 'socks4':
proxy_type = socks.SOCKS4
else:
proxy_type = socks.SOCKS5
logger.warning(f"未知的SOCKS类型: {proxy_type_str},默认使用SOCKS5")

# 解析用户名密码和代理地址
username = None
password = None
if '@' in proxy_addr:
auth_info, proxy_addr = proxy_addr.split('@')
if ':' in auth_info:
username, password = auth_info.split(':', 1)
logger.debug(f"检测到代理认证信息,用户名: {username}")
else:
username = auth_info
password = None
logger.debug(f"检测到代理认证信息,仅用户名: {username}")

# 解析代理地址和端口
if ':' in proxy_addr:
proxy_host, proxy_port_str = proxy_addr.split(':')
try:
proxy_port = int(proxy_port_str)
# 设置默认socket为SOCKS代理
socks.set_default_proxy(
proxy_type,
proxy_host,
proxy_port,
username=username,
password=password
)
socket.socket = socks.socksocket
self.is_socks_proxy = True

# 同时设置环境变量以支持不使用PySocks的库
os.environ["ALL_PROXY"] = proxy_url
logger.info(f"已设置SOCKS{proxy_type_str[-1]}代理: {proxy_host}:{proxy_port}")
return True
except ValueError:
logger.error(f"代理端口无效: {proxy_port_str}")
return False
else:
logger.error(f"代理地址格式错误: {proxy_addr}")
return False
except Exception as e:
logger.error(f"设置SOCKS代理时出错: {e}")
return False

def _setup_http_proxy(self, proxy_url: str) -> bool:
"""设置HTTP代理"""
try:
# HTTP代理设置
os.environ["HTTP_PROXY"] = proxy_url
os.environ["HTTPS_PROXY"] = proxy_url
os.environ["http_proxy"] = proxy_url
os.environ["https_proxy"] = proxy_url
logger.info(f"已设置HTTP/HTTPS代理: {proxy_url}")
return True
except Exception as e:
logger.error(f"设置HTTP代理时出错: {e}")
return False

def clear_proxy(self) -> None:
"""清除所有代理设置"""
# 清除环境变量
self._clear_proxy_env()

# 如果之前设置了SOCKS代理,恢复原始socket
if self.is_socks_proxy:
socket.socket = self.original_socket
self.is_socks_proxy = False
logger.info("已清除SOCKS代理设置")

self.current_proxy = None

def _clear_proxy_env(self) -> None:
"""清除所有代理相关的环境变量"""
proxy_env_vars = [
"HTTP_PROXY", "HTTPS_PROXY", "http_proxy", "https_proxy", "ALL_PROXY"
]
for var in proxy_env_vars:
if var in os.environ:
del os.environ[var]

def get_current_proxy(self) -> Optional[str]:
"""获取当前使用的代理URL"""
return self.current_proxy

def is_using_proxy(self) -> bool:
"""检查是否正在使用代理"""
return self.current_proxy is not None

def setup_no_proxy_hosts(self, hosts: list = None) -> None:
"""设置不使用代理的主机列表"""
if hosts is None:
hosts = ["localhost", "127.0.0.1", "::1", "0.0.0.0"]

os.environ["no_proxy"] = ",".join(hosts)

def get_direct_socket(self):
"""返回未经代理的原始socket类,用于本地连接检测"""
return self.original_socket
9 changes: 6 additions & 3 deletions astrbot/dashboard/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,13 +104,16 @@ def check_port_in_use(self, port: int) -> bool:
跨平台检测端口是否被占用
"""
try:
# 创建 IPv4 TCP Socket
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 获取未经socks代理的socket类
direct_socket = self.core_lifecycle.proxy_manager.get_direct_socket()

# 创建不受代理影响的原始socket
sock = direct_socket(socket.AF_INET, socket.SOCK_STREAM)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
# 设置超时时间
sock.settimeout(2)
result = sock.connect_ex(("127.0.0.1", port))
sock.close()

# result 为 0 表示端口被占用
return result == 0
except Exception as e:
Expand Down
9 changes: 9 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,14 @@ dependencies = [
"filelock>=3.18.0",
"google-genai>=1.14.0",
"googlesearch-python>=1.3.0",
"lark-oapi>=1.4.12",
"lxml-html-clean>=0.4.1",
"mcp>=1.5.0",
"openai>=1.68.2",
"ormsgpack>=1.9.0",
"pillow>=11.1.0",
"pip>=25.0.1",
"httpx[socks]>=0.28.1",
"lark-oapi>=1.4.15",
"lxml-html-clean>=0.4.2",
"mcp>=1.8.0",
Expand All @@ -45,6 +53,7 @@ dependencies = [
"watchfiles>=1.0.5",
"websockets>=15.0.1",
"wechatpy>=1.8.18",
"pysocks>=1.7.1",
]

[project.scripts]
Expand Down
4 changes: 3 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,6 @@ watchfiles
websockets
faiss-cpu
aiosqlite
nh3
nh3pysocks
httpx[socks]
pysocks