#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
宝锐生物 · 销售日报生成器 (v2)

流程：
  1. MCP search_by_mql 拉取「昨日」活动 —— WHERE RELATIVE_DATETIME_EQ(start_time,'yesterday')
     (2026-09-11 实测该函数已恢复正常，返回 107 条 == 硬编码日期区间结果；OFFSET 翻页取全量)
  2. MCP 拉取全量客诉（售后管理空间），客户端按 状态 != '结束' 过滤未关闭
     （中文 WHERE 会触发 MQL syntax error，只能在客户端过滤）
  3. 本周订单：workbench_data.json → shipments.orders（活跃客户/单量，数据到昨日）
     + erp_amount.json → daily（金额权威源，ERP 导出滞后约 1 天）
  4. 生成 latest.md

输出：~/Desktop/Hermes输出-工作类/数据/daily_briefs/latest.md
不推送。
"""
import json, re, os, sys, urllib.request
from datetime import date, timedelta, datetime
from collections import defaultdict, Counter

BASE = os.path.expanduser("~/Desktop/Hermes输出-工作类")
DATA = f"{BASE}/数据"
OUT_PATH = f"{DATA}/daily_briefs/latest.md"
ACT_OUT = "/tmp/daily_brief_activities.json"
COMP_OUT = "/tmp/daily_brief_complaints.json"

MCP_URL = "https://project.feishu.cn/mcp_server/v1"
SALES_PK = "6593cd71471290e3cc6be6e6"
AFTERSALES_PK = "658288abfb8bd616b17025f1"
COMPLAINT_TYPE = "6669433056a98249604376de"

TODAY = date.today()
TARGET = TODAY - timedelta(days=1)          # 昨日
ACTIVITY_URL = "https://project.feishu.cn/xsguanli/sj/detail/{}"
COMPLAINT_URL = "https://project.feishu.cn/br-shgl/khts/detail/{}"


# ────────────────────────── MCP plumbing ──────────────────────────
def load_token():
    with open(os.path.expanduser("~/.hermes/config.yaml"), encoding="utf-8") as f:
        text = f.read()
    m = re.search(r"X-Mcp-Token:\s*[\"']?(\S+?)[\"']?\s*$", text, re.M)
    return m.group(1) if m else None


TOKEN = load_token()
HEADERS = {"Content-Type": "application/json",
           "Accept": "application/json, text/event-stream",
           "X-Mcp-Token": TOKEN}


def rpc(payload, sid=None):
    h = dict(HEADERS)
    if sid:
        h["Mcp-Session-Id"] = sid
    req = urllib.request.Request(MCP_URL, data=json.dumps(payload).encode(),
                                 headers=h, method="POST")
    resp = urllib.request.urlopen(req, timeout=150)
    sid_out = resp.headers.get("Mcp-Session-Id", sid)
    raw = resp.read().decode("utf-8", "replace")
    if raw.lstrip().startswith(("event:", "data:")) or "\ndata:" in raw:
        raw = "\n".join(l[5:].strip() for l in raw.splitlines() if l.startswith("data:"))
    return json.loads(raw), sid_out


def initialize():
    resp, sid = rpc({"jsonrpc": "2.0", "id": 1, "method": "initialize",
                     "params": {"protocolVersion": "2024-11-05", "capabilities": {},
                                "clientInfo": {"name": "daily-brief-v2", "version": "2.0"}}})
    try:
        rpc({"jsonrpc": "2.0", "method": "notifications/initialized"}, sid)
    except Exception:
        pass
    return sid


_HTML_JUNK = re.compile(r"<!--.*?-->|<[^>]{1,40}>|&emsp;|&nbsp;|&amp;nbsp;|&lt;|&gt;|&amp;")
_MD_IMG = re.compile(r"!\[[^\]]*\]\([^)]*\)?")   # 飞书富文本内嵌图片 ![](https://...)


def clean_text(s):
    """去掉活动描述里的 HTML 注释/实体/标签 + Markdown 内嵌图片（飞书富文本残留）"""
    if not s:
        return ""
    s = _HTML_JUNK.sub(" ", s)
    s = _MD_IMG.sub(" ", s)
    return re.sub(r"\s+", " ", s).strip()


def flatten(item):
    """moql_field_list → flat dict (取 label / user name_cn / 原始值)"""
    out = {}
    for f in item.get("moql_field_list", []):
        k = f.get("key")
        v = f.get("value")
        if v is None:
            out[k] = ""
        elif isinstance(v, dict):
            if "key_label_value" in v:
                out[k] = (v["key_label_value"] or {}).get("label", "")
            elif "key_label_value_list" in v:
                out[k] = [(x or {}).get("label", "") for x in (v["key_label_value_list"] or [])]
            elif "user_value" in v:
                out[k] = (v["user_value"] or {}).get("name_cn", "")
            elif "user_value_list" in v:
                out[k] = [(x or {}).get("name_cn", "") for x in (v["user_value_list"] or [])]
            elif "string_value" in v:
                s = v["string_value"] or ""
                out[k] = s.replace("\u003c", "<").replace("\u003e", ">")
            elif "long_value" in v:
                out[k] = str(v["long_value"])
            elif "double_value" in v:
                out[k] = v["double_value"]
            else:
                out[k] = str(v)
        else:
            out[k] = v
    return out


def mql_all(mql_base, project_key, sid, max_offset=1000):
    """OFFSET 翻页拉全量（MCP OFFSET 硬上限 1000）。返回 (raw_items, total, sid)"""
    items, seen, total = [], set(), None
    offset, empty_streak = 0, 0
    while offset <= max_offset:
        q = f"{mql_base} LIMIT 50 OFFSET {offset}"
        resp, sid = rpc({"jsonrpc": "2.0", "id": 100 + offset, "method": "tools/call",
                         "params": {"name": "search_by_mql",
                                    "arguments": {"project_key": project_key, "mql": q}}}, sid)
        if "error" in resp or resp.get("result", {}).get("isError"):
            msg = json.dumps(resp.get("error") or resp["result"], ensure_ascii=False)[:300]
            print(f"  ! page offset={offset} error: {msg}")
            break
        page = []
        for c in resp.get("result", {}).get("content", []):
            if c.get("type") != "text":
                continue
            t = c.get("text", "")
            if t.strip().startswith("logid"):
                continue
            inner = None
            try:
                inner = json.loads(t)
            except Exception:
                mm = re.search(r"\{.*\}", t, re.S)
                if mm:
                    inner = json.loads(mm.group(0))
            if inner is None:
                continue
            if isinstance(inner.get("result"), str):
                inner = json.loads(inner["result"])
            for g, gi in (inner.get("data") or {}).items():
                page.extend(gi or [])
            if inner.get("list"):
                total = inner["list"][0].get("count", total)
            break
        for it in page:
            wid = None
            for f in it.get("moql_field_list", []):
                if f.get("key") == "work_item_id":
                    v = f.get("value") or {}
                    wid = str(v.get("long_value") or v.get("string_value") or "") or None
                    break
            key = wid or json.dumps(it, ensure_ascii=False, sort_keys=True)
            if key not in seen:
                seen.add(key)
                items.append(it)
        print(f"  page offset={offset}: {len(page)} rows (uniq so far {len(items)}/{total})")
        if not page:
            empty_streak += 1
            if empty_streak >= 2:
                break
        else:
            empty_streak = 0
        if len(page) < 50:
            break
        offset += 50
    return items, total, sid


# ────────────────────────── 1. 活动 ──────────────────────────
print("=" * 66)
print(f"每日销售日报生成 v2  |  生成日 {TODAY}  |  数据日(昨日) {TARGET}")
print("=" * 66)

sid = initialize()
print(f"\n[1] 拉取活动：RELATIVE_DATETIME_EQ(start_time,'yesterday')")

ACT_MQL = ("SELECT name, start_time, field_b99055, field_5f20fc, owner, work_item_id "
           "FROM `销售管理`.`活动` "
           "WHERE RELATIVE_DATETIME_EQ(start_time, 'yesterday') "
           "ORDER BY start_time DESC")
raw_acts, act_total, sid = mql_all(ACT_MQL, SALES_PK, sid)
DATA_DATE = TARGET          # 实际数据日期（周末/节假日回退时 != TARGET）

_WD = ["周一", "周二", "周三", "周四", "周五", "周六", "周日"]


def pull_range(day, sid):
    """显式日期区间拉取某一天活动（相对函数失效 / 周末回退时使用）"""
    q = ("SELECT name, start_time, field_b99055, field_5f20fc, owner, work_item_id "
         f"FROM `销售管理`.`活动` "
         f"WHERE start_time >= '{day}' AND start_time < '{day + timedelta(days=1)}' "
         "ORDER BY start_time DESC")
    return mql_all(q, SALES_PK, sid)


# 兜底①：相对函数失效 → 显式日期区间（昨日）
if not raw_acts:
    print(f"  ! RELATIVE_DATETIME_EQ 返回空，回退为显式日期区间 {TARGET}")
    raw_acts, act_total, sid = pull_range(TARGET, sid)

# 兜底②：昨日无活动（周一遇到周日 / 连续假日）→ 最多回退 4 天，取最近有数据的一天
if not raw_acts:
    for back in range(2, 6):
        day = TODAY - timedelta(days=back)
        raw_acts, act_total, sid = pull_range(day, sid)
        print(f"  回退 {day}（{_WD[day.weekday()]}）：{len(raw_acts)} 条")
        if raw_acts:
            DATA_DATE = day
            break

if not raw_acts:
    print("  ! 最近 5 天均无活动记录")

acts = [flatten(a) for a in raw_acts]
with open(ACT_OUT, "w", encoding="utf-8") as f:
    json.dump(acts, f, ensure_ascii=False, indent=1)
print(f"  活动 {len(acts)} 条 (MCP count={act_total}) → {ACT_OUT}")

# ────────────────────────── 2. 客诉 ──────────────────────────
print(f"\n[2] 拉取客诉全量（售后管理 · 客户端过滤未关闭）")
COMP_MQL = ("SELECT name, start_time, owner, work_item_id, work_item_status "
            f"FROM `售后管理`.`{COMPLAINT_TYPE}` ORDER BY start_time DESC")
COMP_CACHE = f"{DATA}/mcp_complaints.json"

comps, comp_total = None, None
# BRIEF_USE_CACHE_COMPLAINTS=1 → 复用当日管线已拉的 mcp_complaints.json（省 2 分钟 MCP 翻页）
if os.environ.get("BRIEF_USE_CACHE_COMPLAINTS") == "1" and os.path.exists(COMP_CACHE):
    try:
        _cd = json.load(open(COMP_CACHE, encoding="utf-8"))
        _ci = _cd.get("items") if isinstance(_cd, dict) else _cd
        comps = [{"name": c.get("name", ""),
                  "start_time": str(c.get("created") or c.get("start_time") or ""),
                  "owner": c.get("creator") or c.get("owner") or "",
                  "work_item_id": str(c.get("id") or c.get("work_item_id") or ""),
                  "work_item_status": [c.get("status") or c.get("node") or ""]}
                 for c in _ci]
        comp_total = len(comps)
        print(f"  复用本地缓存客诉 {len(comps)} 条（{_cd.get('updated','?')}）")
    except Exception as e:
        print(f"  ! 缓存客诉解析失败({e})，回退 MCP 拉取")
        comps = None

if comps is None:
    raw_comp, comp_total, sid = mql_all(COMP_MQL, AFTERSALES_PK, sid)
    comps = [flatten(c) for c in raw_comp]

with open(COMP_OUT, "w", encoding="utf-8") as f:
    json.dump(comps, f, ensure_ascii=False, indent=1)

CLOSED = {"结束", "已关闭"}
open_comps = [c for c in comps
              if (c.get("work_item_status") or [""])[0] not in CLOSED]
open_comps.sort(key=lambda x: x.get("start_time", ""), reverse=True)
cut30 = str(TODAY - timedelta(days=30))
recent_open = [c for c in open_comps if c.get("start_time", "") >= cut30]
print(f"  客诉 {len(comps)} 条 (MCP count={comp_total}) | 未关闭 {len(open_comps)} | 近30天 {len(recent_open)}")

# ────────────────────────── 3. 本周订单 ──────────────────────────
print(f"\n[3] 本周订单")
wk_start = TODAY - timedelta(days=6)
wd = json.load(open(f"{DATA}/workbench_data.json"))
ship_orders = (wd.get("shipments") or {}).get("orders") or []
wk_rows = [o for o in ship_orders if str(o.get("createDate", ""))[:10] >= str(wk_start)]
wk_uniq = {}
for o in wk_rows:
    oid = o.get("orderId")
    if oid not in wk_uniq:
        wk_uniq[oid] = {"customer": o.get("customer", ""), "dept": o.get("dept", ""),
                        "date": str(o.get("createDate", ""))[:10], "amt": 0.0}
    wk_uniq[oid]["amt"] += o.get("taxTotal", 0) or 0
cust_stats = defaultdict(lambda: {"n": 0, "amt": 0.0})
for v in wk_uniq.values():
    cust_stats[v["customer"]]["n"] += 1
    cust_stats[v["customer"]]["amt"] += v["amt"]
top_custs = sorted(cust_stats.items(), key=lambda x: (-x[1]["n"], -x[1]["amt"]))

# ERP 金额（权威源，导出滞后）
erp = json.load(open(f"{DATA}/erp_amount.json"))
erp_daily = erp.get("daily") or {}
erp_win = [(str(wk_start + timedelta(days=i)), erp_daily.get(str(wk_start + timedelta(days=i))))
           for i in range(7)]
erp_total = sum((v or 0) for _, v in erp_win)
erp_last = max([k for k in erp_daily if k] or [""])
print(f"  MCP: {len(wk_uniq)} 单 / {len(cust_stats)} 客户 (¥{sum(v['amt'] for v in wk_uniq.values())/10000:.1f}万)")
print(f"  ERP({wk_start}~{TODAY}) ¥{erp_total/10000:.1f}万  最新入账日 {erp_last}")

# ────────────────────────── 4. 分析 ──────────────────────────
COMPETITORS = [
    ("诺唯赞", ["诺唯赞", "诺维赞", "诺威赞"]),
    ("全式金", ["全式金"]),
    ("菲鹏", ["菲鹏"]),
    ("赛默飞/Thermo", ["赛默飞", "thermo", "ab的酶", "abi"]),
    ("10× Genomics", ["10×", "10x", "10×genomics"]),
    ("近岸蛋白", ["近岸"]),
    ("碧云天", ["碧云天"]),
    ("新海基因", ["新海"]),
    ("雅酶", ["雅酶"]),
    ("弗德生物", ["弗德"]),
    ("酷来博", ["酷来博"]),
    ("义翘", ["义翘"]),
    ("金斯瑞", ["金斯瑞"]),
]
PAYMENT_KW = ["回款", "付款", "结清", "催款", "尾款", "到账"]
COMPLAINT_KW = ["客诉", "投诉", "批间差", "漏液", "非特异性", "换货", "退货", "不稳定", "不合格", "差异大"]
GREEN_SIG = ["性能最优", "测试通过", "采购确认", "优于", "效果不错", "性能还可以", "已安排冻干",
             "来料检性能正常", "效果还不错", "测试结果还不错", "采购", "稳定", "合作", "下单",
             "签约", "中标", "入围", "批量", "扩产", "需求跟进", "意向", "确认采购", "送样测试",
             "试用", "寄样", "满意", "复购", "定点", "国产替代", "投产"]
RED_SIG = ["压价", "威胁切换", "客诉", "退货", "换货", "不供应了", "做不到那么低", "已离职",
           "已替换", "不想再测试", "不做分子", "有问题", "拖尾", "不清晰", "达不到", "不如",
           "差一些", "没有诺唯赞", "替换", "竞争", "夺回", "丢失", "终止", "暂停合作", "降价压力"]

comp_hits, pay_hits, comp_sigs = [], [], []
green, red = [], []
owners = Counter()
desc_missing = 0

for a in acts:
    name = a.get("name", "")
    a["field_b99055"] = clean_text(a.get("field_b99055", "") or "")
    desc = a.get("field_b99055", "")
    cust = a.get("field_5f20fc", "")
    owner = a.get("owner", "")
    wid = a.get("work_item_id", "")
    link = ACTIVITY_URL.format(wid) if wid else ""
    if owner:
        owners[owner] += 1
    if not desc:
        desc_missing += 1
        continue
    low = desc.lower()

    for cname, kws in COMPETITORS:
        hit = next((k for k in kws if k.lower() in low), None)
        if hit:
            i = low.find(hit.lower())
            snip = desc[max(0, i - 40): i + len(hit) + 70].replace("\n", " ").strip()
            comp_hits.append({"comp": cname, "customer": cust or "(未关联客户)",
                              "snippet": snip, "owner": owner, "link": link})
            break

    for kw in PAYMENT_KW:
        if kw in desc:
            pay_hits.append({"customer": cust or "(未关联客户)", "keyword": kw,
                             "owner": owner, "link": link, "desc": desc[:110].replace("\n", " ")})
            break

    for kw in COMPLAINT_KW:
        if kw in desc:
            comp_sigs.append({"customer": cust or "(未关联客户)", "keyword": kw,
                              "owner": owner, "link": link, "desc": desc[:110].replace("\n", " ")})
            break

    g_hit = [s for s in GREEN_SIG if s in desc]
    r_hit = [s for s in RED_SIG if s in desc]
    if g_hit or r_hit:
        e = {"customer": cust or "(未关联客户)", "name": name, "owner": owner,
             "link": link, "desc": desc[:150].replace("\n", " "),
             "strength": len(g_hit) + len(r_hit)}
        if g_hit:
            green.append(e)
        if r_hit:
            red.append(e)

# 竞品去重（同竞品+客户保留一条）
seen, uniq_comp = set(), []
for c in comp_hits:
    k = (c["comp"], c["customer"])
    if k not in seen:
        seen.add(k)
        uniq_comp.append(c)
# 竞品按出现次数排序
comp_rank = Counter(c["comp"] for c in comp_hits)
comp_ranked = sorted(uniq_comp, key=lambda x: -comp_rank[x["comp"]])
# 重点项目去重
seen, uniq_green = set(), []
for p in green:
    k = (p["customer"], p["link"])
    if k not in seen:
        seen.add(k); uniq_green.append(p)
seen, uniq_red = set(), []
for p in red:
    k = (p["customer"], p["link"])
    if k not in seen:
        seen.add(k); uniq_red.append(p)
# 信号强度优先（命中关键词越多越靠前），同强度按客户名稳定排序
uniq_green.sort(key=lambda x: (-x["strength"], x["customer"]))
uniq_red.sort(key=lambda x: (-x["strength"], x["customer"]))

# ────────────────────────── 5. 生成 Markdown ──────────────────────────
WD = ["周一", "周二", "周三", "周四", "周五", "周六", "周日"]
tgt_wd = WD[DATA_DATE.weekday()]
IS_FALLBACK = DATA_DATE != TARGET
_DAY_LBL = f"{DATA_DATE.month}月{DATA_DATE.day}日" if IS_FALLBACK else "昨日"
L = []
L.append("📋 宝锐生物 · 销售日报")
L.append(f"{TODAY}（{WD[TODAY.weekday()]}）｜活动 {len(acts)} 条 ｜ 未关闭客诉 {len(open_comps)} 条 "
         f"｜ 近7天 {len(wk_uniq)} 单")
if IS_FALLBACK:
    L.append(f"📅 活动数据来源：{DATA_DATE.month}月{DATA_DATE.day}日（{tgt_wd}）"
             f"——{TARGET.month}月{TARGET.day}日（{WD[TARGET.weekday()]}）无活动记录已回退"
             f"｜客诉实时｜订单 MCP 至 {max([v['date'] for v in wk_uniq.values()] or ['—'])}"
             f" / ERP 至 {erp_last}")
else:
    L.append(f"📅 数据源：飞书活动 {DATA_DATE}（{tgt_wd}）｜客诉实时"
             f"｜订单 MCP 至 {max([v['date'] for v in wk_uniq.values()] or ['—'])}"
             f" / ERP 至 {erp_last}")
L.append("")

# ⚠️ 今日关注
L.append("⚠️ 今日关注")
notables = []
for c in uniq_comp:
    if any(s in c["snippet"] for s in ["压价", "价格", "元", "做不到", "威胁", "不供应", "替换", "夺回", "竞争"]):
        notables.append(f"⚔️ 竞品｜{c['comp']}｜{c['customer']}｜{c['snippet'][:64]}")
for cs in comp_sigs[:5]:
    notables.append(f"🔴 客诉信号｜{cs['customer']}｜{cs['keyword']}｜{cs['owner']}")
for c in recent_open[:6]:
    notables.append(f"🔴 未关闭客诉｜{(c.get('name','') or '')[:46]}｜{(c.get('work_item_status') or [''])[0]}｜{c.get('owner','')}")
for p in uniq_green[:5]:
    notables.append(f"🟢 重点进展｜{p['customer']}｜{p['desc'][:56]}｜{p['owner']}")
for p in uniq_red[:4]:
    notables.append(f"🔴 风险信号｜{p['customer']}｜{p['desc'][:56]}｜{p['owner']}")
if not notables:
    notables.append("无特殊关注事项")
for n in notables[:22]:
    L.append(f"· {n}")
L.append("")

# 👥 今日活动人员
L.append(f"👥 今日活动人员（{len(owners)}人 / {len(acts)}条活动）")
L.append("· " + "、".join(f"{n}({c})" for n, c in owners.most_common()))
L.append(f"· ⚠️ 未填跟进内容的活动：{desc_missing} 条")
L.append("")

# 🔴 未关闭客诉
L.append(f"🔴 未关闭客诉（历史未关闭 {len(open_comps)} 条 ｜ 近30天新增 {len(recent_open)} 条 ｜ 全量库 {len(comps)} 条）")
if recent_open:
    L.append(f"近30天未关闭明细（{len(recent_open)}条）：")
    for c in recent_open:
        st = (c.get("work_item_status") or [""])[0]
        url = COMPLAINT_URL.format(c.get("work_item_id", ""))
        L.append(f"· {c.get('start_time','')} ｜ {st} ｜ {(c.get('name','') or '')[:56]} ｜ {c.get('owner','')} [详情]({url})")
else:
    L.append("· 近30天无未关闭客诉 ✓")
L.append(f"· 其余 {len(open_comps)-len(recent_open)} 条为 30 天以前挂起的历史客诉") if len(open_comps) > len(recent_open) else None
L.append("")

# 📊 本周活跃客户
L.append(f"📊 近7天活跃客户（{wk_start} ~ {TODAY}）")
L.append(f"· 合计 {len(wk_uniq)} 单 / {len(cust_stats)} 家客户 ｜ 金额（ERP {wk_start}~{TODAY}）：¥{erp_total/10000:.1f}万")
for cn, s in top_custs[:12]:
    amt = f" ¥{s['amt']/10000:.1f}万" if s["amt"] >= 5000 else ""
    L.append(f"· {cn}：{s['n']}单{amt}")
if len(cust_stats) > 12:
    L.append(f"· …另有 {len(cust_stats)-12} 家客户")
L.append("· 说明：客户按订单数排序；单客金额取自 MCP 发货金额（多行金额为 0，故仅 ≥5000 元展示），"
         "周金额以 ERP 口径为准")
L.append("")

# ⚔️ 竞品动态
L.append(f"⚔️ 竞品动态（{len(comp_hits)} 次提及 / {len(uniq_comp)} 条去重）")
if uniq_comp:
    for c in comp_ranked[:18]:
        ls = f" [飞书活动]({c['link']})" if c["link"] else " (无链接)"
        L.append(f"· 【{c['comp']}】{c['customer']}｜{c['snippet'][:90]}｜{c['owner']}{ls}")
    ranks = "、".join(f"{k}×{v}" for k, v in comp_rank.most_common())
    L.append(f"· 今日提及频次：{ranks}")
else:
    L.append(f"· {_DAY_LBL}活动无竞品相关动态")
L.append("")

# 🚀 重点项目
L.append("🚀 重点项目")
if uniq_green:
    L.append(f"🟢 积极进展（{len(uniq_green)}条）：")
    for p in uniq_green[:12]:
        ls = f" [飞书活动]({p['link']})" if p["link"] else ""
        L.append(f"· {p['customer']}｜{p['desc'][:80]}｜{p['owner']}{ls}")
if uniq_red:
    L.append(f"🔴 风险/受阻（{len(uniq_red)}条）：")
    for p in uniq_red[:12]:
        ls = f" [飞书活动]({p['link']})" if p["link"] else ""
        L.append(f"· {p['customer']}｜{p['desc'][:80]}｜{p['owner']}{ls}")
if not uniq_green and not uniq_red:
    L.append("· 无特殊重点项目")
L.append("")

# 💰 回款
L.append(f"💰 回款/付款跟进（{len(pay_hits)}条）")
if pay_hits:
    for p in pay_hits[:10]:
        ls = f" [飞书活动]({p['link']})" if p["link"] else ""
        L.append(f"· {p['customer']}｜{p['desc'][:80]}｜{p['owner']}{ls}")
else:
    L.append(f"· {_DAY_LBL}无回款相关跟进")
L.append("")

L.append("---")
L.append(f"🤖 生成时间：{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}（未推送，仅本地归档）")
L.append(f"· 数据源：飞书项目 MCP `销售管理.活动` / `售后管理.客诉`、workbench_data.json、erp_amount.json")
_cal = (f"活动 = `RELATIVE_DATETIME_EQ(start_time,'yesterday')`（昨日 {len(acts)} 条）"
        if not IS_FALLBACK else
        f"活动 = 相对时间查询 `RELATIVE_DATETIME_EQ(start_time,'yesterday')` 返回 0 条"
        f"（{TARGET} 为 {WD[TARGET.weekday()]}，无活动记录）→ 回退显式日期区间，实取 "
        f"{DATA_DATE} 共 {len(acts)} 条（MCP count={act_total}）")
L.append(f"· 口径：{_cal}；客诉 MCP 不支持中文 WHERE"
         f"（`状态 != '已关闭'` 报语法错），改为全量 {len(comps)} 条 + 客户端过滤，"
         f"关闭态标签实为「结束」")
L.append(f"· 订单：MCP 发货单近 7 天 {len(wk_uniq)} 单至 {max([v['date'] for v in wk_uniq.values()] or ['—'])}；"
         f"ERP 金额统计窗口 {wk_start}~{TODAY}（最新入账 {erp_last}，导出滞后约 1 天）")

os.makedirs(os.path.dirname(OUT_PATH), exist_ok=True)
with open(OUT_PATH, "w", encoding="utf-8") as f:
    f.write("\n".join(L) + "\n")

print(f"\n[4] 报告已写入：{OUT_PATH}")
print(f"   行数 {len(L)}｜关注 {len(notables)}｜竞品 {len(uniq_comp)}｜积极 {len(uniq_green)}｜风险 {len(uniq_red)}｜回款 {len(pay_hits)}")
