From f4011d3ac286d0020ddb6dd12eacdff34675cd12 Mon Sep 17 00:00:00 2001 From: ui_beam <61094940+ui-beam-9@users.noreply.github.com> Date: Fri, 5 Jun 2026 15:20:31 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E5=89=8D=E7=AB=AF?= =?UTF-8?q?=E4=BB=A3=E7=90=86=E6=9C=8D=E5=8A=A1=E5=99=A8=E8=AE=BE=E7=BD=AE?= =?UTF-8?q?=E6=B8=85=E7=A9=BA=E4=BF=9D=E5=AD=98=E5=90=8E=EF=BC=8Chttpx=20?= =?UTF-8?q?=E6=8C=81=E7=BB=AD=E6=8A=A5=20`Unknown=20scheme=20for=20proxy?= =?UTF-8?q?=20URL=20(#5899)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/core/config.py | 32 +++++++++++++++++++++++++------- app/utils/http.py | 12 ++++++++++-- 2 files changed, 35 insertions(+), 9 deletions(-) diff --git a/app/core/config.py b/app/core/config.py index d2d6faf7..35c02be7 100644 --- a/app/core/config.py +++ b/app/core/config.py @@ -9,10 +9,10 @@ import sys import threading from asyncio import AbstractEventLoop from pathlib import Path -from typing import Any, Dict, List, Optional, Tuple, Type +from typing import Any, Dict, List, Optional, Tuple, Type, Union, get_origin, get_args from urllib.parse import quote, urlencode, urlparse -from dotenv import set_key +from dotenv import set_key, unset_key from pydantic import BaseModel, Field, ConfigDict, model_validator from pydantic_settings import BaseSettings, SettingsConfigDict @@ -690,6 +690,18 @@ class Settings(BaseSettings, ConfigModel, LogConfigModel): if isinstance(value, str): value = value.strip() + # 处理 Optional 类型:当值为空字符串且类型允许 None 时,转为 None + # 兼容 typing.Union (Python 3.9) 与 types.UnionType (Python 3.10+ PEP 604) + origin = get_origin(expected_type) + is_union = origin is Union or getattr(origin, "__name__", None) == "UnionType" + if ( + is_union + and type(None) in get_args(expected_type) + and isinstance(value, str) + and not value + ): + return default, str(default) != str(original_value) + try: if expected_type is bool: if isinstance(value, bool): @@ -812,13 +824,19 @@ class Settings(BaseSettings, ConfigModel, LogConfigModel): logger.warning(message) return False, message else: + # 当值为 None 时,从 env 文件中删除该键,恢复为默认值 + if converted_value is None: + unset_key( + dotenv_path=SystemUtils.get_env_path(), + key_to_unset=field_name, + ) + logger.info(f"配置项 '{field_name}' 已清空,从 'app.env' 中移除") + return True, message # 如果是列表、字典或集合类型,将其转换为JSON字符串 if isinstance(converted_value, (list, dict, set)): value_to_write = json.dumps(converted_value) else: - value_to_write = ( - str(converted_value) if converted_value is not None else "" - ) + value_to_write = str(converted_value) set_key( dotenv_path=SystemUtils.get_env_path(), @@ -967,7 +985,7 @@ class Settings(BaseSettings, ConfigModel, LogConfigModel): @property def PROXY(self): - if self.PROXY_HOST: + if self.PROXY_HOST and self.PROXY_HOST.strip(): return { "http": self.PROXY_HOST, "https": self.PROXY_HOST, @@ -1009,7 +1027,7 @@ class Settings(BaseSettings, ConfigModel, LogConfigModel): @property def PROXY_SERVER(self): - if self.PROXY_HOST: + if self.PROXY_HOST and self.PROXY_HOST.strip(): try: parsed = urlparse(self.PROXY_HOST) if not parsed.scheme: diff --git a/app/utils/http.py b/app/utils/http.py index f9a9a14a..3556c99a 100644 --- a/app/utils/http.py +++ b/app/utils/http.py @@ -92,6 +92,9 @@ def _get_shared_async_transport( 会话级状态由调用方在外层 AsyncClient(transport=...) 实例化时单独配置, 每次调用用完即销毁,因此天然无 jar 累积串扰。 """ + # 规范化代理:拒绝空字符串等非法值,防止 httpx 抛出 Unknown scheme for proxy URL + if proxy is not None and (not proxy or not proxy.strip()): + proxy = None try: loop = asyncio.get_running_loop() except RuntimeError: @@ -899,12 +902,17 @@ class AsyncRequestUtils: # 如果已经是字符串格式,直接返回 if isinstance(proxies, str): - return proxies + return proxies.strip() or None # 如果是字典格式,提取http或https代理 if isinstance(proxies, dict): # 优先使用https代理,如果没有则使用http代理 - proxy_url = proxies.get("https") or proxies.get("http") + # 先各自 strip,避免空白字符串阻断裂合取或回退到 http 代理 + https_proxy = proxies.get("https") + http_proxy = proxies.get("http") + https_proxy = https_proxy.strip() if isinstance(https_proxy, str) else None + http_proxy = http_proxy.strip() if isinstance(http_proxy, str) else None + proxy_url = https_proxy or http_proxy if proxy_url: return proxy_url