diff --git a/astrbot/core/config/default.py b/astrbot/core/config/default.py index 6af27337b..a8c5d2a67 100644 --- a/astrbot/core/config/default.py +++ b/astrbot/core/config/default.py @@ -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", @@ -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": "时区", diff --git a/astrbot/core/core_lifecycle.py b/astrbot/core/core_lifecycle.py index 783b1fa98..c38bd6b86 100644 --- a/astrbot/core/core_lifecycle.py +++ b/astrbot/core/core_lifecycle.py @@ -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 @@ -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): """ diff --git a/astrbot/core/utils/proxy_manager.py b/astrbot/core/utils/proxy_manager.py new file mode 100644 index 000000000..832e14f0b --- /dev/null +++ b/astrbot/core/utils/proxy_manager.py @@ -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 \ No newline at end of file diff --git a/astrbot/dashboard/server.py b/astrbot/dashboard/server.py index acdc8a49f..0243d0782 100644 --- a/astrbot/dashboard/server.py +++ b/astrbot/dashboard/server.py @@ -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: diff --git a/pyproject.toml b/pyproject.toml index 438980503..3143e1186 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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", @@ -45,6 +53,7 @@ dependencies = [ "watchfiles>=1.0.5", "websockets>=15.0.1", "wechatpy>=1.8.18", + "pysocks>=1.7.1", ] [project.scripts] diff --git a/requirements.txt b/requirements.txt index ec81128db..1b25cedee 100644 --- a/requirements.txt +++ b/requirements.txt @@ -37,4 +37,6 @@ watchfiles websockets faiss-cpu aiosqlite -nh3 \ No newline at end of file +nh3pysocks +httpx[socks] +pysocks