Files
tw/scripts/chat_log_viewer.py
jimi c39840fe15 feat: 添加 AI Agent 对话测试工具 + 代码优化
主要变更:

- 新增 tests/test_ai_chat.py: AI Agent 对话测试工具

- 优化 core/pydantic_ai_agent.py 和 db/chat_log_db.py

- 清理归档文件,更新文档

Made-with: Cursor
2026-02-28 16:19:35 +08:00

363 lines
12 KiB
Python
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
聊天记录查看器
用法:
python scripts/chat_log_viewer.py # 列出所有客户
python scripts/chat_log_viewer.py <客户ID> # 查看某客户全部对话
python scripts/chat_log_viewer.py -s <关键词> # 全局搜索
python scripts/chat_log_viewer.py -t <客户ID> # 只看今天
python scripts/chat_log_viewer.py -l # 实时监听最新消息10条/刷新)
"""
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
import time
import os
from datetime import datetime
# 强制 UTF-8 输出Windows 终端需要)
if sys.stdout.encoding and sys.stdout.encoding.lower() != "utf-8":
import io
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8", errors="replace")
sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding="utf-8", errors="replace")
from db import chat_log_db as db
# ========== ANSI 颜色 ==========
try:
import ctypes
ctypes.windll.kernel32.SetConsoleMode(ctypes.windll.kernel32.GetStdHandle(-11), 7)
except Exception:
pass
RESET = "\033[0m"
BOLD = "\033[1m"
DIM = "\033[2m"
GREEN = "\033[32m"
CYAN = "\033[36m"
YELLOW = "\033[33m"
BLUE = "\033[34m"
MAGENTA= "\033[35m"
RED = "\033[31m"
WHITE = "\033[97m"
BG_DARK= "\033[48;5;236m"
def clear():
os.system("cls" if os.name == "nt" else "clear")
def header(text: str):
width = 60
print(f"\n{BOLD}{CYAN}{'' * width}{RESET}")
print(f"{BOLD}{CYAN} {text}{RESET}")
print(f"{BOLD}{CYAN}{'' * width}{RESET}\n")
def fmt_time(ts: str) -> str:
"""缩短时间戳显示"""
today = datetime.now().strftime("%Y-%m-%d")
if ts.startswith(today):
return ts[11:16] # 只显示 HH:MM
return ts[:16]
def platform_badge(platform: str) -> str:
badges = {
"AliWorkbench": f"{YELLOW}[淘宝]{RESET}",
"taobao": f"{YELLOW}[淘宝]{RESET}",
"pinduoduo": f"{RED}[拼多多]{RESET}",
"jd": f"{RED}[京东]{RESET}",
"wechat": f"{GREEN}[微信]{RESET}",
"email": f"{BLUE}[邮件]{RESET}",
}
return badges.get(platform, f"{DIM}[{platform}]{RESET}" if platform else "")
def print_bubble(direction: str, message: str, ts: str):
"""打印聊天气泡"""
time_str = fmt_time(ts)
lines = []
if "#*#" in (message or ""):
parts = [p.strip() for p in message.split("#*#") if p.strip()]
if parts:
lines = parts
if not lines:
lines = (message or "").split("\n")
if direction == "in": # 客户来消息 → 左对齐
print(f" {DIM}{time_str}{RESET} {WHITE}买家{RESET}")
for line in lines:
print(f" {BG_DARK} {line} {RESET}")
else: # 客服回复 → 右对齐(缩进)
print(f" {DIM}{time_str}{RESET} {GREEN}客服{RESET}")
for line in lines:
print(f" {GREEN}> {line}{RESET}")
print()
def cmd_list_customers():
"""列出所有客户"""
customers = db.get_customers(limit=100)
if not customers:
print(f"{YELLOW}暂无聊天记录。{RESET}")
return
header(f"客户列表 共 {len(customers)}")
print(f" {'#':<4} {'客户ID':<24} {'姓名':<12} {'平台':<10} {'消息数':>6} {'最后活跃'}")
print(f" {''*4} {''*24} {''*12} {''*10} {''*6} {''*16}")
for i, c in enumerate(customers, 1):
badge = platform_badge(c.get("platform", ""))
name = (c.get("customer_name") or "")[:10]
cid = c["customer_id"]
total = c["total_msgs"]
last = c.get("last_time", "")[:16]
print(f" {i:<4} {CYAN}{cid:<24}{RESET} {name:<12} {badge:<18} {total:>6}{DIM}{last}{RESET}")
print(f"\n{DIM}用法python chat_log_viewer.py <客户ID>{RESET}\n")
def cmd_show_conversation(customer_id: str, today_only: bool = False):
"""显示某客户对话"""
if today_only:
messages = db.get_conversation_today(customer_id)
title = f"今日对话 {customer_id}"
else:
messages = db.get_conversation(customer_id, limit=300)
title = f"对话记录 {customer_id}"
if not messages:
print(f"{YELLOW}该客户暂无记录:{customer_id}{RESET}")
return
header(f"{title} {len(messages)} 条)")
last_date = ""
for m in messages:
ts = m.get("timestamp", "")
date = ts[:10]
if date != last_date:
print(f" {DIM}{''*20} {date} {''*20}{RESET}")
last_date = date
print_bubble(m["direction"], m["message"], ts)
print(f"{DIM} ── 以上共 {len(messages)} 条 ──{RESET}\n")
def cmd_search(keyword: str, customer_id: str = None):
"""搜索关键词"""
results = db.search_messages(keyword, customer_id=customer_id, limit=50)
title = f"搜索 [{keyword}]"
if customer_id:
title += f" 客户:{customer_id}"
header(f"{title}{len(results)}")
if not results:
print(f"{YELLOW}未找到包含 [{keyword}] 的消息。{RESET}")
return
last_cid = ""
for r in results:
cid = r["customer_id"]
if cid != last_cid:
print(f" {CYAN}{cid}{RESET} {r.get('customer_name','')}")
last_cid = cid
direction = "买家" if r["direction"] == "in" else "客服"
color = WHITE if r["direction"] == "in" else GREEN
# 高亮关键词
msg = r["message"].replace(keyword, f"{RED}{BOLD}{keyword}{RESET}{color}")
print(f" {DIM}{r['timestamp'][:16]}{RESET} {color}[{direction}] {msg}{RESET}")
print()
def cmd_live(refresh: int = 3):
"""实时监听最新消息"""
header("实时消息监听 Ctrl+C 退出")
seen_ids = set()
try:
while True:
rows = db.get_latest_messages(20)
new_rows = [r for r in rows if r["id"] not in seen_ids]
if new_rows:
new_rows.reverse()
for r in new_rows:
seen_ids.add(r["id"])
cid = r["customer_id"]
name = r.get("customer_name") or ""
label = f"{CYAN}{cid}{RESET}" + (f" {DIM}({name}){RESET}" if name else "")
print(f"\n{label}")
print_bubble(r["direction"], r["message"], r["timestamp"])
else:
print(f"\r {DIM}等待新消息... {datetime.now().strftime('%H:%M:%S')}{RESET}", end="", flush=True)
time.sleep(refresh)
except KeyboardInterrupt:
print(f"\n{DIM}已退出监听。{RESET}")
def _extract_urls(msg: str) -> list:
if not msg:
return []
parts = [p.strip() for p in msg.split("#*#") if p.strip()]
urls = []
for p in parts:
if p.startswith("http://") or p.startswith("https://"):
urls.append(p)
if not urls and ("http://" in msg or "https://" in msg):
import re as _re
tokens = _re.findall(r'(https?://\S+)', msg)
for t in tokens:
tl = t.lower()
if any(ext in tl for ext in [".jpg", ".jpeg", ".png", ".gif", ".webp", ".bmp"]):
urls.append(t)
return urls
def _msg_refers_images(msg: str) -> bool:
if not msg:
return False
refs = ("图一", "图二", "第一张", "第二张", "这张", "那张", "上面那张", "下面那张", "刚才那张", "上一张", "下一张")
return any(r in msg for r in refs)
def _parse_ts(ts: str):
try:
from datetime import datetime as _dt
return _dt.fromisoformat(ts.replace("Z",""))
except Exception:
return None
def analyze_conversation(messages: list) -> list:
issues = []
n = len(messages)
for i, m in enumerate(messages):
msg = m.get("message") or ""
dir = m.get("direction")
ts = _parse_ts(m.get("timestamp",""))
# 图片后未及时回复
if dir == "in" and _extract_urls(msg):
replied = False
delay_ok = True
for j in range(i+1, min(i+6, n)):
mj = messages[j]
if mj.get("direction") == "out":
replied = True
tsj = _parse_ts(mj.get("timestamp",""))
if ts and tsj and (tsj - ts).total_seconds() > 180:
delay_ok = False
break
if not replied:
issues.append("图片消息后未回复")
elif not delay_ok:
issues.append("图片消息后回复延迟超过3分钟")
# 引用图片但找不到历史图片
if dir == "in" and _msg_refers_images(msg):
has_prev_img = False
for k in range(max(0, i-10), i):
if messages[k].get("direction") == "in" and _extract_urls(messages[k].get("message","")):
has_prev_img = True
break
if not has_prev_img:
issues.append("引用图片但历史中未找到对应图片")
# 订单后未确认/引导
if dir == "in" and ("买家已付款" in msg or "[系统订单信息]" in msg):
confirmed = False
for j in range(i+1, min(i+6, n)):
if messages[j].get("direction") == "out":
confirmed = True
break
if not confirmed:
issues.append("订单消息后未进行确认或引导付款")
# 合成需求未报价格
if dir == "in" and any(k in msg for k in ("抓到", "放到", "合成", "融合", "嵌到", "替换", "P到", "抠出来放到")):
priced = False
for j in range(i+1, min(i+6, n)):
mj = messages[j]
if mj.get("direction") == "out":
rm = mj.get("message","")
if "" in rm:
priced = True
break
if not priced:
issues.append("客户提出合成需求但未给出价格")
# 去重
dedup = []
seen = set()
for it in issues:
if it not in seen:
seen.add(it)
dedup.append(it)
return dedup
def cmd_analyze_all():
customers = db.get_customers(limit=200)
if not customers:
print(f"{YELLOW}暂无聊天记录。{RESET}")
return
header("聊天记录上下文分析")
total_issues = 0
for c in customers:
cid = c["customer_id"]
msgs = db.get_conversation(cid, limit=500)
issues = analyze_conversation(msgs)
if issues:
total_issues += len(issues)
print(f"{CYAN}{cid}{RESET} {c.get('customer_name','')}")
for s in issues:
print(f" - {RED}{s}{RESET}")
print()
if total_issues == 0:
print(f"{GREEN}未发现明显异常。{RESET}")
else:
print(f"{YELLOW}共发现 {total_issues} 项问题(按客户汇总)。{RESET}")
def print_help():
print(f"""
{BOLD}聊天记录查看器{RESET}
{CYAN}python chat_log_viewer.py{RESET} 列出所有客户
{CYAN}python chat_log_viewer.py <客户ID>{RESET} 查看该客户全部对话
{CYAN}python chat_log_viewer.py -t <客户ID>{RESET} 只看今天的对话
{CYAN}python chat_log_viewer.py -s <关键词>{RESET} 全局搜索
{CYAN}python chat_log_viewer.py -l{RESET} 实时监听新消息
{CYAN}python chat_log_viewer.py -a{RESET} 分析上下文,输出异常项
{CYAN}python chat_log_viewer.py -h{RESET} 显示帮助
""")
if __name__ == "__main__":
args = sys.argv[1:]
if not args:
cmd_list_customers()
elif args[0] in ("-h", "--help"):
print_help()
elif args[0] == "-s":
keyword = args[1] if len(args) > 1 else ""
if not keyword:
print(f"{RED}请提供搜索关键词python chat_log_viewer.py -s <关键词>{RESET}")
else:
cmd_search(keyword)
elif args[0] == "-t":
cid = args[1] if len(args) > 1 else ""
if not cid:
print(f"{RED}请提供客户IDpython chat_log_viewer.py -t <客户ID>{RESET}")
else:
cmd_show_conversation(cid, today_only=True)
elif args[0] == "-l":
cmd_live()
elif args[0] == "-a":
cmd_analyze_all()
else:
cmd_show_conversation(args[0])