""" 聊天记录查看器 用法: 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}请提供客户ID:python 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])