init
This commit is contained in:
371
scripts/chat_log_viewer.py
Normal file
371
scripts/chat_log_viewer.py
Normal file
@@ -0,0 +1,371 @@
|
||||
"""
|
||||
聊天记录查看器
|
||||
用法:
|
||||
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:
|
||||
import sqlite3
|
||||
conn = sqlite3.connect(db._DB_PATH)
|
||||
conn.row_factory = sqlite3.Row
|
||||
rows = conn.execute("""
|
||||
SELECT id, customer_id, customer_name, direction, message, timestamp
|
||||
FROM chat_logs
|
||||
ORDER BY id DESC LIMIT 20
|
||||
""").fetchall()
|
||||
conn.close()
|
||||
|
||||
new_rows = [dict(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])
|
||||
Reference in New Issue
Block a user