From 398e15232e2f799d94647870f526111792548057 Mon Sep 17 00:00:00 2001 From: SengokuCola <1026294844@qq.com> Date: Wed, 23 Jul 2025 23:55:15 +0800 Subject: [PATCH] =?UTF-8?q?feat=EF=BC=9A=E5=8D=87=E7=BA=A7loger=5Fviewer?= =?UTF-8?q?=EF=BC=8C=E7=A7=BB=E9=99=A4=E6=97=A0=E7=94=A8=E8=84=9A=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/analyze_expression_similarity.py | 192 --- scripts/analyze_expressions.py | 215 --- scripts/analyze_group_similarity.py | 196 --- scripts/find_similar_expression.py | 252 ---- scripts/log_viewer.py | 1185 ----------------- scripts/log_viewer_optimized.py | 708 +++++++++- scripts/preview_expressions.py | 278 ---- scripts/view_hfc_stats.py | 185 --- src/chat/chat_loop/heartFC_chat.py | 2 +- src/chat/replyer/default_generator.py | 2 +- src/chat/utils/statistic.py | 990 +------------- .../mai_thinking => mais4u}/mai_think.py | 0 src/plugins/built_in/core_actions/reply.py | 2 +- 13 files changed, 672 insertions(+), 3535 deletions(-) delete mode 100644 scripts/analyze_expression_similarity.py delete mode 100644 scripts/analyze_expressions.py delete mode 100644 scripts/analyze_group_similarity.py delete mode 100644 scripts/find_similar_expression.py delete mode 100644 scripts/log_viewer.py delete mode 100644 scripts/preview_expressions.py delete mode 100644 scripts/view_hfc_stats.py rename src/{chat/mai_thinking => mais4u}/mai_think.py (100%) diff --git a/scripts/analyze_expression_similarity.py b/scripts/analyze_expression_similarity.py deleted file mode 100644 index d84d21db1..000000000 --- a/scripts/analyze_expression_similarity.py +++ /dev/null @@ -1,192 +0,0 @@ -import os -import json -from typing import List, Dict, Tuple -import numpy as np -from sklearn.feature_extraction.text import TfidfVectorizer -from sklearn.metrics.pairwise import cosine_similarity -import glob -import sqlite3 -import re -from datetime import datetime - - -def clean_group_name(name: str) -> str: - """清理群组名称,只保留中文和英文字符""" - cleaned = re.sub(r"[^\u4e00-\u9fa5a-zA-Z]", "", name) - if not cleaned: - cleaned = datetime.now().strftime("%Y%m%d") - return cleaned - - -def get_group_name(stream_id: str) -> str: - """从数据库中获取群组名称""" - conn = sqlite3.connect("data/maibot.db") - cursor = conn.cursor() - - cursor.execute( - """ - SELECT group_name, user_nickname, platform - FROM chat_streams - WHERE stream_id = ? - """, - (stream_id,), - ) - - result = cursor.fetchone() - conn.close() - - if result: - group_name, user_nickname, platform = result - if group_name: - return clean_group_name(group_name) - if user_nickname: - return clean_group_name(user_nickname) - if platform: - return clean_group_name(f"{platform}{stream_id[:8]}") - return stream_id - - -def format_timestamp(timestamp: float) -> str: - """将时间戳转换为可读的时间格式""" - if not timestamp: - return "未知" - try: - dt = datetime.fromtimestamp(timestamp) - return dt.strftime("%Y-%m-%d %H:%M:%S") - except Exception as e: - print(f"时间戳格式化错误: {e}") - return "未知" - - -def load_expressions(chat_id: str) -> List[Dict]: - """加载指定群聊的表达方式""" - style_file = os.path.join("data", "expression", "learnt_style", str(chat_id), "expressions.json") - - style_exprs = [] - - if os.path.exists(style_file): - with open(style_file, "r", encoding="utf-8") as f: - style_exprs = json.load(f) - - return style_exprs - - -def find_similar_expressions(expressions: List[Dict], top_k: int = 5) -> Dict[str, List[Tuple[str, float]]]: - """找出每个表达方式最相似的top_k个表达方式""" - if not expressions: - return {} - - # 分别准备情景和表达方式的文本数据 - situations = [expr["situation"] for expr in expressions] - styles = [expr["style"] for expr in expressions] - - # 使用TF-IDF向量化 - vectorizer = TfidfVectorizer() - situation_matrix = vectorizer.fit_transform(situations) - style_matrix = vectorizer.fit_transform(styles) - - # 计算余弦相似度 - situation_similarity = cosine_similarity(situation_matrix) - style_similarity = cosine_similarity(style_matrix) - - # 对每个表达方式找出最相似的top_k个 - similar_expressions = {} - for i, _ in enumerate(expressions): - # 获取相似度分数 - situation_scores = situation_similarity[i] - style_scores = style_similarity[i] - - # 获取top_k的索引(排除自己) - situation_indices = np.argsort(situation_scores)[::-1][1 : top_k + 1] - style_indices = np.argsort(style_scores)[::-1][1 : top_k + 1] - - similar_situations = [] - similar_styles = [] - - # 处理相似情景 - for idx in situation_indices: - if situation_scores[idx] > 0: # 只保留有相似度的 - similar_situations.append( - ( - expressions[idx]["situation"], - expressions[idx]["style"], # 添加对应的原始表达 - situation_scores[idx], - ) - ) - - # 处理相似表达 - for idx in style_indices: - if style_scores[idx] > 0: # 只保留有相似度的 - similar_styles.append( - ( - expressions[idx]["style"], - expressions[idx]["situation"], # 添加对应的原始情景 - style_scores[idx], - ) - ) - - if similar_situations or similar_styles: - similar_expressions[i] = {"situations": similar_situations, "styles": similar_styles} - - return similar_expressions - - -def main(): - # 获取所有群聊ID - style_dirs = glob.glob(os.path.join("data", "expression", "learnt_style", "*")) - chat_ids = [os.path.basename(d) for d in style_dirs] - - if not chat_ids: - print("没有找到任何群聊的表达方式数据") - return - - print("可用的群聊:") - for i, chat_id in enumerate(chat_ids, 1): - group_name = get_group_name(chat_id) - print(f"{i}. {group_name}") - - while True: - try: - choice = int(input("\n请选择要分析的群聊编号 (输入0退出): ")) - if choice == 0: - break - if 1 <= choice <= len(chat_ids): - chat_id = chat_ids[choice - 1] - break - print("无效的选择,请重试") - except ValueError: - print("请输入有效的数字") - - if choice == 0: - return - - # 加载表达方式 - style_exprs = load_expressions(chat_id) - - group_name = get_group_name(chat_id) - print(f"\n分析群聊 {group_name} 的表达方式:") - - similar_styles = find_similar_expressions(style_exprs) - for i, expr in enumerate(style_exprs): - if i in similar_styles: - print("\n" + "-" * 20) - print(f"表达方式:{expr['style']} <---> 情景:{expr['situation']}") - - if similar_styles[i]["styles"]: - print("\n\033[33m相似表达:\033[0m") - for similar_style, original_situation, score in similar_styles[i]["styles"]: - print(f"\033[33m{similar_style},score:{score:.3f},对应情景:{original_situation}\033[0m") - - if similar_styles[i]["situations"]: - print("\n\033[32m相似情景:\033[0m") - for similar_situation, original_style, score in similar_styles[i]["situations"]: - print(f"\033[32m{similar_situation},score:{score:.3f},对应表达:{original_style}\033[0m") - - print( - f"\n激活值:{expr.get('count', 1):.3f},上次激活时间:{format_timestamp(expr.get('last_active_time'))}" - ) - print("-" * 20) - - -if __name__ == "__main__": - main() diff --git a/scripts/analyze_expressions.py b/scripts/analyze_expressions.py deleted file mode 100644 index ecbb3f381..000000000 --- a/scripts/analyze_expressions.py +++ /dev/null @@ -1,215 +0,0 @@ -import os -import json -import time -import re -from datetime import datetime -from typing import Dict, List, Any -import sqlite3 - - -def clean_group_name(name: str) -> str: - """清理群组名称,只保留中文和英文字符""" - # 提取中文和英文字符 - cleaned = re.sub(r"[^\u4e00-\u9fa5a-zA-Z]", "", name) - # 如果清理后为空,使用当前日期 - if not cleaned: - cleaned = datetime.now().strftime("%Y%m%d") - return cleaned - - -def get_group_name(stream_id: str) -> str: - """从数据库中获取群组名称""" - conn = sqlite3.connect("data/maibot.db") - cursor = conn.cursor() - - cursor.execute( - """ - SELECT group_name, user_nickname, platform - FROM chat_streams - WHERE stream_id = ? - """, - (stream_id,), - ) - - result = cursor.fetchone() - conn.close() - - if result: - group_name, user_nickname, platform = result - if group_name: - return clean_group_name(group_name) - if user_nickname: - return clean_group_name(user_nickname) - if platform: - return clean_group_name(f"{platform}{stream_id[:8]}") - return stream_id - - -def load_expressions(chat_id: str) -> tuple[List[Dict[str, Any]], List[Dict[str, Any]], List[Dict[str, Any]]]: - """加载指定群组的表达方式""" - learnt_style_file = os.path.join("data", "expression", "learnt_style", str(chat_id), "expressions.json") - learnt_grammar_file = os.path.join("data", "expression", "learnt_grammar", str(chat_id), "expressions.json") - personality_file = os.path.join("data", "expression", "personality", "expressions.json") - - style_expressions = [] - grammar_expressions = [] - personality_expressions = [] - - if os.path.exists(learnt_style_file): - with open(learnt_style_file, "r", encoding="utf-8") as f: - style_expressions = json.load(f) - - if os.path.exists(learnt_grammar_file): - with open(learnt_grammar_file, "r", encoding="utf-8") as f: - grammar_expressions = json.load(f) - - if os.path.exists(personality_file): - with open(personality_file, "r", encoding="utf-8") as f: - personality_expressions = json.load(f) - - return style_expressions, grammar_expressions, personality_expressions - - -def format_time(timestamp: float) -> str: - """格式化时间戳为可读字符串""" - return datetime.fromtimestamp(timestamp).strftime("%Y-%m-%d %H:%M:%S") - - -def write_expressions(f, expressions: List[Dict[str, Any]], title: str): - """写入表达方式列表""" - if not expressions: - f.write(f"{title}:暂无数据\n") - f.write("-" * 40 + "\n") - return - - f.write(f"{title}:\n") - for expr in expressions: - count = expr.get("count", 0) - last_active = expr.get("last_active_time", time.time()) - f.write(f"场景: {expr['situation']}\n") - f.write(f"表达: {expr['style']}\n") - f.write(f"计数: {count:.4f}\n") - f.write(f"最后活跃: {format_time(last_active)}\n") - f.write("-" * 40 + "\n") - - -def write_group_report( - group_file: str, - group_name: str, - chat_id: str, - style_exprs: List[Dict[str, Any]], - grammar_exprs: List[Dict[str, Any]], -): - """写入群组详细报告""" - with open(group_file, "w", encoding="utf-8") as gf: - gf.write(f"群组: {group_name} (ID: {chat_id})\n") - gf.write("=" * 80 + "\n\n") - - # 写入语言风格 - gf.write("【语言风格】\n") - gf.write("=" * 40 + "\n") - write_expressions(gf, style_exprs, "语言风格") - gf.write("\n") - - # 写入句法特点 - gf.write("【句法特点】\n") - gf.write("=" * 40 + "\n") - write_expressions(gf, grammar_exprs, "句法特点") - - -def analyze_expressions(): - """分析所有群组的表达方式""" - # 获取所有群组ID - style_dir = os.path.join("data", "expression", "learnt_style") - chat_ids = [d for d in os.listdir(style_dir) if os.path.isdir(os.path.join(style_dir, d))] - - # 创建输出目录 - output_dir = "data/expression_analysis" - personality_dir = os.path.join(output_dir, "personality") - os.makedirs(output_dir, exist_ok=True) - os.makedirs(personality_dir, exist_ok=True) - - # 生成时间戳 - timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") - - # 创建总报告 - summary_file = os.path.join(output_dir, f"summary_{timestamp}.txt") - with open(summary_file, "w", encoding="utf-8") as f: - f.write(f"表达方式分析报告 - 生成时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n") - f.write("=" * 80 + "\n\n") - - # 先处理人格表达 - personality_exprs = [] - personality_file = os.path.join("data", "expression", "personality", "expressions.json") - if os.path.exists(personality_file): - with open(personality_file, "r", encoding="utf-8") as pf: - personality_exprs = json.load(pf) - - # 保存人格表达总数 - total_personality = len(personality_exprs) - - # 排序并取前20条 - personality_exprs.sort(key=lambda x: x.get("count", 0), reverse=True) - personality_exprs = personality_exprs[:20] - - # 写入人格表达报告 - personality_report = os.path.join(personality_dir, f"expressions_{timestamp}.txt") - with open(personality_report, "w", encoding="utf-8") as pf: - pf.write("【人格表达方式】\n") - pf.write("=" * 40 + "\n") - write_expressions(pf, personality_exprs, "人格表达") - - # 写入总报告摘要中的人格表达部分 - f.write("【人格表达方式】\n") - f.write("=" * 40 + "\n") - f.write(f"人格表达总数: {total_personality} (显示前20条)\n") - f.write(f"详细报告: {personality_report}\n") - f.write("-" * 40 + "\n\n") - - # 处理各个群组的表达方式 - f.write("【群组表达方式】\n") - f.write("=" * 40 + "\n\n") - - for chat_id in chat_ids: - style_exprs, grammar_exprs, _ = load_expressions(chat_id) - - # 保存总数 - total_style = len(style_exprs) - total_grammar = len(grammar_exprs) - - # 分别排序 - style_exprs.sort(key=lambda x: x.get("count", 0), reverse=True) - grammar_exprs.sort(key=lambda x: x.get("count", 0), reverse=True) - - # 只取前20条 - style_exprs = style_exprs[:20] - grammar_exprs = grammar_exprs[:20] - - # 获取群组名称 - group_name = get_group_name(chat_id) - - # 创建群组子目录(使用清理后的名称) - safe_group_name = clean_group_name(group_name) - group_dir = os.path.join(output_dir, f"{safe_group_name}_{chat_id}") - os.makedirs(group_dir, exist_ok=True) - - # 写入群组详细报告 - group_file = os.path.join(group_dir, f"expressions_{timestamp}.txt") - write_group_report(group_file, group_name, chat_id, style_exprs, grammar_exprs) - - # 写入总报告摘要 - f.write(f"群组: {group_name} (ID: {chat_id})\n") - f.write("-" * 40 + "\n") - f.write(f"语言风格总数: {total_style} (显示前20条)\n") - f.write(f"句法特点总数: {total_grammar} (显示前20条)\n") - f.write(f"详细报告: {group_file}\n") - f.write("-" * 40 + "\n\n") - - print("分析报告已生成:") - print(f"总报告: {summary_file}") - print(f"人格表达报告: {personality_report}") - print(f"各群组详细报告位于: {output_dir}") - - -if __name__ == "__main__": - analyze_expressions() diff --git a/scripts/analyze_group_similarity.py b/scripts/analyze_group_similarity.py deleted file mode 100644 index f1d53ee20..000000000 --- a/scripts/analyze_group_similarity.py +++ /dev/null @@ -1,196 +0,0 @@ -import json -from pathlib import Path -import numpy as np -from sklearn.feature_extraction.text import TfidfVectorizer -from sklearn.metrics.pairwise import cosine_similarity -import matplotlib.pyplot as plt -import seaborn as sns -import sqlite3 - -# 设置中文字体 -plt.rcParams["font.sans-serif"] = ["Microsoft YaHei"] # 使用微软雅黑 -plt.rcParams["axes.unicode_minus"] = False # 用来正常显示负号 -plt.rcParams["font.family"] = "sans-serif" - -# 获取脚本所在目录 -SCRIPT_DIR = Path(__file__).parent - - -def get_group_name(stream_id): - """从数据库中获取群组名称""" - conn = sqlite3.connect("data/maibot.db") - cursor = conn.cursor() - - cursor.execute( - """ - SELECT group_name, user_nickname, platform - FROM chat_streams - WHERE stream_id = ? - """, - (stream_id,), - ) - - result = cursor.fetchone() - conn.close() - - if result: - group_name, user_nickname, platform = result - if group_name: - return group_name - if user_nickname: - return user_nickname - if platform: - return f"{platform}-{stream_id[:8]}" - return stream_id - - -def load_group_data(group_dir): - """加载单个群组的数据""" - json_path = Path(group_dir) / "expressions.json" - if not json_path.exists(): - return [], [], [], 0 - - with open(json_path, "r", encoding="utf-8") as f: - data = json.load(f) - - situations = [] - styles = [] - combined = [] - total_count = sum(item["count"] for item in data) - - for item in data: - count = item["count"] - situations.extend([item["situation"]] * int(count)) - styles.extend([item["style"]] * int(count)) - combined.extend([f"{item['situation']} {item['style']}"] * int(count)) - - return situations, styles, combined, total_count - - -def analyze_group_similarity(): - # 获取所有群组目录 - base_dir = Path("data/expression/learnt_style") - group_dirs = [d for d in base_dir.iterdir() if d.is_dir()] - - # 加载所有群组的数据并过滤 - valid_groups = [] - valid_names = [] - valid_situations = [] - valid_styles = [] - valid_combined = [] - - for d in group_dirs: - situations, styles, combined, total_count = load_group_data(d) - if total_count >= 50: # 只保留数据量大于等于50的群组 - valid_groups.append(d) - valid_names.append(get_group_name(d.name)) - valid_situations.append(" ".join(situations)) - valid_styles.append(" ".join(styles)) - valid_combined.append(" ".join(combined)) - - if not valid_groups: - print("没有找到数据量大于等于50的群组") - return - - # 创建TF-IDF向量化器 - vectorizer = TfidfVectorizer() - - # 计算三种相似度矩阵 - situation_matrix = cosine_similarity(vectorizer.fit_transform(valid_situations)) - style_matrix = cosine_similarity(vectorizer.fit_transform(valid_styles)) - combined_matrix = cosine_similarity(vectorizer.fit_transform(valid_combined)) - - # 对相似度矩阵进行对数变换 - log_situation_matrix = np.log10(situation_matrix * 100 + 1) * 10 / np.log10(4) - log_style_matrix = np.log10(style_matrix * 100 + 1) * 10 / np.log10(4) - log_combined_matrix = np.log10(combined_matrix * 100 + 1) * 10 / np.log10(4) - - # 创建一个大图,包含三个子图 - plt.figure(figsize=(45, 12)) - - # 场景相似度热力图 - plt.subplot(1, 3, 1) - sns.heatmap( - log_situation_matrix, - xticklabels=valid_names, - yticklabels=valid_names, - cmap="YlOrRd", - annot=True, - fmt=".1f", - vmin=0, - vmax=30, - ) - plt.title("群组场景相似度热力图 (对数百分比)") - plt.xticks(rotation=45, ha="right") - - # 表达方式相似度热力图 - plt.subplot(1, 3, 2) - sns.heatmap( - log_style_matrix, - xticklabels=valid_names, - yticklabels=valid_names, - cmap="YlOrRd", - annot=True, - fmt=".1f", - vmin=0, - vmax=30, - ) - plt.title("群组表达方式相似度热力图 (对数百分比)") - plt.xticks(rotation=45, ha="right") - - # 组合相似度热力图 - plt.subplot(1, 3, 3) - sns.heatmap( - log_combined_matrix, - xticklabels=valid_names, - yticklabels=valid_names, - cmap="YlOrRd", - annot=True, - fmt=".1f", - vmin=0, - vmax=30, - ) - plt.title("群组场景+表达方式相似度热力图 (对数百分比)") - plt.xticks(rotation=45, ha="right") - - plt.tight_layout() - plt.savefig(SCRIPT_DIR / "group_similarity_heatmaps.png", dpi=300, bbox_inches="tight") - plt.close() - - # 保存匹配详情到文本文件 - with open(SCRIPT_DIR / "group_similarity_details.txt", "w", encoding="utf-8") as f: - f.write("群组相似度详情\n") - f.write("=" * 50 + "\n\n") - - for i in range(len(valid_names)): - for j in range(i + 1, len(valid_names)): - if log_combined_matrix[i][j] > 50: - f.write(f"群组1: {valid_names[i]}\n") - f.write(f"群组2: {valid_names[j]}\n") - f.write(f"场景相似度: {situation_matrix[i][j]:.4f}\n") - f.write(f"表达方式相似度: {style_matrix[i][j]:.4f}\n") - f.write(f"组合相似度: {combined_matrix[i][j]:.4f}\n") - - # 获取两个群组的数据 - situations1, styles1, _ = load_group_data(valid_groups[i]) - situations2, styles2, _ = load_group_data(valid_groups[j]) - - # 找出共同的场景 - common_situations = set(situations1) & set(situations2) - if common_situations: - f.write("\n共同场景:\n") - for situation in common_situations: - f.write(f"- {situation}\n") - - # 找出共同的表达方式 - common_styles = set(styles1) & set(styles2) - if common_styles: - f.write("\n共同表达方式:\n") - for style in common_styles: - f.write(f"- {style}\n") - - f.write("\n" + "-" * 50 + "\n\n") - - -if __name__ == "__main__": - analyze_group_similarity() diff --git a/scripts/find_similar_expression.py b/scripts/find_similar_expression.py deleted file mode 100644 index 23f9e63d9..000000000 --- a/scripts/find_similar_expression.py +++ /dev/null @@ -1,252 +0,0 @@ -import os -import sys - -sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) - -import json -from typing import List, Dict, Tuple -import numpy as np -from sklearn.feature_extraction.text import TfidfVectorizer -from sklearn.metrics.pairwise import cosine_similarity -import glob -import sqlite3 -import re -from datetime import datetime -import random -from src.llm_models.utils_model import LLMRequest -from src.config.config import global_config - - -def clean_group_name(name: str) -> str: - """清理群组名称,只保留中文和英文字符""" - cleaned = re.sub(r"[^\u4e00-\u9fa5a-zA-Z]", "", name) - if not cleaned: - cleaned = datetime.now().strftime("%Y%m%d") - return cleaned - - -def get_group_name(stream_id: str) -> str: - """从数据库中获取群组名称""" - conn = sqlite3.connect("data/maibot.db") - cursor = conn.cursor() - - cursor.execute( - """ - SELECT group_name, user_nickname, platform - FROM chat_streams - WHERE stream_id = ? - """, - (stream_id,), - ) - - result = cursor.fetchone() - conn.close() - - if result: - group_name, user_nickname, platform = result - if group_name: - return clean_group_name(group_name) - if user_nickname: - return clean_group_name(user_nickname) - if platform: - return clean_group_name(f"{platform}{stream_id[:8]}") - return stream_id - - -def load_expressions(chat_id: str) -> List[Dict]: - """加载指定群聊的表达方式""" - style_file = os.path.join("data", "expression", "learnt_style", str(chat_id), "expressions.json") - - style_exprs = [] - - if os.path.exists(style_file): - with open(style_file, "r", encoding="utf-8") as f: - style_exprs = json.load(f) - - # 如果表达方式超过10个,随机选择10个 - if len(style_exprs) > 50: - style_exprs = random.sample(style_exprs, 50) - print(f"\n从 {len(style_exprs)} 个表达方式中随机选择了 10 个进行匹配") - - return style_exprs - - -def find_similar_expressions_tfidf( - input_text: str, expressions: List[Dict], mode: str = "both", top_k: int = 10 -) -> List[Tuple[str, str, float]]: - """使用TF-IDF方法找出与输入文本最相似的top_k个表达方式""" - if not expressions: - return [] - - # 准备文本数据 - if mode == "style": - texts = [expr["style"] for expr in expressions] - elif mode == "situation": - texts = [expr["situation"] for expr in expressions] - else: # both - texts = [f"{expr['situation']} {expr['style']}" for expr in expressions] - - texts.append(input_text) # 添加输入文本 - - # 使用TF-IDF向量化 - vectorizer = TfidfVectorizer() - tfidf_matrix = vectorizer.fit_transform(texts) - - # 计算余弦相似度 - similarity_matrix = cosine_similarity(tfidf_matrix) - - # 获取输入文本的相似度分数(最后一行) - scores = similarity_matrix[-1][:-1] # 排除与自身的相似度 - - # 获取top_k的索引 - top_indices = np.argsort(scores)[::-1][:top_k] - - # 获取相似表达 - similar_exprs = [] - for idx in top_indices: - if scores[idx] > 0: # 只保留有相似度的 - similar_exprs.append((expressions[idx]["style"], expressions[idx]["situation"], scores[idx])) - - return similar_exprs - - -async def find_similar_expressions_embedding( - input_text: str, expressions: List[Dict], mode: str = "both", top_k: int = 5 -) -> List[Tuple[str, str, float]]: - """使用嵌入模型找出与输入文本最相似的top_k个表达方式""" - if not expressions: - return [] - - # 准备文本数据 - if mode == "style": - texts = [expr["style"] for expr in expressions] - elif mode == "situation": - texts = [expr["situation"] for expr in expressions] - else: # both - texts = [f"{expr['situation']} {expr['style']}" for expr in expressions] - - # 获取嵌入向量 - llm_request = LLMRequest(global_config.model.embedding) - text_embeddings = [] - for text in texts: - embedding = await llm_request.get_embedding(text) - if embedding: - text_embeddings.append(embedding) - - input_embedding = await llm_request.get_embedding(input_text) - if not input_embedding or not text_embeddings: - return [] - - # 计算余弦相似度 - text_embeddings = np.array(text_embeddings) - similarities = np.dot(text_embeddings, input_embedding) / ( - np.linalg.norm(text_embeddings, axis=1) * np.linalg.norm(input_embedding) - ) - - # 获取top_k的索引 - top_indices = np.argsort(similarities)[::-1][:top_k] - - # 获取相似表达 - similar_exprs = [] - for idx in top_indices: - if similarities[idx] > 0: # 只保留有相似度的 - similar_exprs.append((expressions[idx]["style"], expressions[idx]["situation"], similarities[idx])) - - return similar_exprs - - -async def main(): - # 获取所有群聊ID - style_dirs = glob.glob(os.path.join("data", "expression", "learnt_style", "*")) - chat_ids = [os.path.basename(d) for d in style_dirs] - - if not chat_ids: - print("没有找到任何群聊的表达方式数据") - return - - print("可用的群聊:") - for i, chat_id in enumerate(chat_ids, 1): - group_name = get_group_name(chat_id) - print(f"{i}. {group_name}") - - while True: - try: - choice = int(input("\n请选择要分析的群聊编号 (输入0退出): ")) - if choice == 0: - break - if 1 <= choice <= len(chat_ids): - chat_id = chat_ids[choice - 1] - break - print("无效的选择,请重试") - except ValueError: - print("请输入有效的数字") - - if choice == 0: - return - - # 加载表达方式 - style_exprs = load_expressions(chat_id) - - group_name = get_group_name(chat_id) - print(f"\n已选择群聊:{group_name}") - - # 选择匹配模式 - print("\n请选择匹配模式:") - print("1. 匹配表达方式") - print("2. 匹配情景") - print("3. 两者都考虑") - - while True: - try: - mode_choice = int(input("\n请选择匹配模式 (1-3): ")) - if 1 <= mode_choice <= 3: - break - print("无效的选择,请重试") - except ValueError: - print("请输入有效的数字") - - mode_map = {1: "style", 2: "situation", 3: "both"} - mode = mode_map[mode_choice] - - # 选择匹配方法 - print("\n请选择匹配方法:") - print("1. TF-IDF方法") - print("2. 嵌入模型方法") - - while True: - try: - method_choice = int(input("\n请选择匹配方法 (1-2): ")) - if 1 <= method_choice <= 2: - break - print("无效的选择,请重试") - except ValueError: - print("请输入有效的数字") - - while True: - input_text = input("\n请输入要匹配的文本(输入q退出): ") - if input_text.lower() == "q": - break - - if not input_text.strip(): - continue - - if method_choice == 1: - similar_exprs = find_similar_expressions_tfidf(input_text, style_exprs, mode) - else: - similar_exprs = await find_similar_expressions_embedding(input_text, style_exprs, mode) - - if similar_exprs: - print("\n找到以下相似表达:") - for style, situation, score in similar_exprs: - print(f"\n\033[33m表达方式:{style}\033[0m") - print(f"\033[32m对应情景:{situation}\033[0m") - print(f"相似度:{score:.3f}") - print("-" * 20) - else: - print("\n没有找到相似的表达方式") - - -if __name__ == "__main__": - import asyncio - - asyncio.run(main()) diff --git a/scripts/log_viewer.py b/scripts/log_viewer.py deleted file mode 100644 index 248919fa8..000000000 --- a/scripts/log_viewer.py +++ /dev/null @@ -1,1185 +0,0 @@ -import tkinter as tk -from tkinter import ttk, colorchooser, messagebox, filedialog -import json -from pathlib import Path -import threading -import queue -import time -import toml -from datetime import datetime - - -class LogFormatter: - """日志格式化器,同步logger.py的格式""" - - def __init__(self, config, custom_module_colors=None, custom_level_colors=None): - self.config = config - - # 日志级别颜色 - self.level_colors = { - "debug": "#FFA500", # 橙色 - "info": "#0000FF", # 蓝色 - "success": "#008000", # 绿色 - "warning": "#FFFF00", # 黄色 - "error": "#FF0000", # 红色 - "critical": "#800080", # 紫色 - } - - # 模块颜色映射 - 同步logger.py中的MODULE_COLORS - self.module_colors = { - "api": "#00FF00", # 亮绿色 - "emoji": "#00FF00", # 亮绿色 - "chat": "#0080FF", # 亮蓝色 - "config": "#FFFF00", # 亮黄色 - "common": "#FF00FF", # 亮紫色 - "tools": "#00FFFF", # 亮青色 - "lpmm": "#00FFFF", # 亮青色 - "plugin_system": "#FF0080", # 亮红色 - "experimental": "#FFFFFF", # 亮白色 - "person_info": "#008000", # 绿色 - "individuality": "#000080", # 蓝色 - "manager": "#800080", # 紫色 - "llm_models": "#008080", # 青色 - "plugins": "#800000", # 红色 - "plugin_api": "#808000", # 黄色 - "remote": "#8000FF", # 紫蓝色 - } - - # 应用自定义颜色 - if custom_module_colors: - self.module_colors.update(custom_module_colors) - if custom_level_colors: - self.level_colors.update(custom_level_colors) - - # 根据配置决定颜色启用状态 - color_text = self.config.get("color_text", "full") - if color_text == "none": - self.enable_colors = False - self.enable_module_colors = False - self.enable_level_colors = False - elif color_text == "title": - self.enable_colors = True - self.enable_module_colors = True - self.enable_level_colors = False - elif color_text == "full": - self.enable_colors = True - self.enable_module_colors = True - self.enable_level_colors = True - else: - self.enable_colors = True - self.enable_module_colors = True - self.enable_level_colors = False - - def format_log_entry(self, 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", "") - - # 格式化时间戳 - formatted_timestamp = self.format_timestamp(timestamp) - - # 构建输出部分 - parts = [] - tags = [] - - # 日志级别样式配置 - log_level_style = self.config.get("log_level_style", "lite") - - # 时间戳 - if formatted_timestamp: - if log_level_style == "lite" and self.enable_level_colors: - # lite模式下时间戳按级别着色 - parts.append(formatted_timestamp) - tags.append(f"level_{level}") - else: - parts.append(formatted_timestamp) - tags.append("timestamp") - - # 日志级别显示 - if log_level_style == "full": - # 显示完整级别名 - level_text = f"[{level.upper():>8}]" - parts.append(level_text) - if self.enable_level_colors: - tags.append(f"level_{level}") - else: - tags.append("level") - elif log_level_style == "compact": - # 只显示首字母 - level_text = f"[{level.upper()[0]:>8}]" - parts.append(level_text) - if self.enable_level_colors: - tags.append(f"level_{level}") - else: - tags.append("level") - # lite模式不显示级别 - - # 模块名称 - if logger_name: - module_text = f"[{logger_name}]" - parts.append(module_text) - if self.enable_module_colors: - tags.append(f"module_{logger_name}") - else: - tags.append("module") - - # 消息内容 - 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)) - tags.append("message") - - # 处理其他字段 - extras = [] - for key, value in log_entry.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)) - tags.append("extras") - - return parts, tags - - def format_timestamp(self, timestamp): - """格式化时间戳""" - if not timestamp: - return "" - - try: - # 尝试解析ISO格式时间戳 - if "T" in timestamp: - dt = datetime.fromisoformat(timestamp.replace("Z", "+00:00")) - else: - # 假设已经是格式化的字符串 - return timestamp - - # 根据配置格式化 - date_style = self.config.get("date_style", "m-d H:i:s") - 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 dt.strftime(python_format) - except Exception: - return timestamp - - -class LogViewer: - def __init__(self, root): - self.root = root - self.root.title("MaiBot日志查看器") - self.root.geometry("1200x800") - - # 加载配置 - self.load_config() - - # 初始化日志格式化器 - self.formatter = LogFormatter(self.log_config, self.custom_module_colors, self.custom_level_colors) - - # 初始化日志文件路径 - self.current_log_file = Path("logs/app.log.jsonl") - - # 创建主框架 - self.main_frame = ttk.Frame(root) - self.main_frame.pack(fill=tk.BOTH, expand=True, padx=5, pady=5) - - # 创建菜单栏 - self.create_menu() - - # 创建控制面板 - self.control_frame = ttk.Frame(self.main_frame) - self.control_frame.pack(fill=tk.X, pady=(0, 5)) - - # 文件选择框架 - self.file_frame = ttk.LabelFrame(self.control_frame, text="日志文件") - self.file_frame.pack(side=tk.TOP, fill=tk.X, padx=5, pady=(0, 5)) - - # 当前文件显示 - self.current_file_var = tk.StringVar(value=str(self.current_log_file)) - self.file_label = ttk.Label(self.file_frame, textvariable=self.current_file_var, foreground="blue") - self.file_label.pack(side=tk.LEFT, padx=5, pady=2) - - # 选择文件按钮 - select_file_btn = ttk.Button(self.file_frame, text="选择文件", command=self.select_log_file) - select_file_btn.pack(side=tk.RIGHT, padx=5, pady=2) - - # 刷新按钮 - refresh_btn = ttk.Button(self.file_frame, text="刷新", command=self.refresh_log_file) - refresh_btn.pack(side=tk.RIGHT, padx=2, pady=2) - - # 模块选择框架 - 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"] = ["全部", "debug", "info", "warning", "error", "critical"] - 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, - background="#1e1e1e", - foreground="#ffffff", - insertbackground="#ffffff", - selectbackground="#404040", - ) - self.log_text.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) - self.scrollbar.config(command=self.log_text.yview) - - # 配置文本标签样式 - self.configure_text_tags() - - # 模块名映射 - 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() - - # 绑定快捷键 - self.root.bind("", lambda e: self.select_log_file()) - self.root.bind("", lambda e: self.refresh_log_file()) - self.root.bind("", lambda e: self.export_logs()) - - # 更新窗口标题 - self.update_window_title() - - def load_config(self): - """加载配置文件""" - # 默认配置 - self.default_config = { - "log": {"date_style": "m-d H:i:s", "log_level_style": "lite", "color_text": "full", "log_level": "INFO"}, - "viewer": { - "theme": "dark", - "font_size": 10, - "max_lines": 1000, - "auto_scroll": True, - "show_milliseconds": False, - "window": {"width": 1200, "height": 800, "remember_position": True}, - }, - } - - # 从bot_config.toml加载日志配置 - config_path = Path("config/bot_config.toml") - self.log_config = self.default_config["log"].copy() - self.viewer_config = self.default_config["viewer"].copy() - - try: - if config_path.exists(): - with open(config_path, "r", encoding="utf-8") as f: - bot_config = toml.load(f) - if "log" in bot_config: - self.log_config.update(bot_config["log"]) - except Exception as e: - print(f"加载bot配置失败: {e}") - - # 从viewer配置文件加载查看器配置 - viewer_config_path = Path("config/log_viewer_config.toml") - self.custom_module_colors = {} - self.custom_level_colors = {} - - try: - if viewer_config_path.exists(): - with open(viewer_config_path, "r", encoding="utf-8") as f: - viewer_config = toml.load(f) - if "viewer" in viewer_config: - self.viewer_config.update(viewer_config["viewer"]) - - # 加载自定义模块颜色 - if "module_colors" in viewer_config["viewer"]: - self.custom_module_colors = viewer_config["viewer"]["module_colors"] - - # 加载自定义级别颜色 - if "level_colors" in viewer_config["viewer"]: - self.custom_level_colors = viewer_config["viewer"]["level_colors"] - - if "log" in viewer_config: - self.log_config.update(viewer_config["log"]) - except Exception as e: - print(f"加载查看器配置失败: {e}") - - # 应用窗口配置 - window_config = self.viewer_config.get("window", {}) - window_width = window_config.get("width", 1200) - window_height = window_config.get("height", 800) - self.root.geometry(f"{window_width}x{window_height}") - - def save_viewer_config(self): - """保存查看器配置""" - # 准备完整的配置数据 - viewer_config_copy = self.viewer_config.copy() - - # 保存自定义颜色(只保存与默认值不同的颜色) - if self.custom_module_colors: - viewer_config_copy["module_colors"] = self.custom_module_colors - if self.custom_level_colors: - viewer_config_copy["level_colors"] = self.custom_level_colors - - config_data = {"log": self.log_config, "viewer": viewer_config_copy} - - config_path = Path("config/log_viewer_config.toml") - config_path.parent.mkdir(exist_ok=True) - - try: - with open(config_path, "w", encoding="utf-8") as f: - toml.dump(config_data, f) - except Exception as e: - print(f"保存查看器配置失败: {e}") - - def create_menu(self): - """创建菜单栏""" - menubar = tk.Menu(self.root) - self.root.config(menu=menubar) - - # 配置菜单 - config_menu = tk.Menu(menubar, tearoff=0) - menubar.add_cascade(label="配置", menu=config_menu) - config_menu.add_command(label="日志格式设置", command=self.show_format_settings) - config_menu.add_command(label="颜色设置", command=self.show_color_settings) - config_menu.add_command(label="查看器设置", command=self.show_viewer_settings) - config_menu.add_separator() - config_menu.add_command(label="重新加载配置", command=self.reload_config) - - # 文件菜单 - file_menu = tk.Menu(menubar, tearoff=0) - menubar.add_cascade(label="文件", menu=file_menu) - file_menu.add_command(label="选择日志文件", command=self.select_log_file, accelerator="Ctrl+O") - file_menu.add_command(label="刷新当前文件", command=self.refresh_log_file, accelerator="F5") - file_menu.add_separator() - file_menu.add_command(label="导出当前日志", command=self.export_logs, accelerator="Ctrl+S") - - # 工具菜单 - tools_menu = tk.Menu(menubar, tearoff=0) - menubar.add_cascade(label="工具", menu=tools_menu) - tools_menu.add_command(label="清空日志显示", command=self.clear_log_display) - - def show_format_settings(self): - """显示格式设置窗口""" - format_window = tk.Toplevel(self.root) - format_window.title("日志格式设置") - format_window.geometry("400x300") - - frame = ttk.Frame(format_window) - frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10) - - # 日期格式 - ttk.Label(frame, text="日期格式:").pack(anchor="w", pady=2) - date_style_var = tk.StringVar(value=self.log_config.get("date_style", "m-d H:i:s")) - date_entry = ttk.Entry(frame, textvariable=date_style_var, width=30) - date_entry.pack(anchor="w", pady=2) - ttk.Label(frame, text="格式说明: Y=年份, m=月份, d=日期, H=小时, i=分钟, s=秒", font=("", 8)).pack( - anchor="w", pady=2 - ) - - # 日志级别样式 - ttk.Label(frame, text="日志级别样式:").pack(anchor="w", pady=(10, 2)) - level_style_var = tk.StringVar(value=self.log_config.get("log_level_style", "lite")) - level_frame = ttk.Frame(frame) - level_frame.pack(anchor="w", pady=2) - - ttk.Radiobutton(level_frame, text="简洁(lite)", variable=level_style_var, value="lite").pack( - side="left", padx=(0, 10) - ) - ttk.Radiobutton(level_frame, text="紧凑(compact)", variable=level_style_var, value="compact").pack( - side="left", padx=(0, 10) - ) - ttk.Radiobutton(level_frame, text="完整(full)", variable=level_style_var, value="full").pack( - side="left", padx=(0, 10) - ) - - # 颜色文本设置 - ttk.Label(frame, text="文本颜色设置:").pack(anchor="w", pady=(10, 2)) - color_text_var = tk.StringVar(value=self.log_config.get("color_text", "full")) - color_frame = ttk.Frame(frame) - color_frame.pack(anchor="w", pady=2) - - ttk.Radiobutton(color_frame, text="无颜色(none)", variable=color_text_var, value="none").pack( - side="left", padx=(0, 10) - ) - ttk.Radiobutton(color_frame, text="仅标题(title)", variable=color_text_var, value="title").pack( - side="left", padx=(0, 10) - ) - ttk.Radiobutton(color_frame, text="全部(full)", variable=color_text_var, value="full").pack( - side="left", padx=(0, 10) - ) - - # 按钮 - button_frame = ttk.Frame(frame) - button_frame.pack(fill="x", pady=(20, 0)) - - def apply_format(): - self.log_config["date_style"] = date_style_var.get() - self.log_config["log_level_style"] = level_style_var.get() - self.log_config["color_text"] = color_text_var.get() - - # 重新初始化格式化器 - self.formatter = LogFormatter(self.log_config, self.custom_module_colors, self.custom_level_colors) - self.configure_text_tags() - - # 保存配置 - self.save_viewer_config() - - # 重新过滤日志以应用新格式 - self.filter_logs() - - format_window.destroy() - - ttk.Button(button_frame, text="应用", command=apply_format).pack(side="right", padx=(5, 0)) - ttk.Button(button_frame, text="取消", command=format_window.destroy).pack(side="right") - - def show_viewer_settings(self): - """显示查看器设置窗口""" - viewer_window = tk.Toplevel(self.root) - viewer_window.title("查看器设置") - viewer_window.geometry("350x250") - - frame = ttk.Frame(viewer_window) - frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10) - - # 主题设置 - ttk.Label(frame, text="主题:").pack(anchor="w", pady=2) - theme_var = tk.StringVar(value=self.viewer_config.get("theme", "dark")) - theme_frame = ttk.Frame(frame) - theme_frame.pack(anchor="w", pady=2) - ttk.Radiobutton(theme_frame, text="深色", variable=theme_var, value="dark").pack(side="left", padx=(0, 10)) - ttk.Radiobutton(theme_frame, text="浅色", variable=theme_var, value="light").pack(side="left") - - # 字体大小 - ttk.Label(frame, text="字体大小:").pack(anchor="w", pady=(10, 2)) - font_size_var = tk.IntVar(value=self.viewer_config.get("font_size", 10)) - font_size_spin = ttk.Spinbox(frame, from_=8, to=20, textvariable=font_size_var, width=10) - font_size_spin.pack(anchor="w", pady=2) - - # 最大行数 - ttk.Label(frame, text="最大显示行数:").pack(anchor="w", pady=(10, 2)) - max_lines_var = tk.IntVar(value=self.viewer_config.get("max_lines", 1000)) - max_lines_spin = ttk.Spinbox(frame, from_=100, to=10000, increment=100, textvariable=max_lines_var, width=10) - max_lines_spin.pack(anchor="w", pady=2) - - # 自动滚动 - auto_scroll_var = tk.BooleanVar(value=self.viewer_config.get("auto_scroll", True)) - ttk.Checkbutton(frame, text="自动滚动到底部", variable=auto_scroll_var).pack(anchor="w", pady=(10, 2)) - - # 按钮 - button_frame = ttk.Frame(frame) - button_frame.pack(fill="x", pady=(20, 0)) - - def apply_viewer_settings(): - self.viewer_config["theme"] = theme_var.get() - self.viewer_config["font_size"] = font_size_var.get() - self.viewer_config["max_lines"] = max_lines_var.get() - self.viewer_config["auto_scroll"] = auto_scroll_var.get() - - # 应用主题 - self.apply_theme() - - # 保存配置 - self.save_viewer_config() - - viewer_window.destroy() - - ttk.Button(button_frame, text="应用", command=apply_viewer_settings).pack(side="right", padx=(5, 0)) - ttk.Button(button_frame, text="取消", command=viewer_window.destroy).pack(side="right") - - def apply_theme(self): - """应用主题设置""" - theme = self.viewer_config.get("theme", "dark") - font_size = self.viewer_config.get("font_size", 10) - - if theme == "dark": - bg_color = "#1e1e1e" - fg_color = "#ffffff" - select_bg = "#404040" - else: - bg_color = "#ffffff" - fg_color = "#000000" - select_bg = "#c0c0c0" - - self.log_text.config( - background=bg_color, foreground=fg_color, selectbackground=select_bg, font=("Consolas", font_size) - ) - - # 重新配置标签样式 - self.configure_text_tags() - - def configure_text_tags(self): - """配置文本标签样式""" - # 清除现有标签 - for tag in self.log_text.tag_names(): - if tag != "sel": - self.log_text.tag_delete(tag) - - # 基础标签 - self.log_text.tag_configure("timestamp", foreground="#808080") - self.log_text.tag_configure("level", foreground="#808080") - self.log_text.tag_configure("module", foreground="#808080") - self.log_text.tag_configure("message", foreground=self.log_text.cget("foreground")) - self.log_text.tag_configure("extras", foreground="#808080") - - # 日志级别颜色标签 - for level, color in self.formatter.level_colors.items(): - self.log_text.tag_configure(f"level_{level}", foreground=color) - - # 模块颜色标签 - for module, color in self.formatter.module_colors.items(): - self.log_text.tag_configure(f"module_{module}", foreground=color) - - def reload_config(self): - """重新加载配置""" - self.load_config() - self.formatter = LogFormatter(self.log_config, self.custom_module_colors, self.custom_level_colors) - self.configure_text_tags() - self.apply_theme() - self.filter_logs() - - def clear_log_display(self): - """清空日志显示""" - self.log_text.delete(1.0, tk.END) - - def export_logs(self): - """导出当前显示的日志""" - filename = filedialog.asksaveasfilename( - defaultextension=".txt", filetypes=[("文本文件", "*.txt"), ("所有文件", "*.*")] - ) - if filename: - try: - with open(filename, "w", encoding="utf-8") as f: - f.write(self.log_text.get(1.0, tk.END)) - messagebox.showinfo("导出成功", f"日志已导出到: {filename}") - except Exception as e: - messagebox.showerror("导出失败", f"导出日志时出错: {e}") - - 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 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 level_name=level: self.choose_color(level_name) - ) - color_btn.pack(side=tk.RIGHT) - # 显示当前颜色 - color_label = ttk.Label(frame, text="■", foreground=self.formatter.level_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.formatter.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.formatter.level_colors[level])[1] - if color: - self.formatter.level_colors[level] = color - self.custom_level_colors[level] = color # 保存到自定义颜色 - self.configure_text_tags() - self.save_viewer_config() # 自动保存配置 - self.filter_logs() - - def choose_module_color(self, module): - """选择模块颜色""" - color = colorchooser.askcolor(color=self.formatter.module_colors.get(module, "black"))[1] - if color: - self.formatter.module_colors[module] = color - self.custom_module_colors[module] = color # 保存到自定义颜色 - self.configure_text_tags() - self.save_viewer_config() # 自动保存配置 - self.filter_logs() - - def update_module_list(self): - """更新模块列表""" - if self.current_log_file.exists(): - with open(self.current_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.formatter.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.formatter.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): - """监控日志文件变化""" - last_position = 0 - current_monitored_file = None - - while self.running: - # 检查是否需要切换监控的文件 - if current_monitored_file != self.current_log_file: - current_monitored_file = self.current_log_file - last_position = 0 # 重置位置 - - if current_monitored_file.exists(): - try: - # 使用共享读取模式,避免文件锁定 - with open(current_monitored_file, "r", encoding="utf-8", buffering=1) 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 (FileNotFoundError, PermissionError) as e: - # 文件被占用或不存在时,等待更长时间 - print(f"日志文件访问受限: {e}") - time.sleep(1) - continue - except Exception as e: - print(f"读取日志文件时出错: {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 - - # 使用格式化器格式化日志 - parts, tags = self.formatter.format_log_entry(log_entry) - - # 在主线程中更新UI - self.root.after(0, lambda: self.add_formatted_log_line(parts, tags, log_entry)) - - def add_formatted_log_line(self, parts, tags, log_entry): - """添加格式化的日志行到文本框""" - # 控制最大行数 - max_lines = self.viewer_config.get("max_lines", 1000) - current_lines = int(self.log_text.index("end-1c").split(".")[0]) - - if current_lines > max_lines: - # 删除前面的行 - lines_to_delete = current_lines - max_lines + 100 # 一次删除多一些,减少频繁操作 - self.log_text.delete(1.0, f"{lines_to_delete}.0") - - # 插入格式化的文本 - for i, part in enumerate(parts): - if i < len(tags): - tag = tags[i] - # 根据内容类型选择合适的标签 - if tag.startswith("level_"): - if self.formatter.enable_level_colors: - self.log_text.insert(tk.END, part, tag) - else: - self.log_text.insert(tk.END, part, "level") - elif tag.startswith("module_"): - if self.formatter.enable_module_colors: - self.log_text.insert(tk.END, part, tag) - else: - self.log_text.insert(tk.END, part, "module") - else: - self.log_text.insert(tk.END, part, tag) - else: - self.log_text.insert(tk.END, part) - - # 在部分之间添加空格(除了最后一个) - if i < len(parts) - 1: - self.log_text.insert(tk.END, " ") - - self.log_text.insert(tk.END, "\n") - - # 自动滚动 - if self.viewer_config.get("auto_scroll", True): - 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): - parts, tags = self.formatter.format_log_entry(log_entry) - self.add_formatted_log_line(parts, tags, log_entry) - - # 恢复滚动位置(如果不是自动滚动模式) - if not self.viewer_config.get("auto_scroll", True): - 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 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 select_log_file(self): - """选择日志文件""" - filename = filedialog.askopenfilename( - title="选择日志文件", - filetypes=[("JSONL日志文件", "*.jsonl"), ("所有文件", "*.*")], - initialdir="logs" if Path("logs").exists() else ".", - ) - if filename: - new_file = Path(filename) - if new_file != self.current_log_file: - self.current_log_file = new_file - self.current_file_var.set(str(self.current_log_file)) - self.reload_log_file() - - def refresh_log_file(self): - """刷新日志文件""" - self.reload_log_file() - - def reload_log_file(self): - """重新加载日志文件""" - # 清空当前缓存和显示 - self.log_cache.clear() - self.modules.clear() - self.selected_modules.clear() - self.log_text.delete(1.0, tk.END) - - # 清空日志队列 - while not self.log_queue.empty(): - try: - self.log_queue.get_nowait() - except queue.Empty: - break - - # 重新读取整个文件 - if self.current_log_file.exists(): - try: - with open(self.current_log_file, "r", encoding="utf-8") as f: - for line in f: - try: - log_entry = json.loads(line) - self.log_cache.append(log_entry) - - # 收集模块信息 - if "logger_name" in log_entry: - self.modules.add(log_entry["logger_name"]) - - except json.JSONDecodeError: - continue - except Exception as e: - messagebox.showerror("错误", f"读取日志文件失败: {e}") - return - - # 更新模块列表UI - self.update_module_list() - - # 过滤并显示日志 - self.filter_logs() - - # 更新窗口标题 - self.update_window_title() - - def update_window_title(self): - """更新窗口标题""" - filename = self.current_log_file.name - self.root.title(f"MaiBot日志查看器 - {filename}") - - -def main(): - root = tk.Tk() - LogViewer(root) - root.mainloop() - - -if __name__ == "__main__": - main() diff --git a/scripts/log_viewer_optimized.py b/scripts/log_viewer_optimized.py index 3a96e4aac..78ce67720 100644 --- a/scripts/log_viewer_optimized.py +++ b/scripts/log_viewer_optimized.py @@ -1,5 +1,5 @@ import tkinter as tk -from tkinter import ttk, messagebox, filedialog +from tkinter import ttk, messagebox, filedialog, colorchooser import json from pathlib import Path import threading @@ -8,6 +8,7 @@ from datetime import datetime from collections import defaultdict import os import time +import queue class LogIndex: @@ -206,6 +207,23 @@ class LogFormatter: parts.append(str(event)) tags.append("message") + # 处理其他字段 + extras = [] + for key, value in log_entry.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)) + tags.append("extras") + return parts, tags def format_timestamp(self, timestamp): @@ -287,6 +305,7 @@ class VirtualLogDisplay: self.text_widget.tag_configure("level", foreground="#808080") self.text_widget.tag_configure("module", foreground="#808080") self.text_widget.tag_configure("message", foreground="#ffffff") + self.text_widget.tag_configure("extras", foreground="#808080") # 日志级别颜色标签 for level, color in self.formatter.level_colors.items(): @@ -449,7 +468,7 @@ class LogViewer: self.load_config() # 初始化日志格式化器 - self.formatter = LogFormatter(self.log_config, {}, {}) + self.formatter = LogFormatter(self.log_config, self.custom_module_colors, self.custom_level_colors) # 初始化日志文件路径 self.current_log_file = Path("logs/app.log.jsonl") @@ -467,6 +486,9 @@ class LogViewer: self.main_frame = ttk.Frame(root) self.main_frame.pack(fill=tk.BOTH, expand=True, padx=5, pady=5) + # 创建菜单栏 + self.create_menu() + # 创建控制面板 self.create_control_panel() @@ -477,12 +499,30 @@ class LogViewer: # 模块名映射 self.module_name_mapping = { "api": "API接口", + "async_task_manager": "异步任务管理器", + "background_tasks": "后台任务", + "base_tool": "基础工具", + "chat_stream": "聊天流", + "component_registry": "组件注册器", "config": "配置", - "chat": "聊天", - "plugin": "插件", + "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.selected_modules = set() self.modules = set() @@ -491,19 +531,35 @@ class LogViewer: self.level_combo.bind("<>", self.filter_logs) self.search_var.trace("w", self.filter_logs) + # 绑定快捷键 + self.root.bind("", lambda e: self.select_log_file()) + self.root.bind("", lambda e: self.refresh_log_file()) + self.root.bind("", lambda e: self.export_logs()) + # 初始加载文件 if self.current_log_file.exists(): self.load_log_file_async() def load_config(self): """加载配置文件""" + # 默认配置 self.default_config = { - "log": {"date_style": "m-d H:i:s", "log_level_style": "lite", "color_text": "full"}, + "log": {"date_style": "m-d H:i:s", "log_level_style": "lite", "color_text": "full", "log_level": "INFO"}, + "viewer": { + "theme": "dark", + "font_size": 10, + "max_lines": 1000, + "auto_scroll": True, + "show_milliseconds": False, + "window": {"width": 1200, "height": 800, "remember_position": True}, + }, } - self.log_config = self.default_config["log"].copy() - + # 从bot_config.toml加载日志配置 config_path = Path("config/bot_config.toml") + self.log_config = self.default_config["log"].copy() + self.viewer_config = self.default_config["viewer"].copy() + try: if config_path.exists(): with open(config_path, "r", encoding="utf-8") as f: @@ -511,7 +567,377 @@ class LogViewer: if "log" in bot_config: self.log_config.update(bot_config["log"]) except Exception as e: - print(f"加载配置失败: {e}") + print(f"加载bot配置失败: {e}") + + # 从viewer配置文件加载查看器配置 + viewer_config_path = Path("config/log_viewer_config.toml") + self.custom_module_colors = {} + self.custom_level_colors = {} + + try: + if viewer_config_path.exists(): + with open(viewer_config_path, "r", encoding="utf-8") as f: + viewer_config = toml.load(f) + if "viewer" in viewer_config: + self.viewer_config.update(viewer_config["viewer"]) + + # 加载自定义模块颜色 + if "module_colors" in viewer_config["viewer"]: + self.custom_module_colors = viewer_config["viewer"]["module_colors"] + + # 加载自定义级别颜色 + if "level_colors" in viewer_config["viewer"]: + self.custom_level_colors = viewer_config["viewer"]["level_colors"] + + if "log" in viewer_config: + self.log_config.update(viewer_config["log"]) + except Exception as e: + print(f"加载查看器配置失败: {e}") + + # 应用窗口配置 + window_config = self.viewer_config.get("window", {}) + window_width = window_config.get("width", 1200) + window_height = window_config.get("height", 800) + self.root.geometry(f"{window_width}x{window_height}") + + def save_viewer_config(self): + """保存查看器配置""" + # 准备完整的配置数据 + viewer_config_copy = self.viewer_config.copy() + + # 保存自定义颜色(只保存与默认值不同的颜色) + if self.custom_module_colors: + viewer_config_copy["module_colors"] = self.custom_module_colors + if self.custom_level_colors: + viewer_config_copy["level_colors"] = self.custom_level_colors + + config_data = {"log": self.log_config, "viewer": viewer_config_copy} + + config_path = Path("config/log_viewer_config.toml") + config_path.parent.mkdir(exist_ok=True) + + try: + with open(config_path, "w", encoding="utf-8") as f: + toml.dump(config_data, f) + except Exception as e: + print(f"保存查看器配置失败: {e}") + + def create_menu(self): + """创建菜单栏""" + menubar = tk.Menu(self.root) + self.root.config(menu=menubar) + + # 配置菜单 + config_menu = tk.Menu(menubar, tearoff=0) + menubar.add_cascade(label="配置", menu=config_menu) + config_menu.add_command(label="日志格式设置", command=self.show_format_settings) + config_menu.add_command(label="颜色设置", command=self.show_color_settings) + config_menu.add_command(label="查看器设置", command=self.show_viewer_settings) + config_menu.add_separator() + config_menu.add_command(label="重新加载配置", command=self.reload_config) + + # 文件菜单 + file_menu = tk.Menu(menubar, tearoff=0) + menubar.add_cascade(label="文件", menu=file_menu) + file_menu.add_command(label="选择日志文件", command=self.select_log_file, accelerator="Ctrl+O") + file_menu.add_command(label="刷新当前文件", command=self.refresh_log_file, accelerator="F5") + file_menu.add_separator() + file_menu.add_command(label="导出当前日志", command=self.export_logs, accelerator="Ctrl+S") + + # 工具菜单 + tools_menu = tk.Menu(menubar, tearoff=0) + menubar.add_cascade(label="工具", menu=tools_menu) + tools_menu.add_command(label="清空日志显示", command=self.clear_log_display) + + def show_format_settings(self): + """显示格式设置窗口""" + format_window = tk.Toplevel(self.root) + format_window.title("日志格式设置") + format_window.geometry("400x300") + + frame = ttk.Frame(format_window) + frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10) + + # 日期格式 + ttk.Label(frame, text="日期格式:").pack(anchor="w", pady=2) + date_style_var = tk.StringVar(value=self.log_config.get("date_style", "m-d H:i:s")) + date_entry = ttk.Entry(frame, textvariable=date_style_var, width=30) + date_entry.pack(anchor="w", pady=2) + ttk.Label(frame, text="格式说明: Y=年份, m=月份, d=日期, H=小时, i=分钟, s=秒", font=("", 8)).pack( + anchor="w", pady=2 + ) + + # 日志级别样式 + ttk.Label(frame, text="日志级别样式:").pack(anchor="w", pady=(10, 2)) + level_style_var = tk.StringVar(value=self.log_config.get("log_level_style", "lite")) + level_frame = ttk.Frame(frame) + level_frame.pack(anchor="w", pady=2) + + ttk.Radiobutton(level_frame, text="简洁(lite)", variable=level_style_var, value="lite").pack( + side="left", padx=(0, 10) + ) + ttk.Radiobutton(level_frame, text="紧凑(compact)", variable=level_style_var, value="compact").pack( + side="left", padx=(0, 10) + ) + ttk.Radiobutton(level_frame, text="完整(full)", variable=level_style_var, value="full").pack( + side="left", padx=(0, 10) + ) + + # 颜色文本设置 + ttk.Label(frame, text="文本颜色设置:").pack(anchor="w", pady=(10, 2)) + color_text_var = tk.StringVar(value=self.log_config.get("color_text", "full")) + color_frame = ttk.Frame(frame) + color_frame.pack(anchor="w", pady=2) + + ttk.Radiobutton(color_frame, text="无颜色(none)", variable=color_text_var, value="none").pack( + side="left", padx=(0, 10) + ) + ttk.Radiobutton(color_frame, text="仅标题(title)", variable=color_text_var, value="title").pack( + side="left", padx=(0, 10) + ) + ttk.Radiobutton(color_frame, text="全部(full)", variable=color_text_var, value="full").pack( + side="left", padx=(0, 10) + ) + + # 按钮 + button_frame = ttk.Frame(frame) + button_frame.pack(fill="x", pady=(20, 0)) + + def apply_format(): + self.log_config["date_style"] = date_style_var.get() + self.log_config["log_level_style"] = level_style_var.get() + self.log_config["color_text"] = color_text_var.get() + + # 重新初始化格式化器 + self.formatter = LogFormatter(self.log_config, self.custom_module_colors, self.custom_level_colors) + self.log_display.formatter = self.formatter + self.log_display.configure_text_tags() + + # 保存配置 + self.save_viewer_config() + + # 重新过滤日志以应用新格式 + self.filter_logs() + + format_window.destroy() + + ttk.Button(button_frame, text="应用", command=apply_format).pack(side="right", padx=(5, 0)) + ttk.Button(button_frame, text="取消", command=format_window.destroy).pack(side="right") + + def show_viewer_settings(self): + """显示查看器设置窗口""" + viewer_window = tk.Toplevel(self.root) + viewer_window.title("查看器设置") + viewer_window.geometry("350x250") + + frame = ttk.Frame(viewer_window) + frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10) + + # 主题设置 + ttk.Label(frame, text="主题:").pack(anchor="w", pady=2) + theme_var = tk.StringVar(value=self.viewer_config.get("theme", "dark")) + theme_frame = ttk.Frame(frame) + theme_frame.pack(anchor="w", pady=2) + ttk.Radiobutton(theme_frame, text="深色", variable=theme_var, value="dark").pack(side="left", padx=(0, 10)) + ttk.Radiobutton(theme_frame, text="浅色", variable=theme_var, value="light").pack(side="left") + + # 字体大小 + ttk.Label(frame, text="字体大小:").pack(anchor="w", pady=(10, 2)) + font_size_var = tk.IntVar(value=self.viewer_config.get("font_size", 10)) + font_size_spin = ttk.Spinbox(frame, from_=8, to=20, textvariable=font_size_var, width=10) + font_size_spin.pack(anchor="w", pady=2) + + # 最大行数 + ttk.Label(frame, text="最大显示行数:").pack(anchor="w", pady=(10, 2)) + max_lines_var = tk.IntVar(value=self.viewer_config.get("max_lines", 1000)) + max_lines_spin = ttk.Spinbox(frame, from_=100, to=10000, increment=100, textvariable=max_lines_var, width=10) + max_lines_spin.pack(anchor="w", pady=2) + + # 自动滚动 + auto_scroll_var = tk.BooleanVar(value=self.viewer_config.get("auto_scroll", True)) + ttk.Checkbutton(frame, text="自动滚动到底部", variable=auto_scroll_var).pack(anchor="w", pady=(10, 2)) + + # 按钮 + button_frame = ttk.Frame(frame) + button_frame.pack(fill="x", pady=(20, 0)) + + def apply_viewer_settings(): + self.viewer_config["theme"] = theme_var.get() + self.viewer_config["font_size"] = font_size_var.get() + self.viewer_config["max_lines"] = max_lines_var.get() + self.viewer_config["auto_scroll"] = auto_scroll_var.get() + + # 应用主题 + self.apply_theme() + + # 保存配置 + self.save_viewer_config() + + viewer_window.destroy() + + ttk.Button(button_frame, text="应用", command=apply_viewer_settings).pack(side="right", padx=(5, 0)) + ttk.Button(button_frame, text="取消", command=viewer_window.destroy).pack(side="right") + + def apply_theme(self): + """应用主题设置""" + theme = self.viewer_config.get("theme", "dark") + font_size = self.viewer_config.get("font_size", 10) + + # 更新虚拟显示组件的主题 + if theme == "dark": + bg_color = "#1e1e1e" + fg_color = "#ffffff" + select_bg = "#404040" + else: + bg_color = "#ffffff" + fg_color = "#000000" + select_bg = "#c0c0c0" + + self.log_display.text_widget.config( + background=bg_color, foreground=fg_color, selectbackground=select_bg, font=("Consolas", font_size) + ) + + # 重新配置标签样式 + self.log_display.configure_text_tags() + + def reload_config(self): + """重新加载配置""" + self.load_config() + self.formatter = LogFormatter(self.log_config, self.custom_module_colors, self.custom_level_colors) + self.log_display.formatter = self.formatter + self.log_display.configure_text_tags() + self.apply_theme() + self.filter_logs() + + def clear_log_display(self): + """清空日志显示""" + self.log_display.text_widget.delete(1.0, tk.END) + + def export_logs(self): + """导出当前显示的日志""" + filename = filedialog.asksaveasfilename( + defaultextension=".txt", filetypes=[("文本文件", "*.txt"), ("所有文件", "*.*")] + ) + if filename: + try: + # 获取当前显示的所有日志条目 + if self.log_index: + filtered_count = self.log_index.get_filtered_count() + log_lines = [] + for i in range(filtered_count): + log_entry = self.log_index.get_entry_at_filtered_position(i) + if log_entry: + parts, tags = self.formatter.format_log_entry(log_entry) + line_text = " ".join(parts) + log_lines.append(line_text) + + with open(filename, "w", encoding="utf-8") as f: + f.write("\n".join(log_lines)) + messagebox.showinfo("导出成功", f"日志已导出到: {filename}") + else: + messagebox.showwarning("导出失败", "没有日志可导出") + except Exception as e: + messagebox.showerror("导出失败", f"导出日志时出错: {e}") + + 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 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 level_name=level: self.choose_color(level_name) + ) + color_btn.pack(side=tk.RIGHT) + # 显示当前颜色 + color_label = ttk.Label(frame, text="■", foreground=self.formatter.level_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.formatter.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.formatter.level_colors[level])[1] + if color: + self.formatter.level_colors[level] = color + self.custom_level_colors[level] = color # 保存到自定义颜色 + self.log_display.formatter = self.formatter + self.log_display.configure_text_tags() + self.save_viewer_config() # 自动保存配置 + self.filter_logs() + + def choose_module_color(self, module): + """选择模块颜色""" + color = colorchooser.askcolor(color=self.formatter.module_colors.get(module, "black"))[1] + if color: + self.formatter.module_colors[module] = color + self.custom_module_colors[module] = color # 保存到自定义颜色 + self.log_display.formatter = self.formatter + self.log_display.configure_text_tags() + self.save_viewer_config() # 自动保存配置 + self.filter_logs() def create_control_panel(self): """创建控制面板""" @@ -549,30 +975,43 @@ class LogViewer: side=tk.LEFT, padx=2 ) - # 过滤控制框架 - filter_frame = ttk.Frame(self.control_frame) - filter_frame.pack(fill=tk.X, padx=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) # 日志级别选择 - ttk.Label(filter_frame, text="级别:").pack(side=tk.LEFT, padx=2) + 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(filter_frame, textvariable=self.level_var, width=8) + self.level_combo = ttk.Combobox(level_frame, textvariable=self.level_var, width=8) self.level_combo["values"] = ["全部", "debug", "info", "warning", "error", "critical"] self.level_combo.pack(side=tk.LEFT, padx=2) # 搜索框 - ttk.Label(filter_frame, text="搜索:").pack(side=tk.LEFT, padx=(20, 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(filter_frame, textvariable=self.search_var, width=20) + self.search_entry = ttk.Entry(search_frame, textvariable=self.search_var, width=15) self.search_entry.pack(side=tk.LEFT, padx=2) - # 模块选择 - ttk.Label(filter_frame, text="模块:").pack(side=tk.LEFT, padx=(20, 2)) - self.module_var = tk.StringVar(value="全部") - self.module_combo = ttk.Combobox(filter_frame, textvariable=self.module_var, width=15) - self.module_combo.pack(side=tk.LEFT, padx=2) - self.module_combo.bind("<>", self.on_module_selected) - def on_file_loaded(self, log_index, error): """文件加载完成回调""" self.progress_bar.pack_forget() @@ -590,6 +1029,7 @@ class LogViewer: self.status_var.set(f"已加载 {log_index.total_entries} 条日志") # 更新模块列表 + self.modules = set(log_index.module_index.keys()) self.update_module_list() # 应用过滤并显示 @@ -623,22 +1063,11 @@ class LogViewer: # 清空当前数据 self.log_index = LogIndex() - self.modules.clear() self.selected_modules.clear() - self.module_var.set("全部") # 开始异步加载 self.async_loader.load_file_async(str(self.current_log_file), self.on_loading_progress) - def on_module_selected(self, event=None): - """模块选择事件""" - module = self.module_var.get() - if module == "全部": - self.selected_modules = {"全部"} - else: - self.selected_modules = {module} - self.filter_logs() - def filter_logs(self, *args): """过滤日志""" if not self.log_index: @@ -743,7 +1172,7 @@ class LogViewer: def read_new_logs(self, from_position): """读取新的日志条目并返回它们""" new_entries = [] - new_modules_found = False + new_modules = set() # 收集新发现的模块 with open(self.current_log_file, "r", encoding="utf-8") as f: f.seek(from_position) line_count = self.log_index.total_entries @@ -756,14 +1185,20 @@ class LogViewer: logger_name = log_entry.get("logger_name", "") if logger_name and logger_name not in self.modules: - self.modules.add(logger_name) - new_modules_found = True + new_modules.add(logger_name) line_count += 1 except json.JSONDecodeError: continue - if new_modules_found: - self.root.after(0, self.update_module_list) + + # 如果发现了新模块,在主线程中更新模块集合 + if new_modules: + def update_modules(): + self.modules.update(new_modules) + self.update_module_list() + + self.root.after(0, update_modules) + return new_entries def append_new_logs(self, new_entries): @@ -791,15 +1226,196 @@ class LogViewer: self.status_var.set(f"显示 {total_count} 条日志") def update_module_list(self): - """更新模块下拉列表""" - current_selection = self.module_var.get() - self.modules = set(self.log_index.module_index.keys()) - module_values = ["全部"] + sorted(list(self.modules)) - self.module_combo["values"] = module_values - if current_selection in module_values: - self.module_var.set(current_selection) + """更新模块列表""" + # 清空现有选项 + 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.formatter.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.formatter.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: - self.module_var.set("全部") + 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 get_display_name(self, module_name): + """获取模块的显示名称""" + return self.module_name_mapping.get(module_name, module_name) + + 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(): diff --git a/scripts/preview_expressions.py b/scripts/preview_expressions.py deleted file mode 100644 index 1e71120d8..000000000 --- a/scripts/preview_expressions.py +++ /dev/null @@ -1,278 +0,0 @@ -import tkinter as tk -from tkinter import ttk -import json -import os -from pathlib import Path -import networkx as nx -import matplotlib.pyplot as plt -from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg -from sklearn.feature_extraction.text import TfidfVectorizer -from sklearn.metrics.pairwise import cosine_similarity -from collections import defaultdict - - -class ExpressionViewer: - def __init__(self, root): - self.root = root - self.root.title("表达方式预览器") - self.root.geometry("1200x800") - - # 创建主框架 - self.main_frame = ttk.Frame(root) - self.main_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10) - - # 创建左侧控制面板 - self.control_frame = ttk.Frame(self.main_frame) - self.control_frame.pack(side=tk.LEFT, fill=tk.Y, padx=(0, 10)) - - # 创建搜索框 - self.search_frame = ttk.Frame(self.control_frame) - self.search_frame.pack(fill=tk.X, pady=(0, 10)) - - self.search_var = tk.StringVar() - self.search_var.trace("w", self.filter_expressions) - self.search_entry = ttk.Entry(self.search_frame, textvariable=self.search_var) - self.search_entry.pack(side=tk.LEFT, fill=tk.X, expand=True) - ttk.Label(self.search_frame, text="搜索:").pack(side=tk.LEFT, padx=(0, 5)) - - # 创建文件选择下拉框 - self.file_var = tk.StringVar() - self.file_combo = ttk.Combobox(self.search_frame, textvariable=self.file_var) - self.file_combo.pack(side=tk.LEFT, padx=5) - self.file_combo.bind("<>", self.load_file) - - # 创建排序选项 - self.sort_frame = ttk.LabelFrame(self.control_frame, text="排序选项") - self.sort_frame.pack(fill=tk.X, pady=5) - - self.sort_var = tk.StringVar(value="count") - ttk.Radiobutton( - self.sort_frame, text="按计数排序", variable=self.sort_var, value="count", command=self.apply_sort - ).pack(anchor=tk.W) - ttk.Radiobutton( - self.sort_frame, text="按情境排序", variable=self.sort_var, value="situation", command=self.apply_sort - ).pack(anchor=tk.W) - ttk.Radiobutton( - self.sort_frame, text="按风格排序", variable=self.sort_var, value="style", command=self.apply_sort - ).pack(anchor=tk.W) - - # 创建分群选项 - self.group_frame = ttk.LabelFrame(self.control_frame, text="分群选项") - self.group_frame.pack(fill=tk.X, pady=5) - - self.group_var = tk.StringVar(value="none") - ttk.Radiobutton( - self.group_frame, text="不分群", variable=self.group_var, value="none", command=self.apply_grouping - ).pack(anchor=tk.W) - ttk.Radiobutton( - self.group_frame, text="按情境分群", variable=self.group_var, value="situation", command=self.apply_grouping - ).pack(anchor=tk.W) - ttk.Radiobutton( - self.group_frame, text="按风格分群", variable=self.group_var, value="style", command=self.apply_grouping - ).pack(anchor=tk.W) - - # 创建相似度阈值滑块 - self.similarity_frame = ttk.LabelFrame(self.control_frame, text="相似度设置") - self.similarity_frame.pack(fill=tk.X, pady=5) - - self.similarity_var = tk.DoubleVar(value=0.5) - self.similarity_scale = ttk.Scale( - self.similarity_frame, - from_=0.0, - to=1.0, - variable=self.similarity_var, - orient=tk.HORIZONTAL, - command=self.update_similarity, - ) - self.similarity_scale.pack(fill=tk.X, padx=5, pady=5) - ttk.Label(self.similarity_frame, text="相似度阈值: 0.5").pack() - - # 创建显示选项 - self.view_frame = ttk.LabelFrame(self.control_frame, text="显示选项") - self.view_frame.pack(fill=tk.X, pady=5) - - self.show_graph_var = tk.BooleanVar(value=True) - ttk.Checkbutton( - self.view_frame, text="显示关系图", variable=self.show_graph_var, command=self.toggle_graph - ).pack(anchor=tk.W) - - # 创建右侧内容区域 - self.content_frame = ttk.Frame(self.main_frame) - self.content_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) - - # 创建文本显示区域 - self.text_area = tk.Text(self.content_frame, wrap=tk.WORD) - self.text_area.pack(side=tk.TOP, fill=tk.BOTH, expand=True) - - # 添加滚动条 - scrollbar = ttk.Scrollbar(self.text_area, command=self.text_area.yview) - scrollbar.pack(side=tk.RIGHT, fill=tk.Y) - self.text_area.config(yscrollcommand=scrollbar.set) - - # 创建图形显示区域 - self.graph_frame = ttk.Frame(self.content_frame) - self.graph_frame.pack(side=tk.TOP, fill=tk.BOTH, expand=True) - - # 初始化数据 - self.current_data = [] - self.graph = nx.Graph() - self.canvas = None - - # 加载文件列表 - self.load_file_list() - - def load_file_list(self): - expression_dir = Path("data/expression") - files = [] - for root, _, filenames in os.walk(expression_dir): - for filename in filenames: - if filename.endswith(".json"): - rel_path = os.path.relpath(os.path.join(root, filename), expression_dir) - files.append(rel_path) - - self.file_combo["values"] = files - if files: - self.file_combo.set(files[0]) - self.load_file(None) - - def load_file(self, event): - selected_file = self.file_var.get() - if not selected_file: - return - - file_path = os.path.join("data/expression", selected_file) - try: - with open(file_path, "r", encoding="utf-8") as f: - self.current_data = json.load(f) - - self.apply_sort() - self.update_similarity() - - except Exception as e: - self.text_area.delete(1.0, tk.END) - self.text_area.insert(tk.END, f"加载文件时出错: {str(e)}") - - def apply_sort(self): - if not self.current_data: - return - - sort_key = self.sort_var.get() - reverse = sort_key == "count" - - self.current_data.sort(key=lambda x: x.get(sort_key, ""), reverse=reverse) - self.apply_grouping() - - def apply_grouping(self): - if not self.current_data: - return - - group_key = self.group_var.get() - if group_key == "none": - self.display_data(self.current_data) - return - - grouped_data = defaultdict(list) - for item in self.current_data: - key = item.get(group_key, "未分类") - grouped_data[key].append(item) - - self.text_area.delete(1.0, tk.END) - for group, items in grouped_data.items(): - self.text_area.insert(tk.END, f"\n=== {group} ===\n\n") - for item in items: - self.text_area.insert(tk.END, f"情境: {item.get('situation', 'N/A')}\n") - self.text_area.insert(tk.END, f"风格: {item.get('style', 'N/A')}\n") - self.text_area.insert(tk.END, f"计数: {item.get('count', 'N/A')}\n") - self.text_area.insert(tk.END, "-" * 50 + "\n") - - def display_data(self, data): - self.text_area.delete(1.0, tk.END) - for item in data: - self.text_area.insert(tk.END, f"情境: {item.get('situation', 'N/A')}\n") - self.text_area.insert(tk.END, f"风格: {item.get('style', 'N/A')}\n") - self.text_area.insert(tk.END, f"计数: {item.get('count', 'N/A')}\n") - self.text_area.insert(tk.END, "-" * 50 + "\n") - - def update_similarity(self, *args): - if not self.current_data: - return - - threshold = self.similarity_var.get() - self.similarity_frame.winfo_children()[-1].config(text=f"相似度阈值: {threshold:.2f}") - - # 计算相似度 - texts = [f"{item['situation']} {item['style']}" for item in self.current_data] - vectorizer = TfidfVectorizer() - tfidf_matrix = vectorizer.fit_transform(texts) - similarity_matrix = cosine_similarity(tfidf_matrix) - - # 创建图 - self.graph.clear() - for i, item in enumerate(self.current_data): - self.graph.add_node(i, label=f"{item['situation']}\n{item['style']}") - - # 添加边 - for i in range(len(self.current_data)): - for j in range(i + 1, len(self.current_data)): - if similarity_matrix[i, j] > threshold: - self.graph.add_edge(i, j, weight=similarity_matrix[i, j]) - - if self.show_graph_var.get(): - self.draw_graph() - - def draw_graph(self): - if self.canvas: - self.canvas.get_tk_widget().destroy() - - fig = plt.figure(figsize=(8, 6)) - pos = nx.spring_layout(self.graph) - - # 绘制节点 - nx.draw_networkx_nodes(self.graph, pos, node_color="lightblue", node_size=1000, alpha=0.6) - - # 绘制边 - nx.draw_networkx_edges(self.graph, pos, alpha=0.4) - - # 添加标签 - labels = nx.get_node_attributes(self.graph, "label") - nx.draw_networkx_labels(self.graph, pos, labels, font_size=8) - - plt.title("表达方式关系图") - plt.axis("off") - - self.canvas = FigureCanvasTkAgg(fig, master=self.graph_frame) - self.canvas.draw() - self.canvas.get_tk_widget().pack(fill=tk.BOTH, expand=True) - - def toggle_graph(self): - if self.show_graph_var.get(): - self.draw_graph() - else: - if self.canvas: - self.canvas.get_tk_widget().destroy() - self.canvas = None - - def filter_expressions(self, *args): - search_text = self.search_var.get().lower() - if not search_text: - self.apply_sort() - return - - filtered_data = [] - for item in self.current_data: - situation = item.get("situation", "").lower() - style = item.get("style", "").lower() - if search_text in situation or search_text in style: - filtered_data.append(item) - - self.display_data(filtered_data) - - -def main(): - root = tk.Tk() - # app = ExpressionViewer(root) - root.mainloop() - - -if __name__ == "__main__": - main() diff --git a/scripts/view_hfc_stats.py b/scripts/view_hfc_stats.py deleted file mode 100644 index 75e792e25..000000000 --- a/scripts/view_hfc_stats.py +++ /dev/null @@ -1,185 +0,0 @@ -#!/usr/bin/env python3 -""" -HFC性能统计数据查看工具 -""" - -import sys -import json -import argparse -from pathlib import Path -from typing import Dict, Any - -# 添加项目根目录到Python路径 -sys.path.insert(0, str(Path(__file__).parent.parent)) - - -def format_time(seconds: float) -> str: - """格式化时间显示""" - if seconds < 1: - return f"{seconds * 1000:.1f}毫秒" - else: - return f"{seconds:.3f}秒" - - -def display_chat_stats(chat_id: str, stats: Dict[str, Any]): - """显示单个聊天的统计数据""" - print(f"\n=== Chat ID: {chat_id} ===") - print(f"版本: {stats.get('version', 'unknown')}") - print(f"最后更新: {stats['last_updated']}") - - overall = stats["overall"] - print("\n📊 总体统计:") - print(f" 总记录数: {overall['total_records']}") - print(f" 平均总时间: {format_time(overall['avg_total_time'])}") - - print("\n⏱️ 各步骤平均时间:") - for step, avg_time in overall["avg_step_times"].items(): - print(f" {step}: {format_time(avg_time)}") - - print("\n🎯 按动作类型统计:") - by_action = stats["by_action"] - - # 按比例排序 - sorted_actions = sorted(by_action.items(), key=lambda x: x[1]["percentage"], reverse=True) - - for action, action_stats in sorted_actions: - print(f" 📌 {action}:") - print(f" 次数: {action_stats['count']} ({action_stats['percentage']:.1f}%)") - print(f" 平均总时间: {format_time(action_stats['avg_total_time'])}") - - if action_stats["avg_step_times"]: - print(" 步骤时间:") - for step, step_time in action_stats["avg_step_times"].items(): - print(f" {step}: {format_time(step_time)}") - - -def display_comparison(stats_data: Dict[str, Dict[str, Any]]): - """显示多个聊天的对比数据""" - if len(stats_data) < 2: - return - - print("\n=== 多聊天对比 ===") - - # 创建对比表格 - chat_ids = list(stats_data.keys()) - - print("\n📊 总体对比:") - print(f"{'Chat ID':<20} {'版本':<12} {'记录数':<8} {'平均时间':<12} {'最常见动作':<15}") - print("-" * 70) - - for chat_id in chat_ids: - stats = stats_data[chat_id] - overall = stats["overall"] - - # 找到最常见的动作 - most_common_action = max(stats["by_action"].items(), key=lambda x: x[1]["count"]) - most_common_name = most_common_action[0] - most_common_pct = most_common_action[1]["percentage"] - - version = stats.get("version", "unknown") - print( - f"{chat_id:<20} {version:<12} {overall['total_records']:<8} {format_time(overall['avg_total_time']):<12} {most_common_name}({most_common_pct:.0f}%)" - ) - - -def view_session_logs(chat_id: str = None, latest: bool = False): - """查看会话日志文件""" - log_dir = Path("log/hfc_loop") - if not log_dir.exists(): - print("❌ 日志目录不存在") - return - - if chat_id: - pattern = f"{chat_id}_*.json" - else: - pattern = "*.json" - - log_files = list(log_dir.glob(pattern)) - - if not log_files: - print(f"❌ 没有找到匹配的日志文件: {pattern}") - return - - if latest: - # 按文件修改时间排序,取最新的 - log_files.sort(key=lambda f: f.stat().st_mtime, reverse=True) - log_files = log_files[:1] - - for log_file in log_files: - print(f"\n=== 会话日志: {log_file.name} ===") - - try: - with open(log_file, "r", encoding="utf-8") as f: - records = json.load(f) - - if not records: - print(" 空文件") - continue - - print(f" 记录数: {len(records)}") - print(f" 时间范围: {records[0]['timestamp']} ~ {records[-1]['timestamp']}") - - # 统计动作分布 - action_counts = {} - total_time = 0 - - for record in records: - action = record["action_type"] - action_counts[action] = action_counts.get(action, 0) + 1 - total_time += record["total_time"] - - print(f" 总耗时: {format_time(total_time)}") - print(f" 平均耗时: {format_time(total_time / len(records))}") - print(f" 动作分布: {dict(action_counts)}") - - except Exception as e: - print(f" ❌ 读取文件失败: {e}") - - -def main(): - parser = argparse.ArgumentParser(description="HFC性能统计数据查看工具") - parser.add_argument("--chat-id", help="指定要查看的Chat ID") - parser.add_argument("--logs", action="store_true", help="查看会话日志文件") - parser.add_argument("--latest", action="store_true", help="只显示最新的日志文件") - parser.add_argument("--compare", action="store_true", help="显示多聊天对比") - - args = parser.parse_args() - - if args.logs: - view_session_logs(args.chat_id, args.latest) - return - - # 读取统计数据 - stats_file = Path("data/hfc/time.json") - if not stats_file.exists(): - print("❌ 统计数据文件不存在,请先运行一些HFC循环以生成数据") - return - - try: - with open(stats_file, "r", encoding="utf-8") as f: - stats_data = json.load(f) - except Exception as e: - print(f"❌ 读取统计数据失败: {e}") - return - - if not stats_data: - print("❌ 统计数据为空") - return - - if args.chat_id: - if args.chat_id in stats_data: - display_chat_stats(args.chat_id, stats_data[args.chat_id]) - else: - print(f"❌ 没有找到Chat ID '{args.chat_id}' 的数据") - print(f"可用的Chat ID: {list(stats_data.keys())}") - else: - # 显示所有聊天的统计数据 - for chat_id, stats in stats_data.items(): - display_chat_stats(chat_id, stats) - - if args.compare: - display_comparison(stats_data) - - -if __name__ == "__main__": - main() diff --git a/src/chat/chat_loop/heartFC_chat.py b/src/chat/chat_loop/heartFC_chat.py index 53dd469d0..fb0e641ca 100644 --- a/src/chat/chat_loop/heartFC_chat.py +++ b/src/chat/chat_loop/heartFC_chat.py @@ -20,7 +20,7 @@ from src.person_info.person_info import get_person_info_manager from src.plugin_system.base.component_types import ActionInfo, ChatMode from src.plugin_system.apis import generator_api, send_api, message_api from src.chat.willing.willing_manager import get_willing_manager -from src.chat.mai_thinking.mai_think import mai_thinking_manager +from src.mais4u.mai_think import mai_thinking_manager from maim_message.message_base import GroupInfo from src.mais4u.constant_s4u import ENABLE_S4U diff --git a/src/chat/replyer/default_generator.py b/src/chat/replyer/default_generator.py index f41ca8ddc..237639a47 100644 --- a/src/chat/replyer/default_generator.py +++ b/src/chat/replyer/default_generator.py @@ -6,7 +6,7 @@ import re from typing import List, Optional, Dict, Any, Tuple from datetime import datetime -from src.chat.mai_thinking.mai_think import mai_thinking_manager +from src.mais4u.mai_think import mai_thinking_manager from src.common.logger import get_logger from src.config.config import global_config from src.individuality.individuality import get_individuality diff --git a/src/chat/utils/statistic.py b/src/chat/utils/statistic.py index bce8856e5..82d24ea23 100644 --- a/src/chat/utils/statistic.py +++ b/src/chat/utils/statistic.py @@ -43,20 +43,6 @@ ONLINE_TIME = "online_time" TOTAL_MSG_CNT = "total_messages" MSG_CNT_BY_CHAT = "messages_by_chat" -# Focus统计数据的键 -FOCUS_TOTAL_CYCLES = "focus_total_cycles" -FOCUS_AVG_TIMES_BY_STAGE = "focus_avg_times_by_stage" -FOCUS_ACTION_RATIOS = "focus_action_ratios" -FOCUS_CYCLE_CNT_BY_CHAT = "focus_cycle_count_by_chat" -FOCUS_CYCLE_CNT_BY_ACTION = "focus_cycle_count_by_action" -FOCUS_AVG_TIMES_BY_CHAT_ACTION = "focus_avg_times_by_chat_action" -FOCUS_AVG_TIMES_BY_ACTION = "focus_avg_times_by_action" -FOCUS_TOTAL_TIME_BY_CHAT = "focus_total_time_by_chat" -FOCUS_TOTAL_TIME_BY_ACTION = "focus_total_time_by_action" -FOCUS_CYCLE_CNT_BY_VERSION = "focus_cycle_count_by_version" -FOCUS_ACTION_RATIOS_BY_VERSION = "focus_action_ratios_by_version" -FOCUS_AVG_TIMES_BY_VERSION = "focus_avg_times_by_version" - class OnlineTimeRecordTask(AsyncTask): """在线时间记录任务""" @@ -196,8 +182,6 @@ class StatisticOutputTask(AsyncTask): self._format_model_classified_stat(stats["last_hour"]), "", self._format_chat_stat(stats["last_hour"]), - "", - self._format_focus_stat(stats["last_hour"]), self.SEP_LINE, "", ] @@ -466,189 +450,7 @@ class StatisticOutputTask(AsyncTask): break return stats - def _collect_focus_statistics_for_period(self, collect_period: List[Tuple[str, datetime]]) -> Dict[str, Any]: - """ - 收集指定时间段的Focus统计数据 - - :param collect_period: 统计时间段 - """ - if not collect_period: - return {} - - collect_period.sort(key=lambda x: x[1], reverse=True) - - stats = { - period_key: { - FOCUS_TOTAL_CYCLES: 0, - FOCUS_AVG_TIMES_BY_STAGE: defaultdict(list), - FOCUS_ACTION_RATIOS: defaultdict(int), - FOCUS_CYCLE_CNT_BY_CHAT: defaultdict(int), - FOCUS_CYCLE_CNT_BY_ACTION: defaultdict(int), - FOCUS_AVG_TIMES_BY_CHAT_ACTION: defaultdict(lambda: defaultdict(list)), - FOCUS_AVG_TIMES_BY_ACTION: defaultdict(lambda: defaultdict(list)), - "focus_exec_times_by_chat_action": defaultdict(lambda: defaultdict(list)), - FOCUS_TOTAL_TIME_BY_CHAT: defaultdict(float), - FOCUS_TOTAL_TIME_BY_ACTION: defaultdict(float), - FOCUS_CYCLE_CNT_BY_VERSION: defaultdict(int), - FOCUS_ACTION_RATIOS_BY_VERSION: defaultdict(lambda: defaultdict(int)), - FOCUS_AVG_TIMES_BY_VERSION: defaultdict(lambda: defaultdict(list)), - "focus_exec_times_by_version_action": defaultdict(lambda: defaultdict(list)), - "focus_action_ratios_by_chat": defaultdict(lambda: defaultdict(int)), - } - for period_key, _ in collect_period - } - - # 获取 log/hfc_loop 目录下的所有 json 文件 - log_dir = "log/hfc_loop" - if not os.path.exists(log_dir): - logger.warning(f"Focus log directory {log_dir} does not exist") - return stats - - json_files = glob.glob(os.path.join(log_dir, "*.json")) - query_start_time = collect_period[-1][1] - - for json_file in json_files: - try: - # 从文件名解析时间戳 (格式: hash_version_date_time.json) - filename = os.path.basename(json_file) - name_parts = filename.replace(".json", "").split("_") - if len(name_parts) >= 4: - date_str = name_parts[-2] # YYYYMMDD - time_str = name_parts[-1] # HHMMSS - file_time_str = f"{date_str}_{time_str}" - file_time = datetime.strptime(file_time_str, "%Y%m%d_%H%M%S") - - # 如果文件时间在查询范围内,则处理该文件 - if file_time >= query_start_time: - with open(json_file, "r", encoding="utf-8") as f: - cycles_data = json.load(f) - self._process_focus_file_data(cycles_data, stats, collect_period, file_time) - except Exception as e: - logger.warning(f"Failed to process focus file {json_file}: {e}") - continue - - # 计算平均值 - self._calculate_focus_averages(stats) - return stats - - def _process_focus_file_data( - self, - cycles_data: List[Dict], - stats: Dict[str, Any], - collect_period: List[Tuple[str, datetime]], - file_time: datetime, - ): - """ - 处理单个focus文件的数据 - """ - for cycle_data in cycles_data: - try: - # 解析时间戳 - timestamp_str = cycle_data.get("timestamp", "") - if timestamp_str: - cycle_time = datetime.fromisoformat(timestamp_str.replace("Z", "+00:00")) - else: - cycle_time = file_time # 使用文件时间作为后备 - - chat_id = cycle_data.get("chat_id", "unknown") - action_type = cycle_data.get("action_type", "unknown") - total_time = cycle_data.get("total_time", 0.0) - step_times = cycle_data.get("step_times", {}) - version = cycle_data.get("version", "unknown") - - # 更新聊天ID名称映射 - if chat_id not in self.name_mapping: - # 尝试获取实际的聊天名称 - display_name = self._get_chat_display_name_from_id(chat_id) - self.name_mapping[chat_id] = (display_name, cycle_time.timestamp()) - - # 对每个时间段进行统计 - for idx, (_, period_start) in enumerate(collect_period): - if cycle_time >= period_start: - for period_key, _ in collect_period[idx:]: - stat = stats[period_key] - - # 基础统计 - stat[FOCUS_TOTAL_CYCLES] += 1 - stat[FOCUS_ACTION_RATIOS][action_type] += 1 - stat[FOCUS_CYCLE_CNT_BY_CHAT][chat_id] += 1 - stat[FOCUS_CYCLE_CNT_BY_ACTION][action_type] += 1 - stat["focus_action_ratios_by_chat"][chat_id][action_type] += 1 - stat[FOCUS_TOTAL_TIME_BY_CHAT][chat_id] += total_time - stat[FOCUS_TOTAL_TIME_BY_ACTION][action_type] += total_time - - # 版本统计 - stat[FOCUS_CYCLE_CNT_BY_VERSION][version] += 1 - stat[FOCUS_ACTION_RATIOS_BY_VERSION][version][action_type] += 1 - - # 阶段时间统计 - for stage, time_val in step_times.items(): - stat[FOCUS_AVG_TIMES_BY_STAGE][stage].append(time_val) - stat[FOCUS_AVG_TIMES_BY_CHAT_ACTION][chat_id][stage].append(time_val) - stat[FOCUS_AVG_TIMES_BY_ACTION][action_type][stage].append(time_val) - stat[FOCUS_AVG_TIMES_BY_VERSION][version][stage].append(time_val) - - # 专门收集执行动作阶段的时间,按聊天流和action类型分组 - if stage == "执行动作": - stat["focus_exec_times_by_chat_action"][chat_id][action_type].append(time_val) - # 按版本和action类型收集执行时间 - stat["focus_exec_times_by_version_action"][version][action_type].append(time_val) - break - except Exception as e: - logger.warning(f"Failed to process cycle data: {e}") - continue - - def _calculate_focus_averages(self, stats: Dict[str, Any]): - """ - 计算Focus统计的平均值 - """ - for _period_key, stat in stats.items(): - # 计算全局阶段平均时间 - for stage, times in stat[FOCUS_AVG_TIMES_BY_STAGE].items(): - if times: - stat[FOCUS_AVG_TIMES_BY_STAGE][stage] = sum(times) / len(times) - else: - stat[FOCUS_AVG_TIMES_BY_STAGE][stage] = 0.0 - - # 计算按chat_id和action_type的阶段平均时间 - for chat_id, stage_times in stat[FOCUS_AVG_TIMES_BY_CHAT_ACTION].items(): - for stage, times in stage_times.items(): - if times: - stat[FOCUS_AVG_TIMES_BY_CHAT_ACTION][chat_id][stage] = sum(times) / len(times) - else: - stat[FOCUS_AVG_TIMES_BY_CHAT_ACTION][chat_id][stage] = 0.0 - - # 计算按action_type的阶段平均时间 - for action_type, stage_times in stat[FOCUS_AVG_TIMES_BY_ACTION].items(): - for stage, times in stage_times.items(): - if times: - stat[FOCUS_AVG_TIMES_BY_ACTION][action_type][stage] = sum(times) / len(times) - else: - stat[FOCUS_AVG_TIMES_BY_ACTION][action_type][stage] = 0.0 - - # 计算按聊天流和action类型的执行时间平均值 - for chat_id, action_times in stat["focus_exec_times_by_chat_action"].items(): - for action_type, times in action_times.items(): - if times: - stat["focus_exec_times_by_chat_action"][chat_id][action_type] = sum(times) / len(times) - else: - stat["focus_exec_times_by_chat_action"][chat_id][action_type] = 0.0 - - # 计算按版本的阶段平均时间 - for version, stage_times in stat[FOCUS_AVG_TIMES_BY_VERSION].items(): - for stage, times in stage_times.items(): - if times: - stat[FOCUS_AVG_TIMES_BY_VERSION][version][stage] = sum(times) / len(times) - else: - stat[FOCUS_AVG_TIMES_BY_VERSION][version][stage] = 0.0 - - # 计算按版本和action类型的执行时间平均值 - for version, action_times in stat["focus_exec_times_by_version_action"].items(): - for action_type, times in action_times.items(): - if times: - stat["focus_exec_times_by_version_action"][version][action_type] = sum(times) / len(times) - else: - stat["focus_exec_times_by_version_action"][version][action_type] = 0.0 + def _collect_all_statistics(self, now: datetime) -> Dict[str, Dict[str, Any]]: """ @@ -675,15 +477,13 @@ class StatisticOutputTask(AsyncTask): model_req_stat = self._collect_model_request_for_period(stat_start_timestamp) online_time_stat = self._collect_online_time_for_period(stat_start_timestamp, now) message_count_stat = self._collect_message_count_for_period(stat_start_timestamp) - focus_stat = self._collect_focus_statistics_for_period(stat_start_timestamp) # 统计数据合并 - # 合并四类统计数据 + # 合并三类统计数据 for period_key, _ in stat_start_timestamp: stat[period_key].update(model_req_stat[period_key]) stat[period_key].update(online_time_stat[period_key]) stat[period_key].update(message_count_stat[period_key]) - stat[period_key].update(focus_stat[period_key]) if last_all_time_stat: # 若存在上次完整统计数据,则将其与当前统计数据合并 @@ -800,41 +600,6 @@ class StatisticOutputTask(AsyncTask): output.append("") return "\n".join(output) - def _format_focus_stat(self, stats: Dict[str, Any]) -> str: - """ - 格式化Focus统计数据 - """ - if stats[FOCUS_TOTAL_CYCLES] <= 0: - return "" - - output = ["Focus系统统计:", f"总循环数: {stats[FOCUS_TOTAL_CYCLES]}", ""] - - # 全局阶段平均时间 - if stats[FOCUS_AVG_TIMES_BY_STAGE]: - output.append("全局阶段平均时间:") - output.extend(f" {stage}: {avg_time:.3f}秒" for stage, avg_time in stats[FOCUS_AVG_TIMES_BY_STAGE].items()) - output.append("") - - # Action类型比例 - if stats[FOCUS_ACTION_RATIOS]: - total_actions = sum(stats[FOCUS_ACTION_RATIOS].values()) - output.append("Action类型分布:") - for action_type, count in sorted(stats[FOCUS_ACTION_RATIOS].items()): - ratio = (count / total_actions) * 100 if total_actions > 0 else 0 - output.append(f" {action_type}: {count} ({ratio:.1f}%)") - output.append("") - - # 按Chat统计(仅显示前10个) - if stats[FOCUS_CYCLE_CNT_BY_CHAT]: - output.append("按聊天流统计 (前10):") - sorted_chats = sorted(stats[FOCUS_CYCLE_CNT_BY_CHAT].items(), key=lambda x: x[1], reverse=True)[:10] - for chat_id, count in sorted_chats: - chat_name = self.name_mapping.get(chat_id, (chat_id, 0))[0] - output.append(f" {chat_name[:30]}: {count} 循环") - output.append("") - - return "\n".join(output) - def _get_chat_display_name_from_id(self, chat_id: str) -> str: """从chat_id获取显示名称""" try: @@ -865,6 +630,10 @@ class StatisticOutputTask(AsyncTask): logger.warning(f"获取聊天显示名称失败: {e}") return chat_id + def _generate_versions_tab(self, stat: dict) -> str: + """版本对比功能占位符,防止报错""" + return '

版本对比

暂未实现版本对比功能。

' + def _generate_html_report(self, stat: dict[str, Any], now: datetime): """ 生成HTML格式的统计报告 @@ -877,9 +646,6 @@ class StatisticOutputTask(AsyncTask): f'' for period in self.stat_period ] - # 添加Focus统计、版本对比和图表选项卡 - tab_list.append('') - tab_list.append('') tab_list.append('') def _format_stat_data(stat_data: dict[str, Any], div_id: str, start_time: datetime) -> str: @@ -941,53 +707,6 @@ class StatisticOutputTask(AsyncTask): for chat_id, count in sorted(stat_data[MSG_CNT_BY_CHAT].items()) ] ) - - # Focus统计数据 - # focus_action_rows = "" - # focus_chat_rows = "" - # focus_stage_rows = "" - # focus_action_stage_rows = "" - - if stat_data.get(FOCUS_TOTAL_CYCLES, 0) > 0: - # Action类型统计 - total_actions = sum(stat_data[FOCUS_ACTION_RATIOS].values()) if stat_data[FOCUS_ACTION_RATIOS] else 0 - _focus_action_rows = "\n".join( - [ - f"{action_type}{count}{(count / total_actions * 100):.1f}%" - for action_type, count in sorted(stat_data[FOCUS_ACTION_RATIOS].items()) - ] - ) - - # 按聊天流统计 - _focus_chat_rows = "\n".join( - [ - f"{self.name_mapping.get(chat_id, (chat_id, 0))[0]}{count}{stat_data[FOCUS_TOTAL_TIME_BY_CHAT].get(chat_id, 0):.2f}秒" - for chat_id, count in sorted( - stat_data[FOCUS_CYCLE_CNT_BY_CHAT].items(), key=lambda x: x[1], reverse=True - ) - ] - ) - - # 全局阶段时间统计 - _focus_stage_rows = "\n".join( - [ - f"{stage}{avg_time:.3f}秒" - for stage, avg_time in sorted(stat_data[FOCUS_AVG_TIMES_BY_STAGE].items()) - ] - ) - - # 按Action类型的阶段时间统计 - focus_action_stage_items = [] - for action_type, stage_times in stat_data[FOCUS_AVG_TIMES_BY_ACTION].items(): - for stage, avg_time in stage_times.items(): - focus_action_stage_items.append((action_type, stage, avg_time)) - - _focus_action_stage_rows = "\n".join( - [ - f"{action_type}{stage}{avg_time:.3f}秒" - for action_type, stage, avg_time in sorted(focus_action_stage_items) - ] - ) # 生成HTML return f"""
@@ -1052,10 +771,6 @@ class StatisticOutputTask(AsyncTask): _format_stat_data(stat["all_time"], "all_time", datetime.fromtimestamp(local_storage["deploy_time"])) # type: ignore ) - # 添加Focus统计内容 - focus_tab = self._generate_focus_tab(stat) - tab_content_list.append(focus_tab) - # 添加版本对比内容 versions_tab = self._generate_versions_tab(stat) tab_content_list.append(versions_tab) @@ -1210,609 +925,6 @@ class StatisticOutputTask(AsyncTask): with open(self.record_file_path, "w", encoding="utf-8") as f: f.write(html_template) - def _generate_focus_tab(self, stat: dict[str, Any]) -> str: - # sourcery skip: for-append-to-extend, list-comprehension, use-any, use-named-expression, use-next - """生成Focus统计独立分页的HTML内容""" - - # 为每个时间段准备Focus数据 - focus_sections = [] - - for period_name, period_delta, period_desc in self.stat_period: - stat_data = stat.get(period_name, {}) - - if stat_data.get(FOCUS_TOTAL_CYCLES, 0) <= 0: - continue - - # 生成Focus统计数据行 - focus_action_rows = "" - focus_chat_rows = "" - focus_stage_rows = "" - focus_action_stage_rows = "" - - # Action类型统计 - total_actions = sum(stat_data[FOCUS_ACTION_RATIOS].values()) if stat_data[FOCUS_ACTION_RATIOS] else 0 - if total_actions > 0: - focus_action_rows = "\n".join( - [ - f"{action_type}{count}{(count / total_actions * 100):.1f}%" - for action_type, count in sorted(stat_data[FOCUS_ACTION_RATIOS].items()) - ] - ) - - # 按聊天流统计(横向表格,显示各阶段时间差异和不同action的平均时间) - focus_chat_rows = "" - if stat_data[FOCUS_AVG_TIMES_BY_CHAT_ACTION]: - # 获取前三个阶段(不包括执行动作) - basic_stages = ["观察", "规划器"] - existing_basic_stages = [] - for stage in basic_stages: - # 检查是否有任何聊天流在这个阶段有数据 - stage_exists = False - for _chat_id, stage_times in stat_data[FOCUS_AVG_TIMES_BY_CHAT_ACTION].items(): - if stage in stage_times: - stage_exists = True - break - if stage_exists: - existing_basic_stages.append(stage) - - # 获取所有action类型(按出现频率排序) - all_action_types = sorted( - stat_data[FOCUS_ACTION_RATIOS].keys(), key=lambda x: stat_data[FOCUS_ACTION_RATIOS][x], reverse=True - ) - - # 为每个聊天流生成一行 - chat_rows = [] - for chat_id in sorted( - stat_data[FOCUS_CYCLE_CNT_BY_CHAT].keys(), - key=lambda x: stat_data[FOCUS_CYCLE_CNT_BY_CHAT][x], - reverse=True, - ): - chat_name = self.name_mapping.get(chat_id, (chat_id, 0))[0] - cycle_count = stat_data[FOCUS_CYCLE_CNT_BY_CHAT][chat_id] - - # 获取该聊天流的各阶段平均时间 - stage_times = stat_data[FOCUS_AVG_TIMES_BY_CHAT_ACTION].get(chat_id, {}) - - row_cells = [f"{chat_name}
({cycle_count}次循环)"] - - # 添加基础阶段时间 - for stage in existing_basic_stages: - time_val = stage_times.get(stage, 0.0) - row_cells.append(f"{time_val:.3f}秒") - - # 添加每个action类型的平均执行时间 - for action_type in all_action_types: - # 使用真实的按聊天流+action类型分组的执行时间数据 - exec_times_by_chat_action = stat_data.get("focus_exec_times_by_chat_action", {}) - chat_action_times = exec_times_by_chat_action.get(chat_id, {}) - avg_exec_time = chat_action_times.get(action_type, 0.0) - - if avg_exec_time > 0: - row_cells.append(f"{avg_exec_time:.3f}秒") - else: - row_cells.append("-") - - chat_rows.append(f"{''.join(row_cells)}") - - # 生成表头 - stage_headers = "".join([f"{stage}" for stage in existing_basic_stages]) - action_headers = "".join( - [f"{action_type}
(执行)" for action_type in all_action_types] - ) - focus_chat_table_header = f"聊天流{stage_headers}{action_headers}" - focus_chat_rows = focus_chat_table_header + "\n" + "\n".join(chat_rows) - - # 全局阶段时间统计 - focus_stage_rows = "\n".join( - [ - f"{stage}{avg_time:.3f}秒" - for stage, avg_time in sorted(stat_data[FOCUS_AVG_TIMES_BY_STAGE].items()) - ] - ) - - # 聊天流Action选择比例对比表(横向表格) - focus_chat_action_ratios_rows = "" - if stat_data.get("focus_action_ratios_by_chat"): - if all_action_types_for_ratio := sorted( - stat_data[FOCUS_ACTION_RATIOS].keys(), - key=lambda x: stat_data[FOCUS_ACTION_RATIOS][x], - reverse=True, - ): - # 为每个聊天流生成数据行(按循环数排序) - chat_ratio_rows = [] - for chat_id in sorted( - stat_data[FOCUS_CYCLE_CNT_BY_CHAT].keys(), - key=lambda x: stat_data[FOCUS_CYCLE_CNT_BY_CHAT][x], - reverse=True, - ): - chat_name = self.name_mapping.get(chat_id, (chat_id, 0))[0] - total_cycles = stat_data[FOCUS_CYCLE_CNT_BY_CHAT][chat_id] - chat_action_counts = stat_data["focus_action_ratios_by_chat"].get(chat_id, {}) - - row_cells = [f"{chat_name}
({total_cycles}次循环)"] - - # 添加每个action类型的数量和百分比 - for action_type in all_action_types_for_ratio: - count = chat_action_counts.get(action_type, 0) - ratio = (count / total_cycles * 100) if total_cycles > 0 else 0 - if count > 0: - row_cells.append(f"{count}
({ratio:.1f}%)") - else: - row_cells.append("-
(0%)") - - chat_ratio_rows.append(f"{''.join(row_cells)}") - - # 生成表头 - action_headers = "".join([f"{action_type}" for action_type in all_action_types_for_ratio]) - chat_action_ratio_table_header = f"聊天流{action_headers}" - focus_chat_action_ratios_rows = chat_action_ratio_table_header + "\n" + "\n".join(chat_ratio_rows) - - # 按Action类型的阶段时间统计(横向表格) - focus_action_stage_rows = "" - if stat_data[FOCUS_AVG_TIMES_BY_ACTION]: - # 获取所有阶段(按固定顺序) - stage_order = ["观察", "规划器", "执行动作"] - all_stages = [] - for stage in stage_order: - if any(stage in stage_times for stage_times in stat_data[FOCUS_AVG_TIMES_BY_ACTION].values()): - all_stages.append(stage) - - # 为每个Action类型生成一行 - action_rows = [] - for action_type in sorted(stat_data[FOCUS_AVG_TIMES_BY_ACTION].keys()): - stage_times = stat_data[FOCUS_AVG_TIMES_BY_ACTION][action_type] - row_cells = [f"{action_type}"] - - for stage in all_stages: - time_val = stage_times.get(stage, 0.0) - row_cells.append(f"{time_val:.3f}秒") - - action_rows.append(f"{''.join(row_cells)}") - - # 生成表头 - stage_headers = "".join([f"{stage}" for stage in all_stages]) - focus_action_stage_table_header = f"Action类型{stage_headers}" - focus_action_stage_rows = focus_action_stage_table_header + "\n" + "\n".join(action_rows) - - # 计算时间范围 - if period_name == "all_time": - from src.manager.local_store_manager import local_storage - - start_time = datetime.fromtimestamp(local_storage["deploy_time"]) # type: ignore - else: - start_time = datetime.now() - period_delta - - time_range = f"{start_time.strftime('%Y-%m-%d %H:%M:%S')} ~ {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}" - # 生成该时间段的Focus统计HTML - section_html = f""" -
-

{period_desc}Focus统计

-

统计时段: {time_range}

-

总循环数: {stat_data.get(FOCUS_TOTAL_CYCLES, 0)}

- -
-
-

全局阶段平均时间

- - - {focus_stage_rows} -
阶段平均时间
-
- -
-

Action类型分布

- - - {focus_action_rows} -
Action类型次数占比
-
-
- -
-

按聊天流各阶段时间统计

- - - {focus_chat_rows} -
-
- -
-

聊天流Action选择比例对比

- - - {focus_chat_action_ratios_rows} -
-
- -
-

Action类型阶段时间详情

- - - {focus_action_stage_rows} -
-
-
- """ - - focus_sections.append(section_html) - - # 如果没有任何Focus数据 - if not focus_sections: - focus_sections.append(""" -
-

暂无Focus统计数据

-

在指定时间段内未找到任何Focus循环数据。

-

请确保 log/hfc_loop/ 目录下存在相应的JSON文件。

-
- """) - - return f""" -
-

Focus系统详细统计

-

- 数据来源: log/hfc_loop/ 目录下的JSON文件
- 统计内容: 各时间段的Focus循环性能分析 -

- - {"".join(focus_sections)} - - -
- """ - - def _generate_versions_tab(self, stat: dict[str, Any]) -> str: - # sourcery skip: use-named-expression, use-next - """生成版本对比独立分页的HTML内容""" - - # 为每个时间段准备版本对比数据 - version_sections = [] - - for period_name, period_delta, period_desc in self.stat_period: - stat_data = stat.get(period_name, {}) - - if not stat_data.get(FOCUS_CYCLE_CNT_BY_VERSION): - continue - - # 获取所有版本(按循环数排序) - all_versions = sorted( - stat_data[FOCUS_CYCLE_CNT_BY_VERSION].keys(), - key=lambda x: stat_data[FOCUS_CYCLE_CNT_BY_VERSION][x], - reverse=True, - ) - - # 生成版本Action分布表 - focus_version_action_rows = "" - if stat_data[FOCUS_ACTION_RATIOS_BY_VERSION]: - # 获取所有action类型 - all_action_types_for_version = set() - for version_actions in stat_data[FOCUS_ACTION_RATIOS_BY_VERSION].values(): - all_action_types_for_version.update(version_actions.keys()) - all_action_types_for_version = sorted(all_action_types_for_version) - - if all_action_types_for_version: - version_action_rows = [] - for version in all_versions: - version_actions = stat_data[FOCUS_ACTION_RATIOS_BY_VERSION].get(version, {}) - total_cycles = stat_data[FOCUS_CYCLE_CNT_BY_VERSION][version] - - row_cells = [f"{version}
({total_cycles}次循环)"] - - for action_type in all_action_types_for_version: - count = version_actions.get(action_type, 0) - ratio = (count / total_cycles * 100) if total_cycles > 0 else 0 - row_cells.append(f"{count}
({ratio:.1f}%)") - - version_action_rows.append(f"{''.join(row_cells)}") - - # 生成表头 - action_headers = "".join( - [f"{action_type}" for action_type in all_action_types_for_version] - ) - version_action_table_header = f"版本{action_headers}" - focus_version_action_rows = version_action_table_header + "\n" + "\n".join(version_action_rows) - - # 生成版本阶段时间表(按action类型分解执行时间) - focus_version_stage_rows = "" - if stat_data[FOCUS_AVG_TIMES_BY_VERSION]: - # 基础三个阶段 - basic_stages = ["观察", "规划器"] - - # 获取所有action类型用于执行时间列 - all_action_types_for_exec = set() - if stat_data.get("focus_exec_times_by_version_action"): - for version_actions in stat_data["focus_exec_times_by_version_action"].values(): - all_action_types_for_exec.update(version_actions.keys()) - all_action_types_for_exec = sorted(all_action_types_for_exec) - - # 检查哪些基础阶段存在数据 - existing_basic_stages = [] - for stage in basic_stages: - stage_exists = False - for version_stages in stat_data[FOCUS_AVG_TIMES_BY_VERSION].values(): - if stage in version_stages: - stage_exists = True - break - if stage_exists: - existing_basic_stages.append(stage) - - # 构建表格 - if existing_basic_stages or all_action_types_for_exec: - version_stage_rows = [] - - # 为每个版本生成数据行 - for version in all_versions: - version_stages = stat_data[FOCUS_AVG_TIMES_BY_VERSION].get(version, {}) - total_cycles = stat_data[FOCUS_CYCLE_CNT_BY_VERSION][version] - - row_cells = [f"{version}
({total_cycles}次循环)"] - - # 添加基础阶段时间 - for stage in existing_basic_stages: - time_val = version_stages.get(stage, 0.0) - row_cells.append(f"{time_val:.3f}秒") - - # 添加不同action类型的执行时间 - for action_type in all_action_types_for_exec: - # 获取该版本该action类型的平均执行时间 - version_exec_times = stat_data.get("focus_exec_times_by_version_action", {}) - if version in version_exec_times and action_type in version_exec_times[version]: - exec_time = version_exec_times[version][action_type] - row_cells.append(f"{exec_time:.3f}秒") - else: - row_cells.append("-") - - version_stage_rows.append(f"{''.join(row_cells)}") - - # 生成表头 - basic_headers = "".join([f"{stage}" for stage in existing_basic_stages]) - action_headers = "".join( - [ - f"执行时间
[{action_type}]" - for action_type in all_action_types_for_exec - ] - ) - version_stage_table_header = f"版本{basic_headers}{action_headers}" - focus_version_stage_rows = version_stage_table_header + "\n" + "\n".join(version_stage_rows) - - # 计算时间范围 - if period_name == "all_time": - from src.manager.local_store_manager import local_storage - - start_time = datetime.fromtimestamp(local_storage["deploy_time"]) # type: ignore - else: - start_time = datetime.now() - period_delta - time_range = f"{start_time.strftime('%Y-%m-%d %H:%M:%S')} ~ {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}" - # 生成该时间段的版本对比HTML - section_html = f""" -
-

{period_desc}版本对比

-

统计时段: {time_range}

-

包含版本: {len(all_versions)} 个版本

- -
-
-

版本Action类型分布对比

- - - {focus_version_action_rows} -
-
- -
-

版本阶段时间对比

- - - {focus_version_stage_rows} -
-
-
-
- """ - - version_sections.append(section_html) - - # 如果没有任何版本数据 - if not version_sections: - version_sections.append(""" -
-

暂无版本对比数据

-

在指定时间段内未找到任何版本信息。

-

请确保 log/hfc_loop/ 目录下的JSON文件包含版本信息。

-
- """) - - return f""" -
-

Focus HFC版本对比分析

-

- 对比内容: 不同版本的Action类型分布和各阶段性能表现
- 数据来源: log/hfc_loop/ 目录下JSON文件中的version字段 -

- - {"".join(version_sections)} - - -
- """ - def _generate_chart_data(self, stat: dict[str, Any]) -> dict: """生成图表数据""" now = datetime.now() @@ -1906,68 +1018,12 @@ class StatisticOutputTask(AsyncTask): message_by_chat[chat_name] = [0] * len(time_points) message_by_chat[chat_name][interval_index] += 1 - # 查询Focus循环记录 - focus_cycles_by_action = {} - focus_time_by_stage = {} - - log_dir = "log/hfc_loop" - if os.path.exists(log_dir): - json_files = glob.glob(os.path.join(log_dir, "*.json")) - for json_file in json_files: - try: - # 解析文件时间 - filename = os.path.basename(json_file) - name_parts = filename.replace(".json", "").split("_") - if len(name_parts) >= 4: - date_str = name_parts[-2] - time_str = name_parts[-1] - file_time_str = f"{date_str}_{time_str}" - file_time = datetime.strptime(file_time_str, "%Y%m%d_%H%M%S") - - if file_time >= start_time: - with open(json_file, "r", encoding="utf-8") as f: - cycles_data = json.load(f) - - for cycle in cycles_data: - try: - timestamp_str = cycle.get("timestamp", "") - if timestamp_str: - cycle_time = datetime.fromisoformat(timestamp_str.replace("Z", "+00:00")) - else: - cycle_time = file_time - - if cycle_time >= start_time: - # 计算时间间隔索引 - time_diff = (cycle_time - start_time).total_seconds() - interval_index = int(time_diff // interval_seconds) - - if 0 <= interval_index < len(time_points): - action_type = cycle.get("action_type", "unknown") - step_times = cycle.get("step_times", {}) - - # 累计action类型数据 - if action_type not in focus_cycles_by_action: - focus_cycles_by_action[action_type] = [0] * len(time_points) - focus_cycles_by_action[action_type][interval_index] += 1 - - # 累计阶段时间数据 - for stage, time_val in step_times.items(): - if stage not in focus_time_by_stage: - focus_time_by_stage[stage] = [0] * len(time_points) - focus_time_by_stage[stage][interval_index] += time_val - except Exception: - continue - except Exception: - continue - return { "time_labels": time_labels, "total_cost_data": total_cost_data, "cost_by_model": cost_by_model, "cost_by_module": cost_by_module, "message_by_chat": message_by_chat, - "focus_cycles_by_action": focus_cycles_by_action, - "focus_time_by_stage": focus_time_by_stage, } def _generate_chart_tab(self, chart_data: dict) -> str: @@ -2059,14 +1115,8 @@ class StatisticOutputTask(AsyncTask):
-
- -
-
- -
- +
@@ -2169,8 +1219,6 @@ class StatisticOutputTask(AsyncTask): createChart('costByModule', data, timeRange); createChart('costByModel', data, timeRange); createChart('messageByChat', data, timeRange); - createChart('focusCyclesByAction', data, timeRange); - createChart('focusTimeByStage', data, timeRange); }} function createChart(chartType, data, timeRange) {{ @@ -2327,21 +1375,6 @@ class AsyncStatisticOutputTask(AsyncTask): def _collect_message_count_for_period(self, collect_period: List[Tuple[str, datetime]]) -> Dict[str, Any]: return StatisticOutputTask._collect_message_count_for_period(self, collect_period) # type: ignore - def _collect_focus_statistics_for_period(self, collect_period: List[Tuple[str, datetime]]) -> Dict[str, Any]: - return StatisticOutputTask._collect_focus_statistics_for_period(self, collect_period) # type: ignore - - def _process_focus_file_data( - self, - cycles_data: List[Dict], - stats: Dict[str, Any], - collect_period: List[Tuple[str, datetime]], - file_time: datetime, - ): - return StatisticOutputTask._process_focus_file_data(self, cycles_data, stats, collect_period, file_time) # type: ignore - - def _calculate_focus_averages(self, stats: Dict[str, Any]): - return StatisticOutputTask._calculate_focus_averages(self, stats) # type: ignore - @staticmethod def _format_total_stat(stats: Dict[str, Any]) -> str: return StatisticOutputTask._format_total_stat(stats) @@ -2353,9 +1386,6 @@ class AsyncStatisticOutputTask(AsyncTask): def _format_chat_stat(self, stats: Dict[str, Any]) -> str: return StatisticOutputTask._format_chat_stat(self, stats) # type: ignore - def _format_focus_stat(self, stats: Dict[str, Any]) -> str: - return StatisticOutputTask._format_focus_stat(self, stats) # type: ignore - def _generate_chart_data(self, stat: dict[str, Any]) -> dict: return StatisticOutputTask._generate_chart_data(self, stat) # type: ignore @@ -2368,11 +1398,5 @@ class AsyncStatisticOutputTask(AsyncTask): def _get_chat_display_name_from_id(self, chat_id: str) -> str: return StatisticOutputTask._get_chat_display_name_from_id(self, chat_id) # type: ignore - def _generate_focus_tab(self, stat: dict[str, Any]) -> str: - return StatisticOutputTask._generate_focus_tab(self, stat) # type: ignore - - def _generate_versions_tab(self, stat: dict[str, Any]) -> str: - return StatisticOutputTask._generate_versions_tab(self, stat) # type: ignore - def _convert_defaultdict_to_dict(self, data): return StatisticOutputTask._convert_defaultdict_to_dict(self, data) # type: ignore diff --git a/src/chat/mai_thinking/mai_think.py b/src/mais4u/mai_think.py similarity index 100% rename from src/chat/mai_thinking/mai_think.py rename to src/mais4u/mai_think.py diff --git a/src/plugins/built_in/core_actions/reply.py b/src/plugins/built_in/core_actions/reply.py index a5071c4c9..9925ef37c 100644 --- a/src/plugins/built_in/core_actions/reply.py +++ b/src/plugins/built_in/core_actions/reply.py @@ -15,7 +15,7 @@ from src.common.logger import get_logger from src.plugin_system.apis import generator_api, message_api from src.plugins.built_in.core_actions.no_reply import NoReplyAction from src.person_info.person_info import get_person_info_manager -from src.chat.mai_thinking.mai_think import mai_thinking_manager +from src.mais4u.mai_think import mai_thinking_manager from src.mais4u.constant_s4u import ENABLE_S4U logger = get_logger("reply_action")