{html.escape(content)}
+ From faae3edadf8c3cc26744cd436832de8bf4001e43 Mon Sep 17 00:00:00 2001 From: SengokuCola <1026294844@qq.com> Date: Sat, 4 Apr 2026 15:53:31 +0800 Subject: [PATCH] =?UTF-8?q?feat=EF=BC=9A=E8=90=BD=E5=BA=93=E9=BA=A6?= =?UTF-8?q?=E9=BA=A6=E6=96=87=E4=BB=B6=E5=A4=B9=EF=BC=8C=E9=9C=80=E8=A6=81?= =?UTF-8?q?=E6=88=91=E5=B8=AE=E4=BD=A0=E7=A8=B3=E7=A8=B3=E7=9A=84=E6=8E=A5?= =?UTF-8?q?=E4=BD=8Freplyer=E7=9A=84log=E5=90=97=EF=BC=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit feat2 --- src/chat/replyer/maisaka_generator_multi.py | 69 +++++- src/common/logger_color_and_mapping.py | 2 + src/maisaka/chat_loop_service.py | 39 +-- src/maisaka/prompt_cli_renderer.py | 254 +++++++++++++++++--- src/maisaka/prompt_preview_logger.py | 83 +++++++ src/maisaka/reasoning_engine.py | 3 + src/maisaka/runtime.py | 35 ++- src/plugin_runtime/host/message_utils.py | 15 -- 8 files changed, 413 insertions(+), 87 deletions(-) create mode 100644 src/maisaka/prompt_preview_logger.py diff --git a/src/chat/replyer/maisaka_generator_multi.py b/src/chat/replyer/maisaka_generator_multi.py index a3befb72..626dac5a 100644 --- a/src/chat/replyer/maisaka_generator_multi.py +++ b/src/chat/replyer/maisaka_generator_multi.py @@ -1,15 +1,20 @@ +import random +import time from dataclasses import dataclass, field from datetime import datetime from typing import Dict, List, Optional, Tuple -import random -import time +from rich.console import Group, RenderableType +from rich.panel import Panel +from rich.text import Text from sqlmodel import select from src.chat.message_receive.chat_manager import BotChatSession +from src.chat.message_receive.message import SessionMessage +from src.cli.console import console from src.common.database.database import get_db_session -from src.common.data_models.message_component_data_model import MessageSequence, TextComponent from src.common.database.database_model import Expression +from src.common.data_models.message_component_data_model import MessageSequence, TextComponent from src.common.data_models.reply_generation_data_models import ( GenerationMetrics, LLMCompletionResult, @@ -23,9 +28,15 @@ from src.core.types import ActionInfo from src.llm_models.payload_content.message import ImageMessagePart, Message, MessageBuilder, RoleType, TextMessagePart from src.services.llm_service import LLMServiceClient -from src.chat.message_receive.message import SessionMessage -from src.maisaka.context_messages import AssistantMessage, LLMContextMessage, ReferenceMessage, SessionBackedMessage, ToolResultMessage +from src.maisaka.context_messages import ( + AssistantMessage, + LLMContextMessage, + ReferenceMessage, + SessionBackedMessage, + ToolResultMessage, +) from src.maisaka.message_adapter import clone_message_sequence, parse_speaker_content +from src.maisaka.prompt_cli_renderer import PromptCLIVisualizer logger = get_logger("replyer") @@ -509,9 +520,17 @@ class MaisakaReplyGenerator: return request_messages result.completion.request_prompt = prompt_preview - + preview_chat_id = self._resolve_session_id(stream_id) + replyer_prompt_section: RenderableType | None = None if global_config.debug.show_replyer_prompt: - logger.info(f"\nMaisaka 回复器提示词:\n{prompt_preview}\n") + replyer_prompt_section = PromptCLIVisualizer.build_text_section( + prompt_preview, + category="replyer", + chat_id=preview_chat_id, + request_kind="replyer", + subtitle=f"会话流标识:{preview_chat_id}", + folded=global_config.debug.fold_maisaka_thinking, + ) started_at = time.perf_counter() try: @@ -550,5 +569,41 @@ class MaisakaReplyGenerator: f"总耗时毫秒={result.metrics.overall_ms} " f"已选表达编号={result.selected_expression_ids!r}" ) + if global_config.debug.show_replyer_prompt or global_config.debug.show_replyer_reasoning: + summary_lines = [ + f"会话流标识: {preview_chat_id or 'unknown'}", + f"总耗时: {result.metrics.overall_ms} ms", + ] + if result.selected_expression_ids: + summary_lines.append(f"表达习惯编号: {result.selected_expression_ids!r}") + + renderables: List[RenderableType] = [Text("\n".join(summary_lines))] + if replyer_prompt_section is not None: + renderables.append(replyer_prompt_section) + if global_config.debug.show_replyer_reasoning and result.completion.reasoning_text: + renderables.append( + Panel( + Text(result.completion.reasoning_text), + title="回复器思考", + border_style="magenta", + padding=(0, 1), + ) + ) + renderables.append( + Panel( + Text(response_text), + title="回复结果", + border_style="green", + padding=(0, 1), + ) + ) + console.print( + Panel( + Group(*renderables), + title="MaiSaka 回复器结果", + border_style="bright_yellow", + padding=(0, 1), + ) + ) result.text_fragments = [response_text] return True, result diff --git a/src/common/logger_color_and_mapping.py b/src/common/logger_color_and_mapping.py index 863c9c1e..dc4bdbb2 100644 --- a/src/common/logger_color_and_mapping.py +++ b/src/common/logger_color_and_mapping.py @@ -31,6 +31,8 @@ MODULE_COLORS: Dict[str, Tuple[str, Optional[str], bool]] = { "llm_models": ("#008080", None, False), "remote": ("#6c6c6c", None, False), # 深灰色,更不显眼 "planner": ("#008080", None, False), + "maisaka_reasoning_engine": ("#008080", None, False), + "maisaka_runtime": ("#ff5fff", None, False), "relation": ("#af87af", None, False), # 柔和的紫色,不刺眼 # 聊天相关模块 "hfc": ("#d787af", None, False), # 柔和的粉色,不显眼但保持粉色系 diff --git a/src/maisaka/chat_loop_service.py b/src/maisaka/chat_loop_service.py index dabf0091..f7c4ad6f 100644 --- a/src/maisaka/chat_loop_service.py +++ b/src/maisaka/chat_loop_service.py @@ -10,10 +10,8 @@ import json import random from pydantic import BaseModel, Field as PydanticField -from rich.console import Group +from rich.console import RenderableType from rich.panel import Panel - -from src.cli.console import console from src.common.data_models.llm_service_data_models import LLMGenerationOptions from src.common.logger import get_logger from src.common.prompt_i18n import load_prompt @@ -53,6 +51,7 @@ class ChatResponse: built_message_count: int completion_tokens: int total_tokens: int + prompt_section: Optional[RenderableType] = None class ToolFilterSelection(BaseModel): @@ -765,30 +764,17 @@ class MaisakaChatLoopService: if isinstance(raw_tool_definitions, list): all_tools = [item for item in raw_tool_definitions if isinstance(item, dict)] + prompt_section: RenderableType | None = None if global_config.debug.show_maisaka_thinking: - panel_title, panel_border_style = PromptCLIVisualizer.get_request_panel_style(request_kind) image_display_mode: str = "path_link" if global_config.maisaka.show_image_path else "legacy" - if global_config.debug.fold_maisaka_thinking: - prompt_renderable = PromptCLIVisualizer.build_prompt_access_panel( - built_messages, - request_kind=request_kind, - selection_reason=selection_reason, - image_display_mode=image_display_mode, - ) - else: - ordered_panels = PromptCLIVisualizer.build_prompt_panels( - built_messages, - image_display_mode=image_display_mode, - ) - prompt_renderable = Group(*ordered_panels) - console.print( - Panel( - prompt_renderable, - title=panel_title, - subtitle=selection_reason, - border_style=panel_border_style, - padding=(0, 1), - ) + prompt_section = PromptCLIVisualizer.build_prompt_section( + built_messages, + category="planner" if request_kind != "timing_gate" else "timing_gate", + chat_id=self._session_id, + request_kind=request_kind, + selection_reason=selection_reason, + image_display_mode=image_display_mode, + folded=global_config.debug.fold_maisaka_thinking, ) request_started_at = perf_counter() @@ -809,8 +795,6 @@ class MaisakaChatLoopService: interrupt_flag=self._interrupt_flag, ), ) - request_elapsed = perf_counter() - request_started_at - logger.info(f"规划器请求完成,耗时={request_elapsed:.3f} 秒") prompt_stats_text = PromptCLIVisualizer.build_prompt_stats_text( selected_history_count=len(selected_history), @@ -865,6 +849,7 @@ class MaisakaChatLoopService: built_message_count=len(built_messages), completion_tokens=completion_tokens, total_tokens=total_tokens, + prompt_section=prompt_section, ) @staticmethod diff --git a/src/maisaka/prompt_cli_renderer.py b/src/maisaka/prompt_cli_renderer.py index c60cbafd..9652eb7b 100644 --- a/src/maisaka/prompt_cli_renderer.py +++ b/src/maisaka/prompt_cli_renderer.py @@ -2,24 +2,26 @@ from __future__ import annotations -import hashlib -import html -import json from base64 import b64decode from dataclasses import dataclass from enum import Enum from pathlib import Path -from urllib.parse import quote from typing import Any, Dict, List, Literal +from urllib.parse import quote +import hashlib +import html +import json import tempfile from pydantic import BaseModel, Field as PydanticField from rich.console import Group, RenderableType -from rich.pretty import Pretty from rich.panel import Panel +from rich.pretty import Pretty from rich.text import Text +from .prompt_preview_logger import PromptPreviewLogger + PROJECT_ROOT = Path(__file__).parent.parent.parent.absolute().resolve() DATA_IMAGE_DIR = PROJECT_ROOT / "data" / "images" @@ -53,8 +55,6 @@ class _MessageRenderResult: class PromptCLIVisualizer: """负责构建 CLI 下 prompt 展示所需的所有可视化组件。""" - PROMPT_DUMP_DIR = Path(tempfile.gettempdir()) / "maisaka_prompt_dumps" - @staticmethod def get_request_panel_style(request_kind: str) -> tuple[str, str]: """返回不同请求类型对应的标题与边框颜色。""" @@ -62,6 +62,8 @@ class PromptCLIVisualizer: normalized_kind = str(request_kind or "planner").strip().lower() if normalized_kind == "timing_gate": return "MaiSaka 大模型请求 - Timing Gate 子代理", "bright_magenta" + if normalized_kind == "replyer": + return "MaiSaka 回复器 Prompt", "bright_yellow" if normalized_kind == "sub_agent": return "MaiSaka 大模型请求 - 子代理", "bright_blue" return "MaiSaka 大模型请求 - 对话单步", "cyan" @@ -133,19 +135,9 @@ class PromptCLIVisualizer: @staticmethod def _build_file_uri(file_path: Path) -> str: - normalized = file_path.as_posix() + normalized = file_path.resolve().as_posix() return f"file:///{quote(normalized, safe='/:')}" - @classmethod - def _build_prompt_dump_base_path(cls, messages: list[Any]) -> Path: - cls.PROMPT_DUMP_DIR.mkdir(parents=True, exist_ok=True) - try: - payload = json.dumps(messages, ensure_ascii=False, default=str) - except Exception: - payload = repr(messages) - digest = hashlib.sha256(payload.encode("utf-8")).hexdigest() - return cls.PROMPT_DUMP_DIR / digest - @staticmethod def _build_official_image_path(image_format: str, image_base64: str) -> Path | None: normalized_format = PromptCLIVisualizer._normalize_image_format(image_format) @@ -611,15 +603,14 @@ class PromptCLIVisualizer: cls, messages: list[Any], *, + category: str, + chat_id: str, request_kind: str, selection_reason: str, image_display_mode: Literal["legacy", "path_link"], - ) -> Panel: - """构建用于查看完整 prompt 的入口面板。""" + ) -> RenderableType: + """构建用于查看完整 prompt 的折叠入口内容。""" - base_path = cls._build_prompt_dump_base_path(messages) - prompt_dump_path = base_path.with_suffix(".txt") - prompt_dump_path.write_text(cls._build_prompt_dump_text(messages), encoding="utf-8") viewer_messages: list[dict[str, Any]] = [] for message in messages: if isinstance(message, dict): @@ -641,15 +632,22 @@ class PromptCLIVisualizer: ] viewer_messages.append(normalized_message) - viewer_html_path = base_path.with_suffix(".html") - viewer_html_path.write_text( - cls._build_prompt_viewer_html( - viewer_messages, - request_kind=request_kind, - selection_reason=selection_reason, - ), - encoding="utf-8", + prompt_dump_text = cls._build_prompt_dump_text(messages) + viewer_html_text = cls._build_prompt_viewer_html( + viewer_messages, + request_kind=request_kind, + selection_reason=selection_reason, ) + saved_paths = PromptPreviewLogger.save_preview_files( + chat_id, + category, + { + ".html": viewer_html_text, + ".txt": prompt_dump_text, + }, + ) + viewer_html_path = saved_paths[".html"] + prompt_dump_path = saved_paths[".txt"] viewer_uri = cls._build_file_uri(viewer_html_path) dump_uri = cls._build_file_uri(prompt_dump_path) @@ -659,10 +657,198 @@ class PromptCLIVisualizer: Text.from_markup(f"[link={viewer_uri}]点击在浏览器打开富文本 Prompt 视图[/link]", style="bold green"), Text.from_markup(f"[link={dump_uri}]点击直接打开 Prompt 文本[/link]", style="cyan"), ) + return body + + @classmethod + def build_prompt_section( + cls, + messages: list[Any], + *, + category: str, + chat_id: str, + request_kind: str, + selection_reason: str, + image_display_mode: Literal["legacy", "path_link"], + folded: bool, + ) -> Panel: + """构建用于嵌入结果面板中的 Prompt 区块。""" + + panel_title, panel_border_style = cls.get_request_panel_style(request_kind) + if folded: + prompt_renderable = cls.build_prompt_access_panel( + messages, + category=category, + chat_id=chat_id, + request_kind=request_kind, + selection_reason=selection_reason, + image_display_mode=image_display_mode, + ) + else: + ordered_panels = cls.build_prompt_panels( + messages, + image_display_mode=image_display_mode, + ) + prompt_renderable = Group(*ordered_panels) + return Panel( - body, - title=Text(" Prompt 查看入口 ", style="bold white on blue"), - border_style="blue", + prompt_renderable, + title=panel_title, + subtitle=selection_reason, + border_style=panel_border_style, + padding=(0, 1), + ) + + @classmethod + def _build_text_preview_html( + cls, + content: str, + *, + request_kind: str, + subtitle: str, + ) -> str: + panel_title, _ = cls.get_request_panel_style(request_kind) + subtitle_html = f"
{html.escape(content)}
+