diff --git a/.gitignore b/.gitignore index fb46fb75c..8b8757a91 100644 --- a/.gitignore +++ b/.gitignore @@ -16,6 +16,7 @@ MaiBot-Napcat-Adapter /log_debug /src/test nonebot-maibot-adapter/ +MaiMBot-LPMM *.zip run.bat log_debug/ diff --git a/bot.py b/bot.py index 586d76a32..979bd0d47 100644 --- a/bot.py +++ b/bot.py @@ -7,9 +7,12 @@ import time import platform import traceback from dotenv import load_dotenv -from src.common.logger import get_logger -# from src.common.logger import LogConfig, CONFIRM_STYLE_CONFIG +# 最早期初始化日志系统,确保所有后续模块都使用正确的日志格式 +from src.common.logger import initialize_logging +initialize_logging() + +from src.common.logger import get_logger from src.common.crash_logger import install_crash_handler from src.main import MainSystem from rich.traceback import install @@ -42,7 +45,6 @@ driver = None app = None loop = None -# shutdown_requested = False # 新增全局变量 async def request_shutdown() -> bool: diff --git a/reload_config_now.py b/reload_config_now.py new file mode 100644 index 000000000..3399ee4f4 --- /dev/null +++ b/reload_config_now.py @@ -0,0 +1,17 @@ +#!/usr/bin/env python3 +""" +立即重新加载日志配置 +""" + +import sys +from pathlib import Path + +# 添加src目录到路径 +src_path = Path(__file__).parent / "src" +sys.path.insert(0, str(src_path)) + +from common.logger import reload_log_config + +print("🔄 重新加载日志配置...") +reload_log_config() +print("✅ 配置已重新加载!faiss日志已被屏蔽。") \ No newline at end of file diff --git a/scripts/log_viewer.py b/scripts/log_viewer.py new file mode 100644 index 000000000..be91e8486 --- /dev/null +++ b/scripts/log_viewer.py @@ -0,0 +1,560 @@ +import tkinter as tk +from tkinter import ttk, colorchooser +import json +from pathlib import Path +import threading +import queue +import time + +class LogViewer: + def __init__(self, root): + self.root = root + self.root.title("MaiBot日志查看器") + self.root.geometry("1200x800") + + # 创建主框架 + self.main_frame = ttk.Frame(root) + self.main_frame.pack(fill=tk.BOTH, expand=True, padx=5, pady=5) + + # 创建控制面板 + self.control_frame = ttk.Frame(self.main_frame) + self.control_frame.pack(fill=tk.X, pady=(0, 5)) + + # 模块选择框架 + self.module_frame = ttk.LabelFrame(self.control_frame, text="模块") + self.module_frame.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=5) + + # 创建模块选择滚动区域 + self.module_canvas = tk.Canvas(self.module_frame, height=80) + self.module_canvas.pack(side=tk.LEFT, fill=tk.X, expand=True) + + # 创建模块选择内部框架 + self.module_inner_frame = ttk.Frame(self.module_canvas) + self.module_canvas.create_window((0, 0), window=self.module_inner_frame, anchor='nw') + + # 创建右侧控制区域(级别和搜索) + self.right_control_frame = ttk.Frame(self.control_frame) + self.right_control_frame.pack(side=tk.RIGHT, padx=5) + + # 映射编辑按钮 + mapping_btn = ttk.Button(self.right_control_frame, text="模块映射", command=self.edit_module_mapping) + mapping_btn.pack(side=tk.TOP, fill=tk.X, pady=1) + + # 日志级别选择 + level_frame = ttk.Frame(self.right_control_frame) + level_frame.pack(side=tk.TOP, fill=tk.X, pady=1) + ttk.Label(level_frame, text="级别:").pack(side=tk.LEFT, padx=2) + self.level_var = tk.StringVar(value="全部") + self.level_combo = ttk.Combobox(level_frame, textvariable=self.level_var, width=8) + self.level_combo['values'] = ['全部', 'info', 'warning', 'error'] + self.level_combo.pack(side=tk.LEFT, padx=2) + + # 搜索框 + search_frame = ttk.Frame(self.right_control_frame) + search_frame.pack(side=tk.TOP, fill=tk.X, pady=1) + ttk.Label(search_frame, text="搜索:").pack(side=tk.LEFT, padx=2) + self.search_var = tk.StringVar() + self.search_entry = ttk.Entry(search_frame, textvariable=self.search_var, width=15) + self.search_entry.pack(side=tk.LEFT, padx=2) + + # 创建日志显示区域 + self.log_frame = ttk.Frame(self.main_frame) + self.log_frame.pack(fill=tk.BOTH, expand=True) + + # 创建文本框和滚动条 + self.scrollbar = ttk.Scrollbar(self.log_frame) + self.scrollbar.pack(side=tk.RIGHT, fill=tk.Y) + + self.log_text = tk.Text(self.log_frame, wrap=tk.WORD, yscrollcommand=self.scrollbar.set) + self.log_text.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) + self.scrollbar.config(command=self.log_text.yview) + + # 设置默认标签颜色 + self.colors = { + 'info': 'black', + 'warning': 'orange', + 'error': 'red' + } + self.module_colors = {} + + # 模块名映射 + self.module_name_mapping = { + 'api': 'API接口', + 'async_task_manager': '异步任务管理器', + 'background_tasks': '后台任务', + 'base_tool': '基础工具', + 'chat_stream': '聊天流', + 'component_registry': '组件注册器', + 'config': '配置', + 'database_model': '数据库模型', + 'emoji': '表情', + 'heartflow': '心流', + 'local_storage': '本地存储', + 'lpmm': 'LPMM', + 'maibot_statistic': 'MaiBot统计', + 'main_message': '主消息', + 'main': '主程序', + 'memory': '内存', + 'mood': '情绪', + 'plugin_manager': '插件管理器', + 'remote': '远程', + 'willing': '意愿', + } + + # 加载自定义映射 + self.load_module_mapping() + + # 创建日志队列和缓存 + self.log_queue = queue.Queue() + self.log_cache = [] + + # 选中的模块集合 + self.selected_modules = set() + + # 初始化模块列表 + self.modules = set() + self.update_module_list() + + # 绑定事件 + self.level_combo.bind('<>', self.filter_logs) + self.search_var.trace('w', self.filter_logs) + + # 启动日志监控线程 + self.running = True + self.monitor_thread = threading.Thread(target=self.monitor_log_file) + self.monitor_thread.daemon = True + self.monitor_thread.start() + + # 启动日志更新线程 + self.update_thread = threading.Thread(target=self.update_logs) + self.update_thread.daemon = True + self.update_thread.start() + + def show_color_settings(self): + """显示颜色设置窗口""" + color_window = tk.Toplevel(self.root) + color_window.title("颜色设置") + color_window.geometry("300x400") + + # 创建滚动框架 + frame = ttk.Frame(color_window) + frame.pack(fill=tk.BOTH, expand=True, padx=5, pady=5) + + # 创建滚动条 + scrollbar = ttk.Scrollbar(frame) + scrollbar.pack(side=tk.RIGHT, fill=tk.Y) + + # 创建颜色设置列表 + canvas = tk.Canvas(frame, yscrollcommand=scrollbar.set) + canvas.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) + scrollbar.config(command=canvas.yview) + + # 创建内部框架 + inner_frame = ttk.Frame(canvas) + canvas.create_window((0, 0), window=inner_frame, anchor='nw') + + # 添加日志级别颜色设置 + ttk.Label(inner_frame, text="日志级别颜色", font=('', 10, 'bold')).pack(anchor='w', padx=5, pady=5) + for level in ['info', 'warning', 'error']: + frame = ttk.Frame(inner_frame) + frame.pack(fill=tk.X, padx=5, pady=2) + ttk.Label(frame, text=level).pack(side=tk.LEFT) + color_btn = ttk.Button(frame, text="选择颜色", + command=lambda l=level: self.choose_color(l)) + color_btn.pack(side=tk.RIGHT) + # 显示当前颜色 + color_label = ttk.Label(frame, text="■", foreground=self.colors[level]) + color_label.pack(side=tk.RIGHT, padx=5) + + # 添加模块颜色设置 + ttk.Label(inner_frame, text="\n模块颜色", font=('', 10, 'bold')).pack(anchor='w', padx=5, pady=5) + for module in sorted(self.modules): + frame = ttk.Frame(inner_frame) + frame.pack(fill=tk.X, padx=5, pady=2) + ttk.Label(frame, text=module).pack(side=tk.LEFT) + color_btn = ttk.Button(frame, text="选择颜色", + command=lambda m=module: self.choose_module_color(m)) + color_btn.pack(side=tk.RIGHT) + # 显示当前颜色 + color = self.module_colors.get(module, 'black') + color_label = ttk.Label(frame, text="■", foreground=color) + color_label.pack(side=tk.RIGHT, padx=5) + + # 更新画布滚动区域 + inner_frame.update_idletasks() + canvas.config(scrollregion=canvas.bbox("all")) + + # 添加确定按钮 + ttk.Button(color_window, text="确定", command=color_window.destroy).pack(pady=5) + + def choose_color(self, level): + """选择日志级别颜色""" + color = colorchooser.askcolor(color=self.colors[level])[1] + if color: + self.colors[level] = color + self.log_text.tag_configure(level, foreground=color) + self.filter_logs() + + def choose_module_color(self, module): + """选择模块颜色""" + color = colorchooser.askcolor(color=self.module_colors.get(module, 'black'))[1] + if color: + self.module_colors[module] = color + self.log_text.tag_configure(f"module_{module}", foreground=color) + # 更新模块列表中的颜色显示 + self.update_module_color_display(module, color) + self.filter_logs() + + def update_module_color_display(self, module, color): + """更新模块列表中的颜色显示""" + # 遍历模块框架中的所有子控件,找到对应模块的颜色标签并更新 + for widget in self.module_inner_frame.winfo_children(): + if isinstance(widget, ttk.Frame): + # 检查这个框架是否包含目标模块 + for child in widget.winfo_children(): + if isinstance(child, ttk.Checkbutton): + if child.cget('text') == module: + # 找到了对应的模块,更新其颜色标签 + for sibling in widget.winfo_children(): + if isinstance(sibling, ttk.Label) and sibling.cget('text') == '■': + sibling.config(foreground=color) + return + + def update_module_list(self): + """更新模块列表""" + log_file = Path("logs/app.log.jsonl") + if log_file.exists(): + with open(log_file, 'r', encoding='utf-8') as f: + for line in f: + try: + log_entry = json.loads(line) + if 'logger_name' in log_entry: + self.modules.add(log_entry['logger_name']) + except json.JSONDecodeError: + continue + + # 清空现有选项 + for widget in self.module_inner_frame.winfo_children(): + widget.destroy() + + # 计算总模块数(包括"全部") + total_modules = len(self.modules) + 1 + max_cols = min(4, max(2, total_modules)) # 减少最大列数,避免超出边界 + + # 配置网格列权重,让每列平均分配空间 + for i in range(max_cols): + self.module_inner_frame.grid_columnconfigure(i, weight=1, uniform="module_col") + + # 创建一个多行布局 + current_row = 0 + current_col = 0 + + # 添加"全部"选项 + all_frame = ttk.Frame(self.module_inner_frame) + all_frame.grid(row=current_row, column=current_col, padx=3, pady=2, sticky='ew') + + all_var = tk.BooleanVar(value='全部' in self.selected_modules) + all_check = ttk.Checkbutton(all_frame, text="全部", variable=all_var, + command=lambda: self.toggle_module('全部', all_var)) + all_check.pack(side=tk.LEFT) + + # 使用颜色标签替代按钮 + all_color = self.module_colors.get('全部', 'black') + all_color_label = ttk.Label(all_frame, text="■", foreground=all_color, width=2, cursor="hand2") + all_color_label.pack(side=tk.LEFT, padx=2) + all_color_label.bind('', lambda e: self.choose_module_color('全部')) + + current_col += 1 + + # 添加其他模块选项 + for module in sorted(self.modules): + if current_col >= max_cols: + current_row += 1 + current_col = 0 + + frame = ttk.Frame(self.module_inner_frame) + frame.grid(row=current_row, column=current_col, padx=3, pady=2, sticky='ew') + + var = tk.BooleanVar(value=module in self.selected_modules) + + # 使用中文映射名称显示 + display_name = self.get_display_name(module) + if len(display_name) > 12: + display_name = display_name[:10] + "..." + + check = ttk.Checkbutton(frame, text=display_name, variable=var, + command=lambda m=module, v=var: self.toggle_module(m, v)) + check.pack(side=tk.LEFT) + + # 添加工具提示显示完整名称和英文名 + full_tooltip = f"{self.get_display_name(module)}" + if module != self.get_display_name(module): + full_tooltip += f"\n({module})" + self.create_tooltip(check, full_tooltip) + + # 使用颜色标签替代按钮 + color = self.module_colors.get(module, 'black') + color_label = ttk.Label(frame, text="■", foreground=color, width=2, cursor="hand2") + color_label.pack(side=tk.LEFT, padx=2) + color_label.bind('', lambda e, m=module: self.choose_module_color(m)) + + current_col += 1 + + # 更新画布滚动区域 + self.module_inner_frame.update_idletasks() + self.module_canvas.config(scrollregion=self.module_canvas.bbox("all")) + + # 添加垂直滚动条 + if not hasattr(self, 'module_scrollbar'): + self.module_scrollbar = ttk.Scrollbar(self.module_frame, orient=tk.VERTICAL, + command=self.module_canvas.yview) + self.module_scrollbar.pack(side=tk.RIGHT, fill=tk.Y) + self.module_canvas.config(yscrollcommand=self.module_scrollbar.set) + + def create_tooltip(self, widget, text): + """为控件创建工具提示""" + def on_enter(event): + tooltip = tk.Toplevel() + tooltip.wm_overrideredirect(True) + tooltip.wm_geometry(f"+{event.x_root+10}+{event.y_root+10}") + label = ttk.Label(tooltip, text=text, background="lightyellow", relief="solid", borderwidth=1) + label.pack() + widget.tooltip = tooltip + + def on_leave(event): + if hasattr(widget, 'tooltip'): + widget.tooltip.destroy() + del widget.tooltip + + widget.bind('', on_enter) + widget.bind('', on_leave) + + def toggle_module(self, module, var): + """切换模块选择状态""" + if module == '全部': + if var.get(): + self.selected_modules = {'全部'} + else: + self.selected_modules.clear() + else: + if var.get(): + self.selected_modules.add(module) + if '全部' in self.selected_modules: + self.selected_modules.remove('全部') + else: + self.selected_modules.discard(module) + + self.filter_logs() + + def monitor_log_file(self): + """监控日志文件变化""" + log_file = Path("logs/app.log.jsonl") + last_position = 0 + + while self.running: + if log_file.exists(): + try: + with open(log_file, 'r', encoding='utf-8') as f: + f.seek(last_position) + new_lines = f.readlines() + last_position = f.tell() + + for line in new_lines: + try: + log_entry = json.loads(line) + self.log_queue.put(log_entry) + self.log_cache.append(log_entry) + + # 检查是否有新模块 + if 'logger_name' in log_entry: + logger_name = log_entry['logger_name'] + if logger_name not in self.modules: + self.modules.add(logger_name) + # 在主线程中更新模块列表UI + self.root.after(0, self.update_module_list) + + except json.JSONDecodeError: + continue + except Exception as e: + print(f"Error reading log file: {e}") + + time.sleep(0.1) + + def update_logs(self): + """更新日志显示""" + while self.running: + try: + log_entry = self.log_queue.get(timeout=0.1) + self.process_log_entry(log_entry) + except queue.Empty: + continue + + def process_log_entry(self, log_entry): + """处理日志条目""" + # 检查过滤条件 + if not self.should_show_log(log_entry): + return + + # 格式化日志 + timestamp = log_entry.get('timestamp', '') + level = log_entry.get('level', 'info') + logger_name = log_entry.get('logger_name', '') + event = log_entry.get('event', '') + + log_line = f"{timestamp} [{level}] {logger_name}: {event}\n" + + # 在主线程中更新UI + self.root.after(0, lambda: self.add_log_line(log_line, level, logger_name)) + + def add_log_line(self, line, level, logger_name): + """添加日志行到文本框""" + self.log_text.insert(tk.END, line, (level, f"module_{logger_name}")) + # 只有在用户没有手动滚动时才自动滚动到底部 + if self.log_text.yview()[1] >= 0.99: + self.log_text.see(tk.END) + + def should_show_log(self, log_entry): + """检查日志是否应该显示""" + # 检查模块过滤 + if self.selected_modules: + if '全部' not in self.selected_modules: + if log_entry.get('logger_name') not in self.selected_modules: + return False + + # 检查级别过滤 + if self.level_var.get() != '全部': + if log_entry.get('level') != self.level_var.get(): + return False + + # 检查搜索过滤 + search_text = self.search_var.get().lower() + if search_text: + event = str(log_entry.get('event', '')).lower() + logger_name = str(log_entry.get('logger_name', '')).lower() + if search_text not in event and search_text not in logger_name: + return False + + return True + + def filter_logs(self, *args): + """过滤日志""" + # 保存当前滚动位置 + scroll_position = self.log_text.yview() + + # 清空显示 + self.log_text.delete(1.0, tk.END) + + # 重新显示所有符合条件的日志 + for log_entry in self.log_cache: + if self.should_show_log(log_entry): + timestamp = log_entry.get('timestamp', '') + level = log_entry.get('level', 'info') + logger_name = log_entry.get('logger_name', '') + event = log_entry.get('event', '') + log_line = f"{timestamp} [{level}] {logger_name}: {event}\n" + self.log_text.insert(tk.END, log_line, (level, f"module_{logger_name}")) + + # 恢复滚动位置 + self.log_text.yview_moveto(scroll_position[0]) + + def get_display_name(self, module_name): + """获取模块的显示名称""" + return self.module_name_mapping.get(module_name, module_name) + + def load_module_mapping(self): + """加载自定义模块映射""" + mapping_file = Path("config/module_mapping.json") + if mapping_file.exists(): + try: + with open(mapping_file, 'r', encoding='utf-8') as f: + custom_mapping = json.load(f) + self.module_name_mapping.update(custom_mapping) + except Exception as e: + print(f"加载模块映射失败: {e}") + + def save_module_mapping(self): + """保存自定义模块映射""" + mapping_file = Path("config/module_mapping.json") + mapping_file.parent.mkdir(exist_ok=True) + try: + with open(mapping_file, 'w', encoding='utf-8') as f: + json.dump(self.module_name_mapping, f, ensure_ascii=False, indent=2) + except Exception as e: + print(f"保存模块映射失败: {e}") + + def edit_module_mapping(self): + """编辑模块映射""" + mapping_window = tk.Toplevel(self.root) + mapping_window.title("编辑模块映射") + mapping_window.geometry("500x600") + + # 创建滚动框架 + frame = ttk.Frame(mapping_window) + frame.pack(fill=tk.BOTH, expand=True, padx=5, pady=5) + + # 创建滚动条 + scrollbar = ttk.Scrollbar(frame) + scrollbar.pack(side=tk.RIGHT, fill=tk.Y) + + # 创建映射编辑列表 + canvas = tk.Canvas(frame, yscrollcommand=scrollbar.set) + canvas.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) + scrollbar.config(command=canvas.yview) + + # 创建内部框架 + inner_frame = ttk.Frame(canvas) + canvas.create_window((0, 0), window=inner_frame, anchor='nw') + + # 添加标题 + ttk.Label(inner_frame, text="模块映射编辑", font=('', 12, 'bold')).pack(anchor='w', padx=5, pady=5) + ttk.Label(inner_frame, text="英文名 -> 中文名", font=('', 10)).pack(anchor='w', padx=5, pady=2) + + # 映射编辑字典 + mapping_vars = {} + + # 添加现有模块的映射编辑 + all_modules = sorted(self.modules) + for module in all_modules: + frame_row = ttk.Frame(inner_frame) + frame_row.pack(fill=tk.X, padx=5, pady=2) + + ttk.Label(frame_row, text=module, width=20).pack(side=tk.LEFT, padx=5) + ttk.Label(frame_row, text="->").pack(side=tk.LEFT, padx=5) + + var = tk.StringVar(value=self.module_name_mapping.get(module, module)) + mapping_vars[module] = var + entry = ttk.Entry(frame_row, textvariable=var, width=25) + entry.pack(side=tk.LEFT, padx=5) + + # 更新画布滚动区域 + inner_frame.update_idletasks() + canvas.config(scrollregion=canvas.bbox("all")) + + def save_mappings(): + # 更新映射 + for module, var in mapping_vars.items(): + new_name = var.get().strip() + if new_name and new_name != module: + self.module_name_mapping[module] = new_name + elif module in self.module_name_mapping and not new_name: + del self.module_name_mapping[module] + + # 保存到文件 + self.save_module_mapping() + # 更新模块列表显示 + self.update_module_list() + mapping_window.destroy() + + # 添加按钮 + button_frame = ttk.Frame(mapping_window) + button_frame.pack(fill=tk.X, padx=5, pady=5) + ttk.Button(button_frame, text="保存", command=save_mappings).pack(side=tk.RIGHT, padx=5) + ttk.Button(button_frame, text="取消", command=mapping_window.destroy).pack(side=tk.RIGHT, padx=5) + +def main(): + root = tk.Tk() + app = LogViewer(root) + root.mainloop() + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/src/chat/focus_chat/heartFC_chat.py b/src/chat/focus_chat/heartFC_chat.py index 622ce8811..251eea70f 100644 --- a/src/chat/focus_chat/heartFC_chat.py +++ b/src/chat/focus_chat/heartFC_chat.py @@ -14,7 +14,6 @@ from src.chat.focus_chat.heartFC_Cycleinfo import CycleDetail from src.chat.focus_chat.info.info_base import InfoBase from src.chat.focus_chat.info_processors.chattinginfo_processor import ChattingInfoProcessor from src.chat.focus_chat.info_processors.relationship_processor import RelationshipProcessor -from src.chat.focus_chat.info_processors.mind_processor import MindProcessor from src.chat.focus_chat.info_processors.working_memory_processor import WorkingMemoryProcessor # from src.chat.focus_chat.info_processors.action_processor import ActionProcessor @@ -47,7 +46,6 @@ OBSERVATION_CLASSES = { # 定义处理器映射:键是处理器名称,值是 (处理器类, 可选的配置键名) PROCESSOR_CLASSES = { "ChattingInfoProcessor": (ChattingInfoProcessor, None), - "MindProcessor": (MindProcessor, "mind_processor"), "ToolProcessor": (ToolProcessor, "tool_use_processor"), "WorkingMemoryProcessor": (WorkingMemoryProcessor, "working_memory_processor"), "SelfProcessor": (SelfProcessor, "self_identify_processor"), @@ -189,7 +187,6 @@ class HeartFChatting: processor_actual_class = processor_info[0] # 获取实际的类定义 # 根据处理器类名判断是否需要 subheartflow_id if name in [ - "MindProcessor", "ToolProcessor", "WorkingMemoryProcessor", "SelfProcessor", @@ -494,10 +491,10 @@ class HeartFChatting: loop_plan_info = { "action_result": plan_result.get("action_result", {}), - "current_mind": plan_result.get("current_mind", ""), "observed_messages": plan_result.get("observed_messages", ""), } + with Timer("执行动作", cycle_timers): action_type, action_data, reasoning = ( plan_result.get("action_result", {}).get("action_type", "error"), @@ -505,6 +502,7 @@ class HeartFChatting: plan_result.get("action_result", {}).get("reasoning", "未提供理由"), ) + if action_type == "reply": action_str = "回复" elif action_type == "no_reply": diff --git a/src/common/logger.py b/src/common/logger.py index 0ad9da190..a8d0417ca 100644 --- a/src/common/logger.py +++ b/src/common/logger.py @@ -5,11 +5,272 @@ from typing import Callable, Optional import json import structlog +from structlog.dev import ConsoleRenderer +import toml # 创建logs目录 LOG_DIR = Path("logs") LOG_DIR.mkdir(exist_ok=True) +# 读取日志配置 +def load_log_config(): + """从配置文件加载日志设置""" + config_path = Path("config/bot_config.toml") + default_config = { + "date_style": "Y-m-d H:i:s", + "log_level_style": "lite", + "color_text": "title", + "log_level": "INFO", + "suppress_libraries": [], + "library_log_levels": {} + } + + try: + if config_path.exists(): + with open(config_path, 'r', encoding='utf-8') as f: + config = toml.load(f) + return config.get('log', default_config) + except Exception: + pass + + return default_config + +LOG_CONFIG = load_log_config() + +def get_timestamp_format(): + """将配置中的日期格式转换为Python格式""" + date_style = LOG_CONFIG.get("date_style", "Y-m-d H:i:s") + # 转换PHP风格的日期格式到Python格式 + format_map = { + 'Y': '%Y', # 4位年份 + 'm': '%m', # 月份(01-12) + 'd': '%d', # 日期(01-31) + 'H': '%H', # 小时(00-23) + 'i': '%M', # 分钟(00-59) + 's': '%S', # 秒数(00-59) + } + + python_format = date_style + for php_char, python_char in format_map.items(): + python_format = python_format.replace(php_char, python_char) + + return python_format + +def configure_third_party_loggers(): + """配置第三方库的日志级别""" + # 设置全局日志级别 + global_log_level = LOG_CONFIG.get("log_level", "INFO") + root_logger = logging.getLogger() + root_logger.setLevel(getattr(logging, global_log_level.upper(), logging.INFO)) + + # 完全屏蔽的库 + suppress_libraries = LOG_CONFIG.get("suppress_libraries", []) + for lib_name in suppress_libraries: + lib_logger = logging.getLogger(lib_name) + lib_logger.setLevel(logging.CRITICAL + 1) # 设置为比CRITICAL更高的级别,基本屏蔽所有日志 + lib_logger.propagate = False # 阻止向上传播 + + # 设置特定级别的库 + library_log_levels = LOG_CONFIG.get("library_log_levels", {}) + for lib_name, level_name in library_log_levels.items(): + lib_logger = logging.getLogger(lib_name) + level = getattr(logging, level_name.upper(), logging.WARNING) + lib_logger.setLevel(level) + + +def reconfigure_existing_loggers(): + """重新配置所有已存在的logger,解决加载顺序问题""" + # 获取根logger + root_logger = logging.getLogger() + + # 重新设置根logger的所有handler的格式化器 + for handler in root_logger.handlers: + if isinstance(handler, logging.handlers.RotatingFileHandler): + handler.setFormatter(file_formatter) + elif isinstance(handler, logging.StreamHandler): + handler.setFormatter(console_formatter) + + # 遍历所有已存在的logger并重新配置 + logger_dict = logging.getLogger().manager.loggerDict + for name, logger_obj in logger_dict.items(): + if isinstance(logger_obj, logging.Logger): + # 检查是否是第三方库logger + suppress_libraries = LOG_CONFIG.get("suppress_libraries", []) + library_log_levels = LOG_CONFIG.get("library_log_levels", {}) + + # 如果在屏蔽列表中 + if any(name.startswith(lib) for lib in suppress_libraries): + logger_obj.setLevel(logging.CRITICAL + 1) + logger_obj.propagate = False + continue + + # 如果在特定级别设置中 + for lib_name, level_name in library_log_levels.items(): + if name.startswith(lib_name): + level = getattr(logging, level_name.upper(), logging.WARNING) + logger_obj.setLevel(level) + break + + # 强制清除并重新设置所有handler + original_handlers = logger_obj.handlers[:] + for handler in original_handlers: + logger_obj.removeHandler(handler) + + # 如果logger没有handler,让它使用根logger的handler(propagate=True) + if not logger_obj.handlers: + logger_obj.propagate = True + + # 如果logger有自己的handler,重新配置它们 + for handler in original_handlers: + if isinstance(handler, logging.handlers.RotatingFileHandler): + handler.setFormatter(file_formatter) + elif isinstance(handler, logging.StreamHandler): + handler.setFormatter(console_formatter) + logger_obj.addHandler(handler) + +# 定义模块颜色映射 +MODULE_COLORS = { + "api": "\033[92m", # 亮绿色 + "emoji": "\033[92m", # 亮绿色 + "chat": "\033[94m", # 亮蓝色 + "config": "\033[93m", # 亮黄色 + "common": "\033[95m", # 亮紫色 + "tools": "\033[96m", # 亮青色 + "lpmm": "\033[96m", + "plugin_system": "\033[91m", # 亮红色 + "experimental": "\033[97m", # 亮白色 + "person_info": "\033[32m", # 绿色 + "individuality": "\033[34m", # 蓝色 + "manager": "\033[35m", # 紫色 + "llm_models": "\033[36m", # 青色 + "plugins": "\033[31m", # 红色 + "plugin_api": "\033[33m", # 黄色 + "remote": "\033[38;5;93m", # 紫蓝色 +} + +RESET_COLOR = "\033[0m" + + +class ModuleColoredConsoleRenderer: + """自定义控制台渲染器,为不同模块提供不同颜色""" + + def __init__(self, colors=True): + self._colors = colors + self._config = LOG_CONFIG + + # 日志级别颜色 + self._level_colors = { + "debug": "\033[38;5;208m", # 橙色 + "info": "\033[34m", # 蓝色 + "success": "\033[32m", # 绿色 + "warning": "\033[33m", # 黄色 + "error": "\033[31m", # 红色 + "critical": "\033[35m", # 紫色 + } + + # 根据配置决定是否启用颜色 + color_text = self._config.get("color_text", "title") + if color_text == "none": + self._colors = False + elif color_text == "title": + self._enable_module_colors = True + self._enable_level_colors = False + elif color_text == "full": + self._enable_module_colors = True + self._enable_level_colors = True + else: + self._enable_module_colors = True + self._enable_level_colors = False + + def __call__(self, logger, method_name, event_dict): + """渲染日志消息""" + # 获取基本信息 + timestamp = event_dict.get("timestamp", "") + level = event_dict.get("level", "info") + logger_name = event_dict.get("logger_name", "") + event = event_dict.get("event", "") + + # 构建输出 + parts = [] + + # 日志级别样式配置 + log_level_style = self._config.get("log_level_style", "lite") + level_color = self._level_colors.get(level.lower(), "") if self._colors else "" + + # 时间戳(lite模式下按级别着色) + if timestamp: + if log_level_style == "lite" and level_color: + timestamp_part = f"{level_color}{timestamp}{RESET_COLOR}" + else: + timestamp_part = timestamp + parts.append(timestamp_part) + + # 日志级别显示(根据配置样式) + if log_level_style == "full": + # 显示完整级别名并着色 + level_text = level.upper() + if level_color: + level_part = f"{level_color}[{level_text:>8}]{RESET_COLOR}" + else: + level_part = f"[{level_text:>8}]" + parts.append(level_part) + + elif log_level_style == "compact": + # 只显示首字母并着色 + level_text = level.upper()[0] + if level_color: + level_part = f"{level_color}[{level_text:>8}]{RESET_COLOR}" + else: + level_part = f"[{level_text:>8}]" + parts.append(level_part) + + # lite模式不显示级别,只给时间戳着色 + + # 模块名称(带颜色) + if logger_name: + if self._colors and self._enable_module_colors: + module_color = MODULE_COLORS.get(logger_name, "") + if module_color: + module_part = f"{module_color}[{logger_name}]{RESET_COLOR}" + else: + module_part = f"[{logger_name}]" + else: + module_part = f"[{logger_name}]" + parts.append(module_part) + + # 消息内容(确保转换为字符串) + if isinstance(event, str): + parts.append(event) + elif isinstance(event, dict): + # 如果是字典,格式化为可读字符串 + try: + parts.append(json.dumps(event, ensure_ascii=False, indent=None)) + except (TypeError, ValueError): + parts.append(str(event)) + else: + # 其他类型直接转换为字符串 + parts.append(str(event)) + + # 处理其他字段 + extras = [] + for key, value in event_dict.items(): + if key not in ("timestamp", "level", "logger_name", "event"): + # 确保值也转换为字符串 + if isinstance(value, (dict, list)): + try: + value_str = json.dumps(value, ensure_ascii=False, indent=None) + except (TypeError, ValueError): + value_str = str(value) + else: + value_str = str(value) + extras.append(f"{key}={value_str}") + + if extras: + parts.append(" ".join(extras)) + + return " ".join(parts) + + # 配置标准logging以支持文件输出和压缩 logging.basicConfig( level=logging.INFO, @@ -27,21 +288,26 @@ logging.basicConfig( ], ) -structlog.configure( - processors=[ - structlog.contextvars.merge_contextvars, - structlog.processors.add_log_level, - structlog.processors.StackInfoRenderer(), - structlog.dev.set_exc_info, - structlog.processors.TimeStamper(fmt="%Y-%m-%d %H:%M:%S", utc=False), - # 根据输出类型选择不同的渲染器 - structlog.stdlib.ProcessorFormatter.wrap_for_formatter, - ], - wrapper_class=structlog.stdlib.BoundLogger, - context_class=dict, - logger_factory=structlog.stdlib.LoggerFactory(), - cache_logger_on_first_use=True, -) +def configure_structlog(): + """配置structlog""" + structlog.configure( + processors=[ + structlog.contextvars.merge_contextvars, + structlog.processors.add_log_level, + structlog.processors.StackInfoRenderer(), + structlog.dev.set_exc_info, + structlog.processors.TimeStamper(fmt=get_timestamp_format(), utc=False), + # 根据输出类型选择不同的渲染器 + structlog.stdlib.ProcessorFormatter.wrap_for_formatter, + ], + wrapper_class=structlog.stdlib.BoundLogger, + context_class=dict, + logger_factory=structlog.stdlib.LoggerFactory(), + cache_logger_on_first_use=True, + ) + +# 配置structlog +configure_structlog() # 为文件输出配置JSON格式 file_formatter = structlog.stdlib.ProcessorFormatter( @@ -58,12 +324,12 @@ file_formatter = structlog.stdlib.ProcessorFormatter( # 为控制台输出配置可读格式 console_formatter = structlog.stdlib.ProcessorFormatter( - processor=structlog.dev.ConsoleRenderer(colors=True), + processor=ModuleColoredConsoleRenderer(colors=True), foreign_pre_chain=[ structlog.stdlib.add_logger_name, structlog.stdlib.add_log_level, structlog.stdlib.PositionalArgumentsFormatter(), - structlog.processors.TimeStamper(fmt="%Y-%m-%d %H:%M:%S", utc=False), + structlog.processors.TimeStamper(fmt=get_timestamp_format(), utc=False), structlog.processors.StackInfoRenderer(), structlog.processors.format_exc_info, ], @@ -77,6 +343,42 @@ for handler in root_logger.handlers: else: handler.setFormatter(console_formatter) +# 立即配置日志系统,确保最早期的日志也使用正确格式 +def _immediate_setup(): + """立即设置日志系统,在模块导入时就生效""" + # 重新配置structlog + configure_structlog() + + # 清除所有已有的handler,重新配置 + root_logger = logging.getLogger() + for handler in root_logger.handlers[:]: + root_logger.removeHandler(handler) + + # 重新添加配置好的handler + root_logger.addHandler(logging.handlers.RotatingFileHandler( + LOG_DIR / "app.log.jsonl", + maxBytes=10 * 1024 * 1024, + backupCount=5, + encoding="utf-8", + )) + root_logger.addHandler(logging.StreamHandler()) + + # 设置格式化器 + for handler in root_logger.handlers: + if isinstance(handler, logging.handlers.RotatingFileHandler): + handler.setFormatter(file_formatter) + else: + handler.setFormatter(console_formatter) + + # 配置第三方库日志 + configure_third_party_loggers() + + # 重新配置所有已存在的logger + reconfigure_existing_loggers() + +# 立即执行配置 +_immediate_setup() + raw_logger = structlog.get_logger() binds: dict[str, Callable] = {} @@ -114,6 +416,134 @@ def configure_logging( root_logger.setLevel(getattr(logging, level.upper())) +def set_module_color(module_name: str, color_code: str): + """为指定模块设置颜色 + + Args: + module_name: 模块名称 + color_code: ANSI颜色代码,例如 '\033[92m' 表示亮绿色 + """ + MODULE_COLORS[module_name] = color_code + + +def get_module_colors(): + """获取当前模块颜色配置""" + return MODULE_COLORS.copy() + + +def reload_log_config(): + """重新加载日志配置""" + global LOG_CONFIG + LOG_CONFIG = load_log_config() + + # 重新配置console渲染器 + root_logger = logging.getLogger() + for handler in root_logger.handlers: + if isinstance(handler, logging.StreamHandler) and not isinstance(handler, logging.handlers.RotatingFileHandler): + # 这是控制台处理器,更新其格式化器 + handler.setFormatter(structlog.stdlib.ProcessorFormatter( + processor=ModuleColoredConsoleRenderer(colors=True), + foreign_pre_chain=[ + structlog.stdlib.add_logger_name, + structlog.stdlib.add_log_level, + structlog.stdlib.PositionalArgumentsFormatter(), + structlog.processors.TimeStamper(fmt=get_timestamp_format(), utc=False), + structlog.processors.StackInfoRenderer(), + structlog.processors.format_exc_info, + ], + )) + + # 重新配置第三方库日志 + configure_third_party_loggers() + + # 重新配置所有已存在的logger + reconfigure_existing_loggers() + + +def get_log_config(): + """获取当前日志配置""" + return LOG_CONFIG.copy() + + +def force_reset_all_loggers(): + """强制重置所有logger,解决格式不一致问题""" + # 清除所有现有的logger配置 + logging.getLogger().manager.loggerDict.clear() + + # 重新配置根logger + root_logger = logging.getLogger() + root_logger.handlers.clear() + + # 重新添加我们的handler + root_logger.addHandler(logging.handlers.RotatingFileHandler( + LOG_DIR / "app.log.jsonl", + maxBytes=10 * 1024 * 1024, + backupCount=5, + encoding="utf-8", + )) + root_logger.addHandler(logging.StreamHandler()) + + # 设置格式化器 + for handler in root_logger.handlers: + if isinstance(handler, logging.handlers.RotatingFileHandler): + handler.setFormatter(file_formatter) + else: + handler.setFormatter(console_formatter) + + # 设置级别 + global_log_level = LOG_CONFIG.get("log_level", "INFO") + root_logger.setLevel(getattr(logging, global_log_level.upper(), logging.INFO)) + + +def initialize_logging(): + """手动初始化日志系统,确保所有logger都使用正确的配置 + + 在应用程序的早期调用此函数,确保所有模块都使用统一的日志配置 + """ + global LOG_CONFIG + LOG_CONFIG = load_log_config() + configure_third_party_loggers() + reconfigure_existing_loggers() + + # 输出初始化信息 + logger = get_logger("logger") + log_level = LOG_CONFIG.get("log_level", "INFO") + logger.info(f"日志系统已重新初始化,日志级别: {log_level},所有logger已统一配置") + + +def force_initialize_logging(): + """强制重新初始化整个日志系统,解决格式不一致问题""" + global LOG_CONFIG + LOG_CONFIG = load_log_config() + + # 强制重置所有logger + force_reset_all_loggers() + + # 重新配置structlog + configure_structlog() + + # 配置第三方库 + configure_third_party_loggers() + + # 输出初始化信息 + logger = get_logger("logger") + log_level = LOG_CONFIG.get("log_level", "INFO") + logger.info(f"日志系统已强制重新初始化,日志级别: {log_level},所有logger格式已统一") + + +def show_module_colors(): + """显示所有模块的颜色效果""" + logger = get_logger("demo") + print("\n=== 模块颜色展示 ===") + + for module_name, color_code in MODULE_COLORS.items(): + # 临时创建一个该模块的logger来展示颜色 + demo_logger = structlog.get_logger(module_name).bind(logger_name=module_name) + demo_logger.info(f"这是 {module_name} 模块的颜色效果") + + print("=== 颜色展示结束 ===\n") + + def format_json_for_logging(data, indent=2, ensure_ascii=False): """将JSON数据格式化为可读字符串 @@ -126,17 +556,9 @@ def format_json_for_logging(data, indent=2, ensure_ascii=False): str: 格式化后的JSON字符串 """ if isinstance(data, str): - try: - # 如果是JSON字符串,先解析再格式化 - parsed_data = json.loads(data) - return json.dumps(parsed_data, indent=indent, ensure_ascii=ensure_ascii) - except json.JSONDecodeError: - # 如果不是有效JSON,直接返回 - return data + # 如果是JSON字符串,先解析再格式化 + parsed_data = json.loads(data) + return json.dumps(parsed_data, indent=indent, ensure_ascii=ensure_ascii) else: # 如果是对象,直接格式化 - try: - return json.dumps(data, indent=indent, ensure_ascii=ensure_ascii) - except (TypeError, ValueError): - # 如果无法序列化,返回字符串表示 - return str(data) + return json.dumps(data, indent=indent, ensure_ascii=ensure_ascii) diff --git a/template/bot_config_template.toml b/template/bot_config_template.toml index 1292602cc..f870a509a 100644 --- a/template/bot_config_template.toml +++ b/template/bot_config_template.toml @@ -1,5 +1,5 @@ [inner] -version = "2.18.0" +version = "2.19.0" #----以下是给开发人员阅读的,如果你只是部署了麦麦,不需要阅读---- #如果你想要修改配置文件,请在修改后将version的值进行变更 @@ -183,7 +183,15 @@ max_length = 512 # 回复允许的最大长度 max_sentence_num = 8 # 回复允许的最大句子数 enable_kaomoji_protection = false # 是否启用颜文字保护 +[log] +date_style = "Y-m-d H:i:s" # 日期格式 +log_level_style = "lite" # 日志级别样式,可选FULL,compact,lite +color_text = "title" # 日志文本颜色,可选none,title,full +log_level = "INFO" +# 第三方库日志控制 +suppress_libraries = ["faiss","httpx", "urllib3", "asyncio", "websockets", "httpcore", "requests", "peewee", "openai"] # 完全屏蔽的库 +library_log_levels = { "aiohttp" = "WARNING"} # 设置特定库的日志级别 #下面的模型若使用硅基流动则不需要更改,使用ds官方则改成.env自定义的宏,使用自定义模型则选择定位相似的模型自己填写 diff --git a/template/template.env b/template/template.env index dd63a5f45..27f51ddc5 100644 --- a/template/template.env +++ b/template/template.env @@ -11,21 +11,4 @@ xxxxxxx_BASE_URL=https://xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx DEEP_SEEK_KEY= CHAT_ANY_WHERE_KEY= SILICONFLOW_KEY= -xxxxxxx_KEY= - -# 定义日志相关配置 - -# 精简控制台输出格式 -SIMPLE_OUTPUT=true - -# 自定义日志的默认控制台输出日志级别 -CONSOLE_LOG_LEVEL=INFO - -# 自定义日志的默认文件输出日志级别 -FILE_LOG_LEVEL=DEBUG - -# 原生日志的控制台输出日志级别 -DEFAULT_CONSOLE_LOG_LEVEL=SUCCESS - -# 原生日志的默认文件输出日志级别 -DEFAULT_FILE_LOG_LEVEL=DEBUG +xxxxxxx_KEY= \ No newline at end of file