Merge branch 'dev' of https://github.com/MaiM-with-u/MaiBot into dev
This commit is contained in:
@@ -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()
|
|
||||||
@@ -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()
|
|
||||||
@@ -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()
|
|
||||||
@@ -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())
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,5 @@
|
|||||||
import tkinter as tk
|
import tkinter as tk
|
||||||
from tkinter import ttk, messagebox, filedialog
|
from tkinter import ttk, messagebox, filedialog, colorchooser
|
||||||
import json
|
import json
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import threading
|
import threading
|
||||||
@@ -8,6 +8,7 @@ from datetime import datetime
|
|||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
import os
|
import os
|
||||||
import time
|
import time
|
||||||
|
import queue
|
||||||
|
|
||||||
|
|
||||||
class LogIndex:
|
class LogIndex:
|
||||||
@@ -206,6 +207,23 @@ class LogFormatter:
|
|||||||
parts.append(str(event))
|
parts.append(str(event))
|
||||||
tags.append("message")
|
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
|
return parts, tags
|
||||||
|
|
||||||
def format_timestamp(self, timestamp):
|
def format_timestamp(self, timestamp):
|
||||||
@@ -287,6 +305,7 @@ class VirtualLogDisplay:
|
|||||||
self.text_widget.tag_configure("level", foreground="#808080")
|
self.text_widget.tag_configure("level", foreground="#808080")
|
||||||
self.text_widget.tag_configure("module", foreground="#808080")
|
self.text_widget.tag_configure("module", foreground="#808080")
|
||||||
self.text_widget.tag_configure("message", foreground="#ffffff")
|
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():
|
for level, color in self.formatter.level_colors.items():
|
||||||
@@ -449,7 +468,7 @@ class LogViewer:
|
|||||||
self.load_config()
|
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")
|
self.current_log_file = Path("logs/app.log.jsonl")
|
||||||
@@ -467,6 +486,9 @@ class LogViewer:
|
|||||||
self.main_frame = ttk.Frame(root)
|
self.main_frame = ttk.Frame(root)
|
||||||
self.main_frame.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
|
self.main_frame.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
|
||||||
|
|
||||||
|
# 创建菜单栏
|
||||||
|
self.create_menu()
|
||||||
|
|
||||||
# 创建控制面板
|
# 创建控制面板
|
||||||
self.create_control_panel()
|
self.create_control_panel()
|
||||||
|
|
||||||
@@ -477,12 +499,30 @@ class LogViewer:
|
|||||||
# 模块名映射
|
# 模块名映射
|
||||||
self.module_name_mapping = {
|
self.module_name_mapping = {
|
||||||
"api": "API接口",
|
"api": "API接口",
|
||||||
|
"async_task_manager": "异步任务管理器",
|
||||||
|
"background_tasks": "后台任务",
|
||||||
|
"base_tool": "基础工具",
|
||||||
|
"chat_stream": "聊天流",
|
||||||
|
"component_registry": "组件注册器",
|
||||||
"config": "配置",
|
"config": "配置",
|
||||||
"chat": "聊天",
|
"database_model": "数据库模型",
|
||||||
"plugin": "插件",
|
"emoji": "表情",
|
||||||
|
"heartflow": "心流",
|
||||||
|
"local_storage": "本地存储",
|
||||||
|
"lpmm": "LPMM",
|
||||||
|
"maibot_statistic": "MaiBot统计",
|
||||||
|
"main_message": "主消息",
|
||||||
"main": "主程序",
|
"main": "主程序",
|
||||||
|
"memory": "内存",
|
||||||
|
"mood": "情绪",
|
||||||
|
"plugin_manager": "插件管理器",
|
||||||
|
"remote": "远程",
|
||||||
|
"willing": "意愿",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# 加载自定义映射
|
||||||
|
self.load_module_mapping()
|
||||||
|
|
||||||
# 选中的模块集合
|
# 选中的模块集合
|
||||||
self.selected_modules = set()
|
self.selected_modules = set()
|
||||||
self.modules = set()
|
self.modules = set()
|
||||||
@@ -491,19 +531,35 @@ class LogViewer:
|
|||||||
self.level_combo.bind("<<ComboboxSelected>>", self.filter_logs)
|
self.level_combo.bind("<<ComboboxSelected>>", self.filter_logs)
|
||||||
self.search_var.trace("w", self.filter_logs)
|
self.search_var.trace("w", self.filter_logs)
|
||||||
|
|
||||||
|
# 绑定快捷键
|
||||||
|
self.root.bind("<Control-o>", lambda e: self.select_log_file())
|
||||||
|
self.root.bind("<F5>", lambda e: self.refresh_log_file())
|
||||||
|
self.root.bind("<Control-s>", lambda e: self.export_logs())
|
||||||
|
|
||||||
# 初始加载文件
|
# 初始加载文件
|
||||||
if self.current_log_file.exists():
|
if self.current_log_file.exists():
|
||||||
self.load_log_file_async()
|
self.load_log_file_async()
|
||||||
|
|
||||||
def load_config(self):
|
def load_config(self):
|
||||||
"""加载配置文件"""
|
"""加载配置文件"""
|
||||||
|
# 默认配置
|
||||||
self.default_config = {
|
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")
|
config_path = Path("config/bot_config.toml")
|
||||||
|
self.log_config = self.default_config["log"].copy()
|
||||||
|
self.viewer_config = self.default_config["viewer"].copy()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if config_path.exists():
|
if config_path.exists():
|
||||||
with open(config_path, "r", encoding="utf-8") as f:
|
with open(config_path, "r", encoding="utf-8") as f:
|
||||||
@@ -511,7 +567,377 @@ class LogViewer:
|
|||||||
if "log" in bot_config:
|
if "log" in bot_config:
|
||||||
self.log_config.update(bot_config["log"])
|
self.log_config.update(bot_config["log"])
|
||||||
except Exception as e:
|
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):
|
def create_control_panel(self):
|
||||||
"""创建控制面板"""
|
"""创建控制面板"""
|
||||||
@@ -549,30 +975,43 @@ class LogViewer:
|
|||||||
side=tk.LEFT, padx=2
|
side=tk.LEFT, padx=2
|
||||||
)
|
)
|
||||||
|
|
||||||
# 过滤控制框架
|
# 模块选择框架
|
||||||
filter_frame = ttk.Frame(self.control_frame)
|
self.module_frame = ttk.LabelFrame(self.control_frame, text="模块")
|
||||||
filter_frame.pack(fill=tk.X, padx=5)
|
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_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["values"] = ["全部", "debug", "info", "warning", "error", "critical"]
|
||||||
self.level_combo.pack(side=tk.LEFT, padx=2)
|
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_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)
|
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("<<ComboboxSelected>>", self.on_module_selected)
|
|
||||||
|
|
||||||
def on_file_loaded(self, log_index, error):
|
def on_file_loaded(self, log_index, error):
|
||||||
"""文件加载完成回调"""
|
"""文件加载完成回调"""
|
||||||
self.progress_bar.pack_forget()
|
self.progress_bar.pack_forget()
|
||||||
@@ -590,6 +1029,7 @@ class LogViewer:
|
|||||||
self.status_var.set(f"已加载 {log_index.total_entries} 条日志")
|
self.status_var.set(f"已加载 {log_index.total_entries} 条日志")
|
||||||
|
|
||||||
# 更新模块列表
|
# 更新模块列表
|
||||||
|
self.modules = set(log_index.module_index.keys())
|
||||||
self.update_module_list()
|
self.update_module_list()
|
||||||
|
|
||||||
# 应用过滤并显示
|
# 应用过滤并显示
|
||||||
@@ -623,22 +1063,11 @@ class LogViewer:
|
|||||||
|
|
||||||
# 清空当前数据
|
# 清空当前数据
|
||||||
self.log_index = LogIndex()
|
self.log_index = LogIndex()
|
||||||
self.modules.clear()
|
|
||||||
self.selected_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)
|
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):
|
def filter_logs(self, *args):
|
||||||
"""过滤日志"""
|
"""过滤日志"""
|
||||||
if not self.log_index:
|
if not self.log_index:
|
||||||
@@ -743,7 +1172,7 @@ class LogViewer:
|
|||||||
def read_new_logs(self, from_position):
|
def read_new_logs(self, from_position):
|
||||||
"""读取新的日志条目并返回它们"""
|
"""读取新的日志条目并返回它们"""
|
||||||
new_entries = []
|
new_entries = []
|
||||||
new_modules_found = False
|
new_modules = set() # 收集新发现的模块
|
||||||
with open(self.current_log_file, "r", encoding="utf-8") as f:
|
with open(self.current_log_file, "r", encoding="utf-8") as f:
|
||||||
f.seek(from_position)
|
f.seek(from_position)
|
||||||
line_count = self.log_index.total_entries
|
line_count = self.log_index.total_entries
|
||||||
@@ -756,14 +1185,20 @@ class LogViewer:
|
|||||||
|
|
||||||
logger_name = log_entry.get("logger_name", "")
|
logger_name = log_entry.get("logger_name", "")
|
||||||
if logger_name and logger_name not in self.modules:
|
if logger_name and logger_name not in self.modules:
|
||||||
self.modules.add(logger_name)
|
new_modules.add(logger_name)
|
||||||
new_modules_found = True
|
|
||||||
|
|
||||||
line_count += 1
|
line_count += 1
|
||||||
except json.JSONDecodeError:
|
except json.JSONDecodeError:
|
||||||
continue
|
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
|
return new_entries
|
||||||
|
|
||||||
def append_new_logs(self, new_entries):
|
def append_new_logs(self, new_entries):
|
||||||
@@ -791,15 +1226,196 @@ class LogViewer:
|
|||||||
self.status_var.set(f"显示 {total_count} 条日志")
|
self.status_var.set(f"显示 {total_count} 条日志")
|
||||||
|
|
||||||
def update_module_list(self):
|
def update_module_list(self):
|
||||||
"""更新模块下拉列表"""
|
"""更新模块列表"""
|
||||||
current_selection = self.module_var.get()
|
# 清空现有选项
|
||||||
self.modules = set(self.log_index.module_index.keys())
|
for widget in self.module_inner_frame.winfo_children():
|
||||||
module_values = ["全部"] + sorted(list(self.modules))
|
widget.destroy()
|
||||||
self.module_combo["values"] = module_values
|
|
||||||
if current_selection in module_values:
|
# 计算总模块数(包括"全部")
|
||||||
self.module_var.set(current_selection)
|
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("<Button-1>", lambda e: self.choose_module_color("全部"))
|
||||||
|
|
||||||
|
current_col += 1
|
||||||
|
|
||||||
|
# 添加其他模块选项
|
||||||
|
for module in sorted(self.modules):
|
||||||
|
if current_col >= max_cols:
|
||||||
|
current_row += 1
|
||||||
|
current_col = 0
|
||||||
|
|
||||||
|
frame = ttk.Frame(self.module_inner_frame)
|
||||||
|
frame.grid(row=current_row, column=current_col, padx=3, pady=2, sticky="ew")
|
||||||
|
|
||||||
|
var = tk.BooleanVar(value=module in self.selected_modules)
|
||||||
|
|
||||||
|
# 使用中文映射名称显示
|
||||||
|
display_name = self.get_display_name(module)
|
||||||
|
if len(display_name) > 12:
|
||||||
|
display_name = display_name[:10] + "..."
|
||||||
|
|
||||||
|
check = ttk.Checkbutton(
|
||||||
|
frame, text=display_name, variable=var, command=lambda m=module, v=var: self.toggle_module(m, v)
|
||||||
|
)
|
||||||
|
check.pack(side=tk.LEFT)
|
||||||
|
|
||||||
|
# 添加工具提示显示完整名称和英文名
|
||||||
|
full_tooltip = f"{self.get_display_name(module)}"
|
||||||
|
if module != self.get_display_name(module):
|
||||||
|
full_tooltip += f"\n({module})"
|
||||||
|
self.create_tooltip(check, full_tooltip)
|
||||||
|
|
||||||
|
# 使用颜色标签替代按钮
|
||||||
|
color = self.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("<Button-1>", lambda e, m=module: self.choose_module_color(m))
|
||||||
|
|
||||||
|
current_col += 1
|
||||||
|
|
||||||
|
# 更新画布滚动区域
|
||||||
|
self.module_inner_frame.update_idletasks()
|
||||||
|
self.module_canvas.config(scrollregion=self.module_canvas.bbox("all"))
|
||||||
|
|
||||||
|
# 添加垂直滚动条
|
||||||
|
if not hasattr(self, "module_scrollbar"):
|
||||||
|
self.module_scrollbar = ttk.Scrollbar(
|
||||||
|
self.module_frame, orient=tk.VERTICAL, command=self.module_canvas.yview
|
||||||
|
)
|
||||||
|
self.module_scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
|
||||||
|
self.module_canvas.config(yscrollcommand=self.module_scrollbar.set)
|
||||||
|
|
||||||
|
def create_tooltip(self, widget, text):
|
||||||
|
"""为控件创建工具提示"""
|
||||||
|
|
||||||
|
def on_enter(event):
|
||||||
|
tooltip = tk.Toplevel()
|
||||||
|
tooltip.wm_overrideredirect(True)
|
||||||
|
tooltip.wm_geometry(f"+{event.x_root + 10}+{event.y_root + 10}")
|
||||||
|
label = ttk.Label(tooltip, text=text, background="lightyellow", relief="solid", borderwidth=1)
|
||||||
|
label.pack()
|
||||||
|
widget.tooltip = tooltip
|
||||||
|
|
||||||
|
def on_leave(event):
|
||||||
|
if hasattr(widget, "tooltip"):
|
||||||
|
widget.tooltip.destroy()
|
||||||
|
del widget.tooltip
|
||||||
|
|
||||||
|
widget.bind("<Enter>", on_enter)
|
||||||
|
widget.bind("<Leave>", on_leave)
|
||||||
|
|
||||||
|
def toggle_module(self, module, var):
|
||||||
|
"""切换模块选择状态"""
|
||||||
|
if module == "全部":
|
||||||
|
if var.get():
|
||||||
|
self.selected_modules = {"全部"}
|
||||||
|
else:
|
||||||
|
self.selected_modules.clear()
|
||||||
else:
|
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():
|
def main():
|
||||||
|
|||||||
@@ -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("<<ComboboxSelected>>", 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()
|
|
||||||
@@ -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()
|
|
||||||
@@ -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.base.component_types import ActionInfo, ChatMode
|
||||||
from src.plugin_system.apis import generator_api, send_api, message_api
|
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.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 maim_message.message_base import GroupInfo
|
||||||
from src.mais4u.constant_s4u import ENABLE_S4U
|
from src.mais4u.constant_s4u import ENABLE_S4U
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import re
|
|||||||
|
|
||||||
from typing import List, Optional, Dict, Any, Tuple
|
from typing import List, Optional, Dict, Any, Tuple
|
||||||
from datetime import datetime
|
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.common.logger import get_logger
|
||||||
from src.config.config import global_config
|
from src.config.config import global_config
|
||||||
from src.individuality.individuality import get_individuality
|
from src.individuality.individuality import get_individuality
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -15,7 +15,7 @@ from src.common.logger import get_logger
|
|||||||
from src.plugin_system.apis import generator_api, message_api
|
from src.plugin_system.apis import generator_api, message_api
|
||||||
from src.plugins.built_in.core_actions.no_reply import NoReplyAction
|
from src.plugins.built_in.core_actions.no_reply import NoReplyAction
|
||||||
from src.person_info.person_info import get_person_info_manager
|
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
|
from src.mais4u.constant_s4u import ENABLE_S4U
|
||||||
|
|
||||||
logger = get_logger("reply_action")
|
logger = get_logger("reply_action")
|
||||||
|
|||||||
Reference in New Issue
Block a user