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

@@ -1,300 +0,0 @@
# -*- coding: utf-8 -*-
"""
每日聊天汇总定时任务
- 每天 23:50 自动统计当日各店铺数据
- 用 AI 生成自然语言摘要
- 发送到企业微信 Webhook + QQ 邮件
"""
import asyncio
import os
import json
import logging
from datetime import datetime, date, timedelta
from typing import Optional
import httpx
from dotenv import load_dotenv
load_dotenv()
logger = logging.getLogger("cs_agent")
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:
logger.info("[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:
logger.info("[DailySummary] 企业微信推送成功(第%s段)", i + 1)
else:
logger.warning("[DailySummary] 企业微信推送失败: %s", data)
except Exception as e:
logger.exception("[DailySummary] 企业微信推送异常: %s", 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())
logger.info("[DailySummary] 日报邮件已发送至 %s", SUMMARY_EMAIL)
except Exception as e:
logger.exception("[DailySummary] 日报邮件发送失败: %s", e)
# ──────────────────────────────────────────
# 企业微信 Markdown 排版
# ──────────────────────────────────────────
def _build_wechat_markdown(title: str, ai_text: str, raw_text: str, target_date: str = "") -> str:
"""
构建符合企业微信规范的 markdown 内容。
支持:**bold**、<font color="...">、> 引用、``` 代码块、- 列表
不支持:<details>、<summary>、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"- <font color=\"info\">{label}</font> "
f"接待 **{s['unique_customers']}** 人 · "
f"消息 {s['total_msgs']} 条(收{s['recv']}/发{s['sent']}"
f" {first}~{last}"
)
lines.append("")
lines.append(f"<font color=\"comment\">发送时间:{datetime.now().strftime('%H:%M:%S')}</font>")
return "\n".join(lines)
# ──────────────────────────────────────────
# 主入口:生成并推送日报
# ──────────────────────────────────────────
async def send_daily_summary(target_date: str = ""):
"""生成并推送当日汇总"""
if not target_date:
target_date = datetime.now().strftime("%Y-%m-%d")
logger.info("[DailySummary] 开始生成 %s 日报...", target_date)
raw_text = _build_stats_text(target_date)
ai_text = await _ai_summary(raw_text)
title = f"📊 {target_date} 客服日报"
# ── 企业微信 markdown不支持 <details>,用标准语法)──
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)
logger.info("[DailySummary] 日报推送完成")
return ai_text
# ──────────────────────────────────────────
# 定时调度(由 websocket_client 启动)
# ──────────────────────────────────────────
async def scheduler():
"""每天 SEND_HOUR:SEND_MINUTE 触发日报"""
logger.info("[DailySummary] 定时日报已启动,发送时间 %02d:%02d", SEND_HOUR, SEND_MINUTE)
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:
logger.exception("[DailySummary] 日报生成出错: %s", 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))
logger.info("\n=== AI 摘要 ===")
logger.info(result)