From 1c759ad426034649e17bacca226925a4a0af69d5 Mon Sep 17 00:00:00 2001 From: DrSmoothl <1787882683@qq.com> Date: Fri, 13 Mar 2026 01:13:29 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=A2=9E=E5=BC=BA=20Runner=20=E6=97=A5?= =?UTF-8?q?=E5=BF=97=E5=A4=84=E7=90=86=EF=BC=8C=E6=B7=BB=E5=8A=A0=E6=97=A5?= =?UTF-8?q?=E5=BF=97=E6=B6=88=E6=81=AF=E5=BA=8F=E5=88=97=E5=8C=96=E5=92=8C?= =?UTF-8?q?=E6=8E=A7=E5=88=B6=E5=8F=B0=E6=97=A5=E5=BF=97=E8=BE=93=E5=87=BA?= =?UTF-8?q?=E7=AE=A1=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/plugin_runtime/runner/log_handler.py | 47 +++++++++++++++++++++++- src/plugin_runtime/runner/runner_main.py | 34 ++++++++++++++++- 2 files changed, 78 insertions(+), 3 deletions(-) diff --git a/src/plugin_runtime/runner/log_handler.py b/src/plugin_runtime/runner/log_handler.py index 24d5f9b6..3151f7c1 100644 --- a/src/plugin_runtime/runner/log_handler.py +++ b/src/plugin_runtime/runner/log_handler.py @@ -27,6 +27,7 @@ from __future__ import annotations import asyncio import collections import contextlib +import json import logging from typing import TYPE_CHECKING, List, Optional @@ -113,8 +114,10 @@ class RunnerIPCLogHandler(logging.Handler): # 过滤:仅允许插件相关的 logger,跳过第三方库日志 if not any(record.name.startswith(p) for p in self.ALLOWED_LOGGER_PREFIXES): return - # format() 触发 exc_info 格式化并将结果缓存到 record.exc_text - msg = self.format(record) + + # structlog 透传到 stdlib logging 时,record.msg 往往是 event_dict。 + # 这里先提取可读的 event 文本,避免 Host 侧收到一整段 dict 字符串。 + msg = self._serialize_message(record) entry = LogEntry( timestamp_ms=int(record.created * 1000), level=record.levelno, @@ -126,6 +129,46 @@ class RunnerIPCLogHandler(logging.Handler): except Exception: self.handleError(record) + def _serialize_message(self, record: logging.LogRecord) -> str: + """将 LogRecord 序列化为适合 Host 重放的纯文本消息。""" + if isinstance(record.msg, dict): + event_dict = record.msg + event_text = self._stringify_value(event_dict.get("event", "")) + extras = [] + ignored_keys = { + "event", + "logger", + "logger_name", + "level", + "timestamp", + "module", + "lineno", + "pathname", + "_from_structlog", + "_record", + } + for key, value in event_dict.items(): + if key in ignored_keys: + continue + extras.append(f"{key}={self._stringify_value(value)}") + + if extras: + return f"{event_text} {' '.join(extras)}".strip() + return event_text + + # format() 会处理 %s 参数替换和 exc_info 文本拼接。 + return self.format(record) + + @staticmethod + def _stringify_value(value: object) -> str: + """将结构化字段转换为紧凑字符串。""" + if isinstance(value, (dict, list)): + try: + return json.dumps(value, ensure_ascii=False, separators=(",", ":")) + except (TypeError, ValueError): + return str(value) + return str(value) + # ─── 内部方法 ────────────────────────────────────────────────── async def _flush_loop(self) -> None: diff --git a/src/plugin_runtime/runner/runner_main.py b/src/plugin_runtime/runner/runner_main.py index 14fc5ba9..24ddaf24 100644 --- a/src/plugin_runtime/runner/runner_main.py +++ b/src/plugin_runtime/runner/runner_main.py @@ -22,7 +22,7 @@ import time from typing import Any -from src.common.logger import get_logger, initialize_logging +from src.common.logger import get_console_handler, get_logger, initialize_logging from src.plugin_runtime import ENV_IPC_ADDRESS, ENV_PLUGIN_DIRS, ENV_SESSION_TOKEN from src.plugin_runtime.protocol.envelope import ( ComponentDeclaration, @@ -40,6 +40,14 @@ from src.plugin_runtime.runner.rpc_client import RPCClient logger = get_logger("plugin_runtime.runner.main") +def _disable_runner_console_logging() -> None: + """关闭 Runner 的控制台日志输出,避免被 Host 从 stderr 二次包装。""" + root_logger = stdlib_logging.getLogger() + console_handler = get_console_handler() + if console_handler in root_logger.handlers: + root_logger.removeHandler(console_handler) + + class PluginRunner: """插件 Runner @@ -63,6 +71,7 @@ class PluginRunner: # IPC 日志 Handler:握手成功后安装,将所有 stdlib logging 转发到 Host self._log_handler: Optional[RunnerIPCLogHandler] = None + self._suspended_console_handlers: list[stdlib_logging.Handler] = [] async def run(self) -> None: """Runner 主入口""" @@ -123,6 +132,7 @@ class PluginRunner: loop = asyncio.get_running_loop() handler = RunnerIPCLogHandler() handler.start(self._rpc_client, loop) + self._suspend_console_handlers() stdlib_logging.root.addHandler(handler) self._log_handler = handler logger.debug("RunnerIPCLogHandler \u5df2\u5b89\u88c3\uff0c\u63d2\u4ef6\u65e5\u5fd7\u5c06\u901a\u8fc7 IPC \u8f6c\u53d1\u5230\u4e3b\u8fdb\u7a0b") @@ -137,8 +147,29 @@ class PluginRunner: stdlib_logging.root.removeHandler(self._log_handler) await self._log_handler.stop() self._log_handler = None + self._restore_console_handlers() logger.debug("RunnerIPCLogHandler \u5df2\u5378\u8f7d") + def _suspend_console_handlers(self) -> None: + """暂停 Runner 的控制台输出,避免与 IPC 转发重复。""" + if self._suspended_console_handlers: + return + + for handler in list(stdlib_logging.root.handlers): + if isinstance(handler, stdlib_logging.StreamHandler): + stdlib_logging.root.removeHandler(handler) + self._suspended_console_handlers.append(handler) + + def _restore_console_handlers(self) -> None: + """恢复此前暂停的控制台输出。""" + if not self._suspended_console_handlers: + return + + for handler in self._suspended_console_handlers: + if handler not in stdlib_logging.root.handlers: + stdlib_logging.root.addHandler(handler) + self._suspended_console_handlers.clear() + def _inject_context(self, plugin_id: str, instance: object) -> None: """为插件实例创建并注入 PluginContext。 @@ -522,6 +553,7 @@ async def _async_main() -> None: def main() -> None: """进程入口(python -m src.plugin_runtime.runner.runner_main)""" initialize_logging(verbose=False) + _disable_runner_console_logging() asyncio.run(_async_main())