560 lines
23 KiB
Python
560 lines
23 KiB
Python
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('<<ComboboxSelected>>', 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('<Button-1>', 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('<Button-1>', 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('<Enter>', on_enter)
|
||
widget.bind('<Leave>', 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() |