diff --git a/src/config/config.py b/src/config/config.py index ac36a045..850eaa00 100644 --- a/src/config/config.py +++ b/src/config/config.py @@ -56,7 +56,7 @@ CONFIG_DIR: Path = PROJECT_ROOT / "config" BOT_CONFIG_PATH: Path = (CONFIG_DIR / "bot_config.toml").resolve().absolute() MODEL_CONFIG_PATH: Path = (CONFIG_DIR / "model_config.toml").resolve().absolute() MMC_VERSION: str = "1.0.0" -CONFIG_VERSION: str = "8.5.0" +CONFIG_VERSION: str = "8.5.1" MODEL_CONFIG_VERSION: str = "1.13.1" logger = get_logger("config") @@ -74,21 +74,18 @@ class Config(ConfigBase): personality: PersonalityConfig = Field(default_factory=PersonalityConfig) """人格配置类""" + chat: ChatConfig = Field(default_factory=ChatConfig) + """聊天配置类""" + visual: VisualConfig = Field(default_factory=VisualConfig) """视觉配置类""" expression: ExpressionConfig = Field(default_factory=ExpressionConfig) """表达配置类""" - chat: ChatConfig = Field(default_factory=ChatConfig) - """聊天配置类""" - memory: MemoryConfig = Field(default_factory=MemoryConfig) """记忆配置类""" - relationship: RelationshipConfig = Field(default_factory=RelationshipConfig) - """关系配置类""" - message_receive: MessageReceiveConfig = Field(default_factory=MessageReceiveConfig) """消息接收配置类""" diff --git a/src/config/official_configs.py b/src/config/official_configs.py index 7f3cd08e..58d577f6 100644 --- a/src/config/official_configs.py +++ b/src/config/official_configs.py @@ -173,21 +173,6 @@ class VisualConfig(ConfigBase): """_wrap_识图提示词,不建议修改""" -class RelationshipConfig(ConfigBase): - """关系配置类""" - - __ui_parent__ = "debug" - - enable_relationship: bool = Field( - default=True, - json_schema_extra={ - "x-widget": "switch", - "x-icon": "heart", - }, - ) - """是否启用关系系统,关系系统被移除,此部分配置暂时无效""" - - class TalkRulesItem(ConfigBase): platform: str = "" """平台,与ID一起留空表示全局""" @@ -243,6 +228,16 @@ class ChatConfig(ConfigBase): }, ) """上下文长度""" + + planner_interrupt_max_consecutive_count: int = Field( + default=2, + ge=0, + json_schema_extra={ + "x-widget": "input", + "x-icon": "pause-circle", + }, + ) + """Planner 连续被新消息打断的最大次数,0 表示不启用打断""" plan_reply_log_max_per_chat: int = Field( default=1024, @@ -1465,16 +1460,6 @@ class MaiSakaConfig(ConfigBase): ) """MaiSaka 使用的用户名称""" - planner_interrupt_max_consecutive_count: int = Field( - default=2, - ge=0, - json_schema_extra={ - "x-widget": "input", - "x-icon": "pause-circle", - }, - ) - """Planner 连续被新消息打断的最大次数,0 表示不启用打断""" - tool_filter_task_name: str = Field( default="utils", json_schema_extra={ diff --git a/src/maisaka/runtime.py b/src/maisaka/runtime.py index 2fee2bb7..44e33040 100644 --- a/src/maisaka/runtime.py +++ b/src/maisaka/runtime.py @@ -94,7 +94,7 @@ class MaisakaHeartFlowChatting: self._planner_interrupt_consecutive_count = 0 self._planner_interrupt_max_consecutive_count = max( 0, - int(global_config.maisaka.planner_interrupt_max_consecutive_count), + int(global_config.chat.planner_interrupt_max_consecutive_count), ) expr_use, jargon_learn, expr_learn = ExpressionConfigUtils.get_expression_config_for_chat(session_id) diff --git a/src/maisaka/tool_handlers.py b/src/maisaka/tool_handlers.py deleted file mode 100644 index 9aac35c1..00000000 --- a/src/maisaka/tool_handlers.py +++ /dev/null @@ -1,117 +0,0 @@ -""" -MaiSaka 工具处理器。 -""" - -from datetime import datetime -from typing import TYPE_CHECKING, Optional - -import json as _json - -from rich.panel import Panel - -from src.cli.console import console -from src.cli.input_reader import InputReader -from src.llm_models.payload_content.tool_option import ToolCall - -from .context_messages import LLMContextMessage, ToolResultMessage - -if TYPE_CHECKING: - from src.mcp_module import MCPManager - - -class ToolHandlerContext: - """工具处理器共享上下文。""" - - def __init__( - self, - reader: InputReader, - user_input_times: list[datetime], - ) -> None: - self.reader = reader - self.user_input_times = user_input_times - self.last_user_input_time: Optional[datetime] = None - - -async def handle_wait(tc: ToolCall, chat_history: list[LLMContextMessage], ctx: ToolHandlerContext) -> str: - """处理 wait 工具。""" - seconds = (tc.args or {}).get("seconds", 30) - seconds = max(5, min(seconds, 300)) - console.print(f"[accent]调用工具: wait({seconds})[/accent]") - - tool_result = await _do_wait(seconds, ctx) - chat_history.append( - ToolResultMessage( - content=tool_result, - timestamp=datetime.now(), - tool_call_id=tc.call_id, - tool_name=tc.func_name, - ) - ) - return tool_result - - -async def _do_wait(seconds: int, ctx: ToolHandlerContext) -> str: - """等待用户输入,支持超时。""" - console.print(f"[muted]等待用户输入中(超时: {seconds} 秒)...[/muted]") - console.print("[bold magenta]> [/bold magenta]", end="") - - user_input = await ctx.reader.get_line(timeout=seconds) - - if user_input is None: - console.print() - console.print("[muted]等待超时[/muted]") - return "等待超时,未收到用户输入。" - - user_input = user_input.strip() - if not user_input: - return "用户提交了空输入。" - - now = datetime.now() - ctx.last_user_input_time = now - ctx.user_input_times.append(now) - - if user_input.lower() in ("/quit", "/exit", "/q"): - return "[[QUIT]] 用户请求退出。" - - return f"已收到用户输入: {user_input}" - - -async def handle_mcp_tool(tc: ToolCall, chat_history: list[LLMContextMessage], mcp_manager: "MCPManager") -> None: - """处理 MCP 工具调用。""" - args_str = _json.dumps(tc.args or {}, ensure_ascii=False) - args_preview = args_str if len(args_str) <= 120 else args_str[:120] + "..." - console.print(f"[accent]调用 MCP 工具: {tc.func_name}({args_preview})[/accent]") - - with console.status(f"[info]正在执行 MCP 工具 {tc.func_name}...[/info]", spinner="dots"): - result = await mcp_manager.call_tool(tc.func_name, tc.args or {}) - - display_text = result if len(result) <= 800 else result[:800] + "\n...(已截断)" - console.print( - Panel( - display_text, - title=f"MCP 工具:{tc.func_name}", - border_style="bright_green", - padding=(0, 1), - ) - ) - chat_history.append( - ToolResultMessage( - content=result, - timestamp=datetime.now(), - tool_call_id=tc.call_id, - tool_name=tc.func_name, - ) - ) - - -async def handle_unknown_tool(tc: ToolCall, chat_history: list[LLMContextMessage]) -> None: - """处理未知工具调用。""" - console.print(f"[accent]调用未知工具: {tc.func_name}({tc.args})[/accent]") - chat_history.append( - ToolResultMessage( - content=f"未知工具: {tc.func_name}", - timestamp=datetime.now(), - tool_call_id=tc.call_id, - tool_name=tc.func_name, - ) - ) diff --git a/src/webui/api/planner.py b/src/webui/api/planner.py deleted file mode 100644 index c1d36955..00000000 --- a/src/webui/api/planner.py +++ /dev/null @@ -1,303 +0,0 @@ -""" -规划器监控API -提供规划器日志数据的查询接口 - -性能优化: -1. 聊天摘要只统计文件数量和最新时间戳,不读取文件内容 -2. 日志列表使用文件名解析时间戳,只在需要时读取完整内容 -3. 详情按需加载 -""" - -import json -from pathlib import Path -from typing import Dict, List, Optional - -from fastapi import APIRouter, Depends, HTTPException, Query -from pydantic import BaseModel - -from src.webui.dependencies import require_auth - -router = APIRouter(prefix="/api/planner", tags=["planner"], dependencies=[Depends(require_auth)]) - -# 规划器日志目录 -PLAN_LOG_DIR = Path("logs/plan") - - -class ChatSummary(BaseModel): - """聊天摘要 - 轻量级,不读取文件内容""" - - chat_id: str - plan_count: int - latest_timestamp: float - latest_filename: str - - -class PlanLogSummary(BaseModel): - """规划日志摘要""" - - chat_id: str - timestamp: float - filename: str - action_count: int - action_types: List[str] # 动作类型列表 - total_plan_ms: float - llm_duration_ms: float - reasoning_preview: str - - -class PlanLogDetail(BaseModel): - """规划日志详情""" - - type: str - chat_id: str - timestamp: float - prompt: str - reasoning: str - raw_output: str - actions: List[Dict] - timing: Dict - extra: Optional[Dict] = None - - -class PlannerOverview(BaseModel): - """规划器总览 - 轻量级统计""" - - total_chats: int - total_plans: int - chats: List[ChatSummary] - - -class PaginatedChatLogs(BaseModel): - """分页的聊天日志列表""" - - data: List[PlanLogSummary] - total: int - page: int - page_size: int - chat_id: str - - -def parse_timestamp_from_filename(filename: str) -> float: - """从文件名解析时间戳: 1766497488220_af92bdb1.json -> 1766497488.220""" - try: - timestamp_str = filename.split("_")[0] - # 时间戳是毫秒级,需要转换为秒 - return float(timestamp_str) / 1000 - except (ValueError, IndexError): - return 0 - - -@router.get("/overview", response_model=PlannerOverview) -async def get_planner_overview(): - """ - 获取规划器总览 - 轻量级接口 - 只统计文件数量,不读取文件内容 - """ - if not PLAN_LOG_DIR.exists(): - return PlannerOverview(total_chats=0, total_plans=0, chats=[]) - - chats = [] - total_plans = 0 - - for chat_dir in PLAN_LOG_DIR.iterdir(): - if not chat_dir.is_dir(): - continue - - # 只统计json文件数量 - json_files = list(chat_dir.glob("*.json")) - plan_count = len(json_files) - total_plans += plan_count - - if plan_count == 0: - continue - - # 从文件名获取最新时间戳 - latest_file = max(json_files, key=lambda f: parse_timestamp_from_filename(f.name)) - latest_timestamp = parse_timestamp_from_filename(latest_file.name) - - chats.append( - ChatSummary( - chat_id=chat_dir.name, - plan_count=plan_count, - latest_timestamp=latest_timestamp, - latest_filename=latest_file.name, - ) - ) - - # 按最新时间戳排序 - chats.sort(key=lambda x: x.latest_timestamp, reverse=True) - - return PlannerOverview(total_chats=len(chats), total_plans=total_plans, chats=chats) - - -@router.get("/chat/{chat_id}/logs", response_model=PaginatedChatLogs) -async def get_chat_plan_logs( - chat_id: str, - page: int = Query(1, ge=1), - page_size: int = Query(20, ge=1, le=100), - search: Optional[str] = Query(None, description="搜索关键词,匹配提示词内容"), -): - """ - 获取指定聊天的规划日志列表(分页) - 需要读取文件内容获取摘要信息 - 支持搜索提示词内容 - """ - chat_dir = PLAN_LOG_DIR / chat_id - if not chat_dir.exists(): - return PaginatedChatLogs(data=[], total=0, page=page, page_size=page_size, chat_id=chat_id) - - # 先获取所有文件并按时间戳排序 - json_files = list(chat_dir.glob("*.json")) - json_files.sort(key=lambda f: parse_timestamp_from_filename(f.name), reverse=True) - - # 如果有搜索关键词,需要过滤文件 - if search: - search_lower = search.lower() - filtered_files = [] - for log_file in json_files: - try: - with open(log_file, "r", encoding="utf-8") as f: - data = json.load(f) - prompt = data.get("prompt", "") - if search_lower in prompt.lower(): - filtered_files.append(log_file) - except Exception: - continue - json_files = filtered_files - - total = len(json_files) - - # 分页 - 只读取当前页的文件 - offset = (page - 1) * page_size - page_files = json_files[offset : offset + page_size] - - logs = [] - for log_file in page_files: - try: - with open(log_file, "r", encoding="utf-8") as f: - data = json.load(f) - reasoning = data.get("reasoning", "") - actions = data.get("actions", []) - action_types = [a.get("action_type", "") for a in actions if a.get("action_type")] - logs.append( - PlanLogSummary( - chat_id=data.get("chat_id", chat_id), - timestamp=data.get("timestamp", parse_timestamp_from_filename(log_file.name)), - filename=log_file.name, - action_count=len(actions), - action_types=action_types, - total_plan_ms=data.get("timing", {}).get("total_plan_ms", 0), - llm_duration_ms=data.get("timing", {}).get("llm_duration_ms", 0), - reasoning_preview=reasoning[:100] if reasoning else "", - ) - ) - except Exception: - # 文件读取失败时使用文件名信息 - logs.append( - PlanLogSummary( - chat_id=chat_id, - timestamp=parse_timestamp_from_filename(log_file.name), - filename=log_file.name, - action_count=0, - action_types=[], - total_plan_ms=0, - llm_duration_ms=0, - reasoning_preview="[读取失败]", - ) - ) - - return PaginatedChatLogs(data=logs, total=total, page=page, page_size=page_size, chat_id=chat_id) - - -@router.get("/log/{chat_id}/{filename}", response_model=PlanLogDetail) -async def get_log_detail(chat_id: str, filename: str): - """获取规划日志详情 - 按需加载完整内容""" - log_file = PLAN_LOG_DIR / chat_id / filename - if not log_file.exists(): - raise HTTPException(status_code=404, detail="日志文件不存在") - - try: - with open(log_file, "r", encoding="utf-8") as f: - data = json.load(f) - return PlanLogDetail(**data) - except Exception as e: - raise HTTPException(status_code=500, detail=f"读取日志失败: {str(e)}") from e - - -# ========== 兼容旧接口 ========== - - -@router.get("/stats") -async def get_planner_stats(): - """获取规划器统计信息 - 兼容旧接口""" - overview = await get_planner_overview() - - # 获取最近10条计划的摘要 - recent_plans = [] - for chat in overview.chats[:5]: # 从最近5个聊天中获取 - try: - chat_logs = await get_chat_plan_logs(chat.chat_id, page=1, page_size=2) - recent_plans.extend(chat_logs.data) - except Exception: - continue - - # 按时间排序取前10 - recent_plans.sort(key=lambda x: x.timestamp, reverse=True) - recent_plans = recent_plans[:10] - - return { - "total_chats": overview.total_chats, - "total_plans": overview.total_plans, - "avg_plan_time_ms": 0, - "avg_llm_time_ms": 0, - "recent_plans": recent_plans, - } - - -@router.get("/chats") -async def get_chat_list(): - """获取所有聊天ID列表 - 兼容旧接口""" - overview = await get_planner_overview() - return [chat.chat_id for chat in overview.chats] - - -@router.get("/all-logs") -async def get_all_logs(page: int = Query(1, ge=1), page_size: int = Query(20, ge=1, le=100)): - """获取所有规划日志 - 兼容旧接口""" - if not PLAN_LOG_DIR.exists(): - return {"data": [], "total": 0, "page": page, "page_size": page_size} - - # 收集所有文件 - all_files = [] - for chat_dir in PLAN_LOG_DIR.iterdir(): - if chat_dir.is_dir(): - all_files.extend((chat_dir.name, log_file) for log_file in chat_dir.glob("*.json")) - - # 按时间戳排序 - all_files.sort(key=lambda x: parse_timestamp_from_filename(x[1].name), reverse=True) - - total = len(all_files) - offset = (page - 1) * page_size - page_files = all_files[offset : offset + page_size] - - logs = [] - for chat_id, log_file in page_files: - try: - with open(log_file, "r", encoding="utf-8") as f: - data = json.load(f) - reasoning = data.get("reasoning", "") - logs.append( - { - "chat_id": data.get("chat_id", chat_id), - "timestamp": data.get("timestamp", parse_timestamp_from_filename(log_file.name)), - "filename": log_file.name, - "action_count": len(data.get("actions", [])), - "total_plan_ms": data.get("timing", {}).get("total_plan_ms", 0), - "llm_duration_ms": data.get("timing", {}).get("llm_duration_ms", 0), - "reasoning_preview": reasoning[:100] if reasoning else "", - } - ) - except Exception: - continue - - return {"data": logs, "total": total, "page": page, "page_size": page_size} diff --git a/src/webui/api/replier.py b/src/webui/api/replier.py deleted file mode 100644 index fff82267..00000000 --- a/src/webui/api/replier.py +++ /dev/null @@ -1,273 +0,0 @@ -""" -回复器监控API -提供回复器日志数据的查询接口 - -性能优化: -1. 聊天摘要只统计文件数量和最新时间戳,不读取文件内容 -2. 日志列表使用文件名解析时间戳,只在需要时读取完整内容 -3. 详情按需加载 -""" - -import json -from pathlib import Path -from typing import Dict, List, Optional - -from fastapi import APIRouter, Depends, HTTPException, Query -from pydantic import BaseModel - -from src.webui.dependencies import require_auth - -router = APIRouter(prefix="/api/replier", tags=["replier"], dependencies=[Depends(require_auth)]) - -# 回复器日志目录 -REPLY_LOG_DIR = Path("logs/reply") - - -class ReplierChatSummary(BaseModel): - """聊天摘要 - 轻量级,不读取文件内容""" - - chat_id: str - reply_count: int - latest_timestamp: float - latest_filename: str - - -class ReplyLogSummary(BaseModel): - """回复日志摘要""" - - chat_id: str - timestamp: float - filename: str - model: str - success: bool - llm_ms: float - overall_ms: float - output_preview: str - - -class ReplyLogDetail(BaseModel): - """回复日志详情""" - - type: str - chat_id: str - timestamp: float - prompt: str - output: str - processed_output: List[str] - model: str - reasoning: str - think_level: int - timing: Dict - error: Optional[str] = None - success: bool - - -class ReplierOverview(BaseModel): - """回复器总览 - 轻量级统计""" - - total_chats: int - total_replies: int - chats: List[ReplierChatSummary] - - -class PaginatedReplyLogs(BaseModel): - """分页的回复日志列表""" - - data: List[ReplyLogSummary] - total: int - page: int - page_size: int - chat_id: str - - -def parse_timestamp_from_filename(filename: str) -> float: - """从文件名解析时间戳: 1766497488220_af92bdb1.json -> 1766497488.220""" - try: - timestamp_str = filename.split("_")[0] - # 时间戳是毫秒级,需要转换为秒 - return float(timestamp_str) / 1000 - except (ValueError, IndexError): - return 0 - - -@router.get("/overview", response_model=ReplierOverview) -async def get_replier_overview(): - """ - 获取回复器总览 - 轻量级接口 - 只统计文件数量,不读取文件内容 - """ - if not REPLY_LOG_DIR.exists(): - return ReplierOverview(total_chats=0, total_replies=0, chats=[]) - - chats = [] - total_replies = 0 - - for chat_dir in REPLY_LOG_DIR.iterdir(): - if not chat_dir.is_dir(): - continue - - # 只统计json文件数量 - json_files = list(chat_dir.glob("*.json")) - reply_count = len(json_files) - total_replies += reply_count - - if reply_count == 0: - continue - - # 从文件名获取最新时间戳 - latest_file = max(json_files, key=lambda f: parse_timestamp_from_filename(f.name)) - latest_timestamp = parse_timestamp_from_filename(latest_file.name) - - chats.append( - ReplierChatSummary( - chat_id=chat_dir.name, - reply_count=reply_count, - latest_timestamp=latest_timestamp, - latest_filename=latest_file.name, - ) - ) - - # 按最新时间戳排序 - chats.sort(key=lambda x: x.latest_timestamp, reverse=True) - - return ReplierOverview(total_chats=len(chats), total_replies=total_replies, chats=chats) - - -@router.get("/chat/{chat_id}/logs", response_model=PaginatedReplyLogs) -async def get_chat_reply_logs( - chat_id: str, - page: int = Query(1, ge=1), - page_size: int = Query(20, ge=1, le=100), - search: Optional[str] = Query(None, description="搜索关键词,匹配提示词内容"), -): - """ - 获取指定聊天的回复日志列表(分页) - 需要读取文件内容获取摘要信息 - 支持搜索提示词内容 - """ - chat_dir = REPLY_LOG_DIR / chat_id - if not chat_dir.exists(): - return PaginatedReplyLogs(data=[], total=0, page=page, page_size=page_size, chat_id=chat_id) - - # 先获取所有文件并按时间戳排序 - json_files = list(chat_dir.glob("*.json")) - json_files.sort(key=lambda f: parse_timestamp_from_filename(f.name), reverse=True) - - # 如果有搜索关键词,需要过滤文件 - if search: - search_lower = search.lower() - filtered_files = [] - for log_file in json_files: - try: - with open(log_file, "r", encoding="utf-8") as f: - data = json.load(f) - prompt = data.get("prompt", "") - if search_lower in prompt.lower(): - filtered_files.append(log_file) - except Exception: - continue - json_files = filtered_files - - total = len(json_files) - - # 分页 - 只读取当前页的文件 - offset = (page - 1) * page_size - page_files = json_files[offset : offset + page_size] - - logs = [] - for log_file in page_files: - try: - with open(log_file, "r", encoding="utf-8") as f: - data = json.load(f) - output = data.get("output", "") - logs.append( - ReplyLogSummary( - chat_id=data.get("chat_id", chat_id), - timestamp=data.get("timestamp", parse_timestamp_from_filename(log_file.name)), - filename=log_file.name, - model=data.get("model", ""), - success=data.get("success", True), - llm_ms=data.get("timing", {}).get("llm_ms", 0), - overall_ms=data.get("timing", {}).get("overall_ms", 0), - output_preview=output[:100] if output else "", - ) - ) - except Exception: - # 文件读取失败时使用文件名信息 - logs.append( - ReplyLogSummary( - chat_id=chat_id, - timestamp=parse_timestamp_from_filename(log_file.name), - filename=log_file.name, - model="", - success=False, - llm_ms=0, - overall_ms=0, - output_preview="[读取失败]", - ) - ) - - return PaginatedReplyLogs(data=logs, total=total, page=page, page_size=page_size, chat_id=chat_id) - - -@router.get("/log/{chat_id}/{filename}", response_model=ReplyLogDetail) -async def get_reply_log_detail(chat_id: str, filename: str): - """获取回复日志详情 - 按需加载完整内容""" - log_file = REPLY_LOG_DIR / chat_id / filename - if not log_file.exists(): - raise HTTPException(status_code=404, detail="日志文件不存在") - - try: - with open(log_file, "r", encoding="utf-8") as f: - data = json.load(f) - return ReplyLogDetail( - type=data.get("type", "reply"), - chat_id=data.get("chat_id", chat_id), - timestamp=data.get("timestamp", 0), - prompt=data.get("prompt", ""), - output=data.get("output", ""), - processed_output=data.get("processed_output", []), - model=data.get("model", ""), - reasoning=data.get("reasoning", ""), - think_level=data.get("think_level", 0), - timing=data.get("timing", {}), - error=data.get("error"), - success=data.get("success", True), - ) - except Exception as e: - raise HTTPException(status_code=500, detail=f"读取日志失败: {str(e)}") from e - - -# ========== 兼容接口 ========== - - -@router.get("/stats") -async def get_replier_stats(): - """获取回复器统计信息""" - overview = await get_replier_overview() - - # 获取最近10条回复的摘要 - recent_replies = [] - for chat in overview.chats[:5]: # 从最近5个聊天中获取 - try: - chat_logs = await get_chat_reply_logs(chat.chat_id, page=1, page_size=2) - recent_replies.extend(chat_logs.data) - except Exception: - continue - - # 按时间排序取前10 - recent_replies.sort(key=lambda x: x.timestamp, reverse=True) - recent_replies = recent_replies[:10] - - return { - "total_chats": overview.total_chats, - "total_replies": overview.total_replies, - "recent_replies": recent_replies, - } - - -@router.get("/chats") -async def get_replier_chat_list(): - """获取所有聊天ID列表""" - overview = await get_replier_overview() - return [chat.chat_id for chat in overview.chats]