# -*- coding: utf-8 -*- """ 每日聊天汇总定时任务 - 每天 23:50 自动统计当日各店铺数据 - 用 AI 生成自然语言摘要 - 发送到企业微信 Webhook + QQ 邮件 """ import asyncio import os import json from datetime import datetime, date, timedelta from typing import Optional import httpx from dotenv import load_dotenv load_dotenv() WECHAT_WEBHOOK = os.getenv("WECHAT_WEBHOOK", "") SUMMARY_EMAIL = os.getenv("SUMMARY_EMAIL", "") # 收摘要的邮箱 SEND_HOUR = int(os.getenv("SUMMARY_HOUR", "23")) SEND_MINUTE = int(os.getenv("SUMMARY_MINUTE", "50")) # ────────────────────────────────────────── # 统计数据整理 # ────────────────────────────────────────── def _build_stats_text(target_date: str = "") -> str: """整理今日数据,返回给 AI 的原始统计文本""" from db import chat_log_db as db from db.deal_outcome_db import get_daily_summary if not target_date: target_date = datetime.now().strftime("%Y-%m-%d") stats = db.get_daily_stats(target_date) convs = db.get_daily_conversations(target_date) deal_sum = get_daily_summary(target_date) if not stats: return f"{target_date} 当日无任何聊天记录。" # 按 acc_id 分组对话片段 conv_map: dict[str, list] = {} for c in convs: aid = c.get("acc_id") or "未知店铺" conv_map.setdefault(aid, []).append(c) lines = [f"【{target_date} 各店铺数据】\n"] # 成交/未成交汇总(供 AI 摘要与数据分析) lines.append("【成交与未成交】") lines.append(f" 成交:{deal_sum['成交数']} 笔,金额 {deal_sum['成交金额']:.0f} 元") lines.append(f" 未成交:{deal_sum['未成交数']} 笔") if deal_sum["未成交原因分布"]: for reason, cnt in deal_sum["未成交原因分布"].items(): lines.append(f" - {reason}:{cnt} 笔") if deal_sum["成交明细"]: for o in deal_sum["成交明细"][:5]: r = "让价后" if o.get("discount_given") else "直接" lines.append(f" ✓ {o.get('customer_name', '')[:6]} {r}成交 {o.get('amount', 0):.0f}元") if deal_sum["未成交明细"]: for o in deal_sum["未成交明细"][:5]: lines.append(f" ✗ {o.get('customer_name', '')[:6]} {o.get('reason', '')}") lines.append("") for s in stats: acc = s.get("acc_id") or "未知店铺" plat = s.get("platform") or "" label = f"{acc}({plat})" if plat else acc lines.append(f"▶ 店铺:{label}") lines.append(f" 接待客户:{s['unique_customers']} 人,共 {s['total_msgs']} 条消息(收 {s['recv']} 发 {s['sent']})") lines.append(f" 首条:{(s.get('first_msg') or '')[-8:-3]} 末条:{(s.get('last_msg') or '')[-8:-3]}") shop_convs = conv_map.get(acc, []) for c in shop_convs[:6]: # 最多展示6个客户片段 name = c.get("customer_name") or c.get("customer_id", "")[:8] snippet = (c.get("snippet") or "")[:120] lines.append(f" · {name}({c['msg_count']}条){snippet}") if len(shop_convs) > 6: lines.append(f" ... 还有 {len(shop_convs)-6} 位客户") lines.append("") return "\n".join(lines) # ────────────────────────────────────────── # AI 生成摘要 # ────────────────────────────────────────── async def _ai_summary(raw_text: str) -> str: """调用 AI 把统计文本转成自然语言日报""" try: from openai import AsyncOpenAI client = AsyncOpenAI( api_key=os.getenv("OPENAI_API_KEY"), base_url=os.getenv("OPENAI_BASE_URL"), ) model = os.getenv("OPENAI_MODEL", "doubao-seed-2-0-lite-260215") resp = await client.chat.completions.create( model=model, messages=[ { "role": "system", "content": ( "你是一名电商运营助理。根据下面的客服聊天数据," "为老板写一份简洁的当日运营日报(200字以内)。" "要包含:接待总人数、各店铺情况、有无成交或异常情况。" "语气轻松,像发给老板的微信消息,不需要标题。" ), }, {"role": "user", "content": raw_text}, ], max_tokens=300, temperature=0.5, ) return resp.choices[0].message.content.strip() except Exception as e: # AI 失败就直接返回原始统计 return raw_text # ────────────────────────────────────────── # 推送:企业微信 # ────────────────────────────────────────── async def _send_wechat(content: str): """推送到企业微信群机器人(markdown 格式,单条 ≤4096 字节自动分段)""" if not WECHAT_WEBHOOK: print("[DailySummary] 未配置 WECHAT_WEBHOOK,跳过推送") return # 企业微信单条 markdown 限 4096 字节,超长自动分段 encoded = content.encode("utf-8") chunks = [] while encoded: chunk = encoded[:3800].decode("utf-8", errors="ignore") chunks.append(chunk) encoded = encoded[len(chunk.encode("utf-8")):] async with httpx.AsyncClient(timeout=10) as client: for i, chunk in enumerate(chunks): payload = {"msgtype": "markdown", "markdown": {"content": chunk}} try: resp = await client.post(WECHAT_WEBHOOK, json=payload) data = resp.json() if data.get("errcode") == 0: print(f"[DailySummary] 企业微信推送成功(第{i+1}段)") else: print(f"[DailySummary] 企业微信推送失败: {data}") except Exception as e: print(f"[DailySummary] 企业微信推送异常: {e}") # ────────────────────────────────────────── # 推送:邮件 # ────────────────────────────────────────── def _send_email(subject: str, body: str): """发送日报邮件""" if not SUMMARY_EMAIL: return try: from mail.email_sender import email_sender import smtplib from email.mime.text import MIMEText from email.header import Header msg = MIMEText(body, "plain", "utf-8") msg["Subject"] = Header(subject, "utf-8").encode() msg["From"] = f"{Header(email_sender.sender_name, 'utf-8').encode()} <{email_sender.smtp_user}>" msg["To"] = SUMMARY_EMAIL with smtplib.SMTP(email_sender.smtp_host, email_sender.smtp_port) as s: s.starttls() s.login(email_sender.smtp_user, email_sender.smtp_password) s.sendmail(email_sender.smtp_user, [SUMMARY_EMAIL], msg.as_string()) print(f"[DailySummary] 日报邮件已发送至 {SUMMARY_EMAIL}") except Exception as e: print(f"[DailySummary] 日报邮件发送失败: {e}") # ────────────────────────────────────────── # 企业微信 Markdown 排版 # ────────────────────────────────────────── def _build_wechat_markdown(title: str, ai_text: str, raw_text: str, target_date: str = "") -> str: """ 构建符合企业微信规范的 markdown 内容。 支持:**bold**、、> 引用、``` 代码块、- 列表 不支持:
、HTML 标签(除 font/br) """ from db import chat_log_db as db from db.deal_outcome_db import get_daily_summary date = target_date or datetime.now().strftime("%Y-%m-%d") stats = db.get_daily_stats(date) deal_sum = get_daily_summary(date) lines = [f"## {title}\n"] # AI 摘要部分 lines.append("> " + ai_text.replace("\n", "\n> ")) lines.append("") # 成交/未成交 lines.append("**📈 成交与未成交**") lines.append(f"- 成交 **{deal_sum['成交数']}** 笔 · 金额 **{deal_sum['成交金额']:.0f}** 元") lines.append(f"- 未成交 **{deal_sum['未成交数']}** 笔") if deal_sum["未成交原因分布"]: for reason, cnt in deal_sum["未成交原因分布"].items(): lines.append(f" - {reason}:{cnt} 笔") lines.append("") # 各店铺数据表格(企业微信不支持 | 表格,用列表代替) if stats: lines.append("**📋 各店铺明细**") for s in stats: acc = s.get("acc_id") or "未知店铺" plat = s.get("platform") or "" label = f"{acc}({plat})" if plat else acc first = (s.get("first_msg") or "")[-8:-3] last = (s.get("last_msg") or "")[-8:-3] lines.append( f"- {label} " f"接待 **{s['unique_customers']}** 人 · " f"消息 {s['total_msgs']} 条(收{s['recv']}/发{s['sent']})" f" {first}~{last}" ) lines.append("") lines.append(f"发送时间:{datetime.now().strftime('%H:%M:%S')}") return "\n".join(lines) # ────────────────────────────────────────── # 主入口:生成并推送日报 # ────────────────────────────────────────── async def send_daily_summary(target_date: str = ""): """生成并推送当日汇总""" if not target_date: target_date = datetime.now().strftime("%Y-%m-%d") print(f"[DailySummary] 开始生成 {target_date} 日报...") raw_text = _build_stats_text(target_date) ai_text = await _ai_summary(raw_text) title = f"📊 {target_date} 客服日报" # ── 企业微信 markdown(不支持
,用标准语法)── wechat_md = _build_wechat_markdown(title, ai_text, raw_text, target_date) await _send_wechat(wechat_md) # ── 邮件:纯文本 ── email_body = f"{ai_text}\n\n{'='*40}\n\n{raw_text}" _send_email(title, email_body) print(f"[DailySummary] 日报推送完成") return ai_text # ────────────────────────────────────────── # 定时调度(由 websocket_client 启动) # ────────────────────────────────────────── async def scheduler(): """每天 SEND_HOUR:SEND_MINUTE 触发日报""" print(f"[DailySummary] 定时日报已启动,发送时间 {SEND_HOUR:02d}:{SEND_MINUTE:02d}") sent_today: Optional[str] = None # 记录已发日期,防重复 while True: now = datetime.now() today = now.strftime("%Y-%m-%d") if now.hour == SEND_HOUR and now.minute == SEND_MINUTE and sent_today != today: sent_today = today try: await send_daily_summary(today) except Exception as e: print(f"[DailySummary] 日报生成出错: {e}") # 每 30 秒检查一次 await asyncio.sleep(30) # ────────────────────────────────────────── # 命令行手动触发 # ────────────────────────────────────────── if __name__ == "__main__": import sys target = sys.argv[1] if len(sys.argv) > 1 else "" result = asyncio.run(send_daily_summary(target)) print("\n=== AI 摘要 ===") print(result)