refactor: migrate workflow to v2 core and archive legacy modules

This commit is contained in:
2026-03-04 21:52:24 +08:00
parent e1ce17f2aa
commit fa61b11b02
156 changed files with 1781 additions and 2066 deletions

View File

@@ -0,0 +1,362 @@
"""
聊天记录查看器
用法:
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])