diff --git a/.gitignore b/.gitignore index 328a86aa..844e60a5 100644 --- a/.gitignore +++ b/.gitignore @@ -51,6 +51,7 @@ template/compare/model_config_template.toml src/plugins/utils/statistic.py CLAUDE.md MaiBot-Dashboard/ +cloudflare-workers/ # Byte-compiled / optimized / DLL files __pycache__/ diff --git a/bot.py b/bot.py index 7ba9af4b..68c6e110 100644 --- a/bot.py +++ b/bot.py @@ -75,6 +75,15 @@ async def graceful_shutdown(): # sourcery skip: use-named-expression try: logger.info("正在优雅关闭麦麦...") + # 关闭 WebUI 服务器 + try: + from src.webui.webui_server import get_webui_server + webui_server = get_webui_server() + if webui_server and webui_server._server: + await webui_server.shutdown() + except Exception as e: + logger.warning(f"关闭 WebUI 服务器时出错: {e}") + from src.plugin_system.core.events_manager import events_manager from src.plugin_system.base.component_types import EventType diff --git a/changelogs/changelog.md b/changelogs/changelog.md index ba6921be..6f60b468 100644 --- a/changelogs/changelog.md +++ b/changelogs/changelog.md @@ -1,7 +1,10 @@ # Changelog ## [0.11.5] - 2025-11-21 +### 功能更改和修复 +- 优化planner和replyer的协同 +- 细化debug的log -## [0.11.4] - 2025-11-20 +## [0.11.4] - 2025-11-19 ### 🌟 主要更新内容 - **首个官方 Web 管理界面上线**:在此版本之前,MaiBot 没有 WebUI,所有配置需手动编辑 TOML 文件 - **认证系统**:Token 安全登录(支持系统生成 64 位随机令牌 / 自定义 Token),首次配置向导 @@ -41,7 +44,7 @@ 告别手动编辑配置文件,享受现代化图形界面! -## [0.11.3] - 2025-11-19 +## [0.11.3] - 2025-11-18 ### 功能更改和修复 - 优化记忆提取策略 - 优化黑话提取 diff --git a/src/chat/heart_flow/heartFC_chat.py b/src/chat/heart_flow/heartFC_chat.py index dbbd39ef..bb37e29e 100644 --- a/src/chat/heart_flow/heartFC_chat.py +++ b/src/chat/heart_flow/heartFC_chat.py @@ -230,7 +230,7 @@ class HeartFChatting: if (message.is_mentioned or message.is_at) and global_config.chat.mentioned_bot_reply: mentioned_message = message - logger.info(f"{self.log_prefix} 当前talk_value: {global_config.chat.get_talk_value(self.stream_id)}") + # logger.info(f"{self.log_prefix} 当前talk_value: {global_config.chat.get_talk_value(self.stream_id)}") # *控制频率用 if mentioned_message: @@ -410,7 +410,7 @@ class HeartFChatting: # asyncio.create_task(self.chat_history_summarizer.process()) cycle_timers, thinking_id = self.start_cycle() - logger.info(f"{self.log_prefix} 开始第{self._cycle_counter}次思考") + logger.info(f"{self.log_prefix} 开始第{self._cycle_counter}次思考(频率: {global_config.chat.get_talk_value(self.stream_id)})") # 第一步:动作检查 available_actions: Dict[str, ActionInfo] = {} diff --git a/src/chat/knowledge/qa_manager.py b/src/chat/knowledge/qa_manager.py index 9988dc22..1a087a7f 100644 --- a/src/chat/knowledge/qa_manager.py +++ b/src/chat/knowledge/qa_manager.py @@ -92,9 +92,10 @@ class QAManager: # 过滤阈值 result = dyn_select_top_k(result, 0.5, 1.0) - for res in result: - raw_paragraph = self.embed_manager.paragraphs_embedding_store.store[res[0]].str - logger.info(f"找到相关文段,相关系数:{res[1]:.8f}\n{raw_paragraph}\n\n") + if global_config.debug.show_lpmm_paragraph: + for res in result: + raw_paragraph = self.embed_manager.paragraphs_embedding_store.store[res[0]].str + logger.info(f"找到相关文段,相关系数:{res[1]:.8f}\n{raw_paragraph}\n\n") return result, ppr_node_weights diff --git a/src/chat/planner_actions/planner.py b/src/chat/planner_actions/planner.py index 7af3291a..5771feed 100644 --- a/src/chat/planner_actions/planner.py +++ b/src/chat/planner_actions/planner.py @@ -7,7 +7,6 @@ from typing import Dict, Optional, Tuple, List, TYPE_CHECKING, Union from rich.traceback import install from datetime import datetime from json_repair import repair_json - from src.llm_models.utils_model import LLMRequest from src.config.config import global_config, model_config from src.common.logger import get_logger @@ -164,6 +163,45 @@ class ActionPlanner: return item[1] return None + def _replace_message_ids_with_text( + self, text: Optional[str], message_id_list: List[Tuple[str, "DatabaseMessages"]] + ) -> Optional[str]: + """将文本中的 m+数字 消息ID替换为原消息内容,并添加双引号""" + if not text: + return text + + id_to_message = {msg_id: msg for msg_id, msg in message_id_list} + + # 匹配m后带2-4位数字,前后不是字母数字下划线 + pattern = r"(? str: + msg_id = match.group(0) + message = id_to_message.get(msg_id) + if not message: + logger.warning(f"{self.log_prefix}planner理由引用 {msg_id} 未找到对应消息,保持原样") + return msg_id + + msg_text = (message.processed_plain_text or message.display_message or "").strip() + if not msg_text: + logger.warning(f"{self.log_prefix}planner理由引用 {msg_id} 的消息内容为空,保持原样") + return msg_id + + preview = msg_text if len(msg_text) <= 100 else f"{msg_text[:97]}..." + logger.info(f"{self.log_prefix}planner理由引用 {msg_id} -> 消息({preview})") + return f"消息({msg_text})" + + return re.sub(pattern, _replace, text) + def _parse_single_action( self, action_json: dict, @@ -176,7 +214,10 @@ class ActionPlanner: try: action = action_json.get("action", "no_reply") - reasoning = action_json.get("reason", "未提供原因") + original_reasoning = action_json.get("reason", "未提供原因") + reasoning = self._replace_message_ids_with_text(original_reasoning, message_id_list) + if reasoning is None: + reasoning = original_reasoning action_data = {key: value for key, value in action_json.items() if key not in ["action", "reason"]} # 非no_reply动作需要target_message_id target_message = None @@ -573,9 +614,6 @@ class ActionPlanner: # 调用LLM llm_content, (reasoning_content, _, _) = await self.planner_llm.generate_response_async(prompt=prompt) - # logger.info(f"{self.log_prefix}规划器原始提示词: {prompt}") - # logger.info(f"{self.log_prefix}规划器原始响应: {llm_content}") - if global_config.debug.show_planner_prompt: logger.info(f"{self.log_prefix}规划器原始提示词: {prompt}") logger.info(f"{self.log_prefix}规划器原始响应: {llm_content}") @@ -604,6 +642,7 @@ class ActionPlanner: if llm_content: try: json_objects, extracted_reasoning = self._extract_json_from_markdown(llm_content) + extracted_reasoning = self._replace_message_ids_with_text(extracted_reasoning, message_id_list) or "" if json_objects: logger.debug(f"{self.log_prefix}从响应中提取到{len(json_objects)}个JSON对象") filtered_actions_list = list(filtered_actions.items()) diff --git a/src/chat/utils/chat_history_summarizer.py b/src/chat/utils/chat_history_summarizer.py index f471781d..7fa55834 100644 --- a/src/chat/utils/chat_history_summarizer.py +++ b/src/chat/utils/chat_history_summarizer.py @@ -107,7 +107,7 @@ class ChatHistorySummarizer: self.last_check_time = current_time return - logger.info( + logger.debug( f"{self.log_prefix} 开始处理聊天概括,时间窗口: {self.last_check_time:.2f} -> {current_time:.2f}" ) @@ -119,7 +119,7 @@ class ChatHistorySummarizer: before_count = len(self.current_batch.messages) self.current_batch.messages.extend(new_messages) self.current_batch.end_time = current_time - logger.info(f"{self.log_prefix} 批次更新: {before_count} -> {len(self.current_batch.messages)} 条消息") + logger.info(f"{self.log_prefix} 更新聊天话题: {before_count} -> {len(self.current_batch.messages)} 条消息") else: # 创建新批次 self.current_batch = MessageBatch( @@ -127,7 +127,7 @@ class ChatHistorySummarizer: start_time=new_messages[0].time if new_messages else current_time, end_time=current_time, ) - logger.info(f"{self.log_prefix} 新建批次: {len(new_messages)} 条消息") + logger.info(f"{self.log_prefix} 新建聊天话题: {len(new_messages)} 条消息") # 检查是否需要打包 await self._check_and_package(current_time) diff --git a/src/common/server.py b/src/common/server.py index ebdc9fa2..88608677 100644 --- a/src/common/server.py +++ b/src/common/server.py @@ -1,5 +1,4 @@ from fastapi import FastAPI, APIRouter -from fastapi.middleware.cors import CORSMiddleware # 新增导入 from typing import Optional from uvicorn import Config, Server as UvicornServer import asyncio @@ -17,21 +16,6 @@ class Server: self._server: Optional[UvicornServer] = None self.set_address(host, port) - # 配置 CORS - origins = [ - "http://localhost:7999", # 允许的前端源 - "http://127.0.0.1:7999", - # 在生产环境中,您应该添加实际的前端域名 - ] - - self.app.add_middleware( - CORSMiddleware, - allow_origins=origins, - allow_credentials=True, # 是否支持 cookie - allow_methods=["*"], # 允许所有 HTTP 方法 - allow_headers=["*"], # 允许所有 HTTP 请求头 - ) - def register_router(self, router: APIRouter, prefix: str = ""): """注册路由 diff --git a/src/config/official_configs.py b/src/config/official_configs.py index 454f6976..fc084ecb 100644 --- a/src/config/official_configs.py +++ b/src/config/official_configs.py @@ -581,9 +581,15 @@ class DebugConfig(ConfigBase): show_jargon_prompt: bool = False """是否显示jargon相关提示词""" + show_memory_prompt: bool = False + """是否显示记忆检索相关prompt""" + show_planner_prompt: bool = False """是否显示planner相关提示词""" + show_lpmm_paragraph: bool = False + """是否显示lpmm找到的相关文段日志""" + @dataclass class ExperimentalConfig(ConfigBase): diff --git a/src/jargon/jargon_miner.py b/src/jargon/jargon_miner.py index db1b79f9..4319ad3d 100644 --- a/src/jargon/jargon_miner.py +++ b/src/jargon/jargon_miner.py @@ -384,10 +384,10 @@ class JargonMiner: logger.error(f"jargon {content} 推断2解析失败: {e}") return - logger.info(f"jargon {content} 推断2提示词: {prompt2}") - logger.info(f"jargon {content} 推断2结果: {response2}") - logger.info(f"jargon {content} 推断1提示词: {prompt1}") - logger.info(f"jargon {content} 推断1结果: {response1}") + # logger.info(f"jargon {content} 推断2提示词: {prompt2}") + # logger.info(f"jargon {content} 推断2结果: {response2}") + # logger.info(f"jargon {content} 推断1提示词: {prompt1}") + # logger.info(f"jargon {content} 推断1结果: {response1}") if global_config.debug.show_jargon_prompt: logger.info(f"jargon {content} 推断2提示词: {prompt2}") diff --git a/src/main.py b/src/main.py index b442f29d..09ead248 100644 --- a/src/main.py +++ b/src/main.py @@ -36,25 +36,13 @@ class MainSystem: # 使用消息API替代直接的FastAPI实例 self.app: MessageServer = get_global_api() self.server: Server = get_global_server() + self.webui_server = None # 独立的 WebUI 服务器 - # 注册 WebUI API 路由 - self._register_webui_routes() + # 设置独立的 WebUI 服务器 + self._setup_webui_server() - # 设置 WebUI(开发/生产模式) - self._setup_webui() - - def _register_webui_routes(self): - """注册 WebUI API 路由""" - try: - from src.webui.routes import router as webui_router - - self.server.register_router(webui_router) - logger.info("WebUI API 路由已注册") - except Exception as e: - logger.warning(f"注册 WebUI API 路由失败: {e}") - - def _setup_webui(self): - """设置 WebUI(根据环境变量决定模式)""" + def _setup_webui_server(self): + """设置独立的 WebUI 服务器""" import os webui_enabled = os.getenv("WEBUI_ENABLED", "false").lower() == "true" @@ -65,11 +53,22 @@ class MainSystem: webui_mode = os.getenv("WEBUI_MODE", "production").lower() try: - from src.webui.manager import setup_webui + from src.webui.webui_server import get_webui_server - setup_webui(mode=webui_mode) + self.webui_server = get_webui_server() + + if webui_mode == "development": + logger.info("📝 WebUI 开发模式已启用") + logger.info("🌐 后端 API 将运行在 http://0.0.0.0:8001") + logger.info("💡 请手动启动前端开发服务器: cd MaiBot-Dashboard && bun dev") + logger.info("💡 前端将运行在 http://localhost:7999") + else: + logger.info("✅ WebUI 生产模式已启用") + logger.info(f"🌐 WebUI 将运行在 http://0.0.0.0:8001") + logger.info("💡 请确保已构建前端: cd MaiBot-Dashboard && bun run build") + except Exception as e: - logger.error(f"设置 WebUI 失败: {e}") + logger.error(f"❌ 初始化 WebUI 服务器失败: {e}") async def initialize(self): """初始化系统组件""" @@ -164,6 +163,10 @@ class MainSystem: self.server.run(), ] + # 如果 WebUI 服务器已初始化,添加到任务列表 + if self.webui_server: + tasks.append(self.webui_server.start()) + await asyncio.gather(*tasks) # async def forget_memory_task(self): diff --git a/src/memory_system/memory_retrieval.py b/src/memory_system/memory_retrieval.py index e9187eb0..7d129bbc 100644 --- a/src/memory_system/memory_retrieval.py +++ b/src/memory_system/memory_retrieval.py @@ -16,8 +16,8 @@ from src.llm_models.payload_content.message import MessageBuilder, RoleType, Mes logger = get_logger("memory_retrieval") -THINKING_BACK_NOT_FOUND_RETENTION_SECONDS = 3600 # 未找到答案记录保留时长 -THINKING_BACK_CLEANUP_INTERVAL_SECONDS = 300 # 清理频率 +THINKING_BACK_NOT_FOUND_RETENTION_SECONDS = 36000 # 未找到答案记录保留时长 +THINKING_BACK_CLEANUP_INTERVAL_SECONDS = 3000 # 清理频率 _last_not_found_cleanup_ts: float = 0.0 @@ -340,7 +340,8 @@ async def _react_agent_solve_question( max_iterations=max_iterations, ) - logger.info(f"ReAct Agent 第 {iteration + 1} 次Prompt: {prompt}") + if global_config.debug.show_memory_prompt: + logger.info(f"ReAct Agent 第 {iteration + 1} 次Prompt: {prompt}") success, response, reasoning_content, model_name, tool_calls = await llm_api.generate_with_model_with_tools( prompt, model_config=model_config.model_task_config.tool_use, @@ -380,42 +381,43 @@ async def _react_agent_solve_question( messages.extend(_conversation_messages) - # 优化日志展示 - 合并所有消息到一条日志 - log_lines = [] - for idx, msg in enumerate(messages, 1): - role_name = msg.role.value if hasattr(msg.role, "value") else str(msg.role) + if global_config.debug.show_memory_prompt: + # 优化日志展示 - 合并所有消息到一条日志 + log_lines = [] + for idx, msg in enumerate(messages, 1): + role_name = msg.role.value if hasattr(msg.role, "value") else str(msg.role) - # 处理内容 - 显示完整内容,不截断 - if isinstance(msg.content, str): - full_content = msg.content - content_type = "文本" - elif isinstance(msg.content, list): - text_parts = [item for item in msg.content if isinstance(item, str)] - image_count = len([item for item in msg.content if isinstance(item, tuple)]) - full_content = "".join(text_parts) if text_parts else "" - content_type = f"混合({len(text_parts)}段文本, {image_count}张图片)" - else: - full_content = str(msg.content) - content_type = "未知" + # 处理内容 - 显示完整内容,不截断 + if isinstance(msg.content, str): + full_content = msg.content + content_type = "文本" + elif isinstance(msg.content, list): + text_parts = [item for item in msg.content if isinstance(item, str)] + image_count = len([item for item in msg.content if isinstance(item, tuple)]) + full_content = "".join(text_parts) if text_parts else "" + content_type = f"混合({len(text_parts)}段文本, {image_count}张图片)" + else: + full_content = str(msg.content) + content_type = "未知" - # 构建单条消息的日志信息 - msg_info = f"\n[消息 {idx}] 角色: {role_name} 内容类型: {content_type}\n========================================" + # 构建单条消息的日志信息 + msg_info = f"\n[消息 {idx}] 角色: {role_name} 内容类型: {content_type}\n========================================" - if full_content: - msg_info += f"\n{full_content}" + if full_content: + msg_info += f"\n{full_content}" - if msg.tool_calls: - msg_info += f"\n 工具调用: {len(msg.tool_calls)}个" - for tool_call in msg.tool_calls: - msg_info += f"\n - {tool_call}" + if msg.tool_calls: + msg_info += f"\n 工具调用: {len(msg.tool_calls)}个" + for tool_call in msg.tool_calls: + msg_info += f"\n - {tool_call}" - if msg.tool_call_id: - msg_info += f"\n 工具调用ID: {msg.tool_call_id}" + if msg.tool_call_id: + msg_info += f"\n 工具调用ID: {msg.tool_call_id}" - log_lines.append(msg_info) + log_lines.append(msg_info) - # 合并所有消息为一条日志输出 - logger.info(f"消息列表 (共{len(messages)}条):{''.join(log_lines)}") + # 合并所有消息为一条日志输出 + logger.info(f"消息列表 (共{len(messages)}条):{''.join(log_lines)}") return messages @@ -1068,7 +1070,8 @@ async def build_memory_retrieval_prompt( request_type="memory.question", ) - logger.info(f"记忆检索问题生成提示词: {question_prompt}") + if global_config.debug.show_memory_prompt: + logger.info(f"记忆检索问题生成提示词: {question_prompt}") logger.info(f"记忆检索问题生成响应: {response}") if not success: diff --git a/src/webui/config_routes.py b/src/webui/config_routes.py index c4a4d417..84c660ae 100644 --- a/src/webui/config_routes.py +++ b/src/webui/config_routes.py @@ -319,6 +319,58 @@ async def update_bot_config_section(section_name: str, section_data: Any = Body( raise HTTPException(status_code=500, detail=f"更新配置节失败: {str(e)}") +# ===== 原始 TOML 文件操作接口 ===== + + +@router.get("/bot/raw") +async def get_bot_config_raw(): + """获取麦麦主程序配置的原始 TOML 内容""" + try: + config_path = os.path.join(CONFIG_DIR, "bot_config.toml") + if not os.path.exists(config_path): + raise HTTPException(status_code=404, detail="配置文件不存在") + + with open(config_path, "r", encoding="utf-8") as f: + raw_content = f.read() + + return {"success": True, "content": raw_content} + except HTTPException: + raise + except Exception as e: + logger.error(f"读取配置文件失败: {e}") + raise HTTPException(status_code=500, detail=f"读取配置文件失败: {str(e)}") + + +@router.post("/bot/raw") +async def update_bot_config_raw(raw_content: str = Body(..., embed=True)): + """更新麦麦主程序配置(直接保存原始 TOML 内容,会先验证格式)""" + try: + # 验证 TOML 格式 + try: + config_data = tomlkit.loads(raw_content) + except Exception as e: + raise HTTPException(status_code=400, detail=f"TOML 格式错误: {str(e)}") + + # 验证配置数据结构 + try: + Config.from_dict(config_data) + except Exception as e: + raise HTTPException(status_code=400, detail=f"配置数据验证失败: {str(e)}") + + # 保存配置文件 + config_path = os.path.join(CONFIG_DIR, "bot_config.toml") + with open(config_path, "w", encoding="utf-8") as f: + f.write(raw_content) + + logger.info("麦麦主程序配置已更新(原始模式)") + return {"success": True, "message": "配置已保存"} + except HTTPException: + raise + except Exception as e: + logger.error(f"保存配置文件失败: {e}") + raise HTTPException(status_code=500, detail=f"保存配置文件失败: {str(e)}") + + @router.post("/model/section/{section_name}") async def update_model_config_section(section_name: str, section_data: Any = Body(...)): """更新模型配置的指定节(保留注释和格式)""" @@ -364,3 +416,144 @@ async def update_model_config_section(section_name: str, section_data: Any = Bod except Exception as e: logger.error(f"更新配置节失败: {e}") raise HTTPException(status_code=500, detail=f"更新配置节失败: {str(e)}") + + +# ===== 适配器配置管理接口 ===== + + +@router.get("/adapter-config/path") +async def get_adapter_config_path(): + """获取保存的适配器配置文件路径""" + try: + # 从 data/webui.json 读取路径偏好 + webui_data_path = os.path.join("data", "webui.json") + if not os.path.exists(webui_data_path): + return {"success": True, "path": None} + + import json + with open(webui_data_path, "r", encoding="utf-8") as f: + webui_data = json.load(f) + + adapter_config_path = webui_data.get("adapter_config_path") + if not adapter_config_path: + return {"success": True, "path": None} + + # 检查文件是否存在并返回最后修改时间 + if os.path.exists(adapter_config_path): + import datetime + mtime = os.path.getmtime(adapter_config_path) + last_modified = datetime.datetime.fromtimestamp(mtime).isoformat() + return {"success": True, "path": adapter_config_path, "lastModified": last_modified} + else: + return {"success": True, "path": adapter_config_path, "lastModified": None} + + except Exception as e: + logger.error(f"获取适配器配置路径失败: {e}") + raise HTTPException(status_code=500, detail=f"获取配置路径失败: {str(e)}") + + +@router.post("/adapter-config/path") +async def save_adapter_config_path(data: dict[str, str] = Body(...)): + """保存适配器配置文件路径偏好""" + try: + path = data.get("path") + if not path: + raise HTTPException(status_code=400, detail="路径不能为空") + + # 保存到 data/webui.json + webui_data_path = os.path.join("data", "webui.json") + import json + + # 读取现有数据 + if os.path.exists(webui_data_path): + with open(webui_data_path, "r", encoding="utf-8") as f: + webui_data = json.load(f) + else: + webui_data = {} + + # 更新路径 + webui_data["adapter_config_path"] = path + + # 保存 + os.makedirs("data", exist_ok=True) + with open(webui_data_path, "w", encoding="utf-8") as f: + json.dump(webui_data, f, ensure_ascii=False, indent=2) + + logger.info(f"适配器配置路径已保存: {path}") + return {"success": True, "message": "路径已保存"} + + except HTTPException: + raise + except Exception as e: + logger.error(f"保存适配器配置路径失败: {e}") + raise HTTPException(status_code=500, detail=f"保存路径失败: {str(e)}") + + +@router.get("/adapter-config") +async def get_adapter_config(path: str): + """从指定路径读取适配器配置文件""" + try: + if not path: + raise HTTPException(status_code=400, detail="路径参数不能为空") + + # 检查文件是否存在 + if not os.path.exists(path): + raise HTTPException(status_code=404, detail=f"配置文件不存在: {path}") + + # 检查文件扩展名 + if not path.endswith(".toml"): + raise HTTPException(status_code=400, detail="只支持 .toml 格式的配置文件") + + # 读取文件内容 + with open(path, "r", encoding="utf-8") as f: + content = f.read() + + logger.info(f"已读取适配器配置: {path}") + return {"success": True, "content": content} + + except HTTPException: + raise + except Exception as e: + logger.error(f"读取适配器配置失败: {e}") + raise HTTPException(status_code=500, detail=f"读取配置失败: {str(e)}") + + +@router.post("/adapter-config") +async def save_adapter_config(data: dict[str, str] = Body(...)): + """保存适配器配置到指定路径""" + try: + path = data.get("path") + content = data.get("content") + + if not path: + raise HTTPException(status_code=400, detail="路径不能为空") + if content is None: + raise HTTPException(status_code=400, detail="配置内容不能为空") + + # 检查文件扩展名 + if not path.endswith(".toml"): + raise HTTPException(status_code=400, detail="只支持 .toml 格式的配置文件") + + # 验证 TOML 格式 + try: + import toml + toml.loads(content) + except Exception as e: + raise HTTPException(status_code=400, detail=f"TOML 格式错误: {str(e)}") + + # 确保目录存在 + os.makedirs(os.path.dirname(path) if os.path.dirname(path) else ".", exist_ok=True) + + # 保存文件 + with open(path, "w", encoding="utf-8") as f: + f.write(content) + + logger.info(f"适配器配置已保存: {path}") + return {"success": True, "message": "配置已保存"} + + except HTTPException: + raise + except Exception as e: + logger.error(f"保存适配器配置失败: {e}") + raise HTTPException(status_code=500, detail=f"保存配置失败: {str(e)}") + diff --git a/src/webui/manager.py b/src/webui/manager.py deleted file mode 100644 index 4dc472e2..00000000 --- a/src/webui/manager.py +++ /dev/null @@ -1,109 +0,0 @@ -"""WebUI 管理器 - 处理开发/生产环境的 WebUI 启动""" - -import os -from pathlib import Path -from src.common.logger import get_logger -from .token_manager import get_token_manager - -logger = get_logger("webui") - - -def setup_webui(mode: str = "production") -> bool: - """ - 设置 WebUI - - Args: - mode: 运行模式,"development" 或 "production" - - Returns: - bool: 是否成功设置 - """ - # 初始化 Token 管理器(确保 token 文件存在) - token_manager = get_token_manager() - current_token = token_manager.get_token() - logger.info(f"🔑 WebUI Access Token: {current_token}") - logger.info("💡 请使用此 Token 登录 WebUI") - - if mode == "development": - return setup_dev_mode() - else: - return setup_production_mode() - - -def setup_dev_mode() -> bool: - """设置开发模式 - 仅启用 CORS,前端自行启动""" - from src.common.server import get_global_server - from .logs_ws import router as logs_router - - # 注册 WebSocket 日志路由(开发模式也需要) - server = get_global_server() - server.register_router(logs_router) - logger.info("✅ WebSocket 日志推送路由已注册") - - logger.info("📝 WebUI 开发模式已启用") - logger.info("🌐 请手动启动前端开发服务器: cd webui && npm run dev") - logger.info("💡 前端将运行在 http://localhost:7999") - return True - - -def setup_production_mode() -> bool: - """设置生产模式 - 挂载静态文件""" - try: - from src.common.server import get_global_server - from starlette.responses import FileResponse - from .logs_ws import router as logs_router - import mimetypes - - # 确保正确的 MIME 类型映射 - mimetypes.init() - mimetypes.add_type("application/javascript", ".js") - mimetypes.add_type("application/javascript", ".mjs") - mimetypes.add_type("text/css", ".css") - mimetypes.add_type("application/json", ".json") - - server = get_global_server() - - # 注册 WebSocket 日志路由 - server.register_router(logs_router) - logger.info("✅ WebSocket 日志推送路由已注册") - - base_dir = Path(__file__).parent.parent.parent - static_path = base_dir / "webui" / "dist" - - if not static_path.exists(): - logger.warning(f"❌ WebUI 静态文件目录不存在: {static_path}") - logger.warning("💡 请先构建前端: cd webui && npm run build") - return False - - if not (static_path / "index.html").exists(): - logger.warning(f"❌ 未找到 index.html: {static_path / 'index.html'}") - logger.warning("💡 请确认前端已正确构建") - return False - - # 处理 SPA 路由 - @server.app.get("/{full_path:path}") - async def serve_spa(full_path: str): - """服务单页应用""" - # API 路由不处理 - if full_path.startswith("api/"): - return None - - # 检查文件是否存在 - file_path = static_path / full_path - if file_path.is_file(): - # 自动检测 MIME 类型 - media_type = mimetypes.guess_type(str(file_path))[0] - return FileResponse(file_path, media_type=media_type) - - # 返回 index.html(SPA 路由) - return FileResponse(static_path / "index.html", media_type="text/html") - - host = os.getenv("HOST", "127.0.0.1") - port = os.getenv("PORT", "8000") - logger.info("✅ WebUI 生产模式已挂载") - logger.info(f"🌐 访问 http://{host}:{port} 查看 WebUI") - return True - - except Exception as e: - logger.error(f"挂载 WebUI 静态文件失败: {e}") - return False diff --git a/src/webui/webui_server.py b/src/webui/webui_server.py new file mode 100644 index 00000000..61d279e2 --- /dev/null +++ b/src/webui/webui_server.py @@ -0,0 +1,148 @@ +"""独立的 WebUI 服务器 - 运行在 0.0.0.0:8001""" + +import os +import asyncio +import mimetypes +from pathlib import Path +from fastapi import FastAPI +from fastapi.responses import FileResponse +from uvicorn import Config, Server as UvicornServer +from src.common.logger import get_logger + +logger = get_logger("webui_server") + + +class WebUIServer: + """独立的 WebUI 服务器""" + + def __init__(self, host: str = "0.0.0.0", port: int = 8001): + self.host = host + self.port = port + self.app = FastAPI(title="MaiBot WebUI") + self._server = None + + # 显示 Access Token + self._show_access_token() + + # 重要:先注册 API 路由,再设置静态文件 + self._register_api_routes() + self._setup_static_files() + + def _show_access_token(self): + """显示 WebUI Access Token""" + try: + from src.webui.token_manager import get_token_manager + + token_manager = get_token_manager() + current_token = token_manager.get_token() + logger.info(f"🔑 WebUI Access Token: {current_token}") + logger.info("💡 请使用此 Token 登录 WebUI") + except Exception as e: + logger.error(f"❌ 获取 Access Token 失败: {e}") + + def _setup_static_files(self): + """设置静态文件服务""" + # 确保正确的 MIME 类型映射 + mimetypes.init() + mimetypes.add_type("application/javascript", ".js") + mimetypes.add_type("application/javascript", ".mjs") + mimetypes.add_type("text/css", ".css") + mimetypes.add_type("application/json", ".json") + + base_dir = Path(__file__).parent.parent.parent + static_path = base_dir / "webui" / "dist" + + if not static_path.exists(): + logger.warning(f"❌ WebUI 静态文件目录不存在: {static_path}") + logger.warning("💡 请先构建前端: cd webui && npm run build") + return + + if not (static_path / "index.html").exists(): + logger.warning(f"❌ 未找到 index.html: {static_path / 'index.html'}") + logger.warning("💡 请确认前端已正确构建") + return + + # 处理 SPA 路由 - 注意:这个路由优先级最低 + @self.app.get("/{full_path:path}", include_in_schema=False) + async def serve_spa(full_path: str): + """服务单页应用 - 只处理非 API 请求""" + # 如果是根路径,直接返回 index.html + if not full_path or full_path == "/": + return FileResponse(static_path / "index.html", media_type="text/html") + + # 检查是否是静态文件 + file_path = static_path / full_path + if file_path.is_file() and file_path.exists(): + # 自动检测 MIME 类型 + media_type = mimetypes.guess_type(str(file_path))[0] + return FileResponse(file_path, media_type=media_type) + + # 其他路径返回 index.html(SPA 路由) + return FileResponse(static_path / "index.html", media_type="text/html") + + logger.info(f"✅ WebUI 静态文件服务已配置: {static_path}") + + def _register_api_routes(self): + """注册所有 WebUI API 路由""" + try: + # 导入所有 WebUI 路由 + from src.webui.routes import router as webui_router + from src.webui.logs_ws import router as logs_router + + # 注册路由 + self.app.include_router(webui_router) + self.app.include_router(logs_router) + + logger.info("✅ WebUI API 路由已注册") + except Exception as e: + logger.error(f"❌ 注册 WebUI API 路由失败: {e}") + + async def start(self): + """启动服务器""" + config = Config( + app=self.app, + host=self.host, + port=self.port, + log_config=None, + access_log=False, + ) + self._server = UvicornServer(config=config) + + logger.info("🌐 WebUI 服务器启动中...") + logger.info(f"🌐 访问地址: http://{self.host}:{self.port}") + + try: + await self._server.serve() + except Exception as e: + logger.error(f"❌ WebUI 服务器运行错误: {e}") + raise + + async def shutdown(self): + """关闭服务器""" + if self._server: + logger.info("正在关闭 WebUI 服务器...") + self._server.should_exit = True + try: + await asyncio.wait_for(self._server.shutdown(), timeout=3.0) + logger.info("✅ WebUI 服务器已关闭") + except asyncio.TimeoutError: + logger.warning("⚠️ WebUI 服务器关闭超时") + except Exception as e: + logger.error(f"❌ WebUI 服务器关闭失败: {e}") + finally: + self._server = None + + +# 全局 WebUI 服务器实例 +_webui_server = None + + +def get_webui_server() -> WebUIServer: + """获取全局 WebUI 服务器实例""" + global _webui_server + if _webui_server is None: + # 从环境变量读取配置 + host = os.getenv("WEBUI_HOST", "0.0.0.0") + port = int(os.getenv("WEBUI_PORT", "8001")) + _webui_server = WebUIServer(host=host, port=port) + return _webui_server diff --git a/template/bot_config_template.toml b/template/bot_config_template.toml index 8bbfaf9d..2c1da1be 100644 --- a/template/bot_config_template.toml +++ b/template/bot_config_template.toml @@ -1,5 +1,5 @@ [inner] -version = "6.21.6" +version = "6.21.8" #----以下是给开发人员阅读的,如果你只是部署了麦麦,不需要阅读---- #如果你想要修改配置文件,请递增version的值 @@ -211,6 +211,9 @@ show_prompt = false # 是否显示prompt show_replyer_prompt = false # 是否显示回复器prompt show_replyer_reasoning = false # 是否显示回复器推理 show_jargon_prompt = false # 是否显示jargon相关提示词 +show_memory_prompt = false # 是否显示记忆检索相关提示词 +show_planner_prompt = false # 是否显示planner的prompt和原始返回结果 +show_lpmm_paragraph = false # 是否显示lpmm找到的相关文段日志 [maim_message] auth_token = [] # 认证令牌,用于API验证,为空则不启用验证 diff --git a/template/template.env b/template/template.env index d19678e7..b6dd0e5c 100644 --- a/template/template.env +++ b/template/template.env @@ -1,6 +1,9 @@ +# 麦麦主程序配置 HOST=127.0.0.1 PORT=8000 -# WebUI 配置 +# WebUI 独立服务器配置 WEBUI_ENABLED=true -WEBUI_MODE=production # 生产模式 \ No newline at end of file +WEBUI_MODE=production # 模式: development(开发) 或 production(生产) +WEBUI_HOST=0.0.0.0 # WebUI 服务器监听地址 +WEBUI_PORT=8001 # WebUI 服务器端口 \ No newline at end of file diff --git a/webui/dist/assets/charts-DU5SeejN.js b/webui/dist/assets/charts-B1JvyJzO.js similarity index 72% rename from webui/dist/assets/charts-DU5SeejN.js rename to webui/dist/assets/charts-B1JvyJzO.js index 4b6aaf32..7d2ae929 100644 Binary files a/webui/dist/assets/charts-DU5SeejN.js and b/webui/dist/assets/charts-B1JvyJzO.js differ diff --git a/webui/dist/assets/icons-BdGv2zEo.js b/webui/dist/assets/icons-BdGv2zEo.js deleted file mode 100644 index 8b732ced..00000000 Binary files a/webui/dist/assets/icons-BdGv2zEo.js and /dev/null differ diff --git a/webui/dist/assets/icons-D6w7t-x9.js b/webui/dist/assets/icons-D6w7t-x9.js new file mode 100644 index 00000000..2cac469f Binary files /dev/null and b/webui/dist/assets/icons-D6w7t-x9.js differ diff --git a/webui/dist/assets/index-BmybQ2BG.css b/webui/dist/assets/index-BmybQ2BG.css new file mode 100644 index 00000000..bb6e4a9a Binary files /dev/null and b/webui/dist/assets/index-BmybQ2BG.css differ diff --git a/webui/dist/assets/index-CRpNZMM_.js b/webui/dist/assets/index-CRpNZMM_.js new file mode 100644 index 00000000..4b579329 Binary files /dev/null and b/webui/dist/assets/index-CRpNZMM_.js differ diff --git a/webui/dist/assets/index-C_Xpfn5c.css b/webui/dist/assets/index-C_Xpfn5c.css deleted file mode 100644 index c53a20a4..00000000 Binary files a/webui/dist/assets/index-C_Xpfn5c.css and /dev/null differ diff --git a/webui/dist/assets/index-pMcRRAxj.js b/webui/dist/assets/index-pMcRRAxj.js deleted file mode 100644 index 7a2399d9..00000000 Binary files a/webui/dist/assets/index-pMcRRAxj.js and /dev/null differ diff --git a/webui/dist/index.html b/webui/dist/index.html index 379c953d..d2be1e7e 100644 Binary files a/webui/dist/index.html and b/webui/dist/index.html differ