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:
@@ -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
@@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user