fix: безопасная перекодировка stdout/stderr и рекурсивная проверка 429

- reconfigure() вместо нового TextIOWrapper — не ломается при редиректе
- _is_429() с рекурсивным обходом ExceptionGroup (anyio оборачивает 429)
- except BaseException в _load_mcp / mcp_call для перехвата ExceptionGroup

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-06-04 19:00:14 +03:00
parent 9cb3731dda
commit 863ea54eaf
2 changed files with 57 additions and 4 deletions
+17
View File
@@ -5,10 +5,27 @@
python run_pipeline.py <id1> <id2> # решить конкретные задания по ID python run_pipeline.py <id1> <id2> # решить конкретные задания по ID
""" """
import asyncio import asyncio
import io
import os import os
import sys import sys
os.environ["PYTHONIOENCODING"] = "utf-8" os.environ["PYTHONIOENCODING"] = "utf-8"
# Перенастраиваем stdout/stderr на UTF-8 уже после старта Python
# Используем reconfigure() — безопасно при любом типе перенаправления вывода.
try:
if hasattr(sys.stdout, "reconfigure"):
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
elif hasattr(sys.stdout, "buffer"):
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8", errors="replace")
except Exception:
pass
try:
if hasattr(sys.stderr, "reconfigure"):
sys.stderr.reconfigure(encoding="utf-8", errors="replace")
elif hasattr(sys.stderr, "buffer"):
sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding="utf-8", errors="replace")
except Exception:
pass
os.environ["PYTHONUNBUFFERED"] = "1" os.environ["PYTHONUNBUFFERED"] = "1"
os.environ["NO_PROXY"] = ( os.environ["NO_PROXY"] = (
"openrouter.ai,platform.brojs.ru,git.brojs.ru," "openrouter.ai,platform.brojs.ru,git.brojs.ru,"
+40 -4
View File
@@ -8,10 +8,29 @@
""" """
import asyncio import asyncio
import base64 import base64
import io
import json import json
import os import os
import sys import sys
# Перенастраиваем stdout/stderr на UTF-8 (Windows cp1251 не осиливает →, ✅ и т.д.)
# reconfigure() меняет кодировку у существующего враппера на месте — безопаснее, чем
# создавать новый TextIOWrapper поверх буфера (последнее ломается при перенаправлении в файл).
try:
if hasattr(sys.stdout, "reconfigure"):
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
elif hasattr(sys.stdout, "buffer"):
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8", errors="replace")
except Exception:
pass
try:
if hasattr(sys.stderr, "reconfigure"):
sys.stderr.reconfigure(encoding="utf-8", errors="replace")
elif hasattr(sys.stderr, "buffer"):
sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding="utf-8", errors="replace")
except Exception:
pass
# Обходим локальный прокси # Обходим локальный прокси
os.environ["NO_PROXY"] = "openrouter.ai,platform.brojs.ru,git.brojs.ru," + os.environ.get("NO_PROXY", "") os.environ["NO_PROXY"] = "openrouter.ai,platform.brojs.ru,git.brojs.ru," + os.environ.get("NO_PROXY", "")
@@ -55,6 +74,23 @@ _BACKOFF = [15, 30, 60, 120, 240]
_INTER_CALL_DELAY = 1.5 # секунд между последовательными MCP-вызовами _INTER_CALL_DELAY = 1.5 # секунд между последовательными MCP-вызовами
def _is_429(exc: BaseException) -> bool:
"""Рекурсивно проверяет, содержит ли исключение (или вложенные) ошибку 429.
Нужно потому что anyio оборачивает HTTP-ошибки в ExceptionGroup,
и '429' есть только во вложенном исключении, а не в str(ExceptionGroup).
"""
if "429" in str(exc):
return True
# ExceptionGroup (Python 3.11+ / anyio): смотрим вложенные
if hasattr(exc, "exceptions"):
return any(_is_429(sub) for sub in exc.exceptions)
# __cause__ / __context__
if exc.__cause__ is not None and exc.__cause__ is not exc:
return _is_429(exc.__cause__)
return False
async def _load_mcp(): async def _load_mcp():
global _mcp_tools, _mcp_client global _mcp_tools, _mcp_client
if _mcp_tools: if _mcp_tools:
@@ -76,8 +112,8 @@ async def _load_mcp():
_mcp_tools = {t.name: t for t in tools} _mcp_tools = {t.name: t for t in tools}
print(f" [mcp] Загружено {len(_mcp_tools)} инструментов") print(f" [mcp] Загружено {len(_mcp_tools)} инструментов")
return return
except Exception as e: except BaseException as e:
if "429" not in str(e) or i == len(_BACKOFF): if not _is_429(e) or i == len(_BACKOFF):
raise raise
@@ -105,8 +141,8 @@ async def mcp_call(name: str, args: dict):
if isinstance(result, list): if isinstance(result, list):
return next((x["text"] for x in result if x.get("type") == "text"), str(result)) return next((x["text"] for x in result if x.get("type") == "text"), str(result))
return str(result) return str(result)
except Exception as e: except BaseException as e:
if "429" not in str(e) or i == len(_BACKOFF): if not _is_429(e) or i == len(_BACKOFF):
raise raise