Files
F6--/张阳脚本/udesk/日志查看器.py
2026-06-02 15:08:26 +08:00

1347 lines
58 KiB
Python

import sys
import os
# 修复Conda环境中文路径导致的Tcl/Tk初始化问题
if sys.platform == 'win32':
# 直接设置TCL/TK路径,避免中文路径编码问题
env_prefix = os.path.join(os.path.dirname(sys.executable), '..', '..', 'Library', 'lib')
env_prefix = os.path.normpath(env_prefix)
tcl_dir = os.path.join(env_prefix, 'tcl8.6')
tk_dir = os.path.join(env_prefix, 'tk8.6')
if os.path.isdir(tcl_dir):
os.environ['TCL_LIBRARY'] = tcl_dir
if os.path.isdir(tk_dir):
os.environ['TK_LIBRARY'] = tk_dir
import tkinter as tk
from tkinter import ttk, messagebox, filedialog
from datetime import datetime, timedelta
import requests
import json
import os
from openpyxl import Workbook
import anthropic
from difflib import SequenceMatcher
from openpyxl.styles import Font, PatternFill, Alignment, Border, Side
import threading
class UdeskLogViewer:
def __init__(self, root):
self.root = root
self.config_file = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'config.json')
self.load_config()
win_cfg = self.config['window']
self.root.title("智能客服节点日志查看器")
self.root.geometry(f"{win_cfg['width']}x{win_cfg['height']}")
self.root.minsize(win_cfg['min_width'], win_cfg['min_height'])
self.cookies = self.config.get('cookies', {})
if not self.cookies:
self.cookies = {
'ut_user_id': 'null',
'ut_global_id': '%22ca426419-74d7-4dc8-bc5c-c866b096b297%22',
'_gcl_au': '1.1.505382516.1778740165',
'_ga': 'GA1.1.968517445.1778741596',
'sensorsdata2015jssdkcross': '%7B%22distinct_id%22%3A%2219e2542d1f1efd-0b7accb742cea4-4c657b58-2073600-19e2542d1f21d61%22%2C%22%24device_id%22%3A%2219e2542d1f1efd-0b7accb742cea4-4c657b58-2073600-19e2542d1f21d61%22%2C%22props%22%3A%7B%7D%7D',
'Qs_lvt_102458': '1778741596%2C1779092047',
'Hm_lvt_85cdbdd6ba7f014cd503e9f1cd5e5ba0': '1778741596,1779092047',
'Qs_pv_102458': '4477521906714103000%2C2213764348932670000%2C1655296003018746400%2C2955048170989231600%2C930806901093578800',
'_ga_WPQK651LHJ': 'GS2.1.s1779095976$o3$g0$t1779095976$j60$l0$h0',
}
token = self.config.get('api_token', '')
self.headers = {
'Accept': '*/*',
'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6',
'Connection': 'keep-alive',
'Referer': f'https://agent.udesk.cn/app/{self.config["app_id"]}/logs',
'Sec-Fetch-Dest': 'empty',
'Sec-Fetch-Mode': 'cors',
'Sec-Fetch-Site': 'same-origin',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36 Edg/148.0.0.0',
'authorization': f'Bearer {token}' if token else '',
'content-type': 'application/json',
'sec-ch-ua': '"Chromium";v="148", "Microsoft Edge";v="148", "Not/A)Brand";v="99"',
'sec-ch-ua-mobile': '?0',
'sec-ch-ua-platform': '"Windows"',
}
self.app_id = self.config['app_id']
self.records = []
self.logs = []
self.audit_file = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'audit_cache.json')
self.review_results = {}
self.test_cases = {}
self.init_llm_client()
self.root.protocol("WM_DELETE_WINDOW", self.on_closing)
self.create_widgets()
def load_config(self):
self.default_config = {
'app_id': 'a46947ea-c1bf-4884-b6ba-9d7be32e18c4',
'api_token': '',
'cookies': {},
'key_node_titles': ['大模型', '单次反思', '大模型二次生成'],
'window': {
'width': 1400,
'height': 800,
'min_width': 1200,
'min_height': 600
},
'default_date_range': {'days_before': 1},
'columns': {
'index': 50,
'chat_id': 120,
'title': 100,
'time': 120,
'query': 150,
'node_output': 150,
'node_audit': 100,
'answer': 150
},
'excel_export': {'default_dir': 'desktop'}
}
if os.path.exists(self.config_file):
try:
with open(self.config_file, 'r', encoding='utf-8') as f:
config = json.load(f)
self.config = {**self.default_config, **config}
except Exception:
self.config = self.default_config.copy()
else:
self.config = self.default_config.copy()
self.KEY_NODE_TITLES = self.config['key_node_titles']
def save_config(self):
try:
with open(self.config_file, 'w', encoding='utf-8') as f:
json.dump(self.config, f, ensure_ascii=False, indent=2)
except Exception as e:
messagebox.showerror("错误", f"保存配置失败: {str(e)}")
def show_config_dialog(self):
dialog = tk.Toplevel(self.root)
dialog.title("系统配置")
dialog.geometry("600x500")
dialog.resizable(True, True)
notebook = ttk.Notebook(dialog)
notebook.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
basic_frame = ttk.Frame(notebook, padding=10)
notebook.add(basic_frame, text="基础配置")
node_frame = ttk.Frame(notebook, padding=10)
notebook.add(node_frame, text="关键节点")
self.config_entries = {}
row = 0
ttk.Label(basic_frame, text="Curl命令(粘贴后点击解析):").grid(row=row, column=0, sticky="w", pady=5, columnspan=2)
row += 1
curl_text = tk.Text(basic_frame, width=80, height=8)
curl_text.grid(row=row, column=0, sticky="w", pady=5, columnspan=2)
self.config_entries['curl'] = curl_text
row += 1
def parse_curl_cmd():
curl_cmd = curl_text.get("1.0", tk.END)
if curl_cmd.strip():
parsed = self.parse_curl(curl_cmd)
if parsed['app_id']:
self.config_entries['app_id'].delete(0, tk.END)
self.config_entries['app_id'].insert(0, parsed['app_id'])
self.config['app_id'] = parsed['app_id']
if parsed['token']:
self.config_entries['api_token'].delete(0, tk.END)
self.config_entries['api_token'].insert(0, parsed['token'])
self.config['api_token'] = parsed['token']
self.headers['authorization'] = f'Bearer {parsed["token"]}'
if parsed['cookies']:
self.config['cookies'] = parsed['cookies']
self.cookies = parsed['cookies']
messagebox.showinfo("成功", "Curl解析完成!配置已立即生效,可直接获取数据。")
ttk.Button(basic_frame, text="解析Curl", command=parse_curl_cmd).grid(row=row, column=0, sticky="w", pady=5)
row += 1
ttk.Label(basic_frame, text="App ID:").grid(row=row, column=0, sticky="w", pady=5)
app_id_entry = ttk.Entry(basic_frame, width=50)
app_id_entry.insert(0, self.config['app_id'])
app_id_entry.grid(row=row, column=1, sticky="w", pady=5)
self.config_entries['app_id'] = app_id_entry
row += 1
ttk.Label(basic_frame, text="API Token:").grid(row=row, column=0, sticky="w", pady=5)
token_entry = ttk.Entry(basic_frame, width=80)
token_entry.insert(0, self.config.get('api_token', ''))
token_entry.grid(row=row, column=1, sticky="w", pady=5)
self.config_entries['api_token'] = token_entry
row += 1
ttk.Label(basic_frame, text="窗口宽度:").grid(row=row, column=0, sticky="w", pady=5)
win_width_entry = ttk.Entry(basic_frame, width=10)
win_width_entry.insert(0, str(self.config['window']['width']))
win_width_entry.grid(row=row, column=1, sticky="w", pady=5)
self.config_entries['window_width'] = win_width_entry
row += 1
ttk.Label(basic_frame, text="窗口高度:").grid(row=row, column=0, sticky="w", pady=5)
win_height_entry = ttk.Entry(basic_frame, width=10)
win_height_entry.insert(0, str(self.config['window']['height']))
win_height_entry.grid(row=row, column=1, sticky="w", pady=5)
self.config_entries['window_height'] = win_height_entry
row += 1
ttk.Label(basic_frame, text="默认日期范围(天):").grid(row=row, column=0, sticky="w", pady=5)
days_entry = ttk.Entry(basic_frame, width=10)
days_entry.insert(0, str(self.config['default_date_range']['days_before']))
days_entry.grid(row=row, column=1, sticky="w", pady=5)
self.config_entries['days_before'] = days_entry
row += 1
ttk.Label(basic_frame, text="序号列宽度:").grid(row=row, column=0, sticky="w", pady=5)
idx_entry = ttk.Entry(basic_frame, width=10)
idx_entry.insert(0, str(self.config['columns']['index']))
idx_entry.grid(row=row, column=1, sticky="w", pady=5)
self.config_entries['col_index'] = idx_entry
row += 1
ttk.Label(basic_frame, text="节点输出列宽度:").grid(row=row, column=0, sticky="w", pady=5)
output_entry = ttk.Entry(basic_frame, width=10)
output_entry.insert(0, str(self.config['columns']['node_output']))
output_entry.grid(row=row, column=1, sticky="w", pady=5)
self.config_entries['col_output'] = output_entry
row += 1
ttk.Label(basic_frame, text="审核列宽度:").grid(row=row, column=0, sticky="w", pady=5)
audit_entry = ttk.Entry(basic_frame, width=10)
audit_entry.insert(0, str(self.config['columns']['node_audit']))
audit_entry.grid(row=row, column=1, sticky="w", pady=5)
self.config_entries['col_audit'] = audit_entry
ttk.Label(node_frame, text="关键节点列表(每行一个节点名称):").pack(anchor="w", pady=5)
self.node_text = tk.Text(node_frame, height=10, width=60)
self.node_text.pack(fill=tk.BOTH, expand=True, pady=5)
for title in self.KEY_NODE_TITLES:
self.node_text.insert(tk.END, title + "\n")
def apply_config():
try:
self.config['app_id'] = self.config_entries['app_id'].get().strip()
self.config['api_token'] = self.config_entries['api_token'].get().strip()
if not self.config.get('cookies'):
self.config['cookies'] = {}
self.config['window']['width'] = int(self.config_entries['window_width'].get())
self.config['window']['height'] = int(self.config_entries['window_height'].get())
self.config['default_date_range']['days_before'] = int(self.config_entries['days_before'].get())
self.config['columns']['index'] = int(self.config_entries['col_index'].get())
self.config['columns']['node_output'] = int(self.config_entries['col_output'].get())
self.config['columns']['node_audit'] = int(self.config_entries['col_audit'].get())
node_text_content = self.node_text.get(1.0, tk.END).strip()
if node_text_content:
self.config['key_node_titles'] = [line.strip() for line in node_text_content.split('\n') if line.strip()]
self.save_config()
messagebox.showinfo("成功", "配置已保存!部分配置需要重启程序才能生效。")
dialog.destroy()
except ValueError as e:
messagebox.showerror("错误", f"配置值格式错误: {str(e)}")
except Exception as e:
messagebox.showerror("错误", f"保存配置失败: {str(e)}")
def reset_config():
self.config = self.default_config.copy()
self.save_config()
messagebox.showinfo("成功", "已恢复默认配置,需要重启程序生效。")
dialog.destroy()
button_frame = ttk.Frame(dialog)
button_frame.pack(fill=tk.X, padx=10, pady=10)
ttk.Button(button_frame, text="应用", command=apply_config).pack(side=tk.RIGHT, padx=5)
ttk.Button(button_frame, text="重置默认", command=reset_config).pack(side=tk.RIGHT, padx=5)
ttk.Button(button_frame, text="关闭", command=dialog.destroy).pack(side=tk.RIGHT, padx=5)
dialog.transient(self.root)
dialog.grab_set()
self.root.wait_window(dialog)
def parse_curl(self, curl_cmd):
result = {
'url': '',
'headers': {},
'cookies': {},
'app_id': '',
'token': ''
}
import re
lines = curl_cmd.strip().split('\n')
for line in lines:
line = line.strip()
if line.startswith("curl '"):
url = line[6:-1].strip()
result['url'] = url
if 'apps/' in url:
parts = url.split('apps/')
if len(parts) > 1:
app_id_part = parts[1].split('/')[0]
result['app_id'] = app_id_part
elif line.startswith("-H '") and "authorization" in line.lower():
auth_match = re.search(r"authorization:\s*Bearer\s+([^\s'\"]+)", line, re.IGNORECASE)
if auth_match:
result['token'] = auth_match.group(1)
elif line.startswith("-H '") and "referer" in line.lower():
referer_match = re.search(r"referer:\s*['\"]([^'\"]+)['\"]", line, re.IGNORECASE)
if referer_match:
referer_value = referer_match.group(1)
if 'apps/' in referer_value:
parts = referer_value.split('apps/')
if len(parts) > 1:
app_id_part = parts[1].split('/')[0]
if app_id_part:
result['app_id'] = app_id_part
elif line.startswith("-b '"):
cookies_str = line[4:-1]
cookie_pairs = cookies_str.split('; ')
for pair in cookie_pairs:
if '=' in pair:
key, value = pair.split('=', 1)
result['cookies'][key.strip()] = value.strip()
return result
def init_llm_client(self):
api_key = "sk-cp-ayedGY_WYs9N0n2hYlAhbYYAYodr7ym7a1y8DgdyCcgx439ONVJzIgZmaR7JmB5bh4iA5ZiLlFy6dOLpHSLtmG8G5WH4EKLDLZXM9gbwAupxZUuqIAUnUEk"
try:
self.llm_client = anthropic.Anthropic(
api_key=api_key,
base_url="https://api.minimaxi.com/anthropic",
timeout=60.0
)
self.llm_available = True
except Exception:
self.llm_available = False
def load_test_cases(self, excel_path):
try:
import pandas as pd
df = pd.read_excel(excel_path)
self.test_cases = {}
for _, row in df.iterrows():
question = str(row['提问问题']).strip()
answer = str(row['答案']).strip()
if question:
self.test_cases[question] = answer
return True
except Exception as e:
messagebox.showerror("错误", f"加载测试用例失败: {str(e)}")
return False
def match_question(self, user_query, threshold=0.7):
best_match = None
best_score = 0
for std_question in self.test_cases.keys():
score = SequenceMatcher(None, user_query, std_question).ratio()
if score > best_score and score >= threshold:
best_score = score
best_match = std_question
return best_match, best_score
def evaluate_consistency(self, generated_answer, standard_answer):
system_prompt = """
你是一个专业的答案一致性评审助手。请按照以下标准评判生成答案与标准答案的一致性:
一致性评分标准:
- 10分:完全一致,内容、逻辑、步骤完全相同
- 8-9分:基本一致,核心内容相同,表述略有差异
- 6-7分:部分一致,核心思路相同,但有遗漏或错误步骤
- 4-5分:不太一致,只有部分内容相关
- 0-3分:不一致,内容无关或错误
请输出JSON格式,包含:
- score: 0-10的整数分数
- confidence: 0-100的整数置信度
- reason: 简短的评审理由(不超过100字)
"""
user_prompt = f"""
【生成答案】
{generated_answer}
【标准答案】
{standard_answer}
请根据上述标准进行评审,输出JSON格式结果。
"""
try:
message = self.llm_client.messages.create(
model="MiniMax-M2.7",
max_tokens=500,
system=system_prompt,
messages=[
{
"role": "user",
"content": [
{"type": "text", "text": user_prompt}
]
}
]
)
response_text = ""
for block in message.content:
if block.type == "text":
response_text += block.text
response_text = response_text.strip()
if response_text.startswith("```json"):
response_text = response_text[7:]
if response_text.endswith("```"):
response_text = response_text[:-3]
response_text = response_text.strip()
result = json.loads(response_text)
return result
except json.JSONDecodeError:
return {"score": 0, "confidence": 50, "reason": f"解析失败"}
except Exception as e:
return {"score": 0, "confidence": 0, "reason": f"API调用失败: {str(e)[:30]}"}
def show_review_dialog(self):
if not self.llm_available:
messagebox.showerror("错误", "大模型服务不可用")
return
dialog = tk.Toplevel(self.root)
dialog.title("大模型评测")
dialog.geometry("500x400")
ttk.Label(dialog, text="选择测试用例文件:").pack(pady=5)
excel_path = tk.StringVar()
ttk.Entry(dialog, textvariable=excel_path, width=50).pack(pady=5)
ttk.Button(dialog, text="浏览", command=lambda: excel_path.set(filedialog.askopenfilename(filetypes=[("Excel文件", "*.xlsx")]))).pack(pady=5)
ttk.Label(dialog, text="选择要评测的节点(可多选):").pack(pady=5)
selected_nodes = []
all_nodes = set()
for rec in self.records:
all_nodes.update(rec.get('key_outputs', {}).keys())
all_nodes.add('最终回答')
node_vars = {}
node_frame = ttk.Frame(dialog)
node_frame.pack(pady=5, fill=tk.X, padx=10)
for node in sorted(all_nodes):
var = tk.BooleanVar()
node_vars[node] = var
ttk.Checkbutton(node_frame, text=node, variable=var).pack(anchor='w')
def start_review():
if not excel_path.get():
messagebox.showwarning("提示", "请选择测试用例文件")
return
selected = [node for node, var in node_vars.items() if var.get()]
if not selected:
messagebox.showwarning("提示", "请至少选择一个节点")
return
if not self.load_test_cases(excel_path.get()):
return
dialog.destroy()
self.run_review(selected)
ttk.Button(dialog, text="开始评测", command=start_review).pack(pady=10)
ttk.Button(dialog, text="取消", command=dialog.destroy).pack(pady=5)
dialog.transient(self.root)
dialog.grab_set()
self.root.wait_window(dialog)
def run_review(self, selected_nodes):
self.update_status("正在进行大模型评测...")
self.review_results = {}
def review_thread():
total = len(self.records)
processed = 0
for idx, rec in enumerate(self.records):
user_query = rec.get('user_query', '').strip()
matched_question, match_score = self.match_question(user_query)
if matched_question and match_score >= 0.7:
standard_answer = self.test_cases[matched_question]
key = f"{rec['chat_log_id']}_{rec['wf_run_id']}"
self.review_results[key] = {
'matched_question': matched_question,
'match_score': match_score,
'standard_answer': standard_answer,
'node_results': {}
}
for node_name in selected_nodes:
if node_name == '最终回答':
generated_answer = rec.get('final_answer', '')
else:
generated_answer = rec.get('key_outputs', {}).get(node_name, '')
if generated_answer.strip():
result = self.evaluate_consistency(generated_answer, standard_answer)
self.review_results[key]['node_results'][node_name] = result
processed += 1
self.update_status(f"评测进度: {processed}/{total}")
self.update_status("大模型评测完成!")
messagebox.showinfo("成功", "大模型评测完成!结果将在导出Excel时包含")
thread = threading.Thread(target=review_thread)
thread.start()
def create_widgets(self):
top_frame = ttk.Frame(self.root, padding="10")
top_frame.pack(fill=tk.X, expand=False)
last_date_range = self.config.get('last_date_range', {})
start_date_val = last_date_range.get('start_date', '')
end_date_val = last_date_range.get('end_date', '')
if not start_date_val or not end_date_val:
days_before = self.config['default_date_range'].get('days_before', 1)
start_date_val = (datetime.now() - timedelta(days=days_before)).strftime("%Y-%m-%d")
end_date_val = datetime.now().strftime("%Y-%m-%d")
ttk.Label(top_frame, text="开始时间:").grid(row=0, column=0, padx=5, pady=5, sticky="w")
self.start_date = ttk.Entry(top_frame, width=12)
self.start_date.insert(0, start_date_val)
self.start_date.grid(row=0, column=1, padx=5, pady=5)
ttk.Label(top_frame, text="结束时间:").grid(row=0, column=2, padx=5, pady=5, sticky="w")
self.end_date = ttk.Entry(top_frame, width=12)
self.end_date.insert(0, end_date_val)
self.end_date.grid(row=0, column=3, padx=5, pady=5)
self.fetch_btn = ttk.Button(top_frame, text="获取数据", command=self.start_fetch)
self.fetch_btn.grid(row=0, column=4, padx=5, pady=5)
self.export_btn = ttk.Button(top_frame, text="导出Excel", command=self.export_excel, state=tk.DISABLED)
self.export_btn.grid(row=0, column=5, padx=5, pady=5)
self.review_btn = ttk.Button(top_frame, text="大模型评测", command=self.show_review_dialog, state=tk.DISABLED)
self.review_btn.grid(row=0, column=6, padx=5, pady=5)
self.config_btn = ttk.Button(top_frame, text="配置", command=self.show_config_dialog)
self.config_btn.grid(row=0, column=7, padx=5, pady=5)
self.status_label = ttk.Label(top_frame, text="状态: 就绪")
self.status_label.grid(row=1, column=0, columnspan=6, padx=5, pady=5, sticky="w")
self.notebook = ttk.Notebook(self.root)
self.notebook.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
self.audit_frame = ttk.Frame(self.notebook)
self.notebook.add(self.audit_frame, text="关键节点审核")
self.log_frame = ttk.Frame(self.notebook)
self.notebook.add(self.log_frame, text="全流程日志")
self.create_audit_tree()
self.create_log_tree()
def create_audit_tree(self):
self.audit_columns = ["index", "chat_id", "title", "time", "query"]
for title in self.KEY_NODE_TITLES:
self.audit_columns.append(f"node_{title}")
self.audit_columns.append(f"audit_{title}")
self.audit_columns.append("answer")
self.audit_tree = ttk.Treeview(self.audit_frame, columns=self.audit_columns, show="headings")
self.audit_tree.heading("index", text="序号")
self.audit_tree.heading("chat_id", text="对话ID")
self.audit_tree.heading("title", text="会话标题")
self.audit_tree.heading("time", text="时间")
self.audit_tree.heading("query", text="用户问题")
for title in self.KEY_NODE_TITLES:
self.audit_tree.heading(f"node_{title}", text=title)
self.audit_tree.heading(f"audit_{title}", text=f"{title}审核")
self.audit_tree.heading("answer", text="最终回答")
col_cfg = self.config['columns']
self.audit_tree.column("index", width=col_cfg['index'], anchor="center")
self.audit_tree.column("chat_id", width=col_cfg['chat_id'])
self.audit_tree.column("title", width=col_cfg['title'])
self.audit_tree.column("time", width=col_cfg['time'])
self.audit_tree.column("query", width=col_cfg['query'])
for title in self.KEY_NODE_TITLES:
self.audit_tree.column(f"node_{title}", width=col_cfg['node_output'])
self.audit_tree.column(f"audit_{title}", width=col_cfg['node_audit'], anchor="center")
self.audit_tree.column("answer", width=col_cfg['answer'])
self.audit_tree.bind("<ButtonRelease-1>", self.on_audit_select)
self.audit_tree.bind("<Double-1>", self.on_double_click_audit)
scrollbar = ttk.Scrollbar(self.audit_frame, orient="vertical", command=self.audit_tree.yview)
self.audit_tree.configure(yscrollcommand=scrollbar.set)
scrollbar_x = ttk.Scrollbar(self.audit_frame, orient="horizontal", command=self.audit_tree.xview)
self.audit_tree.configure(xscrollcommand=scrollbar_x.set)
scrollbar.pack(side="right", fill="y")
scrollbar_x.pack(side="bottom", fill="x")
self.audit_tree.pack(fill=tk.BOTH, expand=True)
self.detail_frame = ttk.Frame(self.audit_frame)
self.detail_frame.pack(fill=tk.X, expand=False, pady=5)
self.detail_text = tk.Text(self.detail_frame, height=8, wrap=tk.WORD)
self.detail_text.pack(fill=tk.BOTH, expand=True)
self.detail_text.insert(tk.END, "点击表格中的行查看详情...\n双击审核列可填写审核结果(可选:正确/错误/需改进)")
self.detail_text.config(state=tk.DISABLED)
def create_log_tree(self):
columns = ("index", "chat_id", "wf_id", "node_idx", "node_type", "node_title", "status", "start_time", "end_time")
self.log_tree = ttk.Treeview(self.log_frame, columns=columns, show="headings")
self.log_tree.heading("index", text="序号")
self.log_tree.heading("chat_id", text="对话ID")
self.log_tree.heading("wf_id", text="工作流ID")
self.log_tree.heading("node_idx", text="节点序号")
self.log_tree.heading("node_type", text="节点类型")
self.log_tree.heading("node_title", text="节点标题")
self.log_tree.heading("status", text="状态")
self.log_tree.heading("start_time", text="开始时间")
self.log_tree.heading("end_time", text="结束时间")
self.log_tree.column("index", width=50, anchor="center")
self.log_tree.column("chat_id", width=120)
self.log_tree.column("wf_id", width=120)
self.log_tree.column("node_idx", width=60, anchor="center")
self.log_tree.column("node_type", width=100)
self.log_tree.column("node_title", width=120)
self.log_tree.column("status", width=80, anchor="center")
self.log_tree.column("start_time", width=130)
self.log_tree.column("end_time", width=130)
self.log_tree.bind("<ButtonRelease-1>", self.on_log_select)
scrollbar = ttk.Scrollbar(self.log_frame, orient="vertical", command=self.log_tree.yview)
self.log_tree.configure(yscrollcommand=scrollbar.set)
scrollbar_x = ttk.Scrollbar(self.log_frame, orient="horizontal", command=self.log_tree.xview)
self.log_tree.configure(xscrollcommand=scrollbar_x.set)
scrollbar.pack(side="right", fill="y")
scrollbar_x.pack(side="bottom", fill="x")
self.log_tree.pack(fill=tk.BOTH, expand=True)
self.log_detail_frame = ttk.Frame(self.log_frame)
self.log_detail_frame.pack(fill=tk.X, expand=False, pady=5)
self.log_input_text = tk.Text(self.log_detail_frame, height=6, wrap=tk.WORD)
self.log_input_text.pack(fill=tk.BOTH, expand=True, side=tk.LEFT, padx=5)
self.log_input_text.insert(tk.END, "输入参数...")
self.log_input_text.config(state=tk.DISABLED)
self.log_output_text = tk.Text(self.log_detail_frame, height=6, wrap=tk.WORD)
self.log_output_text.pack(fill=tk.BOTH, expand=True, side=tk.RIGHT, padx=5)
self.log_output_text.insert(tk.END, "输出结果...")
self.log_output_text.config(state=tk.DISABLED)
def on_closing(self):
self.save_audit_cache()
self.root.destroy()
def save_audit_cache(self):
try:
cache = {}
for rec in self.records:
key = f"{rec['chat_log_id']}_{rec['wf_run_id']}"
audit_data = {}
for field_key, audit_val in rec['key_audit_fields'].items():
if audit_val:
audit_data[field_key] = audit_val
if audit_data:
cache[key] = audit_data
existing = {}
if os.path.exists(self.audit_file):
with open(self.audit_file, 'r', encoding='utf-8') as f:
existing = json.load(f)
existing.update(cache)
with open(self.audit_file, 'w', encoding='utf-8') as f:
json.dump(existing, f, ensure_ascii=False, indent=2)
except Exception:
pass
def load_audit_cache(self):
if not os.path.exists(self.audit_file):
return
try:
with open(self.audit_file, 'r', encoding='utf-8') as f:
cache = json.load(f)
for rec in self.records:
key = f"{rec['chat_log_id']}_{rec['wf_run_id']}"
if key in cache:
for field_key, audit_val in cache[key].items():
if field_key in rec['key_audit_fields']:
rec['key_audit_fields'][field_key] = audit_val
except Exception:
pass
def update_status(self, text):
self.status_label.config(text=f"状态: {text}")
self.root.update_idletasks()
def fetch_nodes(self, wf_run_id):
if not wf_run_id:
return []
try:
response = requests.get(
f'https://agent.udesk.cn/api/backend/new/apps/{self.app_id}/wfRuns/{wf_run_id}/nodeExecutions',
cookies=self.cookies,
headers=self.headers,
)
data = response.json()
return data.get('data', {}).get('list', [])
except Exception as e:
return []
def fetch_all_messages(self, chat_log_id):
all_messages = []
fid = None
page_size = 10
while True:
try:
c_params = {'id': chat_log_id, 'page_size': str(page_size)}
if fid:
c_params['fid'] = fid
c_resp = requests.get(
f'https://agent.udesk.cn/api/backend/apps/{self.app_id}/chatMessage',
params=c_params,
cookies=self.cookies,
headers=self.headers,
timeout=30
)
data = c_resp.json()
messages = data.get('data', {}).get('list', [])
if not messages:
break
all_messages.extend(messages)
if len(messages) < page_size:
break
fid = messages[0].get('id')
if not fid:
break
except Exception as e:
break
return all_messages
def start_fetch(self):
if self.fetch_btn["state"] == tk.DISABLED:
return
start_date = self.start_date.get()
end_date = self.end_date.get()
try:
datetime.strptime(start_date, "%Y-%m-%d")
datetime.strptime(end_date, "%Y-%m-%d")
except ValueError:
messagebox.showerror("错误", "日期格式不正确,请使用 YYYY-MM-DD 格式")
return
self.config['last_date_range'] = {
'start_date': start_date,
'end_date': end_date
}
self.save_config()
self.fetch_btn.config(state=tk.DISABLED)
self.export_btn.config(state=tk.DISABLED)
self.update_status("正在获取数据...")
thread = threading.Thread(target=self.fetch_data, args=(start_date, end_date))
thread.start()
def fetch_data(self, start_date, end_date):
try:
self.records = []
self.logs = []
params = {
'page_number': '1',
'page_size': '10',
'start_time': f'{start_date} 00:00',
'end_time': f'{end_date} 23:59',
'order_by': '-created_at',
'status': 'all',
}
self.update_status("正在连接服务器...")
self.update_status(f"App ID: {self.app_id}")
self.update_status(f"Token存在: {'' if self.headers.get('authorization', '').startswith('Bearer ') else ''}")
self.update_status(f"Cookies数量: {len(self.cookies)}")
response = requests.get(
f'https://agent.udesk.cn/api/backend/apps/{self.app_id}/log/chat/pages',
params=params,
cookies=self.cookies,
headers=self.headers,
timeout=30
)
self.update_status(f"响应状态码: {response.status_code}")
if response.status_code != 200:
try:
error_data = response.json()
error_msg = error_data.get('msg', error_data.get('message', '未知错误'))
except:
error_msg = response.text[:200]
self.update_status(f"请求失败: {response.status_code} - {error_msg}")
raise Exception(f"HTTP错误: {response.status_code} - {error_msg}")
data = response.json()
total = data.get('data', {}).get('total', 0)
total_page = max(total // 10 + (1 if total % 10 else 0), 1)
self.update_status(f"{total} 条对话,{total_page}")
chart_list = []
for page in range(1, total_page + 1):
params['page_number'] = str(page)
response = requests.get(
f'https://agent.udesk.cn/api/backend/apps/{self.app_id}/log/chat/pages',
params=params,
cookies=self.cookies,
headers=self.headers,
)
page_data = response.json().get('data', {}).get('list', [])
chart_list.extend(page_data)
self.update_status(f"获取第 {page}/{total_page} 页...")
log_idx = 0
for idx, chart in enumerate(chart_list):
chat_log_id = chart.get('chat_log_id', '')
chat_title = chart.get('title') or chart.get('chat_log_name') or ''
created_time = str(chart.get('created_time', ''))
messages = self.fetch_all_messages(chat_log_id)
for msg in messages:
wf_run_id = msg.get('workflow_run_id', '')
if not wf_run_id:
continue
nodes = self.fetch_nodes(wf_run_id)
user_query = ''
for n in nodes:
if n.get('node_type') == 'start' and n.get('outputs', {}).get('sys.query'):
user_query = n['outputs']['sys.query']
break
key_outputs = {}
key_audit_fields = {}
final_answer = ''
title_counter = {}
for n in nodes:
title = n.get('title', '')
node_output = (n.get('outputs') or {}).get('text', '')
if title in self.KEY_NODE_TITLES:
if title in title_counter:
title_counter[title] += 1
unique_title = f"{title}-{title_counter[title]}"
else:
title_counter[title] = 1
unique_title = title
key_outputs[unique_title] = node_output
key_audit_fields[unique_title] = ''
if n.get('node_type') == 'answer':
final_answer = (n.get('outputs') or {}).get('answer', '')
self.records.append({
'chat_log_id': chat_log_id,
'chat_title': chat_title,
'created_time': created_time,
'user_query': user_query,
'wf_run_id': wf_run_id,
'nodes': nodes,
'key_outputs': key_outputs,
'key_audit_fields': key_audit_fields,
'final_answer': final_answer,
})
for node in nodes:
log_idx += 1
created_at = node.get('created_at') or node.get('started_at', 0)
finished_at = node.get('finished_at', 0)
self.logs.append({
'idx': log_idx,
'chat_log_id': chat_log_id,
'user_query': user_query,
'wf_run_id': wf_run_id,
'node_index': node.get('index', ''),
'node_type': node.get('node_type', ''),
'node_title': node.get('title', ''),
'status': node.get('status', ''),
'start_time': self.ts_to_datetime(created_at),
'end_time': self.ts_to_datetime(finished_at),
'inputs': node.get('inputs'),
'outputs': node.get('outputs'),
'error': node.get('error', ''),
})
self.update_status(f"处理第 {idx+1}/{len(chart_list)} 条对话...")
self.load_audit_cache()
self.root.after(0, self.update_trees)
self.update_status(f"完成!共 {len(self.records)} 条审核记录,{len(self.logs)} 条日志")
self.export_btn.config(state=tk.NORMAL)
self.review_btn.config(state=tk.NORMAL)
except Exception as e:
self.update_status(f"获取失败: {str(e)}")
messagebox.showerror("错误", f"获取数据失败: {str(e)}")
finally:
self.fetch_btn.config(state=tk.NORMAL)
def ts_to_datetime(self, ts):
try:
return datetime.fromtimestamp(int(ts)).strftime('%Y-%m-%d %H:%M:%S')
except Exception:
return ''
def update_trees(self):
for item in self.audit_tree.get_children():
self.audit_tree.delete(item)
for idx, rec in enumerate(self.records):
values = [
idx+1,
rec['chat_log_id'],
rec['chat_title'],
rec['created_time'],
self.truncate_text(rec['user_query'], 30),
]
for title in self.KEY_NODE_TITLES:
output_text = ''
audit_text = ''
if title in rec['key_outputs']:
output_text = self.truncate_text(rec['key_outputs'].get(title, ''), 30)
audit_text = rec['key_audit_fields'].get(title, '')
elif f"{title}-1" in rec['key_outputs']:
output_text = self.truncate_text(rec['key_outputs'].get(f"{title}-1", ''), 30)
audit_text = rec['key_audit_fields'].get(f"{title}-1", '')
values.append(output_text)
values.append(audit_text)
values.append(self.truncate_text(rec['final_answer'], 30))
self.audit_tree.insert("", "end", values=values)
for item in self.log_tree.get_children():
self.log_tree.delete(item)
for log in self.logs:
tag = 'failed' if log['status'] == 'failed' else ''
self.log_tree.insert("", "end", values=(
log['idx'],
log['chat_log_id'],
log['wf_run_id'],
log['node_index'],
log['node_type'],
log['node_title'],
log['status'],
log['start_time'],
log['end_time'],
), tags=(tag,))
self.log_tree.tag_configure('failed', foreground='red')
def truncate_text(self, text, max_len):
if text and len(text) > max_len:
return text[:max_len] + "..."
return text
def on_audit_select(self, event):
item = self.audit_tree.selection()
if not item:
return
idx = int(self.audit_tree.item(item[0])['values'][0]) - 1
if idx < 0 or idx >= len(self.records):
return
rec = self.records[idx]
detail = f"""对话ID: {rec['chat_log_id']}
会话标题: {rec['chat_title']}
时间: {rec['created_time']}
【用户问题】
{rec['user_query']}
"""
for title in sorted(rec['key_outputs'].keys()):
detail += f"{title}\n{rec['key_outputs'].get(title, '')}\n\n"
detail += f"【最终回答】\n{rec['final_answer']}\n\n【审核状态】\n"
for title in sorted(rec['key_audit_fields'].keys()):
audit_val = rec['key_audit_fields'].get(title, '')
detail += f"{title}: {audit_val if audit_val else '未审核'}\n"
self.detail_text.config(state=tk.NORMAL)
self.detail_text.delete(1.0, tk.END)
self.detail_text.insert(tk.END, detail)
self.detail_text.config(state=tk.DISABLED)
def on_double_click_audit(self, event):
item = self.audit_tree.selection()
if not item:
return
idx = int(self.audit_tree.item(item[0])['values'][0]) - 1
if idx < 0 or idx >= len(self.records):
return
col_num = int(self.audit_tree.identify_column(event.x).replace('#', ''))
for i, title in enumerate(self.KEY_NODE_TITLES):
audit_col_idx = 6 + i * 2
if col_num == audit_col_idx:
for key in sorted(self.records[idx]['key_audit_fields'].keys()):
if key.startswith(title):
self.show_audit_dialog(idx, key, f"{key}审核")
return
def show_audit_dialog(self, idx, field_key, title):
dialog = tk.Toplevel(self.root)
dialog.title(title)
dialog.geometry("400x250")
current_value = self.records[idx]['key_audit_fields'].get(field_key, '')
ttk.Label(dialog, text=f"请选择{title}结果:", padding=10).pack()
var = tk.StringVar(value=current_value)
options = ["正确", "错误", "需改进", ""]
for opt in options:
ttk.Radiobutton(dialog, text=opt, variable=var, value=opt).pack(anchor='w', padx=20, pady=2)
ttk.Label(dialog, text="备注(可选):", padding=10).pack()
note_entry = ttk.Entry(dialog, width=40)
note_entry.pack(padx=10)
def save():
self.records[idx]['key_audit_fields'][field_key] = var.get()
self.update_trees()
self.save_audit_cache()
dialog.destroy()
self.update_status(f"已保存:{title} = {var.get()}")
ttk.Button(dialog, text="保存", command=save).pack(pady=10)
ttk.Button(dialog, text="取消", command=dialog.destroy).pack(pady=5)
dialog.transient(self.root)
dialog.grab_set()
self.root.wait_window(dialog)
def on_log_select(self, event):
item = self.log_tree.selection()
if not item:
return
idx = int(self.log_tree.item(item[0])['values'][0]) - 1
if idx < 0 or idx >= len(self.logs):
return
log = self.logs[idx]
self.log_input_text.config(state=tk.NORMAL)
self.log_input_text.delete(1.0, tk.END)
self.log_input_text.insert(tk.END, json.dumps(log['inputs'], ensure_ascii=False, indent=2))
self.log_input_text.config(state=tk.DISABLED)
self.log_output_text.config(state=tk.NORMAL)
self.log_output_text.delete(1.0, tk.END)
self.log_output_text.insert(tk.END, json.dumps(log['outputs'], ensure_ascii=False, indent=2))
self.log_output_text.config(state=tk.DISABLED)
def export_excel(self):
if not self.records:
messagebox.showwarning("提示", "没有数据可导出")
return
try:
desktop = os.path.expanduser("~\\Desktop")
default_dir = desktop if os.path.exists(desktop) else os.path.expanduser("~")
except:
default_dir = os.path.expanduser("~")
file_path = filedialog.asksaveasfilename(
defaultextension=".xlsx",
filetypes=[("Excel文件", "*.xlsx")],
initialdir=default_dir,
initialfile=f"智能客服节点审核_{datetime.now().strftime('%Y%m%d_%H%M%S')}.xlsx"
)
if not file_path:
return
try:
self.update_status("正在生成Excel...")
wb = Workbook()
ws1 = wb.active
ws1.title = '关键节点审核'
header_font = Font(name='微软雅黑', bold=True, size=11, color='FFFFFF')
header_fill = PatternFill(start_color='4472C4', end_color='4472C4', fill_type='solid')
key_fill = PatternFill(start_color='FFF2CC', end_color='FFF2CC', fill_type='solid')
data_font = Font(name='微软雅黑', size=10)
thin_border = Border(left=Side(style='thin'), right=Side(style='thin'), top=Side(style='thin'), bottom=Side(style='thin'))
wrap_alignment = Alignment(horizontal='left', vertical='top', wrap_text=True)
center_alignment = Alignment(horizontal='center', vertical='center', wrap_text=True)
sheet1_headers = ['序号', '对话ID', '会话标题', '对话时间', '用户问题']
for title in self.KEY_NODE_TITLES:
sheet1_headers.append(f'{title}_输出')
sheet1_headers.append(f'{title}审核')
sheet1_headers.extend(['最终回答', '备注'])
key_cols = []
for col_idx, header in enumerate(sheet1_headers, 1):
cell = ws1.cell(row=1, column=col_idx, value=header)
cell.font = header_font
cell.fill = header_fill
cell.alignment = center_alignment
cell.border = thin_border
if col_idx >= 6:
key_cols.append(col_idx)
for row_idx, rec in enumerate(self.records):
r = row_idx + 2
values = [
row_idx + 1,
rec['chat_log_id'],
rec['chat_title'],
rec['created_time'],
rec['user_query'],
]
for title in self.KEY_NODE_TITLES:
output_text = ''
audit_text = ''
if title in rec['key_outputs']:
output_text = rec['key_outputs'].get(title, '')
audit_text = rec['key_audit_fields'].get(title, '')
elif f"{title}-1" in rec['key_outputs']:
output_text = rec['key_outputs'].get(f"{title}-1", '')
audit_text = rec['key_audit_fields'].get(f"{title}-1", '')
values.append(output_text)
values.append(audit_text)
values.extend([rec['final_answer'], ''])
for col_idx, val in enumerate(values, 1):
cell = ws1.cell(row=r, column=col_idx, value=val)
cell.font = data_font
cell.alignment = wrap_alignment
cell.border = thin_border
if col_idx in key_cols:
cell.fill = key_fill
ws1.column_dimensions['A'].width = 6
ws1.column_dimensions['B'].width = 38
ws1.column_dimensions['C'].width = 20
ws1.column_dimensions['D'].width = 20
ws1.column_dimensions['E'].width = 40
col_letter = 'F'
for _ in self.KEY_NODE_TITLES:
ws1.column_dimensions[col_letter].width = 50
col_letter = chr(ord(col_letter) + 1)
ws1.column_dimensions[col_letter].width = 12
col_letter = chr(ord(col_letter) + 1)
ws1.column_dimensions[col_letter].width = 50
col_letter = chr(ord(col_letter) + 1)
ws1.column_dimensions[col_letter].width = 20
ws1.freeze_panes = 'E2'
ws1.auto_filter.ref = f'A1:{col_letter}{len(self.records) + 1}'
ws2 = wb.create_sheet('全流程日志明细')
sheet2_headers = ['序号', '对话ID', '用户问题', '工作流运行ID', '节点序号', '节点类型', '节点标题', '节点状态', '执行开始时间', '执行结束时间', '节点输入(JSON)', '节点输出(JSON)', '错误信息']
for col_idx, header in enumerate(sheet2_headers, 1):
cell = ws2.cell(row=1, column=col_idx, value=header)
cell.font = header_font
cell.fill = PatternFill(start_color='548235', end_color='548235', fill_type='solid')
cell.alignment = center_alignment
cell.border = thin_border
for row_idx, log in enumerate(self.logs):
r = row_idx + 2
values = [
log['idx'],
log['chat_log_id'],
log['user_query'],
log['wf_run_id'],
log['node_index'],
log['node_type'],
log['node_title'],
log['status'],
log['start_time'],
log['end_time'],
json.dumps(log['inputs'], ensure_ascii=False),
json.dumps(log['outputs'], ensure_ascii=False),
log['error'],
]
for col_idx, val in enumerate(values, 1):
cell = ws2.cell(row=r, column=col_idx, value=val)
cell.font = data_font
cell.alignment = wrap_alignment
cell.border = thin_border
if log['status'] == 'failed':
cell.fill = PatternFill(start_color='FFC7CE', end_color='FFC7CE', fill_type='solid')
ws2.column_dimensions['A'].width = 6
ws2.column_dimensions['B'].width = 38
ws2.column_dimensions['C'].width = 40
ws2.column_dimensions['D'].width = 38
ws2.column_dimensions['E'].width = 8
ws2.column_dimensions['F'].width = 22
ws2.column_dimensions['G'].width = 20
ws2.column_dimensions['H'].width = 12
ws2.column_dimensions['I'].width = 20
ws2.column_dimensions['J'].width = 20
ws2.column_dimensions['K'].width = 60
ws2.column_dimensions['L'].width = 60
ws2.column_dimensions['M'].width = 30
ws2.freeze_panes = 'F2'
ws2.auto_filter.ref = f'A1:M{len(self.logs) + 1}'
if self.review_results:
ws3 = wb.create_sheet('大模型评测结果')
sheet3_headers = ['序号', '对话ID', '用户问题', '匹配问题', '匹配度', '标准回答']
reviewed_nodes = set()
for key, result in self.review_results.items():
reviewed_nodes.update(result.get('node_results', {}).keys())
for node in sorted(reviewed_nodes):
sheet3_headers.append(f'{node}_一致性评分')
sheet3_headers.append(f'{node}_置信度(%)')
sheet3_headers.append(f'{node}_评审理由')
for col_idx, header in enumerate(sheet3_headers, 1):
cell = ws3.cell(row=1, column=col_idx, value=header)
cell.font = header_font
cell.fill = PatternFill(start_color='7030A0', end_color='7030A0', fill_type='solid')
cell.alignment = center_alignment
cell.border = thin_border
row_idx = 0
for rec in self.records:
key = f"{rec['chat_log_id']}_{rec['wf_run_id']}"
if key in self.review_results:
row_idx += 1
r = row_idx + 2
result = self.review_results[key]
values = [
row_idx,
rec['chat_log_id'],
rec['user_query'],
result.get('matched_question', ''),
f"{result.get('match_score', 0):.2f}",
result.get('standard_answer', '')[:200],
]
for node in sorted(reviewed_nodes):
node_result = result.get('node_results', {}).get(node, {})
values.append(node_result.get('score', ''))
values.append(node_result.get('confidence', ''))
values.append(node_result.get('reason', '')[:100])
for col_idx, val in enumerate(values, 1):
cell = ws3.cell(row=r, column=col_idx, value=val)
cell.font = data_font
cell.alignment = wrap_alignment
cell.border = thin_border
ws3.column_dimensions['A'].width = 6
ws3.column_dimensions['B'].width = 38
ws3.column_dimensions['C'].width = 40
ws3.column_dimensions['D'].width = 40
ws3.column_dimensions['E'].width = 12
ws3.column_dimensions['F'].width = 50
col_letter = 'G'
for _ in reviewed_nodes:
ws3.column_dimensions[col_letter].width = 15
col_letter = chr(ord(col_letter) + 1)
ws3.column_dimensions[col_letter].width = 12
col_letter = chr(ord(col_letter) + 1)
ws3.column_dimensions[col_letter].width = 40
col_letter = chr(ord(col_letter) + 1)
ws3.freeze_panes = 'F2'
wb.save(file_path)
self.update_status("Excel导出完成")
messagebox.showinfo("成功", f"Excel已导出到:\n{file_path}")
except PermissionError:
messagebox.showerror("错误", f"无法保存到该位置!请检查权限或选择其他路径。")
self.update_status("保存失败:权限不足")
except Exception as e:
messagebox.showerror("错误", f"导出Excel时发生错误:{str(e)}")
self.update_status("保存失败")
if __name__ == "__main__":
root = tk.Tk()
app = UdeskLogViewer(root)
root.mainloop()