#!/usr/bin/env python3
"""Build the final smart-canteen sales/channel playbook and its tool workbook.

The script intentionally keeps all claims and evidence boundaries explicit.  It
generates reviewable Markdown/CSV/HTML in Git and can create a binary XLSX in a
separate delivery directory using direct OOXML (no openpyxl round-trip).
"""

from __future__ import annotations

import argparse
import csv
import html
import shutil
import subprocess
import sys
import tempfile
from pathlib import Path
from xml.sax.saxutils import escape, quoteattr


HERE = Path(__file__).resolve().parent
REPO = HERE.parents[5]


PACKAGES = [
    {
        "code": "S00",
        "name": "统一业务底座",
        "customer_signal": "人员、补贴、餐次、支付、退款和报表分散在多套系统",
        "problem": "规则靠人工、入口各记各账、异常无法追溯",
        "software": "组织/人员/账户、消费策略、订单、支付退款、结算对账、报表运营",
        "hardware": "按场景接入消费终端、读卡器、扫码、人脸等身份入口",
        "result": "先形成统一账户、统一策略、统一订单、统一报表",
        "proof": "四个重点项目共同验证",
    },
    {
        "code": "S10",
        "name": "高频交易与多入口",
        "customer_signal": "午餐高峰排队；消费机、闸机、线上入口并行",
        "problem": "高峰拥堵、入口规则不一、支付与退款难统一",
        "software": "实时交易、策略校验、多支付、退款、设备状态、统一订单",
        "hardware": "消费终端、闸机/通道读卡、身份介质、网络与边缘设备",
        "result": "不同入口执行同一套身份与消费规则并统一入账",
        "proof": "机场、赛迪",
    },
    {
        "code": "S20",
        "name": "线上订餐与智能取餐",
        "customer_signal": "员工错峰、预订、外带；备餐和取餐状态经常脱节",
        "problem": "现场排队、错拿漏拿、档口备餐无依据、订单闭环断裂",
        "software": "移动端/H5、菜单与预订、备餐履约、取餐核验、消息与报表",
        "hardware": "取餐柜、厨房打印/叫号、档口终端、扫码/刷脸核验",
        "result": "预订分流—备餐—存柜—核验取餐—订单完成",
        "proof": "金斯瑞",
    },
    {
        "code": "S30",
        "name": "补贴称重与营养",
        "customer_signal": "按量取餐、补贴复杂、浪费难控、营养信息不足",
        "problem": "绑盘、称重、计价、补贴、结算任何一步脱节都会错账",
        "software": "补贴/额度、绑盘、菜品与价格、称重计价、营养分析、异常修正",
        "hardware": "绑盘机、智能餐盘、单/双秤称重台、保温售餐台、营养屏",
        "result": "每次取餐动作可计量、可结算、可追溯，并支持节约运营",
        "proof": "西康宾馆",
    },
    {
        "code": "S40",
        "name": "食安证据链",
        "customer_signal": "监管检查频繁；晨检、留样、温湿度和后厨视频各自为政",
        "problem": "记录靠纸、过程不可追溯、异常发现晚、检查时临时补材料",
        "software": "人员晨检、留样、消毒、温湿度、AI事件、台账与监管报表",
        "hardware": "晨检仪、留样柜、测温与检测设备、摄像机、AI分析盒、NVR/大屏",
        "result": "把日常动作沉淀成连续、可查询的食安证据",
        "proof": "江西206、滨州、网信办等项目材料；对外需按证据分级",
    },
    {
        "code": "S50",
        "name": "采购库存与经营",
        "customer_signal": "多供应商、多仓、多基地；采购、验收、库存和成本脱节",
        "problem": "账实不一致、价格难比、损耗难追、经营分析滞后",
        "software": "计划采购、供应商、询报价、验收入库、库存、成本与经营分析",
        "hardware": "智能收货秤、双摄验收秤、AI识别与经营分析大屏",
        "result": "从采购到消耗建立可核验的经营链路",
        "proof": "三全、滨州、网信办等方案/项目证据；以项目验收为准",
    },
    {
        "code": "S60",
        "name": "集成、部署与服务",
        "customer_signal": "已有一卡通、财务/OA/SAP；要求本地化、国产化或集团多组织",
        "problem": "接口边界不清、适配承诺过早、上线后责任不清",
        "software": "接口适配、主数据同步、财务凭证、单点登录、监控运维、权限审计",
        "hardware": "服务器/云资源、网络安全、接口网关及项目所需基础设施",
        "result": "把第三方系统和部署条件纳入可验收、可运维的交付范围",
        "proof": "已有一卡通/财务/SAP/OA等接口证据；国产化和高可用需专项矩阵",
    },
]


COMMERCIAL_PACKAGES = [
    ["C01", "企业员工食堂标准包", "大型企业、制造基地、研发园区", "S00 + S20；按需叠加 S10/S30", "先解决高峰、预订取餐、补贴与员工体验", "金斯瑞 / 西康宾馆"],
    ["C02", "政企园区多入口包", "机场、政府、国企、产业园、物业", "S00 + S10 + S60；按需叠加 S40", "先统一身份、策略、入口、订单与对账", "机场 / 赛迪"],
    ["C03", "学校与强监管食安包", "学校、教育主管单位、团餐集团", "S00 + S40 + S50 + S60；按需叠加 S30", "先建立食安证据链，再贯通采购、库存和营养", "江西206 / 滨州 / 网信办（按证据等级使用）"],
]


CASES = [
    {
        "id": "CASE-01",
        "project": "机场",
        "scene": "大体量员工食堂、高频交易",
        "people": "观察期 8,039 个活跃用户",
        "problem": "多餐次连续高峰；卡、码、线上支付并存；退款与报表要统一",
        "solution": "S00 + S10：统一账户/策略/订单，以消费终端承接主交易并接入外部订单",
        "metrics": "895,908 笔有效订单；订单流水 1,942.78 万元；终端订单占 89.43%",
        "validated": "真实、高频、多支付团餐交易闭环",
        "boundary": "不等于弹性伸缩、负载均衡、RTO/RPO已通过专项验收；流水不等于收入",
        "pitch": "当客户担心高峰和多支付混乱时，用机场证明我们能把现场交易主链路跑起来。",
        "image": "../2026-08-06-airport-high-frequency-scene/assets/airport-high-frequency-panorama.png",
        "detail": "../2026-08-06-airport-high-frequency-scene/airport-high-frequency-scene.html",
        "image_note": "AI 场景还原，仅用于说明动线与设备协同，不作为现场验收证据。",
    },
    {
        "id": "CASE-02",
        "project": "金斯瑞",
        "scene": "大型企业、多档口、线上预订与取餐",
        "people": "近 30 日 6,459 个活跃下单账户；25.9 万笔订单（快照口径）",
        "problem": "多档口午高峰；线上预订、备餐、存柜和取餐容易脱节",
        "solution": "S00 + S20：移动订餐、档口备餐、智能取餐柜、身份核验与订单履约",
        "metrics": "有效订单证据 746,681 笔；7,604 个注册账户；近 30 日平均日活 3,688.87",
        "validated": "企业前厅最完整的多档口 + 线上订餐 + 取餐硬件协同",
        "boundary": "不同快照的累计/近30日口径不得混加；现场图片外发需项目授权",
        "pitch": "当客户既要现场消费又要线上预订和错峰取餐时，用金斯瑞讲完整前厅。",
        "image": "../2026-08-06-genscript-front-hall-scene/assets/genscript-front-hall-panorama.png",
        "detail": "../2026-08-06-genscript-front-hall-scene/genscript-front-hall-scene.html",
        "image_note": "AI 场景示意；真实现场图已建立微盘索引，对外使用需授权。",
    },
    {
        "id": "CASE-03",
        "project": "赛迪物业",
        "scene": "写字楼、产业园、物业型多入口消费",
        "people": "观察期 3,677 个活跃用户",
        "problem": "消费机、闸机和线上订餐入口不同，容易形成多套规则和多本账",
        "solution": "S00 + S10：不同入口调用统一身份和策略，最终沉淀为统一订单与报表",
        "metrics": "145,955 笔有效订单；订单流水 268.34 万元；闸机订单占 18.55%",
        "validated": "园区多入口业务可以统一账户、统一策略、统一订单、统一报表",
        "boundary": "不能用运行数据反推具体硬件型号或最终安装验收数量",
        "pitch": "当物业说入口多、商户多、规则杂时，先卖统一规则和统一账，不先卖闸机型号。",
        "image": "../2026-08-06-saidi-multi-entry-scene/assets/saidi-multi-entry-panorama.png",
        "detail": "../2026-08-06-saidi-multi-entry-scene/saidi-multi-entry-scene.html",
        "image_note": "AI 场景示意；真实现场图仅限授权后的客户案例材料。",
    },
    {
        "id": "CASE-04",
        "project": "西康宾馆",
        "scene": "宾馆/机构员工食堂、补贴、按量取餐",
        "people": "349 条员工记录、348 个有效账户；近期 236 个活跃用户",
        "problem": "补贴和超额规则复杂；绑盘、称重、计价、结算任一步脱节都会错账",
        "solution": "S00 + S30：补贴策略、绑盘机、智能餐盘、称重台、计价结算与异常处理",
        "metrics": "累计 121,920 笔有效订单；近期 30 日 7,589 笔；近期 100% 来自绑盘链路",
        "validated": "补贴—绑盘—称重—计价—结算的连续业务状态链",
        "boundary": "历史累计金额异常仍需数据治理；不对外表述为全部数据已完全闭环",
        "pitch": "当客户要按量取餐、控制浪费又要保留补贴时，用西康讲软硬件一体闭环。",
        "image": "../2026-08-06-xikang-subsidy-weighing-scene/assets/xikang-subsidy-weighing-panorama.png",
        "detail": "../2026-08-06-xikang-subsidy-weighing-scene/xikang-subsidy-weighing-scene.html",
        "image_note": "AI 场景还原，仅用于产品讲解；现场图对外发布待授权。",
    },
]


DISCOVERY = [
    ["D01", "客户与场景", "是什么类型单位？几个园区/食堂/餐厅？", "判断 C01/C02/C03 主包", "未明确场景不得直接报设备"],
    ["D02", "服务人群", "员工/学生/访客/亲友/招待分别多少人？", "决定账户与策略复杂度", "人数要区分注册、活跃、峰值"],
    ["D03", "高峰压力", "午高峰多少人、多久完成、多少餐线/档口？", "判断 S10 终端与入口配置", "没有压测不得承诺并发数字"],
    ["D04", "现有流程", "从进门、选餐、结算到对账，哪一步最痛？", "找第一价值点", "一次只锁定 1—2 个首要问题"],
    ["D05", "消费策略", "补贴、额度、餐次、本人/亲友/招待/外卖规则？", "判断 S00 策略范围", "规则必须形成可验收示例"],
    ["D06", "订餐取餐", "是否需要预订、外带、取餐柜、叫号、备餐打印？", "判断 S20", "先画履约状态，不先报柜机"],
    ["D07", "称重营养", "是否按量取餐、控浪费、做营养展示或个体建议？", "判断 S30", "营养建议边界需单独确认"],
    ["D08", "食安监管", "晨检、留样、温湿度、后厨视频、监管台账怎么做？", "判断 S40", "检查项必须对应证据输出"],
    ["D09", "采购库存", "供应商、收货、库存、成本、损耗是否要统一？", "判断 S50", "设备量纲与工程量分开"],
    ["D10", "现有系统", "已有一卡通、财务、OA、SAP、门禁或支付平台吗？", "判断 S60 接口清单", "‘可对接’不等于已完成"],
    ["D11", "部署环境", "云/本地？网络区划？国产 CPU/OS/DB/中间件要求？", "形成适配矩阵", "无测试证据不承诺国产化"],
    ["D12", "硬件现状", "哪些设备利旧、哪些新增、现场型号和协议是什么？", "形成兼容与替换策略", "必须看铭牌/协议/现场照片"],
    ["D13", "验收目标", "用什么指标证明项目成功？", "锁定 G2 验收矩阵", "主流程、数量、接口、性能分别验"],
    ["D14", "项目边界", "预算、工期、决策人、使用人、IT/财务/后勤谁负责？", "建立推进图", "缺决策人和工期不进入正式报价"],
    ["D15", "售后运营", "谁维护设备、处理退款、调整策略、看报表？", "定义 G6 移交与续费", "未定义 owner 不关闭交付"],
]


OBJECTIONS = [
    ["O01", "我们只需要几台消费机", "可以从终端切入，但先确认补贴、身份、退款和对账，否则设备只是新的孤岛。", "先做 30 分钟业务流程图，再给 S00+S10 最小组合。", "不要只按台数报价"],
    ["O02", "我们已经有一卡通", "我们不要求替换一卡通，重点是把团餐策略、订单和硬件动作接入现有身份体系。", "收集主数据、余额/补贴、支付、退款、对账接口清单。", "接口须联调与验收"],
    ["O03", "你们能扛多少并发", "机场等项目证明了真实高频运行，但并发上限要以目标部署拓扑和压测报告回答。", "输出目标峰值、压测场景、容量与故障演练计划。", "不凭订单量承诺 HA/RTO/RPO"],
    ["O04", "能不能国产化", "可以按 CPU、操作系统、数据库、中间件逐项评估，最终以适配矩阵和测试结果为准。", "在 G2 冻结环境版本与验收用例。", "禁止口头承诺全面适配"],
    ["O05", "能不能接财务/SAP/OA", "已有相关项目/代码证据，但每家主数据、凭证和对账口径不同。", "先做接口清单、字段映射、异常和重试机制评估。", "支持对接≠本项目已验收"],
    ["O06", "硬件能不能换别家", "可评估，但协议、固件、驱动、性能和售后备件都要纳入适配。", "走 G3 ECN，重新确认价格、交期、接口和验收。", "禁止口头替换"],
    ["O07", "为什么比普通收银贵", "客户买到的是身份、补贴、订餐、硬件、结算和运营的闭环，而不是单机收款。", "用客户当前人工成本、排队、错账和监管风险算价值。", "不虚构 ROI"],
    ["O08", "怎么保证不会货不对版", "所有可销售项使用内部 SKU + 官方型号 + 版本/规格，并经过 G0—G6 门禁。", "报价附 SKU 版 BOM，出库登记铭牌/SN，验收按 SKU。", "CPT-FT248 等冲突项暂停新售"],
    ["O09", "上线后谁维护", "售后接管必须包含项目、SKU、SN、版本、质保、备件、权限和联系人。", "签约前明确 SLA 和各方职责。", "服务 SKU/SLA 未冻结前不做确定承诺"],
    ["O10", "能不能先做试点", "可以，用一个高价值主流程验证，再决定扩容。", "锁定试点人群、点位、周期、通过指标和退出条件。", "试点不等于无限定制"],
]


CHANNEL_RACI = [
    ["线索与客户关系", "主责", "支持", "渠道负责关系、预算与决策链；康比特提供场景材料"],
    ["需求发现与现场调研", "协同", "主责", "共同完成 15 问；康比特负责技术与流程判断"],
    ["标准组合与方案", "协同", "主责", "渠道不得自行新增未定标型号或能力承诺"],
    ["报价与商务", "主责", "协同", "报价必须引用内部 SKU；价格与商务政策按授权执行"],
    ["接口/国产化/性能承诺", "不得单独承诺", "主责", "必须经过适配、压测或验收评审"],
    ["合同与型号冻结", "协同", "主责", "G2 之前不得签约或下采购"],
    ["采购、出库与实施", "支持现场协调", "主责", "铭牌/SN/版本和变更均进入台账"],
    ["验收与回款材料", "主责商务协同", "主责技术证据", "按数量、主流程、接口、性能分别验收"],
    ["售后与续费增购", "主责客户经营", "主责产品服务", "共同做运行复盘与二次需求识别"],
]


ASSETS = [
    ["A01", "本手册 HTML", "销售新人/授权渠道主入口", "smart-canteen-sales-channel-playbook.html", "内部与授权渠道", "项目名称、指标、图片外发前复核"],
    ["A02", "本手册 Markdown", "AI 复用与后续维护真源", "smart-canteen-sales-channel-playbook.md", "内部", "不直接外发"],
    ["A03", "销售工具 Excel", "需求访谈、组合选择、异议与渠道协同", "智慧食堂销售与渠道作战工具.xlsx（交付目录）", "内部与授权渠道", "不含价格"],
    ["A04", "四项目能力总结", "机场/金斯瑞/赛迪/西康案例证据", "../2026-08-06-four-project-product-proof/four-project-product-proof.html", "内部；外发需授权", "现场图和客户指标需复核"],
    ["A05", "标准产品项目定标", "型号、SKU、28项目证据与交付门禁", "../2026-08-06-standard-product-project-model-gate/standard-product-project-model-gate.html", "内部", "A 类型号证据不等于完整验收"],
    ["A06", "产品定标基表 Excel", "报价/采购/交付/售后协同", "../2026-08-06-standard-product-project-model-gate/AI营养智慧食堂-标准产品项目定标基表.xlsx", "内部", "受控使用"],
    ["A07", "机场场景页", "高频交易", "../2026-08-06-airport-high-frequency-scene/airport-high-frequency-scene.html", "内部；外发需授权", "AI 场景图不作验收证据"],
    ["A08", "金斯瑞场景页", "企业完整前厅", "../2026-08-06-genscript-front-hall-scene/genscript-front-hall-scene.html", "内部；外发需授权", "快照口径需注明"],
    ["A09", "赛迪场景页", "园区多入口", "../2026-08-06-saidi-multi-entry-scene/saidi-multi-entry-scene.html", "内部；外发需授权", "不反推设备型号"],
    ["A10", "西康场景页", "补贴称重营养", "../2026-08-06-xikang-subsidy-weighing-scene/xikang-subsidy-weighing-scene.html", "内部；外发需授权", "不扩大历史金额结论"],
]


BOUNDARIES = [
    ["真实订单与活跃", "可证明系统被真实使用并承载对应业务链路", "不能单独证明弹性伸缩、负载均衡、动静/冷热分离或 RTO/RPO"],
    ["订单流水", "可描述订单口径的交易规模", "不等于合同收入、确认收入或回款"],
    ["现场照片/设备注册", "可证明某阶段现场或设备存在", "不等于最终安装数量和客户验收"],
    ["接口代码/项目资料", "可证明有适配基础或实施记录", "不等于当前客户已联调并验收"],
    ["国产化方案", "可进入环境适配评估", "无 CPU/OS/DB/中间件矩阵和测试记录不得承诺全面适配"],
    ["AI 场景图", "可直观解释人、设备和流程关系", "不是真实现场照片，不作品牌、型号、数量或验收证据"],
    ["标准型号 A 类证据", "可写成存在型号级项目应用证据", "仍需铭牌/SN、出库和验收补齐后才是完整闭环"],
]


QUICKSTART = [
    ["1", "先问场景", "客户是谁、多少人、几个点位、哪一步最痛"],
    ["2", "再选组合", "从 C01/C02/C03 选主包，再叠加 S10—S60"],
    ["3", "用案例证明", "机场讲高频，金斯瑞讲完整前厅，赛迪讲多入口，西康讲称重"],
    ["4", "先锁能力边界", "把已验证、需适配、待客户确认分开"],
    ["5", "最后定型号", "内部 SKU + 官方型号 + 版本/规格 + 数量 + 必选配件"],
    ["6", "按门禁交付", "G0 建档—G2 冻结—G4 出库—G5 验收—G6 售后"],
]


def write_csv(path: Path, header: list[str], rows: list[list[str]]) -> None:
    with path.open("w", encoding="utf-8-sig", newline="") as fh:
        writer = csv.writer(fh, lineterminator="\n")
        writer.writerow(header)
        writer.writerows(rows)


def md_table(headers: list[str], rows: list[list[str]]) -> str:
    def clean(value: str) -> str:
        return str(value).replace("|", "\\|").replace("\n", "<br>")

    lines = ["| " + " | ".join(map(clean, headers)) + " |"]
    lines.append("|" + "|".join(["---"] * len(headers)) + "|")
    lines.extend("| " + " | ".join(clean(v) for v in row) + " |" for row in rows)
    return "\n".join(lines)


def build_markdown() -> str:
    pkg_rows = [[p[k] for k in ("code", "name", "customer_signal", "problem", "software", "hardware", "result", "proof")] for p in PACKAGES]
    case_rows = [[c[k] for k in ("project", "scene", "people", "problem", "solution", "metrics", "validated", "boundary")] for c in CASES]
    lines = [
        "# 智慧营养团餐销售与渠道作战手册",
        "",
        "> 版本：V1.0｜日期：2026-08-06｜适用：销售新人、售前、交付、售后与授权渠道",
        "> 分发边界：内部与授权渠道评审。客户名称、运营指标与现场图片对外使用前必须完成授权和口径复核。",
        "",
        "## 一句话产品定位",
        "",
        "我们卖的不是一台消费机或一套后台菜单，而是把**身份与账户—消费策略—订餐/取餐—硬件动作—支付结算—对账运营—营养食安增值**连接成一条可运行、可验收、可持续服务的团餐业务闭环。",
        "",
        "## 新人三分钟：按这个顺序卖",
        "",
        md_table(["步骤", "动作", "做到什么"], QUICKSTART),
        "",
        "## 客户买的不是功能，而是五个结果",
        "",
        "1. 高峰期更快：身份、策略和支付在入口处快速完成，减少排队。",
        "2. 规则自动执行：补贴、额度、餐次、本人/亲友/招待等不再靠人工记忆。",
        "3. 多入口一本账：现场消费、线上预订、取餐柜、闸机和称重进入统一订单。",
        "4. 软硬件不脱节：设备动作成为业务状态，异常可追溯。",
        "5. 可交付、可运营：从报价型号到出库、验收、售后有同一条产品主数据。",
        "",
        "## 七个销售产品包",
        "",
        "> S00—S60 是销售组合码，不代替正式内部 SKU。正式报价必须从产品定标基表选择唯一 SKU 和官方型号。",
        "",
        md_table(["组合码", "产品包", "客户信号", "客户问题", "软件", "硬件", "结果", "项目证明"], pkg_rows),
        "",
        "## 三个可直接复制的商业标准包",
        "",
        md_table(["编号", "标准包", "目标客户", "建议组合", "第一价值点", "优先案例"], COMMERCIAL_PACKAGES),
        "",
        "## 四个最强案例怎么讲",
        "",
        md_table(["项目", "场景", "人群", "问题", "解决方案", "运行指标", "验证能力", "边界"], case_rows),
        "",
        "四个项目累计有效订单证据合计 **1,910,464 笔**。该数字是不同项目、不同观察窗口的证据汇总，只用于说明真实运行规模，不作为同周期经营统计。",
        "",
        "## 销售发现 15 问",
        "",
        md_table(["ID", "主题", "必问问题", "用途", "停止规则"], DISCOVERY),
        "",
        "## 售前—交付—售后七道门",
        "",
        "- G0 商机建档：场景、人群、餐线、点位、接口、部署环境不全，不出标准报价。",
        "- G1 标准选型：使用内部 SKU、官方型号、数量和必选配件；禁止自由文本型号。",
        "- G2 型号冻结：样品/铭牌、接口协议、版本、国产化环境与验收口径一起冻结。",
        "- G3 ECN 变更：替代、减配、升级必须评估价格、交期、接口和验收。",
        "- G4 出库移交：采购型号、到货铭牌、装箱单一致，并登记 SN/MAC/固件/软件版本。",
        "- G5 安装验收：分别验证数量、主流程、接口、性能、照片和客户签字。",
        "- G6 售后接管：项目、SKU、SN、版本、质保、备件、权限与联系人齐备。",
        "",
        "## 常见异议怎么回答",
        "",
        md_table(["ID", "客户异议", "回答框架", "下一步", "边界"], OBJECTIONS),
        "",
        "## 渠道怎么与我们配合",
        "",
        md_table(["环节", "渠道", "康比特", "协作规则"], CHANNEL_RACI),
        "",
        "## 30 秒标准话术",
        "",
        "> 我们做的不是单一收银设备，而是智慧营养团餐的业务底座。它能把人员身份、补贴和消费规则、现场与线上订单、取餐柜/闸机/称重台等硬件、支付退款和财务对账接成一条闭环。机场、金斯瑞、赛迪、西康宾馆已经分别验证了高频交易、企业完整前厅、园区多入口和补贴称重四类场景。我们会先根据客户人群和痛点选标准组合，再冻结 SKU 和型号，确保售前、交付和售后拿到的是同一套产品。",
        "",
        "## 能力证据与承诺边界",
        "",
        md_table(["现有证据", "可以证明", "不能扩大"], BOUNDARIES),
        "",
        "## 资产导航",
        "",
        md_table(["ID", "资产", "用途", "路径", "分发范围", "注意"], ASSETS),
        "",
        "## 销售下一步",
        "",
        "第一次客户沟通结束后，必须留下五个输出：**一张现状流程图、一套建议组合、一份适配差距、一个证明案例、一张型号冻结清单**。没有这五项，就还没有从聊天进入可交付商机。",
        "",
        "## 当前产品治理拍板",
        "",
        "- 新销售软件统一采用 V2.0；V1.0/V6.5 只作为存量版本维护。",
        "- `CPT-FT248` 因一号两物暂停新售，待平托盘/分隔盘拆分官方型号。",
        "- 双摄智能收货秤统一为 `C-F160`，不得再写成 `C-F150（双摄版）`。",
        "- P301/P50/P51/P308、TB600 等实物型号需 OEM/铭牌证据后才能建立受控别名。",
        "- 服务器/大屏采用基础硬件 SKU + 软件/部署包；云服务、实施、维保需补服务 SKU 与 SLA。",
        "",
        "## 证据来源",
        "",
        "- 四项目产品证明与各项目场景页。",
        "- 28 项目主档与生产库只读聚合快照。",
        "- 用户提供的 2026 数科事业部销售数据与 AI 营养智慧食堂标准产品方案。",
        "- 标准产品—项目定标与交付门禁基表。",
        "",
        "**审阅状态：待销售负责人、产品负责人、交付负责人和渠道负责人联合确认。**",
    ]
    return "\n".join(lines) + "\n"


def table_html(headers: list[str], rows: list[list[str]], compact: bool = False) -> str:
    cls = "compact" if compact else ""
    head = "".join(f"<th>{html.escape(h)}</th>" for h in headers)
    body = "".join("<tr>" + "".join(f"<td>{html.escape(str(v))}</td>" for v in row) + "</tr>" for row in rows)
    return f'<div class="table-wrap"><table class="{cls}"><thead><tr>{head}</tr></thead><tbody>{body}</tbody></table></div>'


def build_html() -> str:
    package_cards = "\n".join(
        f'''<article class="package-card" id="{p['code'].lower()}">
          <div class="package-head"><span class="package-code">{p['code']}</span><h3>{html.escape(p['name'])}</h3></div>
          <p class="signal"><b>听到这些话：</b>{html.escape(p['customer_signal'])}</p>
          <div class="package-grid"><div><span>问题</span><p>{html.escape(p['problem'])}</p></div><div><span>软件</span><p>{html.escape(p['software'])}</p></div><div><span>硬件</span><p>{html.escape(p['hardware'])}</p></div><div><span>结果</span><p>{html.escape(p['result'])}</p></div></div>
          <p class="proof"><b>优先证明：</b>{html.escape(p['proof'])}</p>
        </article>'''
        for p in PACKAGES
    )
    case_sections = "\n".join(
        f'''<article class="case" id="{c['id'].lower()}">
          <div class="case-visual"><img src="{html.escape(c['image'])}" alt="{html.escape(c['project'])}场景化产品全景图"><p>{html.escape(c['image_note'])}</p></div>
          <div class="case-copy">
            <div class="eyebrow">{c['id']} · {html.escape(c['scene'])}</div><h3>{html.escape(c['project'])}</h3>
            <p class="people">服务人群｜{html.escape(c['people'])}</p>
            <dl><div><dt>客户的问题</dt><dd>{html.escape(c['problem'])}</dd></div><div><dt>我们的组合</dt><dd>{html.escape(c['solution'])}</dd></div><div><dt>运行证据</dt><dd>{html.escape(c['metrics'])}</dd></div><div><dt>验证了什么</dt><dd>{html.escape(c['validated'])}</dd></div></dl>
            <blockquote>{html.escape(c['pitch'])}</blockquote>
            <p class="boundary"><b>不能扩大：</b>{html.escape(c['boundary'])}</p>
            <a class="text-link" href="{html.escape(c['detail'])}">查看该项目完整场景页 →</a>
          </div>
        </article>'''
        for c in CASES
    )
    objection_html = "\n".join(
        f'''<details><summary><span>{html.escape(row[0])}</span>{html.escape(row[1])}</summary><div class="answer"><p><b>怎么回答：</b>{html.escape(row[2])}</p><p><b>推进动作：</b>{html.escape(row[3])}</p><p class="boundary"><b>边界：</b>{html.escape(row[4])}</p></div></details>'''
        for row in OBJECTIONS
    )
    quick_cards = "".join(
        f'<div class="quick-card"><span>{r[0]}</span><h3>{html.escape(r[1])}</h3><p>{html.escape(r[2])}</p></div>' for r in QUICKSTART
    )
    case_metrics = [
        ("191.0 万+", "四项目有效订单证据合计*"),
        ("28", "项目资产池"),
        ("63", "标准产品候选"),
        ("11", "已有型号级一致证据的项目"),
    ]
    metrics_html = "".join(f'<div><strong>{v}</strong><span>{html.escape(k)}</span></div>' for v, k in case_metrics)
    commercial = table_html(["编号", "标准包", "目标客户", "建议组合", "第一价值点", "优先案例"], COMMERCIAL_PACKAGES)
    discovery = table_html(["ID", "主题", "必问问题", "用途", "停止规则"], DISCOVERY, True)
    channel = table_html(["环节", "渠道", "康比特", "协作规则"], CHANNEL_RACI, True)
    boundaries = table_html(["现有证据", "可以证明", "不能扩大"], BOUNDARIES, True)
    assets = table_html(["ID", "资产", "用途", "路径", "分发范围", "注意"], ASSETS, True)
    return f'''<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>智慧营养团餐销售与渠道作战手册</title>
<style>
:root{{--ink:#17324d;--ink-2:#29475f;--green:#1e7a62;--green-2:#2c9a7c;--mint:#eaf4f0;--paper:#f7f8f5;--white:#fff;--line:#d8e1df;--amber:#b8752a;--amber-bg:#fbf2e4;--red:#9a4e45;--muted:#6d7c85;--shadow:0 18px 50px rgba(23,50,77,.10)}}
*{{box-sizing:border-box}}html{{scroll-behavior:smooth}}body{{margin:0;background:var(--paper);color:#233640;font-family:"PingFang SC","Microsoft YaHei","Noto Sans CJK SC",system-ui,sans-serif;line-height:1.72}}a{{color:inherit}}img{{max-width:100%;display:block}}.topbar{{position:sticky;top:0;z-index:30;background:rgba(247,248,245,.94);backdrop-filter:blur(16px);border-bottom:1px solid var(--line)}}.nav{{max-width:1200px;margin:auto;padding:12px 24px;display:flex;align-items:center;gap:22px;overflow:auto;white-space:nowrap}}.brand{{display:flex;align-items:center;gap:10px;margin-right:auto;font-size:13px;font-weight:700;color:var(--ink)}}.brand-mark{{width:28px;height:28px;border-radius:9px;background:linear-gradient(135deg,var(--ink),var(--green));display:grid;place-items:center;color:white}}.nav a{{font-size:13px;text-decoration:none;color:var(--ink-2)}}.nav a:hover{{color:var(--green)}}.hero{{background:radial-gradient(circle at 80% 10%,rgba(53,157,125,.20),transparent 32%),linear-gradient(135deg,#102b43 0%,#173b4f 52%,#1e6b5c 100%);color:white;padding:92px 24px 72px}}.hero-inner{{max-width:1200px;margin:auto;display:grid;grid-template-columns:1.25fr .75fr;gap:70px;align-items:end}}.kicker,.eyebrow{{font-size:12px;letter-spacing:.16em;text-transform:uppercase;font-weight:700;color:#72d4b5}}.hero h1{{font-size:clamp(42px,6vw,76px);line-height:1.03;margin:18px 0 24px;letter-spacing:-.04em}}.hero .lead{{font-size:20px;max-width:740px;color:rgba(255,255,255,.84)}}.hero-note{{font-size:12px;color:rgba(255,255,255,.62);margin-top:26px}}.hero-panel{{border:1px solid rgba(255,255,255,.22);background:rgba(255,255,255,.08);padding:28px;border-radius:22px}}.hero-panel h2{{margin:0 0 16px;font-size:18px}}.hero-panel ol{{padding-left:22px;margin:0}}.hero-panel li{{margin:8px 0;color:rgba(255,255,255,.82)}}.metrics{{max-width:1200px;margin:-30px auto 0;padding:0 24px;display:grid;grid-template-columns:repeat(4,1fr);gap:12px;position:relative;z-index:2}}.metrics div{{background:white;border:1px solid var(--line);box-shadow:var(--shadow);border-radius:18px;padding:22px}}.metrics strong{{display:block;color:var(--green);font-size:30px}}.metrics span{{font-size:12px;color:var(--muted)}}main{{max-width:1200px;margin:auto;padding:44px 24px 100px}}section{{padding:70px 0;border-bottom:1px solid var(--line)}}section:last-child{{border:0}}.section-head{{display:grid;grid-template-columns:.7fr 1.3fr;gap:56px;margin-bottom:34px;align-items:start}}.section-head h2{{margin:4px 0;font-size:38px;line-height:1.16;color:var(--ink);letter-spacing:-.03em}}.section-head p{{margin:8px 0;color:var(--muted);font-size:17px}}.quick-grid{{display:grid;grid-template-columns:repeat(3,1fr);gap:14px}}.quick-card{{background:white;border:1px solid var(--line);border-radius:18px;padding:24px;min-height:180px}}.quick-card span{{display:grid;place-items:center;width:34px;height:34px;border-radius:10px;background:var(--mint);color:var(--green);font-weight:800}}.quick-card h3{{margin:24px 0 6px;color:var(--ink)}}.quick-card p{{margin:0;color:var(--muted);font-size:14px}}.positioning{{background:white;border-left:5px solid var(--green);padding:30px 34px;border-radius:0 18px 18px 0;font-size:24px;color:var(--ink);box-shadow:var(--shadow)}}.value-chain{{display:grid;grid-template-columns:repeat(7,1fr);gap:8px;margin-top:24px}}.value-chain div{{position:relative;background:var(--ink);color:white;padding:20px 10px;text-align:center;border-radius:12px;font-size:13px}}.value-chain div:not(:last-child)::after{{content:"→";position:absolute;right:-12px;color:var(--green);font-weight:800;z-index:2}}.package-list{{display:grid;gap:16px}}.package-card{{background:white;border:1px solid var(--line);border-radius:20px;padding:26px 28px}}.package-head{{display:flex;align-items:center;gap:14px}}.package-head h3{{margin:0;color:var(--ink);font-size:23px}}.package-code{{font-weight:800;color:white;background:var(--green);border-radius:9px;padding:5px 10px}}.signal{{background:var(--mint);padding:12px 16px;border-radius:10px;color:var(--ink-2)}}.package-grid{{display:grid;grid-template-columns:repeat(4,1fr);gap:12px}}.package-grid>div{{border-top:2px solid var(--line);padding-top:12px}}.package-grid span{{font-size:11px;letter-spacing:.12em;color:var(--green);font-weight:800}}.package-grid p{{font-size:14px;margin:6px 0;color:#485e69}}.proof{{margin:18px 0 0;color:var(--muted);font-size:13px}}.case{{display:grid;grid-template-columns:1.08fr .92fr;gap:32px;background:white;border:1px solid var(--line);border-radius:24px;padding:18px;margin-bottom:24px;box-shadow:0 10px 30px rgba(23,50,77,.06)}}.case-visual img{{width:100%;height:100%;min-height:470px;object-fit:cover;border-radius:16px}}.case-visual p{{font-size:11px;color:var(--muted);padding:2px 6px;margin:8px 0 0}}.case-copy{{padding:24px 22px 24px 4px}}.case-copy h3{{font-size:42px;line-height:1;margin:12px 0 10px;color:var(--ink)}}.people{{color:var(--green);font-weight:700}}dl{{margin:22px 0}}dl div{{display:grid;grid-template-columns:90px 1fr;gap:16px;padding:12px 0;border-top:1px solid var(--line)}}dt{{font-weight:700;color:var(--ink)}}dd{{margin:0;color:#536770}}blockquote{{margin:20px 0;background:var(--ink);color:white;border-radius:14px;padding:18px 20px;font-weight:600}}.boundary{{background:var(--amber-bg);color:#744b22;padding:12px 15px;border-radius:10px;font-size:13px}}.text-link{{display:inline-block;margin-top:12px;color:var(--green);font-weight:700;text-decoration:none}}.table-wrap{{overflow:auto;background:white;border:1px solid var(--line);border-radius:16px}}table{{width:100%;border-collapse:collapse;min-width:900px}}th{{background:var(--ink);color:white;text-align:left;font-size:12px;letter-spacing:.04em;padding:14px 16px;position:sticky;top:0}}td{{padding:14px 16px;border-bottom:1px solid var(--line);vertical-align:top;font-size:14px}}tr:last-child td{{border-bottom:0}}tr:hover td{{background:#f5faf8}}table.compact td{{font-size:13px;padding:12px 14px}}.gates{{display:grid;grid-template-columns:repeat(7,1fr);gap:8px}}.gate{{background:white;border:1px solid var(--line);border-radius:14px;padding:16px;position:relative}}.gate b{{display:block;color:var(--green);font-size:18px}}.gate span{{font-weight:700;color:var(--ink);display:block;margin:8px 0}}.gate p{{font-size:12px;color:var(--muted);margin:0}}.scripts{{display:grid;grid-template-columns:1fr 1fr;gap:18px}}.script{{background:var(--ink);color:white;border-radius:20px;padding:28px}}.script h3{{color:#75d6b6;margin-top:0}}.script p{{color:rgba(255,255,255,.82)}}details{{background:white;border:1px solid var(--line);border-radius:14px;margin:10px 0}}summary{{cursor:pointer;padding:17px 20px;font-weight:700;color:var(--ink)}}summary span{{display:inline-block;color:var(--green);width:52px}}.answer{{padding:0 20px 18px}}.answer p{{margin:8px 0}}.channel-value{{display:grid;grid-template-columns:repeat(4,1fr);gap:14px;margin-bottom:24px}}.channel-value div{{background:var(--mint);border-radius:16px;padding:22px}}.channel-value b{{display:block;color:var(--green);font-size:24px}}.channel-value p{{margin:8px 0 0;color:var(--ink-2)}}.final-cta{{background:linear-gradient(135deg,var(--ink),#205c54);border-radius:26px;color:white;padding:46px;display:grid;grid-template-columns:1.1fr .9fr;gap:40px;align-items:center}}.final-cta h2{{font-size:38px;margin:0 0 12px}}.deliverables{{display:grid;gap:8px}}.deliverables span{{background:rgba(255,255,255,.10);border:1px solid rgba(255,255,255,.18);border-radius:10px;padding:10px 14px}}.note{{font-size:12px;color:var(--muted);margin-top:12px}}footer{{background:#102b43;color:rgba(255,255,255,.66);padding:30px 24px;text-align:center;font-size:12px}}
@media(max-width:900px){{.hero-inner,.section-head,.case,.scripts,.final-cta{{grid-template-columns:1fr}}.metrics{{grid-template-columns:repeat(2,1fr)}}.quick-grid{{grid-template-columns:repeat(2,1fr)}}.package-grid,.channel-value{{grid-template-columns:1fr 1fr}}.value-chain,.gates{{grid-template-columns:1fr}}.case-visual img{{min-height:300px}}}}
@media(max-width:560px){{.hero{{padding-top:60px}}.hero h1{{font-size:42px}}.hero .lead{{font-size:17px}}.metrics,.quick-grid,.package-grid,.channel-value{{grid-template-columns:1fr}}.section-head h2{{font-size:31px}}main{{padding-left:16px;padding-right:16px}}.nav{{padding:10px 16px}}.case{{padding:10px}}.case-copy{{padding:18px 10px}}.case-copy h3{{font-size:36px}}dl div{{grid-template-columns:1fr;gap:4px}}}}
@media print{{.topbar{{display:none}}body{{background:white;font-size:10pt}}.hero{{padding:34px 24px}}.hero h1{{font-size:34pt}}.metrics{{margin:12px auto 0}}section{{padding:30px 0;break-inside:auto}}.case,.package-card,.quick-card,details,.final-cta{{break-inside:avoid;box-shadow:none}}.case{{grid-template-columns:1fr 1fr}}.case-visual img{{min-height:0}}details .answer{{display:block}}.table-wrap{{overflow:visible}}table{{min-width:0}}th{{position:static}}a{{text-decoration:none}}}}
</style>
</head>
<body>
<nav class="topbar"><div class="nav"><div class="brand"><span class="brand-mark">K</span>KANGBITE DIGITAL</div><a href="#quick">新人开局</a><a href="#architecture">产品架构</a><a href="#packages">产品组合</a><a href="#cases">案例证据</a><a href="#discovery">需求发现</a><a href="#delivery">交付门禁</a><a href="#channel">渠道协作</a><a href="#boundaries">承诺边界</a></div></nav>
<header class="hero"><div class="hero-inner"><div><div class="kicker">智慧营养团餐 · 销售与渠道作战手册 V1.0</div><h1>卖的是业务闭环<br>不是设备堆砌</h1><p class="lead">让销售新人知道第一句话怎么说、下一步怎么问；让渠道伙伴知道我们能解决什么、用什么项目证明、怎样把项目稳稳交付。</p><p class="hero-note">内部与授权渠道使用｜客户名称、运营指标和现场图片对外使用前须完成授权与口径复核</p></div><aside class="hero-panel"><h2>记住唯一销售顺序</h2><ol><li>先问场景、人群和问题</li><li>再选标准产品组合</li><li>用真实项目证明</li><li>明确能力与证据边界</li><li>最后冻结 SKU、型号和验收</li></ol></aside></div></header>
<div class="metrics">{metrics_html}</div>
<main>
<section id="quick"><div class="section-head"><div><div class="eyebrow">START HERE</div><h2>新人三分钟<br>按这个顺序卖</h2></div><p>第一轮沟通的目标不是立刻报价，而是把客户从“我要几台设备”带到“我要解决哪条业务链”。只要能完成下面六步，新人就不会把智慧团餐卖成硬件采购。</p></div><div class="quick-grid">{quick_cards}</div><p class="note">* 四项目有效订单证据合计来自不同项目和观察窗口，只说明真实运行规模，不作为同周期经营统计。</p></section>
<section id="architecture"><div class="section-head"><div><div class="eyebrow">PRODUCT STORY</div><h2>一句话讲清<br>我们到底卖什么</h2></div><p>客户真正购买的是业务结果：高峰更快、规则自动执行、多入口一本账、软硬件不脱节，以及售前到售后拿到同一套产品。</p></div><div class="positioning">我们把身份与账户、消费策略、点餐取餐、硬件交互、支付结算、对账运营和营养食安增值，连接成一条可运行、可验收、可持续服务的团餐业务闭环。</div><div class="value-chain"><div>身份与账户</div><div>消费策略</div><div>订餐 / 取餐</div><div>硬件动作</div><div>支付结算</div><div>对账运营</div><div>营养 / 食安</div></div></section>
<section id="packages"><div class="section-head"><div><div class="eyebrow">SOLUTION PACKAGES</div><h2>七个产品包<br>三个标准卖法</h2></div><p>S00—S60 是销售组合码，用来帮助销售快速判断，不代替正式 SKU。正式报价必须回到标准产品定标基表，锁定内部 SKU、官方型号、版本和数量。</p></div><div class="package-list">{package_cards}</div><h3 style="margin-top:42px;color:var(--ink);font-size:28px">三个可直接复制的商业标准包</h3>{commercial}</section>
<section id="cases"><div class="section-head"><div><div class="eyebrow">PROOF, NOT PROMISES</div><h2>四个项目<br>四种最强证明</h2></div><p>案例不是项目简介，而是销售证据：客户是什么场景、服务什么人、遇到什么问题、用了什么软硬件、最后验证了什么。每个案例同时标注不能扩大的边界。</p></div>{case_sections}</section>
<section id="discovery"><div class="section-head"><div><div class="eyebrow">DISCOVERY</div><h2>第一次见客户<br>必须问完 15 个问题</h2></div><p>问完以后，销售要能给出一个主包、若干增购包和一组待适配项。若场景、人数、峰值、接口、部署和验收仍不清楚，就不能进入正式报价。</p></div>{discovery}</section>
<section id="delivery"><div class="section-head"><div><div class="eyebrow">DELIVERY GATES</div><h2>解决“货不对版”<br>靠七道门</h2></div><p>从售前开始就使用同一套产品主数据。销售、采购、交付、售后都认内部 SKU、官方型号和版本规格；替换必须走 ECN。</p></div><div class="gates"><div class="gate"><b>G0</b><span>商机建档</span><p>场景、人群、点位、接口、部署</p></div><div class="gate"><b>G1</b><span>标准选型</span><p>SKU、型号、数量、配件</p></div><div class="gate"><b>G2</b><span>型号冻结</span><p>封样、协议、版本、验收</p></div><div class="gate"><b>G3</b><span>ECN 变更</span><p>替代、减配、升级可追溯</p></div><div class="gate"><b>G4</b><span>出库移交</span><p>铭牌、SN、固件、装箱单</p></div><div class="gate"><b>G5</b><span>安装验收</span><p>数量、流程、接口、性能</p></div><div class="gate"><b>G6</b><span>售后接管</span><p>质保、备件、权限、联系人</p></div></div><div class="scripts" style="margin-top:28px"><div class="script"><h3>立即执行的产品拍板</h3><p>新售软件统一 V2.0；CPT-FT248 暂停新售；双摄收货秤统一 C-F160；历史实物型号必须补 OEM/铭牌证据；服务项必须有服务 SKU 与 SLA。</p><a class="text-link" href="../2026-08-06-standard-product-project-model-gate/standard-product-project-model-gate.html">打开产品定标与门禁 →</a></div><div class="script"><h3>销售必须带走的五项输出</h3><p>一张现状流程图、一套建议组合、一份适配差距、一个证明案例、一张型号冻结清单。缺一项，都还没有进入可交付商机。</p><a class="text-link" href="../2026-08-06-standard-product-project-model-gate/AI营养智慧食堂-标准产品项目定标基表.xlsx">打开产品定标基表 Excel →</a></div></div></section>
<section id="objections"><div class="section-head"><div><div class="eyebrow">OBJECTIONS</div><h2>客户常问什么<br>销售怎么接</h2></div><p>回答的目标不是争辩，而是把问题转成下一步可验证动作。尤其是并发、国产化、外部接口和硬件替换，必须进入专项评估，不能口头兜底。</p></div>{objection_html}</section>
<section id="channel"><div class="section-head"><div><div class="eyebrow">CHANNEL PLAY</div><h2>渠道为什么<br>愿意与我们一起卖</h2></div><p>渠道负责客户关系、预算和本地协同；康比特负责产品组合、技术边界、型号冻结、交付证据和持续服务。双方共同把单次项目变成可复制的场景方案。</p></div><div class="channel-value"><div><b>更快</b><p>用标准包和案例缩短从线索到方案的时间</p></div><div><b>更准</b><p>先匹配业务问题，再配置软硬件</p></div><div><b>更稳</b><p>G0—G6 门禁减少错型、漏项和返工</p></div><div><b>更长</b><p>从交易底座延展到营养、食安、采购与运维</p></div></div>{channel}<div class="scripts" style="margin-top:28px"><div class="script"><h3>30 秒话术</h3><p>我们做的不是单一收银设备，而是智慧营养团餐的业务底座。它把人员身份、补贴和消费规则、现场与线上订单、取餐柜/闸机/称重台等硬件、支付退款和财务对账接成一条闭环。</p></div><div class="script"><h3>3 分钟话术结构</h3><p>先讲客户场景和问题；再讲对应产品组合与主流程；用最匹配的项目指标证明；说明待适配边界；最后邀请客户一起完成现状流程图和型号冻结清单。</p></div></div></section>
<section id="boundaries"><div class="section-head"><div><div class="eyebrow">TRUST BOUNDARY</div><h2>能力强<br>也要说得准确</h2></div><p>可信是最重要的销售资产。已经有证据的能力讲清楚；需要适配、压测或客户确认的能力，明确下一步怎样验证。</p></div>{boundaries}<h3 style="margin-top:40px;color:var(--ink);font-size:28px">完整资产导航</h3>{assets}</section>
<section><div class="final-cta"><div><div class="eyebrow">NEXT MEETING</div><h2>下一次客户会<br>就交付这五样</h2><p>把“产品很强”变成客户看得懂、内部交得出、渠道能复制的商机材料。</p></div><div class="deliverables"><span>01｜客户现状流程图</span><span>02｜建议产品组合</span><span>03｜适配与证据差距</span><span>04｜最匹配的证明案例</span><span>05｜SKU / 型号冻结清单</span></div></div></section>
</main>
<footer>智慧营养团餐销售与渠道作战手册 V1.0 · 2026-08-06 · 待销售 / 产品 / 交付 / 渠道联合评审</footer>
</body></html>'''


def col_letter(n: int) -> str:
    result = ""
    while n:
        n, rem = divmod(n - 1, 26)
        result = chr(65 + rem) + result
    return result


def build_xlsx(output: Path, skill_dir: Path) -> None:
    sheets = [
        ("三分钟开局", ["步骤", "动作", "做到什么"], QUICKSTART),
        ("场景产品组合", ["组合码", "产品包", "客户信号", "客户问题", "软件", "硬件", "结果", "项目证明"], [[p[k] for k in ("code", "name", "customer_signal", "problem", "software", "hardware", "result", "proof")] for p in PACKAGES]),
        ("商业标准包", ["编号", "标准包", "目标客户", "建议组合", "第一价值点", "优先案例"], COMMERCIAL_PACKAGES),
        ("案例证据", ["项目", "场景", "人群", "问题", "解决方案", "运行指标", "验证能力", "边界"], [[c[k] for k in ("project", "scene", "people", "problem", "solution", "metrics", "validated", "boundary")] for c in CASES]),
        ("需求访谈", ["ID", "主题", "必问问题", "用途", "停止规则"], DISCOVERY),
        ("异议处理", ["ID", "客户异议", "回答框架", "下一步", "边界"], OBJECTIONS),
        ("渠道分工", ["环节", "渠道", "康比特", "协作规则"], CHANNEL_RACI),
        ("承诺边界", ["现有证据", "可以证明", "不能扩大"], BOUNDARIES),
        ("资产导航", ["ID", "资产", "用途", "路径", "分发范围", "注意"], ASSETS),
    ]
    strings: list[str] = []
    index: dict[str, int] = {}
    refs = 0

    def sid(value: str) -> int:
        nonlocal refs
        text = str(value)
        refs += 1
        if text not in index:
            index[text] = len(strings)
            strings.append(text)
        return index[text]

    tmp = Path(tempfile.mkdtemp(prefix="smart_canteen_sales_xlsx_"))
    work = tmp / "work"
    shutil.copytree(skill_dir / "templates" / "minimal_xlsx", work)
    worksheet_dir = work / "xl" / "worksheets"
    for existing in worksheet_dir.glob("sheet*.xml"):
        existing.unlink()

    workbook_sheets = []
    rels = [
        '<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet" Target="worksheets/sheet1.xml"/>',
        '<Relationship Id="rId2" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles" Target="styles.xml"/>',
        '<Relationship Id="rId3" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/sharedStrings" Target="sharedStrings.xml"/>',
    ]
    overrides = []

    for i, (name, headers, rows) in enumerate(sheets, 1):
        rid = "rId1" if i == 1 else f"rId{i + 2}"
        workbook_sheets.append(f'<sheet name={quoteattr(name)} sheetId="{i}" r:id="{rid}"/>')
        if i > 1:
            rels.append(f'<Relationship Id="{rid}" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet" Target="worksheets/sheet{i}.xml"/>')
        overrides.append(f'<Override PartName="/xl/worksheets/sheet{i}.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"/>')
        ncols = len(headers)
        widths = []
        for ci, header in enumerate(headers, 1):
            max_len = max([len(str(header))] + [len(str(row[ci - 1])) for row in rows])
            width = min(max(max_len * 1.55, 12), 46)
            widths.append(f'<col min="{ci}" max="{ci}" width="{width:.1f}" customWidth="1"/>')
        title = f"智慧营养团餐销售与渠道作战工具｜{name}"
        sheet_rows = [f'<row r="1" ht="30" customHeight="1"><c r="A1" t="s" s="13"><v>{sid(title)}</v></c></row>']
        header_cells = "".join(f'<c r="{col_letter(ci)}3" t="s" s="14"><v>{sid(h)}</v></c>' for ci, h in enumerate(headers, 1))
        sheet_rows.append(f'<row r="3" ht="26" customHeight="1">{header_cells}</row>')
        for ri, row in enumerate(rows, 4):
            cells = "".join(f'<c r="{col_letter(ci)}{ri}" t="s" s="15"><v>{sid(v)}</v></c>' for ci, v in enumerate(row, 1))
            sheet_rows.append(f'<row r="{ri}" ht="48" customHeight="1">{cells}</row>')
        ref = f"A3:{col_letter(ncols)}{len(rows) + 3}"
        worksheet = f'''<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
<sheetViews><sheetView workbookViewId="0"><pane ySplit="3" topLeftCell="A4" activePane="bottomLeft" state="frozen"/></sheetView></sheetViews>
<sheetFormatPr defaultRowHeight="18"/>
<cols>{''.join(widths)}</cols>
<sheetData>{''.join(sheet_rows)}</sheetData>
<autoFilter ref="{ref}"/>
<mergeCells count="1"><mergeCell ref="A1:{col_letter(ncols)}1"/></mergeCells>
<pageMargins left="0.25" right="0.25" top="0.5" bottom="0.5" header="0.2" footer="0.2"/>
<pageSetup orientation="landscape" fitToWidth="1" fitToHeight="0"/>
</worksheet>'''
        (worksheet_dir / f"sheet{i}.xml").write_text(worksheet, encoding="utf-8")

    workbook = f'''<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<workbook xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"><sheets>{''.join(workbook_sheets)}</sheets><calcPr calcId="191029" fullCalcOnLoad="1"/></workbook>'''
    (work / "xl" / "workbook.xml").write_text(workbook, encoding="utf-8")
    rel_xml = f'''<?xml version="1.0" encoding="UTF-8" standalone="yes"?><Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">{''.join(rels)}</Relationships>'''
    (work / "xl" / "_rels" / "workbook.xml.rels").write_text(rel_xml, encoding="utf-8")
    content_types = f'''<?xml version="1.0" encoding="UTF-8" standalone="yes"?><Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types"><Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/><Default Extension="xml" ContentType="application/xml"/><Override PartName="/xl/workbook.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml"/>{''.join(overrides)}<Override PartName="/xl/styles.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml"/><Override PartName="/xl/sharedStrings.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sharedStrings+xml"/></Types>'''
    (work / "[Content_Types].xml").write_text(content_types, encoding="utf-8")
    shared_items = "".join(f"<si><t>{escape(s)}</t></si>" for s in strings)
    shared = f'''<?xml version="1.0" encoding="UTF-8" standalone="yes"?><sst xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" count="{refs}" uniqueCount="{len(strings)}">{shared_items}</sst>'''
    (work / "xl" / "sharedStrings.xml").write_text(shared, encoding="utf-8")

    # Append three semantic display styles without altering the template's existing xfs.
    styles = (work / "xl" / "styles.xml").read_text(encoding="utf-8")
    styles = styles.replace('<fonts count="5">', '<fonts count="8">').replace(
        "</fonts>",
        '<font><b/><sz val="15"/><name val="Microsoft YaHei"/><color rgb="00FFFFFF"/></font>'
        '<font><b/><sz val="10"/><name val="Microsoft YaHei"/><color rgb="00FFFFFF"/></font>'
        '<font><sz val="10"/><name val="Microsoft YaHei"/><color rgb="00233640"/></font></fonts>',
        1,
    )
    styles = styles.replace('<fills count="3">', '<fills count="5">').replace(
        "</fills>",
        '<fill><patternFill patternType="solid"><fgColor rgb="0017324D"/><bgColor indexed="64"/></patternFill></fill>'
        '<fill><patternFill patternType="solid"><fgColor rgb="001E7A62"/><bgColor indexed="64"/></patternFill></fill></fills>',
        1,
    )
    styles = styles.replace('<borders count="1">', '<borders count="2">').replace(
        "</borders>",
        '<border><left style="thin"><color rgb="00D8E1DF"/></left><right style="thin"><color rgb="00D8E1DF"/></right><top style="thin"><color rgb="00D8E1DF"/></top><bottom style="thin"><color rgb="00D8E1DF"/></bottom><diagonal/></border></borders>',
        1,
    )
    styles = styles.replace('<cellXfs count="13">', '<cellXfs count="16">').replace(
        "</cellXfs>",
        '<xf numFmtId="0" fontId="5" fillId="3" borderId="0" xfId="0" applyFont="1" applyFill="1" applyAlignment="1"><alignment horizontal="left" vertical="center"/></xf>'
        '<xf numFmtId="0" fontId="6" fillId="4" borderId="1" xfId="0" applyFont="1" applyFill="1" applyBorder="1" applyAlignment="1"><alignment horizontal="left" vertical="center" wrapText="1"/></xf>'
        '<xf numFmtId="0" fontId="7" fillId="0" borderId="1" xfId="0" applyFont="1" applyBorder="1" applyAlignment="1"><alignment horizontal="left" vertical="top" wrapText="1"/></xf></cellXfs>',
        1,
    )
    (work / "xl" / "styles.xml").write_text(styles, encoding="utf-8")

    output.parent.mkdir(parents=True, exist_ok=True)
    subprocess.run([sys.executable, str(skill_dir / "scripts" / "xlsx_pack.py"), str(work), str(output)], check=True)
    subprocess.run([sys.executable, str(skill_dir / "scripts" / "formula_check.py"), str(output)], check=True)
    shutil.rmtree(tmp)


def main() -> None:
    parser = argparse.ArgumentParser()
    parser.add_argument("--xlsx-output", type=Path, help="Optional delivery path for the generated XLSX")
    parser.add_argument("--xlsx-skill-dir", type=Path, default=Path("/Users/jack/.codex/skills/minimax-xlsx"))
    args = parser.parse_args()

    HERE.mkdir(parents=True, exist_ok=True)
    write_csv(HERE / "scenario-package-matrix.csv", ["组合码", "产品包", "客户信号", "客户问题", "软件", "硬件", "客户结果", "项目证明"], [[p[k] for k in ("code", "name", "customer_signal", "problem", "software", "hardware", "result", "proof")] for p in PACKAGES])
    write_csv(HERE / "case-proof-matrix.csv", ["案例ID", "项目", "场景", "服务人群", "客户问题", "解决方案", "运行指标", "验证能力", "证据边界", "销售话术"], [[c[k] for k in ("id", "project", "scene", "people", "problem", "solution", "metrics", "validated", "boundary", "pitch")] for c in CASES])
    write_csv(HERE / "sales-discovery-checklist.csv", ["ID", "主题", "必问问题", "用途", "停止规则"], DISCOVERY)
    write_csv(HERE / "objection-handling.csv", ["ID", "客户异议", "回答框架", "下一步", "边界"], OBJECTIONS)
    write_csv(HERE / "channel-collaboration-raci.csv", ["环节", "渠道", "康比特", "协作规则"], CHANNEL_RACI)
    write_csv(HERE / "asset-index.csv", ["ID", "资产", "用途", "路径", "分发范围", "注意"], ASSETS)
    write_csv(HERE / "evidence-boundary.csv", ["现有证据", "可以证明", "不能扩大"], BOUNDARIES)
    (HERE / "smart-canteen-sales-channel-playbook.md").write_text(build_markdown(), encoding="utf-8")
    (HERE / "smart-canteen-sales-channel-playbook.html").write_text(build_html(), encoding="utf-8")
    if args.xlsx_output:
        build_xlsx(args.xlsx_output.resolve(), args.xlsx_skill_dir.resolve())


if __name__ == "__main__":
    main()
