#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""《客户拉通大表》DEMO · 产品介绍（自包含单文件 HTML）生成器
================================================================
产物：知识沉淀/客户拉通大表_产品介绍_20261011.html
- 自包含、零外链（无 CDN / 无 fetch / 无外部字体）
- 可投屏也可打印
- 品牌套件（Logo + slogan）由 脚本/brand_kit.py 注入
- 页首/页脚含「DEMO 原型 · 示例数据，未上线」标识；正文不出现「已上线」类表述

七段（顺序固定）：
  01 一句话定位 · 02 它解决什么问题 · 03 两大层 · 04 六大机制
  05 数据从哪来 · 06 当前状态与上线路径 · 07 可度量的价值

功能名 / 字段名 / 集合名 / 货号 一律与素材逐字一致：
  结构清单 知识沉淀/客户拉通大表_结构清单_20261010.md（§1–§13）
  数据源盘点 知识沉淀/客户拉通大表_可复用数据源盘点_20261010.md
  原型本体 工具/客户拉通大表_demo.html
已核实数字（标出处）：
  · 替换周期 ≈60 天 → 结构清单 §13.2（天隆 M5244-ZJ4 → MD2104-003006）
  · 集团归集口径漏 68.6% → 脚本/统计_天隆_按抬头_货号统计.py 交叉核对输出
  · 机会性 ¥109.8 万 → 结构清单 §13（天隆 PFM4211）

用法：
  env -u PYTHONPATH /usr/bin/python3 脚本/build_customer_thread_intro.py
"""
import io
import os
import sys

HERE = os.path.dirname(os.path.abspath(__file__))
ROOT = os.path.dirname(HERE)
if HERE not in sys.path:
    sys.path.insert(0, HERE)
import brand_kit as BK  # noqa: E402

OUT = os.path.join(ROOT, '知识沉淀', '客户拉通大表_产品介绍_20261011.html')
BUILD_DATE = '2026-10-11'

# ── 待确认项（§6）逐字取自盘点 §5 / 结构清单 §7 ─────────────────
PENDING = [
    '飞书可写性：`create_workitem` 可用的 `work_item_type` / `template_id`（现 `销售工作台服务.py:3392-3397` 用 `template_id:\'700373\'`，是否通用）待确认。',
    '客户 ID 是否随活动返回：`field_5f20fc` 读回只有 label（客户名），MQL 能否取客户 `work_item_id` 待确认。',
    '商机「关联客户」字段名是否存在于 `story` 表 —— 影响 `opportunities.customer_id` 回填。',
    '课题组口径：以 `ketizu_list.json`（1744，飞书商机）还是 ERP 商机(飞书)#（88.68% 覆盖）为准。',
    '订单 `field_8d9153` 是否 = 出货批号#（值形如 `20260417`）待确认。',
]

# ── §7 可度量价值：指标 → 上线后取数位置 ─────────────────────────
METRICS = [
    ('资源闭环率', '已闭环待办 / 全部资源协助待办',
     '分子分母均来自集合 `support_todos`（`status` 归一后统计；§1 表 D）；事件留痕在 `thread_view`（`event_type = todo_close`）。'),
    ('替换周期', '替换完成日 − 客诉发生日（基线 ≈60 天）',
     '集合 `replacement_cases`：`replace_done_date − complaint_date` 即 `cycle_days`（§13.2 A/C）。'),
    ('机会性商机断尾预警数', '机会性商机连续 N 个月（默认 3）无活动 / 无订单 的条数',
     '`opportunities.nature = opportunistic`（§13.1 A）叠加 `thread_view` / `activities` 的最新事件时间计算沉默月数。'),
    ('活动填写合格率（段级校验拦截数）', '提交被拦截次数 / 提交尝试次数',
     '前端 `#formErr` 拦截计数 + 采集字段 `activities.desc_content`（段级校验口径见结构清单 §12.3）。'),
    ('周会决议落盘条数', '提交后落盘 `support_todos` 的决议条数',
     '集合 `support_todos` 中 `source = 周会决议`、`todo_id` 前缀 `WR-` 的文档计数（§11.2）。'),
]


# ══════════════════════════════════════════════════════════════════
# CSS
# ══════════════════════════════════════════════════════════════════
CSS = """
:root{
  --pri:#1852B6; --pri-d:#0d3a85; --bg:#ffffff; --soft:#f4f7fb; --card:#ffffff;
  --line:#e3e9f2; --ink:#1a2434; --sub:#5b6b82; --mut:#93a1b5;
  --done:#16A34A; --doing:#1852B6; --stuck:#C0392B; --todo:#B9C2CE;
  --warn-bg:#FEF3C7; --warn-bd:#F59E0B; --warn-ink:#92400E;
  --wrap:1440px; --gap:14px; --pad:16px; --rad:12px;
  --fs-num:32px; --fs-title:15px; --fs-body:12.5px; --fs-note:10.5px;
}
*{box-sizing:border-box}
body{margin:0;background:var(--bg);color:var(--ink);
  font-family:-apple-system,BlinkMacSystemFont,"PingFang SC","Microsoft YaHei",sans-serif}
.wrap{max-width:var(--wrap);margin:0 auto;padding:18px 22px 34px;font-size:var(--fs-body)}
h1{font-size:var(--fs-num);line-height:1.12;margin:0;font-weight:800;color:var(--pri-d);letter-spacing:.2px}
.head-row{display:flex;align-items:flex-end;gap:14px;flex-wrap:wrap;margin-top:4px}
.head-row .sub{color:var(--sub);font-size:var(--fs-body)}
.demo-bar{background:linear-gradient(90deg,#FEF3C7,#FDE68A);border:1px solid var(--warn-bd);
  color:var(--warn-ink);border-radius:10px;padding:9px 14px;margin:13px 0 0;font-size:13px;
  font-weight:700;display:flex;align-items:center;gap:9px}
.demo-bar .dot{width:9px;height:9px;border-radius:50%;background:var(--warn-bd);flex:none}
.sec{margin-top:22px;border-top:2px solid var(--line);padding-top:14px}
.sec-hd{display:flex;align-items:baseline;gap:11px;margin:0 0 10px}
.sec-no{font-size:var(--fs-title);font-weight:800;color:#fff;background:var(--pri);
  border-radius:7px;padding:2px 10px;letter-spacing:.5px;flex:none}
.sec-hd h2{font-size:19px;margin:0;font-weight:800;color:var(--pri-d)}
.sec-hd .en{color:var(--mut);font-size:var(--fs-note);font-weight:600;margin-left:auto}
p{margin:7px 0;line-height:1.75;color:#26364d}
.lead{font-size:15px;line-height:1.8;background:linear-gradient(90deg,#eaf1fd,#f7faff);
  border-left:4px solid var(--pri);border-radius:10px;padding:13px 18px;color:#1c2f4a;font-weight:600}
.lead b{color:var(--pri-d)}
code{background:#eef3fb;color:#0d3a85;border-radius:4px;padding:0 5px;font-size:11.5px;
  font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace}
ul.tick{margin:6px 0 6px 2px;padding-left:20px;line-height:1.8;color:#26364d}
ul.tick li{margin:4px 0}
ul.tick li b{color:var(--pri-d)}
.grid3{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:var(--gap);align-items:stretch}
.grid2{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:var(--gap);align-items:stretch}
@media(max-width:1040px){.grid3{grid-template-columns:repeat(2,minmax(0,1fr))}}
@media(max-width:720px){.grid3,.grid2{grid-template-columns:1fr}}
.card{background:var(--card);border:1px solid var(--line);border-radius:var(--rad);
  padding:13px var(--pad);display:flex;flex-direction:column}
.card .kk{font-size:var(--fs-note);font-weight:800;color:var(--pri);letter-spacing:.5px;margin-bottom:3px}
.card h3{font-size:var(--fs-title);margin:0 0 5px;font-weight:800;color:var(--pri-d)}
.card .one{font-size:var(--fs-body);line-height:1.7;color:#26364d;font-weight:600}
.card .why{margin-top:auto;padding-top:8px;font-size:var(--fs-note);color:var(--sub);line-height:1.65;
  border-top:1px dashed #d8e2f1}
.card .why b{color:var(--stuck)}
.layer{border:1px solid var(--line);border-radius:var(--rad);padding:13px 16px;background:var(--soft)}
.layer .tag{display:inline-block;font-size:var(--fs-note);font-weight:800;color:#fff;
  border-radius:6px;padding:1px 9px;margin-bottom:6px}
.layer.l1 .tag{background:#5b21b6}
.layer.l2 .tag{background:#0f7a3d}
.layer h3{margin:2px 0 6px;font-size:var(--fs-title);color:var(--pri-d)}
.figbox{border:1px solid var(--line);border-radius:var(--rad);background:#fbfcfe;padding:12px 10px;
  margin-top:12px;overflow:auto}
.figcap{text-align:center;color:var(--mut);font-size:var(--fs-note);margin-top:7px}
table.mt{border-collapse:separate;border-spacing:0;width:100%;font-size:var(--fs-body);margin-top:8px}
table.mt th,table.mt td{border-bottom:1px solid var(--line);padding:8px 10px;vertical-align:top;text-align:left}
table.mt thead th{background:#dfe9f8;color:var(--pri-d);font-weight:800;font-size:12px;
  border-bottom:1px solid #c9d8ee}
table.mt tbody tr:nth-child(even) td{background:#f9fbfe}
table.mt td.mn{font-weight:800;color:var(--pri-d)}
table.mt td .frm{color:var(--sub);font-size:11.5px}
.src{font-size:var(--fs-note);color:var(--mut);margin-top:5px;line-height:1.6}
.src b{color:var(--stuck);font-weight:700}
.pill{display:inline-block;border-radius:6px;padding:1px 8px;font-size:var(--fs-note);
  font-weight:800;border:1px solid transparent;margin:1px 3px 1px 0}
.pill.opp{background:#fdeedd;color:#9a5b09;border-color:#f3d3a8}
.pill.con{background:#dcf5e6;color:#0f7a3d;border-color:#a6ddba}
.pill.unk{background:#eef1f5;color:#64748b;border-color:#d5dbe4}
.pill.b{background:#e3edfd;color:var(--pri);border-color:#b9cbe8}
.steps{display:flex;flex-wrap:wrap;gap:9px;margin:10px 0 4px}
.step{border:1px solid var(--line);border-radius:10px;padding:9px 13px;background:#fff;
  font-size:12px;font-weight:700;color:var(--pri-d);position:relative;flex:1 1 150px}
.step .n{display:inline-block;width:20px;height:20px;line-height:20px;text-align:center;border-radius:50%;
  background:var(--pri);color:#fff;font-size:11px;margin-right:7px;font-weight:800}
.callout{background:#fef7f6;border:1px solid #efc8c3;border-left:4px solid var(--stuck);
  border-radius:10px;padding:11px 16px;margin-top:12px;font-size:var(--fs-body);line-height:1.75;color:#5b2320}
.callout b{color:var(--stuck)}
.foot-note{border-top:1px solid var(--line);margin-top:22px;padding-top:11px;color:var(--mut);
  font-size:var(--fs-note);line-height:1.7}
.foot-note b{color:var(--sub)}
@media print{
  body{background:#fff}
  .wrap{max-width:none;padding:0 6mm}
  .sec{break-inside:avoid}
  .card,.layer,.figbox,.callout,table.mt{break-inside:avoid}
  .demo-bar{-webkit-print-color-adjust:exact;print-color-adjust:exact}
  .bior-header,.bior-footer{-webkit-print-color-adjust:exact;print-color-adjust:exact}
}
"""


# ══════════════════════════════════════════════════════════════════
# 内联 SVG 示意图（严禁在 <svg> 内写 <title>/<br>）
# ══════════════════════════════════════════════════════════════════
def svg_two_layers():
    """§3 两层联动示意图：第二层活动录入 → 抓取/回写 → 第一层项目推移表"""
    return """
<svg width="1120" height="330" viewBox="0 0 1120 330" role="img" aria-label="两层联动示意图" xmlns="http://www.w3.org/2000/svg">
  <defs>
    <marker id="ar1" markerWidth="9" markerHeight="9" refX="7" refY="3" orient="auto">
      <path d="M0,0 L7,3 L0,6 z" fill="#1852B6"/>
    </marker>
    <marker id="ar2" markerWidth="9" markerHeight="9" refX="7" refY="3" orient="auto">
      <path d="M0,0 L7,3 L0,6 z" fill="#0f7a3d"/>
    </marker>
  </defs>
  <rect x="8" y="46" width="700" height="112" rx="10" fill="#f3ecfb" stroke="#8b5cf6" stroke-width="1.4"/>
  <text x="24" y="70" font-size="13.5" font-weight="800" fill="#5b21b6">第一层 · 项目推移表（一行一商机）</text>
  <text x="24" y="92" font-size="11.5" fill="#3f3f5c">组1 项目基本信息 ｜ 组2 项目关键节点（可向右延展）｜ 组3 资源协助（可向右延展）</text>
  <g font-size="10.5" fill="#3f3f5c">
    <rect x="24" y="104" width="96" height="22" rx="5" fill="#e6ddff" stroke="#8b5cf6"/><text x="72" y="119" text-anchor="middle">陌生开发</text>
    <rect x="126" y="104" width="96" height="22" rx="5" fill="#e6ddff" stroke="#8b5cf6"/><text x="174" y="119" text-anchor="middle">方案匹配</text>
    <rect x="228" y="104" width="96" height="22" rx="5" fill="#dcf5e6" stroke="#16A34A"/><text x="276" y="119" text-anchor="middle">中试</text>
    <rect x="330" y="104" width="96" height="22" rx="5" fill="#e3edfd" stroke="#1852B6"/><text x="378" y="119" text-anchor="middle">临床试验</text>
    <rect x="432" y="104" width="120" height="22" rx="5" fill="#eef1f5" stroke="#B9C2CE"/><text x="492" y="119" text-anchor="middle">据结果向右延展</text>
    <rect x="558" y="104" width="132" height="22" rx="5" fill="#efe7fb" stroke="#8b5cf6"/><text x="624" y="119" text-anchor="middle">资源协助 ＋1 列</text>
  </g>
  <rect x="8" y="196" width="700" height="118" rx="10" fill="#e9f8ef" stroke="#16A34A" stroke-width="1.4"/>
  <text x="24" y="220" font-size="13.5" font-weight="800" fill="#0f7a3d">第二层 · 活动录入与待办（一行一活动）</text>
  <g font-size="10.5" fill="#214a33">
    <rect x="24" y="232" width="150" height="24" rx="5" fill="#d4f0df" stroke="#16A34A"/><text x="99" y="248" text-anchor="middle">活动类型（3 类）</text>
    <rect x="182" y="232" width="196" height="24" rx="5" fill="#d4f0df" stroke="#16A34A"/><text x="280" y="248" text-anchor="middle">模板化活动描述（段级校验）</text>
    <rect x="386" y="232" width="190" height="24" rx="5" fill="#d4f0df" stroke="#16A34A"/><text x="481" y="248" text-anchor="middle">资源协作（多选 4 类）</text>
    <rect x="24" y="264" width="150" height="24" rx="5" fill="#d4f0df" stroke="#16A34A"/><text x="99" y="280" text-anchor="middle">待办事项（多条）</text>
    <rect x="182" y="264" width="196" height="24" rx="5" fill="#d4f0df" stroke="#16A34A"/><text x="280" y="280" text-anchor="middle">周会决议（三岗各填一栏）</text>
    <rect x="386" y="264" width="190" height="24" rx="5" fill="#d4f0df" stroke="#16A34A"/><text x="481" y="280" text-anchor="middle">三轨落盘（本地 / CB / 飞书）</text>
  </g>
  <line x1="600" y1="196" x2="600" y2="166" stroke="#0f7a3d" stroke-width="2" marker-end="url(#ar2)"/>
  <text x="368" y="186" font-size="10.5" fill="#0f7a3d" font-weight="700">① 抓取：按「客户名称 + 商机名称」匹配</text>
  <line x1="600" y1="166" x2="600" y2="196" stroke="#1852B6" stroke-width="2" marker-end="url(#ar1)"/>
  <text x="760" y="186" text-anchor="end" font-size="10.5" fill="#1852B6" font-weight="700">② 回写：节点推进 · 资源协助 ＋1 列</text>
  <rect x="770" y="46" width="340" height="268" rx="10" fill="#f7faff" stroke="#b9cbe8" stroke-width="1.4"/>
  <text x="788" y="70" font-size="13" font-weight="800" fill="#0d3a85">联动规则（逐字口径）</text>
  <text x="788" y="96" font-size="11.5" fill="#33465f">① 由活动的「基本信息、关联商机」抓取第一层项目基本信息</text>
  <text x="788" y="120" font-size="11.5" fill="#33465f">② 项目基本信息中的「竞对信息」可手动维护</text>
  <text x="788" y="144" font-size="11.5" fill="#33465f">③ 项目关键节点可手动维护</text>
  <text x="788" y="168" font-size="11.5" fill="#33465f">④ 资源协作由销售在第二层手动多选触发</text>
  <text x="788" y="188" font-size="11" fill="#5b6b82">（技术支持测试项目单 / 研发项目单 / 售后 / 其他）</text>
  <line x1="788" y1="204" x2="1092" y2="204" stroke="#e3e9f2" stroke-width="1"/>
  <text x="788" y="226" font-size="11" fill="#5b6b82">未匹配到第一层行 → 不回写，提示「未匹配商机行」</text>
  <text x="788" y="246" font-size="11" fill="#5b6b82">列不硬编码：新增列只插一条 column_defs</text>
  <text x="788" y="266" font-size="11" fill="#5b6b82">行/格 ID 未回填 → 前端标黄「待关联」</text>
  <text x="788" y="292" font-size="11" fill="#5b6b82">只追加不覆盖（append-only）</text>
</svg>"""


def svg_data_flow():
    """§5 数据对接图：ERP / 飞书 / CloudBase 三方与拉通大表"""
    return """
<svg width="1120" height="360" viewBox="0 0 1120 360" role="img" aria-label="数据对接图" xmlns="http://www.w3.org/2000/svg">
  <defs>
    <marker id="d_ar" markerWidth="9" markerHeight="9" refX="7" refY="3" orient="auto">
      <path d="M0,0 L7,3 L0,6 z" fill="#5b6b82"/>
    </marker>
  </defs>
  <rect x="10" y="24" width="300" height="150" rx="10" fill="#eef3fb" stroke="#1852B6" stroke-width="1.4"/>
  <text x="28" y="48" font-size="13.5" font-weight="800" fill="#0d3a85">① ERP 订单</text>
  <text x="28" y="72" font-size="11.5" fill="#33465f">销售订单（诊断原料 / 生命科学）</text>
  <text x="28" y="94" font-size="11.5" fill="#33465f">对接键 = 单据编号</text>
  <text x="28" y="114" font-size="11" fill="#5b6b82">飞书 `销售订单.name` ≡ ERP `单据编号`</text>
  <text x="28" y="132" font-size="11" fill="#5b6b82">例 ZHBR260907070（两侧一致）</text>
  <text x="28" y="152" font-size="11" fill="#5b6b82">字段：货号# / 价税合计 / 备货部门…</text>
  <text x="28" y="166" font-size="11" fill="#5b6b82">生命科学含「商机(飞书)#」列</text>

  <rect x="10" y="196" width="300" height="148" rx="10" fill="#e9f8ef" stroke="#16A34A" stroke-width="1.4"/>
  <text x="28" y="220" font-size="13.5" font-weight="800" fill="#0f7a3d">② 飞书（MCP）</text>
  <text x="28" y="242" font-size="11" fill="#33465f">销售管理空间 6593cd71471290e3cc6be6e6</text>
  <text x="28" y="259" font-size="11" fill="#33465f">售后管理空间 658288abfb8bd616b17025f1</text>
  <text x="28" y="278" font-size="10.5" fill="#5b6b82">客户 65ae1e403c87b152f3365ca6 ｜ 活动 65ae1e5d44338dbe7c39a29a</text>
  <text x="28" y="295" font-size="10.5" fill="#5b6b82">销售订单 662612aea6bb7089fea044ef ｜ 商机 story</text>
  <text x="28" y="312" font-size="10.5" fill="#5b6b82">客诉(客户反馈单) 6669433056a98249604376de</text>
  <text x="28" y="332" font-size="10.5" fill="#5b6b82">工具 search_by_mql / create_workitem</text>

  <rect x="810" y="96" width="300" height="180" rx="10" fill="#f7faff" stroke="#b9cbe8" stroke-width="1.4"/>
  <text x="828" y="120" font-size="13.5" font-weight="800" fill="#0d3a85">③ CloudBase 集合</text>
  <text x="828" y="142" font-size="11" fill="#33465f">env bier-sales-d0gatbvlx288724e9</text>
  <text x="828" y="162" font-size="11" fill="#5b6b82">沿用：activities（现有集合）</text>
  <text x="828" y="180" font-size="11" fill="#5b6b82">客户/商机/待办/反馈：
    <tspan x="828" dy="17">cust_master · opportunities · support_todos</tspan>
    <tspan x="828" dy="17">dept_feedback · orders · thread_view</tspan>
    <tspan x="828" dy="17">wide_rows · column_defs · replacement_cases</tspan></text>

  <rect x="404" y="96" width="312" height="180" rx="12" fill="#fff" stroke="#1852B6" stroke-width="2"/>
  <text x="560" y="132" font-size="16" font-weight="800" fill="#0d3a85" text-anchor="middle">客户拉通大表</text>
  <text x="560" y="158" font-size="11.5" fill="#33465f" text-anchor="middle">第一层 项目推移表 ＋ 第二层 活动录入与待办</text>
  <line x1="430" y1="172" x2="690" y2="172" stroke="#e3e9f2" stroke-width="1"/>
  <text x="560" y="194" font-size="11" fill="#5b6b82" text-anchor="middle">集团归集口径：只能用 `集团客户#` 列</text>
  <text x="560" y="214" font-size="11" fill="#C0392B" text-anchor="middle" font-weight="700">不可用客户名关键词 —— 实测漏 68.6%</text>
  <text x="560" y="238" font-size="10" fill="#93a1b5" text-anchor="middle">三轨落盘：本地记录 / CloudBase / 飞书一键复制</text>
  <text x="560" y="258" font-size="10.5" fill="#93a1b5" text-anchor="middle">去向：成交 / 闭环（append-only 只追加）</text>

  <line x1="310" y1="99" x2="404" y2="150" stroke="#5b6b82" stroke-width="1.6" marker-end="url(#d_ar)"/>
  <line x1="310" y1="270" x2="404" y2="220" stroke="#5b6b82" stroke-width="1.6" marker-end="url(#d_ar)"/>
  <line x1="716" y1="186" x2="810" y2="186" stroke="#5b6b82" stroke-width="1.6" marker-end="url(#d_ar)"/>
  <text x="352" y="115" font-size="10.5" fill="#5b6b82">单据编号</text>
  <text x="338" y="292" font-size="10.5" fill="#5b6b82">work_item_id</text>
  <text x="740" y="180" font-size="10.5" fill="#5b6b82">写集合</text>
</svg>"""


# ══════════════════════════════════════════════════════════════════
# 页面正文
# ══════════════════════════════════════════════════════════════════
def build_body():
    def card(kk, title, one, why):
        return ('<div class="card"><div class="kk">%s</div><h3>%s</h3>'
                '<div class="one">%s</div><div class="why">为什么要它：%s</div></div>'
                % (kk, title, one, why))

    mech = ''.join([
        card('机制 ①', '两层联动',
             '第二层提交一条活动 → 第一层对应商机行自动更新（节点推进、资源协助 ＋1 列）。',
             '活动不再只躺在飞书里，一处录入，项目视图即时同步。'),
        card('机制 ②', '按角色分字段权限',
             '销售 / 技术支持 / 研发各写各栏，<b>管理员不受限</b>（可全字段读写）。',
             '跨部门协作时谁改哪一格有边界，避免互相覆盖、责任说不清。'),
        card('机制 ③', '资源协作手动触发 ＋ 自动加列',
             '第二层「资源协作（多选）」勾选 技术支持测试项目单 / 研发项目单 / 售后 / 其他，每选一类第一层「资源协助」组自动加一列（列名带序号 N）。',
             '资源请求由人明确发起、系统留痕，不靠口头催办。'),
        card('机制 ④', '段级校验（缺段拦截）',
             '活动类型 = 测试结果跟进时，描述须含 6 个必填段（客户需求 / 宝锐方案 / 测试结果 / 问题现象 / 客户反馈 / 待办事项），缺段 → 提交被拦截并提示缺哪段。',
             '把「填写质量」前移到入口，避免事后补不齐、口径不一致。'),
        card('机制 ⑤', '三轨落盘（可追溯）',
             '一条活动同时写三处：本地记录（活动编号）／ CloudBase 集合 ／ 飞书描述（一键复制）。',
             '任一轨都能回溯同一动作，换系统、换人都不丢记录。'),
        card('机制 ⑥', '两块预警',
             '④ 机会性商机流失预警（机会性商机 连续 N 个月无活动 / 无订单 → 提醒；过预计成交窗口 60 天仍未成交 → 复盘）＋ ② 客诉 → 替换 → 采购恢复周期（6 阶段进度条，替换周期基线 ≈60 天）。',
             '把「下季度能自动预警」做成规则，机会性断尾与客诉长尾提前暴露。'),
    ])

    metrics_rows = ''.join(
        '<tr><td class="mn">%s</td><td class="frm">%s</td><td>%s</td></tr>' % (n, f, s)
        for n, f, s in METRICS
    )

    pending_li = ''.join('<li>%s</li>' % x for x in PENDING)

    return """
<div class="wrap">
  <h1>客户拉通大表 · 产品介绍</h1>
  <div class="head-row">
    <span class="sub">内部使用 · 两层设计（第一层项目推移表 / 第二层活动录入与待办）</span>
  </div>
  <div class="demo-bar"><span class="dot"></span>DEMO 原型 · 示例数据，未上线 · 本页仅说明设计与口径，非线上产品</div>

  <!-- ========== 01 一句话定位 ========== -->
  <div class="sec" id="sec-1">
    <div class="sec-hd"><span class="sec-no">01</span><h2>一句话定位</h2><span class="en">Positioning</span></div>
    <div class="lead">把「<b>销售活动 → 资源协作 → 部门闭环 → 商机成交</b>」拉通成<b>一条可追溯链路</b>的客户项目运营平台 —— 一行一商机看得见项目走到哪、一条活动拉得动跨部门资源，并把机会性商机断尾与客诉替换周期变成可预警的规则。</div>
    <p class="src">它以《客户拉通大表》原型（<code>工具/客户拉通大表_demo.html</code>）为载体，后端落在 CloudBase 七表 + 宽表/列定义表（结构清单 §1 / §8）。</p>
  </div>

  <!-- ========== 02 它解决什么问题 ========== -->
  <div class="sec" id="sec-2">
    <div class="sec-hd"><span class="sec-no">02</span><h2>它解决什么问题</h2><span class="en">The gap</span></div>
    <ul class="tick">
      <li><b>信息分散</b>：活动在飞书、订单在 ERP、待办散在多个地方；查一个客户项目的来龙去脉要拼好几张表，且各表 ID 互不打通。</li>
      <li><b>跨部门协作靠人催、无留痕</b>：销售、技术支持、研发之间的请求与回复靠口头/群里喊，谁在何时做了什么，缺一条统一的过程记录。</li>
      <li><b>机会性商机断尾无预警</b>：上年度的机会性商机（如一次性大单）本年度未延续时，往往事后才发现，没有提前提醒机制。</li>
      <li><b>客诉 → 替换 → 采购恢复无周期度量</b>：替换做了多久、采购何时恢复，没有统一口径与基线，无法度量服务效率。</li>
    </ul>
    <p class="src">依据：结构清单 §13（机会性商机断尾、客诉替换周期两个真实案例）；数据源盘点 §4（三类 ID 缺口：活动→客户 / 活动·订单→商机 / 商机→客户）。</p>
  </div>

  <!-- ========== 03 两大层 ========== -->
  <div class="sec" id="sec-3">
    <div class="sec-hd"><span class="sec-no">03</span><h2>两大层</h2><span class="en">Two layers</span></div>
    <div class="grid2">
      <div class="layer l1">
        <span class="tag">第一层</span>
        <h3>项目推移表（一行一商机）</h3>
        <p><b>里程碑轨道</b>：默认形态为「形态1 里程碑轨道」（一行一商机、8 节点圆点串、按 4 个部门分组、行可点）。项目按「部门：大客户部 / 销售拓展部 / 生物制药销售部 / 国际业务部」分组。</p>
        <p><b>商机性质标记</b>：每行带 chip —— <span class="pill opp">机会性</span><span class="pill con">持续性</span><span class="pill unk">待判</span>；工具条提供「全部 / 机会性 / 持续性 / 待判」筛选（点选即过滤第一层行，可复位）。</p>
        <p><b>项目关键节点可向右延展</b>：陌生开发 → 方案匹配 → 中试 → 临床试验 → 已确认被用于注册 → 已转产 → 客户已获证 → 新证迭代（＋ 新增节点列）。</p>
        <p><b>资源协助列每选一类 ＋1 列</b>：列名 = 选项名 ＋ 序号 N（技术支持测试项目单N / 研发项目单N / 售后N / 其他N）。</p>
        <p><b>点行展开第二层</b>：点任意一行 → 下方展开该商机的资源泳道矩阵（形态B）。</p>
      </div>
      <div class="layer l2">
        <span class="tag">第二层</span>
        <h3>活动录入与待办（一行一活动）</h3>
        <p><b>活动类型 ＋ 模板化活动描述（段级校验）</b>：活动类型 3 类（测试结果跟进 / 采购跟进 / 客户档案完善）；描述按类型套模板，测试结果跟进须含 6 个必填段（段级校验，缺段拦截）。</p>
        <p><b>四类资源协作手动多选</b>：技术支持测试项目单 / 研发项目单 / 售后 / 其他（「其他」必填说明）—— 由销售手动多选触发，选中即生成承接待办。</p>
        <p><b>待办事项</b>：事项 / 负责人 / 时间节点（×N，可加多条）。</p>
        <p><b>周会决议（三岗各填一栏）</b>：销售 / 技术支持 / 研发 三岗各填一栏，提交后落盘待办库 <code>support_todos</code>。</p>
        <p><b>三轨落盘</b>：本地记录（活动编号）／ CloudBase 集合 ／ 飞书描述一键复制。</p>
      </div>
    </div>
    <div class="figbox">%s<div class="figcap">图 1 · 两层联动：第二层活动录入 →（按「客户名称 + 商机名称」抓取）→ 第一层项目推移表 →（回写：节点推进 · 资源协助加列）</div></div>
  </div>

  <!-- ========== 04 六大机制 ========== -->
  <div class="sec" id="sec-4">
    <div class="sec-hd"><span class="sec-no">04</span><h2>六大机制</h2><span class="en">Six mechanisms</span></div>
    <div class="grid3">%s</div>
    <table class="mt" style="margin-top:12px">
      <thead><tr><th style="width:190px">商机性质判定（机会性判别三条 · §13.1）</th><th>可执行口径（输入 = 客户近 12 个月订单 + 商机框架字段 + 金额占比）</th></tr></thead>
      <tbody>
        <tr><td class="mn">机会性（任一命中）</td><td>① 该客户近 12 个月该品类订单 ≤1 单（无历史复购）；② 无框架 / 年度协议字段；③ 预计金额占客户年采购 &gt;40%% 且集中在单一项目。</td></tr>
        <tr><td class="mn">持续性</td><td>三条均不满足机会性条件（有稳定复购、 有框架协议、 金额分散且占年采购 ≤40%%）。</td></tr>
        <tr><td class="mn">待判</td><td>复购 / 框架 / 金额占比三项数据任一缺失到无法判定 → 转人工复核。</td></tr>
      </tbody>
    </table>
  </div>

  <!-- ========== 05 数据从哪来 ========== -->
  <div class="sec" id="sec-5">
    <div class="sec-hd"><span class="sec-no">05</span><h2>数据从哪来</h2><span class="en">Data sources</span></div>
    <div class="figbox">%s<div class="figcap">图 2 · 数据对接：ERP 订单（对接键 = 单据编号）· 飞书（活动/客户/商机/客诉）· CloudBase 集合 → 客户拉通大表 → 成交 / 闭环</div></div>
    <div class="grid3" style="margin-top:12px">
      <div class="card"><div class="kk">来源 ①</div><h3>ERP 订单</h3>
        <div class="one">订单记录，<b>对接键 = 单据编号</b>（飞书 <code>销售订单.name</code> ≡ ERP <code>单据编号</code>，两侧完全一致）。</div>
        <div class="why">对接键是既有的、现成的，不需要新造编码就能把订单挂到客户项目上。</div></div>
      <div class="card"><div class="kk">来源 ②</div><h3>飞书</h3>
        <div class="one">活动 / 客户 / 商机 / 客诉四类工作项；空间与表 ID 见右侧清单（盘点 §2）。</div>
        <div class="why">销售日常在飞书写活动，拉通大表直接复用，不要求二次录入。</div></div>
      <div class="card"><div class="kk">来源 ③</div><h3>CloudBase 集合</h3>
        <div class="one">落库集合见对接图（<code>activities</code> 沿用现有集合，其余新建）。</div>
        <div class="why">统一落到一套集合，才谈得上跨表关联与「只追加不覆盖」的留痕。</div></div>
    </div>
    <table class="mt">
      <thead><tr><th style="width:210px">飞书实体</th><th>可调用 ID / 字段（逐字）</th><th style="width:220px">关联到</th></tr></thead>
      <tbody>
        <tr><td class="mn">销售管理空间</td><td><code>6593cd71471290e3cc6be6e6</code></td><td>全部飞书表</td></tr>
        <tr><td class="mn">售后管理空间</td><td><code>658288abfb8bd616b17025f1</code></td><td>客诉</td></tr>
        <tr><td class="mn">客户 · 工作项类型</td><td><code>65ae1e403c87b152f3365ca6</code></td><td>cust_master（表A）</td></tr>
        <tr><td class="mn">销售订单 · 工作项类型</td><td><code>662612aea6bb7089fea044ef</code></td><td>orders（表F）</td></tr>
        <tr><td class="mn">活动 · 工作项类型</td><td><code>65ae1e5d44338dbe7c39a29a</code></td><td>activities（表C）</td></tr>
        <tr><td class="mn">客诉(客户反馈单) · 工作项类型</td><td><code>6669433056a98249604376de</code></td><td>dept_feedback（表E）/ 售后</td></tr>
        <tr><td class="mn">商机 · 工作项类型</td><td><code>story</code></td><td>opportunities（表B）</td></tr>
        <tr><td class="mn">订单 · 单据编号（对接键）</td><td><code>name</code>（例 <code>ZHBR260907070</code>）</td><td>orders ↔ ERP</td></tr>
      </tbody>
    </table>
    <div class="callout"><b>集团归集口径（硬性）</b>：集团维度只能用 ERP 表的 <code>集团客户#</code> 列做归集键 —— <b>不能用客户名关键词匹配</b>。实测：某集团客户名不含集团关键字的抬头会被整个漏掉，实测漏 <b>68.6%%</b> 金额。<br><span class="src">出处：<code>脚本/统计_天隆_按抬头_货号统计.py</code> 交叉核对输出「2026 集团归集总额 1228060.00 ｜ 客户名含『天隆』385310.00 ｜ 差 842750.00」→ 842750 / 1228060 ≈ 68.6%%。</span></div>
  </div>

  <!-- ========== 06 当前状态与上线路径 ========== -->
  <div class="sec" id="sec-6">
    <div class="sec-hd"><span class="sec-no">06</span><h2>当前状态与上线路径</h2><span class="en">Status &amp; rollout</span></div>
    <p><b>当前状态：DEMO 原型，示例数据，未上线。</b>所有页面（基线 <code>工具/客户拉通大表_demo.html</code>、仪表盘 <code>工具/客户拉通大表_仪表盘.html</code>）为<b>零外链、数据内嵌</b>的静态页，不联网；后端七表结构已设计但尚未建库。</p>
    <div class="steps">
      <div class="step"><span class="n">1</span>建 CloudBase 集合（七表 + 宽表 / 列定义表）</div>
      <div class="step"><span class="n">2</span>权限与鉴权（按角色分字段 · 服务端二次校验）</div>
      <div class="step"><span class="n">3</span>小范围试点（大客户部）</div>
      <div class="step"><span class="n">4</span>推广</div>
    </div>
    <p style="margin-top:10px"><b>待确认项</b>（沿用数据源盘点 §5 / 结构清单 §7）：</p>
    <ul class="tick">%s</ul>
  </div>

  <!-- ========== 07 可度量的价值 ========== -->
  <div class="sec" id="sec-7">
    <div class="sec-hd"><span class="sec-no">07</span><h2>可度量的价值</h2><span class="en">Measurable value</span></div>
    <p>每项指标都给出<b>上线后从哪张表 / 哪个字段取数</b>（DEMO 为示例值，不在此页给出执行数字）：</p>
    <table class="mt">
      <thead><tr><th style="width:230px">度量指标</th><th style="width:250px">口径 / 公式</th><th>上线后取数位置</th></tr></thead>
      <tbody>%s</tbody>
    </table>
    <p class="src">口径来源：结构清单 §1（表 D/E/G）、§9.8、§11.2、§12.3、§13.1 / §13.2。替换周期基线 ≈60 天（天隆 <code>M5244-ZJ4</code> → <code>MD2104-003006</code>，客诉 2026-07-01 → 替换完成 2026-08-30，出处：结构清单 §13.2）。</p>
  </div>

  <div class="foot-note">
    <b>本页性质</b>：DEMO 原型的产品介绍（内部使用）· 生成日期 %s · 生成器 <code>脚本/build_customer_thread_intro.py</code>，验收 <code>脚本/verify_customer_thread_intro.py</code>。<br>
    <b>口径来源</b>：结构清单 <code>知识沉淀/客户拉通大表_结构清单_20261010.md</code>（§1–§13）、数据源盘点 <code>知识沉淀/客户拉通大表_可复用数据源盘点_20261010.md</code>、原型 <code>工具/客户拉通大表_demo.html</code>。<br>
    <b>声明</b>：本页描述的功能名、字段名、货号、集合名一律与上述素材逐字一致；未上线，示例数据；不含任何执行数字。
  </div>
</div>""" % (svg_two_layers(), mech, svg_data_flow(), pending_li, metrics_rows, BUILD_DATE)


def build_html():
    body = build_body()
    html = ('<!DOCTYPE html>\n<html lang="zh-CN">\n<head>\n<meta charset="utf-8">\n'
            '<meta name="viewport" content="width=device-width,initial-scale=1">\n'
            '<title>客户拉通大表 · 产品介绍（DEMO 原型 · 示例数据，未上线）</title>\n'
            '<style>\n' + CSS + '\n</style>\n</head>\n<body>\n' + body + '\n</body>\n</html>\n')
    html = BK.inject(html, theme='dark', tag='客户拉通大表 · DEMO',
                     footer_extra='DEMO 原型 · 示例数据，未上线 · 内部使用')
    return html


def main():
    os.makedirs(os.path.dirname(OUT), exist_ok=True)
    html = build_html()
    with io.open(OUT, 'w', encoding='utf-8') as f:
        f.write(html)
    print('✅ 已生成 %s（%d 字节）' % (OUT, len(html.encode('utf-8'))))
    # 生成期自检（不替代 playwright 验收）
    v = BK.verify(html)
    bad = [k for k, ok in v.items() if not ok]
    print('   brand_kit：%s' % ('✅ 8 项齐备' if not bad else '❌ 缺 ' + ', '.join(bad)))
    for k in ('已上线', '生产环境正式运行'):
        print('   负面断言「%s」：%s' % (k, '✅ 不存在' if k not in html else '❌ 出现'))
    return 0


if __name__ == '__main__':
    sys.exit(main())
