#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""分子诊断 / 诊断原料 每日情报简报 v2（按 MiniMax 市场调研 skill 逻辑重构）。

相对 v1 的四条修复（源自 eval-2026-10-13）：
  ① watchlist 两层：客户主体 + 行业龙头/同赛道（只盯客户=看不到行业）
  ② 招投标领域门槛 + 标题黑名单 + URL 校验（防数字虚高 106→28）
  ③ GENERIC_KW 黑名单：通用词（招标中标/获证/新产品）不算关联证据（防假关联）
  ④ 丢弃量全部写进信源健康度表，不静默丢数据

输出：runs/<date>_日报.md ／ <date>_日报.csv ／ <date>_health.json ／ <date>_push_daily.txt
用法：intel_daily_v2.py [YYYYMMDD] [first|daily]
"""
import csv
import json
import os
import re
import sys
import time
from datetime import datetime, timedelta

import requests
from bs4 import BeautifulSoup

W = '/Users/liuxinyuan/Desktop/Hermes输出-工作类'
RUNS = os.path.join(W, '数据/market_intel/runs')
V01 = os.path.join(W, '数据/market_intel/v01')
UA = ('Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 '
      '(KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36')
S = requests.Session()
S.headers.update({'User-Agent': UA, 'Accept-Language': 'zh-CN,zh;q=0.9'})

DAY = sys.argv[1] if len(sys.argv) > 1 and sys.argv[1].isdigit() else datetime.now().strftime('%Y%m%d')
MODE = 'daily'
for a in sys.argv[1:]:
    if a in ('first', 'daily'):
        MODE = a
WIN_DAYS = 1 if MODE == 'daily' else 1095
WIN_FROM = (datetime.now() - timedelta(days=WIN_DAYS)).strftime('%Y-%m-%d')
TARGET = (datetime.now() - timedelta(days=1)).strftime('%Y-%m-%d')
for a in sys.argv[1:]:
    if re.match(r'^20\d{2}-\d{2}-\d{2}$', a):
        TARGET = a
os.makedirs(RUNS, exist_ok=True)

# ── 过滤规则（eval 修复 ②③）──
TENDER_DOMAIN = ['医院', '医疗', '检验', '检测', '诊断', '试剂', '核酸', 'PCR', '测序', '实验室', '医学', '生物', '疾控', '血站', '病理', '仪器']
DROP_TITLE = ['农田', '公路', '桥梁', '煤矿', '污水', '管网', '供热', '绿化', '道路', '水利', '学校', '安置', '装修', '精装',
              '土建', '市政', '轨道', '电网', '光伏', '风电', '垃圾', '公园', '食堂', '宿舍', '电梯', '消防', '房建']
GENERIC_KW = ['招标中标', '获证', '新产品', '中标', '招标', '动态', '公告', '发布', '注册证']
EVENT_RULES = [
    ('产品与发布', r'注册证|获批|批准|上市|发布|推出|新品|获得.*证|取得.*证|备案'),
    ('招投标', r'采购|招标|中标|成交|遴选|询价|竞争性谈判'),
    ('资本运作', r'收购|增资|减持|股权|回购|定增|终止.*项目|投资|设立|分派|减值'),
    ('合作', r'合作|签约|协议|战略|联合|共建|许可'),
    ('经营数据', r'业绩|营收|净利|亏损|同比|预告|快报|季度'),
    ('学术论文', r'Cell|Nature|Science|PNAS|论文|揭示|发现|研究|SCI'),
    ('厂商活动', r'论坛|峰会|展会|研讨会|邀您|相约|讲堂|沙龙|直播|培训班'),
]
TRACK_RULES = [
    ('分子诊断-核酸检测（PCR/qPCR）', r'pcr|荧光|核酸|分型|定量|rt-?pcr|扩增'),
    ('分子诊断-测序（NGS/tNGS/mNGS）', r'测序|ngs|tngs|mngs|基因组建库|伴随诊断'),
    ('分子诊断-恒温扩增（LAMP/RPA）', r'lamp|恒温|等温|rpa'),
    ('分子POCT', r'poct|快速检测|一体机|即时检测'),
    ('化学发光/免疫诊断', r'化学发光|免疫|抗原|抗体|胶体金|层析'),
    ('血凝/生化', r'血凝|生化|凝血'),
    ('ICL 第三方检验', r'医学检验所|第三方|icL|检验实验室'),
    ('科研试剂/生命科学工具', r'科研|试剂盒.*科研|生命科学|单细胞|组学|蛋白|抗体.*科研'),
    ('原料与 CDMO', r'酶|原料|cdmo|蛋白表达|发酵|质粒'),
    ('动物诊断', r'动物|宠物|兽医|疫病'),
    ('司法/海关检测', r'法医|司法|海关|检疫'),
]
REF_TYPES = {'学术论文', '厂商活动'}


def ev_type(t):
    for name, pat in EVENT_RULES:
        if re.search(pat, t, re.I):
            return name
    return '一般动态'


def track(t):
    for name, pat in TRACK_RULES:
        if re.search(pat, t, re.I):
            return name
    return '未归类'


def valid_url(u):
    return bool(re.match(r'^https?://', u or ''))


TENDER_MUST = ['招标公告', '中标', '成交', '采购公告', '遴选', '询价', '竞争性谈判', '资格预审', '结果公告', '征询', '单一来源', '废标', '招标采购公告']
TENDER_JUNK = ['市场规模', '行业报告', '价格', '产业图谱', '白皮书', '研报', '前景', '趋势预测',
               '通知公告', '招标采购_', '集中采购平台', '医保局', '专栏', '专题', '首页', '导航',
               '解决方案', '官网', '公司简介', '招聘', '论坛', '展会']


def tender_ok(title, url):
    """招投标：URL + 领域词 + 公告类词 + 栏目页/报告页剔除（返回 (保留?, 丢弃原因))"""
    if not valid_url(url):
        return False, '无效URL'
    if any(k in title for k in DROP_TITLE):
        return False, '非本领域'
    if not any(k in title for k in TENDER_DOMAIN):
        return False, '非本领域'
    if any(k in title for k in TENDER_JUNK):
        return False, '栏目页/报告页'
    if not any(k in title for k in TENDER_MUST):
        return False, '非公告类'
    if len(title) < 12:
        return False, '标题过短'
    return True, ''


HEALTH = []


def rec(src, cat, code, kept, raw, dropped, t0, state):
    HEALTH.append({'信源': src, '分类': cat, 'HTTP': code, '保留': kept, '原始': raw,
                   '过滤': dropped, '耗时': '%dms' % int((time.time() - t0) * 1000), '状态': state})


# ════════ 1. 巨潮公告：三层 watchlist ════════
def collect_cninfo():
    ents = []
    ann = json.load(open(os.path.join(W, '数据/market_intel/客户上市状态_2026-10-13.json'), encoding='utf-8'))
    EXC = {'海尔施集团', '佰奥莱博集团'}
    for nm, v in ann.items():
        if nm in EXC or not v.get('上市') or not v['匹配']:
            continue
        m = v['匹配'][0]
        ents.append((m['代码'], m.get('orgId'), nm, '客户主体'))
    for nm, hits in json.load(open(os.path.join(W, '数据/market_intel/客户上市状态_补测.json'), encoding='utf-8')).items():
        if hits and nm not in [e[2] for e in ents] and nm not in EXC:
            ents.append((hits[0][0], None, nm, '客户主体'))
    for x in json.load(open(os.path.join(V01, 'watchlist_v2.json'), encoding='utf-8'))['items']:
        if x.get('orgId'):
            ents.append((x['代码'], x['orgId'], x['简称'] or x['名称'], x['层']))
    ents.append(('688105', '9900046657', '诺唯赞', '竞品上市主体'))

    items, t0 = [], time.time()
    ok, raw_all, kept_all = 0, 0, 0
    for code, org, nm, tier in ents:
        try:
            r = S.post('http://www.cninfo.com.cn/new/hisAnnouncement/query',
                       data={'stock': '%s,%s' % (code, org or ''), 'tabName': 'fulltext', 'pageSize': 30,
                             'pageNum': 1, 'column': 'sse', 'category': ''}, timeout=25)
            j = r.json()
            a = j.get('announcements') or []
            raw_all += len(a)
            for x in a:
                t = int(x.get('announcementTime') or 0) / 1000
                d = datetime.fromtimestamp(t).strftime('%Y-%m-%d') if t else ''
                if d and d < WIN_FROM:
                    continue
                if MODE == 'daily' and d and d[:10] != TARGET:
                    continue
                items.append({'来源': '巨潮公告 API', '信源分类': 'disclosure', '层': tier, '主体': nm,
                              '代码': code, '标题': x.get('announcementTitle', ''), '日期': d,
                              '日期推断': False, '链接': 'http://static.cninfo.com.cn/' + str(x.get('adjunctUrl', ''))})
                kept_all += 1
            ok += 1
            time.sleep(1.0)
        except Exception:
            pass
    rec('巨潮公告查询 API（%d 主体）' % len(ents), 'disclosure', 200, kept_all, raw_all, raw_all - kept_all,
        t0, '正常' if ok else '失败')
    print('  R1 巨潮：%d/%d 主体成功 ｜ 窗口内 %d 条' % (ok, len(ents), kept_all))
    return items


# ════════ 2. 竞品官网 ════════
COMP_WIN = 14  # daily 模式下竞品新闻的回看天数


COMP_SRC = [
    ('诺唯赞', 'https://www.vazyme.com/news/1/', 'sel', 'div.Newscon .p_item', 'p.e_text-5 a', 'p.e_timeFormat-7'),
    ('翌圣', 'https://www.yeasen.com/news/', 're', r'/news/detail/\d+'),
    ('全式金', 'https://www.transgen.com/news/list_1.html', 're', r'/news/\d+\.html'),
    ('赛默飞', 'https://www.thermofisher.cn/cn/zh/home/about-us/news-gallery.html', 're', r'press-release-china/\d{4}-\d{2}-\d{2}'),
    ('罗氏诊断', 'https://diagnostics.roche.com/global/en/sitemap.xml', 'xml', r'news-listing/20\d\d/'),
    ('菲鹏', 'https://www.faponbiotech.com/', 're', r'/press-\d+'),
    ('宝生物', 'https://www.takara.co.jp/', 'js', ''),
    ('迈迪安', 'https://www.meridianbioscience.com/', 'blocked', ''),
]


def collect_comp():
    items = []
    for brand, url, kind, *rest in COMP_SRC:
        t0 = time.time()
        if kind in ('js', 'blocked'):
            rec('%s 新闻列表' % brand, 'competitor', 200, 0, 0, 0, t0, '⚠️ 需人工（JS 渲染/风控）' if kind == 'js' else '⚠️ 需人工（Cloudflare）')
            continue
        try:
            r = S.get(url, timeout=30)
            r.encoding = r.apparent_encoding or r.encoding
            soup = BeautifulSoup(r.text, 'lxml')
            raw, kept, drops = 0, 0, 0
            got = []
            if kind == 'sel':
                sel, tsel, dsel = rest
                for it in soup.select(sel):
                    a = it.select_one(tsel) or it.find('a', href=True)
                    if not a:
                        continue
                    raw += 1
                    href = a['href']
                    if href.startswith('/'):
                        href = re.match(r'https?://[^/]+', url).group(0) + href
                    dt = it.select_one(dsel)
                    got.append((a.get_text(' ', strip=True), href, dt.get_text(strip=True) if dt else ''))
            elif kind == 'xml':
                pat = re.compile(rest[0])
                pairs = re.findall(r'<loc>\s*([^<\s]+)\s*</loc>\s*(?:<lastmod>\s*([^<\s]+)\s*</lastmod>)?', r.text)
                for loc, lm in pairs:
                    if pat.search(loc):
                        raw += 1
                        got.append((loc.rstrip('/').split('/')[-1].replace('.html', '').replace('-', ' '), loc, lm or ''))
            else:
                pat = re.compile(rest[0])
                for a in soup.find_all('a', href=True):
                    if pat.search(a['href']) and len(a.get_text(strip=True)) >= 8:
                        raw += 1
                        href = a['href']
                        if href.startswith('/'):
                            href = re.match(r'https?://[^/]+', url).group(0) + href
                        dm = re.search(r'20\d{2}-\d{2}-\d{2}', a.get_text(' ') + ' ' + href)
                        got.append((re.sub(r'\s+\d{1,4}\s+\d{4}-\d{2}-\d{2}\s*$', '', a.get_text(' ', strip=True)), href, dm.group(0) if dm else ''))
            for title, href, d in got:
                if not valid_url(href):
                    drops += 1
                    continue
                if len((title or '').strip()) < 8:
                    drops += 1
                    continue
                dd = d[:10]
                if not dd:
                    drops += 1                    # 无日期条目一律丢，并计数（防空标题/空日期渗入）
                    continue
                if dd < WIN_FROM:
                    drops += 1
                    continue
                if MODE == 'daily':
                    lim = (datetime.now() - timedelta(days=COMP_WIN)).strftime('%Y-%m-%d')
                    if dd < lim:
                        drops += 1
                        continue
                items.append({'来源': '%s 新闻列表' % brand, '信源分类': 'competitor', '层': '竞品', '主体': brand,
                              '代码': '', '标题': title, '日期': dd, '日期推断': not bool(dd), '链接': href})
                kept += 1
            state = '正常' if kept else ('· 今日无新增' if raw else '⚠️ 需人工')
            rec('%s 新闻列表' % brand, 'competitor', r.status_code, kept, raw, drops, t0, state)
            print('  R2 %-8s 原始 %d ｜ 保留 %d ｜ 丢弃 %d' % (brand, raw, kept, drops))
        except Exception as e:
            rec('%s 新闻列表' % brand, 'competitor', 0, 0, 0, 0, t0, '⚠️ 抓取失败：%s' % str(e)[:30])
    return items


# ════════ 3. 行业媒体 + 招投标（检索源）════════
def collect_media_tender():
    items = []
    t0 = time.time()
    try:
        r = S.get('http://news.bioon.com/', timeout=25)
        r.encoding = r.apparent_encoding or 'utf-8'
        soup = BeautifulSoup(r.text, 'lxml')
        raw = kept = drops = 0
        for a in soup.find_all('a', href=True):
            href = a['href']
            if not re.search(r'/article/[0-9a-f]+', href):
                continue
            raw += 1
            title = a.get_text(' ', strip=True)
            if len(title) < 10:
                drops += 1
                continue
            if not valid_url(href):
                drops += 1
                continue
            items.append({'来源': '行业媒体｜生物谷 新闻', '信源分类': 'media', '层': '行业', '主体': '行业媒体',
                          '代码': '', '标题': title, '日期': datetime.now().strftime('%Y-%m-%d'),
                          '日期推断': True, '链接': href})
            kept += 1
        rec('生物谷 新闻', 'media', r.status_code, kept, raw, drops, t0, '正常' if kept else '· 今日无新增')
        print('  R3 生物谷：原始 %d ｜ 保留 %d' % (raw, kept))
    except Exception as e:
        rec('生物谷 新闻', 'media', 0, 0, 0, 0, t0, '⚠️ %s' % str(e)[:30])
    # JS 渲染媒体源（照实标注）
    for nm in ('检验医学网', '动脉网 医疗频道'):
        rec(nm, 'media', 200, 0, 0, 0, time.time(), '⚠️ 需人工（JS 渲染）')
    # 招投标：检索源
    q = os.path.join(RUNS, DAY + '_search.json')
    t0 = time.time()
    raw = kept = drops = 0
    if os.path.exists(q):
        for it in json.load(open(q, encoding='utf-8')).get('items', []):
            raw += 1
            ok, why = tender_ok(it.get('标题', ''), it.get('链接', ''))
            if not ok:
                drops += 1
                continue
            items.append({'来源': '招投标检索源', '信源分类': 'tender', '层': '行业', '主体': '招投标',
                          '代码': '', '标题': it.get('标题', ''), '日期': '', '日期推断': True,
                          '链接': it.get('链接', '')})
            kept += 1
    rec('招投标检索源（兜底）', 'tender', 200, kept, raw, drops, t0, '正常' if kept else '· 今日无新增')
    for nm in ('全国平台 交易公开列表', '北京交易中心', '陕西交易中心'):
        rec(nm, 'tender', 200, 0, 0, 0, time.time(), '⚠️ 需人工（JS 渲染）')
    rec('NMPA 政务服务门户', 'regulator', 200, 0, 0, 0, time.time(), '⚠️ 需人工（412 风控）')
    print('  R4 招投标检索源：原始 %d ｜ 保留 %d ｜ 丢弃 %d' % (raw, kept, drops))
    return items


# ════════ 关联判断（修复 ③：通用词不算证据）════════
CLIENTS = None


def clients():
    global CLIENTS
    if CLIENTS is None:
        CLIENTS = {}
        a = json.load(open(os.path.join(W, '数据/market_intel/客户上市状态_2026-10-13.json'), encoding='utf-8'))
        for nm, v in a.items():
            if v.get('上市') and v['匹配']:
                CLIENTS[v['匹配'][0].get('简称') or nm] = nm
                CLIENTS[str(v['匹配'][0]['代码'])] = nm
    return CLIENTS


def relate(it):
    """返回 (关联客户 或 '', 依据)。只认主体标识，不认通用词。"""
    c = clients()
    txt = it['标题'] + ' ' + it.get('主体', '')
    for k, v in c.items():
        if k and len(k) >= 2 and k in txt:
            return v, '上市简称「%s」' % k
    if it.get('代码') and it['代码'] in txt:
        return c.get(it['代码'], ''), '股票代码「%s」' % it['代码']
    return '', ''


print('══ %s 日报 v2（窗口 %s，模式 %s）══' % (DAY, WIN_FROM if MODE == 'first' else TARGET, MODE))
all_items = collect_cninfo() + collect_comp() + collect_media_tender()

# 分类 + 关联 + 需跟进
for it in all_items:
    it['事件类型'] = ev_type(it['标题'])
    it['赛道'] = track(it['标题'])
    it['关联客户'], it['关联依据'] = relate(it)
    it['需跟进'] = it['事件类型'] not in REF_TYPES

need = [i for i in all_items if i['需跟进']]
ref = [i for i in all_items if not i['需跟进']]
cust_items = [i for i in all_items if i.get('层') in ('客户主体',)]
lead_items = [i for i in all_items if i.get('层') in ('行业龙头', '同赛道厂商')]
comp_items = [i for i in all_items if i.get('层') == '竞品']
tender_items = [i for i in all_items if i['信源分类'] == 'tender']

# CSV
csv_p = os.path.join(RUNS, DAY + ('_日报_first.csv' if MODE == 'first' else '_日报.csv'))
with open(csv_p, 'w', encoding='utf-8-sig', newline='') as f:
    w = csv.writer(f)
    w.writerow(['日期', '是否新增', '需跟进', '事件类型', '赛道', '品牌/主体', '信源', '标题', '发布日期', '发布日期为推断', 'URL'])
    for i in all_items:
        w.writerow([i['日期'], '新增', '是' if i['需跟进'] else '否', i['事件类型'], i['赛道'],
                    i['主体'], i['来源'], i['标题'], i['日期'], '是' if i['日期推断'] else '否', i['链接']])

json.dump({'健康度': HEALTH, '条目数': len(all_items), '需跟进': len(need)},
          open(os.path.join(RUNS, DAY + ('_health_first.json' if MODE == 'first' else '_health.json')),
               'w', encoding='utf-8'), ensure_ascii=False, indent=1)
json.dump(all_items, open(os.path.join(RUNS, DAY + ('_daily_v2_first.json' if MODE == 'first' else '_daily_v2.json')),
                          'w', encoding='utf-8'), ensure_ascii=False, indent=1)

print('\n══ 汇总 ══')
print('  条目 %d ｜ 需跟进 %d ｜ 参考 %d' % (len(all_items), len(need), len(ref)))
print('  客户主体 %d ｜ 行业龙头/同赛道 %d ｜ 竞品 %d ｜ 招投标 %d' % (
    len(cust_items), len(lead_items), len(comp_items), len(tender_items)))
tot_raw = sum(h['原始'] for h in HEALTH)
tot_drop = sum(h['过滤'] for h in HEALTH)
print('  信源健康：原始 %d ｜ 保留 %d ｜ 过滤丢弃 %d（丢弃量已在健康度表逐源登记）' % (tot_raw, len(all_items), tot_drop))
print('  CSV → %s' % csv_p)


# ════════ 日报渲染（对齐 MiniMax 日报结构）════════
def md_list(items, maxn=None):
    out = []
    seen = set()
    for i in items:
        k = i['标题'][:40]
        if k in seen:
            continue
        seen.add(k)
        lead = '🔗 **%s**　' % i['关联客户'] if i['关联客户'] else ''
        dep = '（对标客户：%s）　' % i['主体'] if i['层'] == '竞品' else ''
        basis = '　依据：%s' % i['关联依据'] if i.get('关联依据') else ''
        out.append('- 🆕 %s%s**%s｜%s** `%s`　%s' % (lead, dep, i['主体'], i['来源'], i['事件类型'], i['标题'][:96]))
        d = (i['日期'] or '—') + basis
        out.append('  - %s%s · <%s>' % (d, '（日期由页面推断，需人工确认）' if i['日期推断'] else '', i['链接']))
        if maxn and len(out) // 2 >= maxn:
            out.append('- …另有 %d 条，见同目录 CSV' % (len(items) - maxn))
            break
    return out


L = []
L.append('# 分子诊断 / 诊断原料 每日情报简报　%s\n' % datetime.now().strftime('%Y-%m-%d'))
_reach = sum(1 for h in HEALTH if h['HTTP'] == 200)
_auto = sum(1 for h in HEALTH if '⚠️' not in h['状态'])
L.append('> 采集窗口 %s ｜ 采集条目 %d 条 ｜ **需跟进 %d 条**' % (
    TARGET if MODE == 'daily' else WIN_FROM + ' 起', len(all_items), len(need)))
L.append('> 信源：可达 %d/%d（HTTP 200）｜ 可自动抽取 %d 源，%d 源需人工浏览（已在异常区列明）' % (
    _reach, len(HEALTH), _auto, len(HEALTH) - _auto))
L.append('>')
L.append('> 本简报只收录带原始链接的公开信息，每条可点开溯源；无法二次验证的一律不写入。')
L.append('> 「需跟进」= 招投标 / 资本运作 / 新品发布 / 合作 / 经营数据，或已归入具体赛道；学术论文与厂商活动归入文末参考信息。\n')

warn = [h for h in HEALTH if '⚠️' in h['状态']]
if warn:
    L.append('## ⚠️ 采集异常（先看这里）\n')
    for h in warn:
        L.append('- **%s**：%s' % (h['信源'], h['状态'].replace('⚠️ ', '')))
    L.append('')
ok_none = [h for h in HEALTH if h['状态'] == '· 今日无新增']
if ok_none:
    L.append('> 以下信源今日确认无新增（已抓到但均在时间窗外或属营销内容，属正常情况）：%s\n' % '、'.join(h['信源'] for h in ok_none))

L.append('## 🔗 今日客户相关动态（%d 条）\n' % len(cust_items))
L += md_list(cust_items) or ['- 今日无（客户主体公告时间窗外或无新增）']
L.append('')
L.append('## 🏢 行业龙头 / 同赛道厂商（%d 条，非我方客户，判断景气度）\n' % len(lead_items))
L += md_list(lead_items) or ['- 今日无']
L.append('')
L.append('## ⚔️ 竞品动态（%d 条）\n' % len(comp_items))
L += md_list(comp_items) or ['- 今日无']
L.append('')

L.append('## 📊 今日概览\n')
from collections import Counter as _C
tc = _C(i['赛道'] for i in all_items)
L.append('| 赛道 | 条目数 |')
L.append('|---|---:|')
for k, v in tc.most_common():
    L.append('| %s | %d |' % (k, v))
L.append('')
for tk, _n in tc.most_common():
    grp = [i for i in all_items if i['赛道'] == tk]
    L.append('## %s（%d 条）\n' % (tk, len(grp)))
    L += md_list(grp, maxn=6)
    L.append('')

L.append('## 🏗 招投标 / 采购（%d 条，已过领域门槛）\n' % len(tender_items))
L += md_list(tender_items, maxn=8) or ['- 今日无通过领域过滤的条目']
L.append('')
L.append('## 📎 参考信息（学术/活动类，需跟进价值较低，共 %d 条）\n' % len(ref))
L.append('构成：%s\n' % '｜'.join('%s %d 条' % (k, v) for k, v in _C(i['事件类型'] for i in ref).most_common()))
L += md_list(ref, maxn=6)
L.append('')

L.append('## 🔍 信源健康度\n')
L.append('| 信源 | 分类 | HTTP | 保留 | 原始 | 过滤 | 耗时 | 状态 |')
L.append('|---|---|---:|---:|---:|---:|---:|---|')
for h in HEALTH:
    L.append('| %s | %s | %s | %d | %d | %d | %s | %s |' % (
        h['信源'], h['分类'], h['HTTP'], h['保留'], h['原始'], h['过滤'], h['耗时'], h['状态']))
L.append('')
L.append('---\n')
L.append('**合规声明**：全部内容来自公开可访问渠道，未使用登录态、未绕过验证码或风控、未采集付费内容。\n')
L.append('**能力边界**：NMPA 医疗器械注册证查询当前无可用公开接口（主站 412 风控），本简报不含注册证数据；'
         '上市客户的拿证动作经巨潮公告获取。')

md = '\n'.join(L)
md_p = os.path.join(RUNS, DAY + ('_日报_first.md' if MODE == 'first' else '_日报.md'))
open(md_p, 'w', encoding='utf-8').write(md)

# 推送文案（飞书）
P = ['【分子诊断日报 · %s】' % datetime.now().strftime('%m-%d')]
P.append('条目 %d ｜ 需跟进 %d ｜ 客户 %d ｜ 龙头/同赛道 %d ｜ 竞品 %d ｜ 招投标 %d' % (
    len(all_items), len(need), len(cust_items), len(lead_items), len(comp_items), len(tender_items)))
P.append('')
if cust_items:
    P.append('■ 客户动态')
    for i in cust_items[:6]:
        P.append('· %s｜%s｜%s' % (i['主体'][:16], i['事件类型'], i['标题'][:52]))
        P.append('  %s' % i['链接'])
if lead_items:
    P.append('■ 行业龙头/同赛道')
    for i in lead_items[:6]:
        P.append('· %s｜%s｜%s' % (i['主体'][:14], i['日期'] or '—', i['标题'][:50]))
if comp_items:
    P.append('■ 竞品')
    for i in comp_items[:5]:
        P.append('· %s｜%s' % (i['主体'], i['标题'][:52]))
if tender_items:
    P.append('■ 招投标（过领域门槛）')
    for i in tender_items[:5]:
        P.append('· %s' % i['标题'][:58])
w = [h for h in HEALTH if '⚠️' in h['状态']]
if w:
    P.append('■ 采集异常：%s' % '；'.join(h['信源'] for h in w))
P.append('')
P.append('完整日报：runs/%s ｜ 信源保留 %d / 原始 %d（过滤 %d）' % (
    os.path.basename(md_p), len(all_items), sum(h['原始'] for h in HEALTH), sum(h['过滤'] for h in HEALTH)))
push = '\n'.join(P)
open(os.path.join(RUNS, DAY + '_push_daily.txt'), 'w', encoding='utf-8').write(push)

print('\n日报 md → %s' % md_p)
print('推送文案 → %s_push_daily.txt（%d 字）' % (DAY, len(push)))
print(push[:500])
