Explorar el Código

feat(skill): 新增 JS ESM 智能报价技能(GLM视觉识别+计价引擎+手机端HTML报告+CLI)

- 纯 JS ESM 模块,浏览器/CLI 共用;不改动任何现有 py/html 程序
- 计价模型与 Excel 预算表同源(投影面积/厨房延米/三档材质/客户分型/活动叠加/浮动授权区间)
- glm-5.3-flash 两阶段识别+尺寸交叉验证(尺寸线直读>比例推算>面积先验)
- 手机端优先单文件 HTML 报告,内嵌计价引擎支持销售边聊边调
- 零依赖 S3 SigV4 发布器(本地 out/ 兜底);12 项单测全通过
Hermes Agent hace 2 semanas
padre
commit
5f2a82100e

+ 39 - 0
changelog/user/2026-08-31-skill目录-JS智能报价技能.md

@@ -0,0 +1,39 @@
+# 协作记录 · 2026-08-31 · skill/ 目录:JS ESM 智能报价技能(智能体版)
+
+> 约定:本目录用于记录协作开发过程中的关键讨论与决策,随 git 提交入库。
+
+## 一、任务背景
+
+销售团队需要一个「发户型图 + 说客户情况 → 出报价单」的智能体技能。原 HTML 页面(AI快速报价_Demo2.optimizing.html)逻辑完整但不可复用;Python 难以浏览器/CLI 共用。决定:**不影响任何现有程序,在 skill/ 目录用纯 JS ESM 重写计价与识别管线,供智能体调用**。
+
+## 二、技术决策
+
+| 决策点 | 结论 | 理由 |
+|---|---|---|
+| 语言/模块 | JS ESM(src/*.js),浏览器与 CLI 同构 | 一套函数两端复用;CLI 用 node bin/fpc.js |
+| 识别模型 | glm-5.3-flash(api.fmode.cn 网关) | 用户指定;视觉结构化读图实测通过。注意:思考型模型,需 max_tokens≥8192 并兜底读 reasoning_content,JSON 解析带截断修复+严格重试 |
+| 两阶段识别 | analyze(全图结构化)→ measure(柜体尺寸)→ reconcile(交叉验证+并集合并) | 分阶段降低漏项;measure 漏的柜体由 analysis 补(低置信度) |
+| 尺寸策略 | 图上尺寸线直读(0.95) > VLM 按比例推算(0.5) > 房间面积先验(0.35) | 营销图无标注时也能给合理估值,报告带「估」角标 |
+| 2D 重绘 | 自写 SVG 生成器(零依赖),效果不佳自动降级原图 | 手机矢量缩放+DOM 可交互;避免引入大型绘图库 |
+| 报价模型 | 与 Excel 预算表同源:投影面积×材质档单价 / 厨房计费延米(L/U型扣转角+冰箱位) / 客户三型分策略 / 活动叠加 / 浮动区间=授权边界 | 保持与现有人工报价口径一致 |
+| 报告 | 单文件 HTML,手机端优先,计价引擎内嵌(strip ESM 打进 <script>),销售操作台即时重算 | 微信里点开即用;边聊边调 |
+| 发布 | 零依赖 S3 SigV4(~/.fpc/publish.json 配置),未配置时落本地 out/ | 用户个人 S3 凭证待提供 |
+
+## 三、交付物
+
+- `skill/src/`:defaults.js(规则库)/ quote.js(计价引擎)/ vision.js(GLM 视觉+交叉验证)/ geometry.js / redraw.js(SVG)/ parse.js(PDF/图片)/ report.js(HTML 模板)/ publish.js / index.js(管线+CLI)
+- `skill/bin/fpc.js`:命令行入口
+- `skill/test/run.js`:12 项断言全通过(计价/名称归一/报告渲染)
+- 智能体技能:已装到 Hermes `skills/floorplan-quotation/`
+- 测试样例:testdata/sample1_shidaichengzhi_q1.jpg(时代之城Q-1,营销图无尺寸标注)
+
+## 四、验证结果(glm-5.3-flash 真实调用)
+
+- 图片链路:7 组柜体(玄关鞋柜/厨房橱柜/主卧衣柜/次卧衣柜/阳台柜/浴室柜/衣帽间),¥150,900,谈价区间 ¥128,265~¥173,535
+- PDF 链路(构造样例 PDF):识别→出单正常
+- 已修复:厨房名收敛(防绕过延米计价)、同名柜体重复计价合并、reasoning 模型空正文、SVG tspan 换行
+
+## 五、遗留
+
+- S3 凭证待用户提供后配置 `~/.fpc/publish.json` 即可 `--publish`
+- 真实施工图(有尺寸标注的 CAD 导出图)识别精度待销售实测定标

+ 4 - 0
skill/bin/fpc.js

@@ -0,0 +1,4 @@
+#!/usr/bin/env node
+// fpc CLI 入口
+import { main } from '../src/index.js';
+main().catch((e) => { console.error('执行失败:', e.message || e); process.exit(1); });

+ 797 - 0
skill/out/报价单-PDF链路测试-08312338.html

@@ -0,0 +1,797 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+<head>
+<meta charset="UTF-8">
+<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
+<meta name="format-detection" content="telephone=no">
+<title>PDF链路测试 · 全屋定制报价方案</title>
+<style>
+:root{--p:#1F497D;--pa:#2B6CB0;--bg:#F5F7FA;--card:#fff;--bd:#E2E8F0;--tx:#2D3748;--mu:#718096;--ok:#38A169;--wa:#D69E2E;--dg:#E53E3E;--r:12px}
+*{box-sizing:border-box;margin:0;padding:0;-webkit-tap-highlight-color:transparent}
+body{font-family:-apple-system,BlinkMacSystemFont,"PingFang SC","Microsoft YaHei",sans-serif;background:var(--bg);color:var(--tx);line-height:1.55;padding-bottom:calc(88px + env(safe-area-inset-bottom))}
+header{background:linear-gradient(135deg,#1F497D,#2B6CB0);color:#fff;padding:18px 16px 14px}
+header h1{font-size:17px;font-weight:700}
+header .sub{font-size:12px;opacity:.85;margin-top:2px}
+.wrap{padding:12px;max-width:640px;margin:0 auto}
+.hero{background:var(--card);border-radius:var(--r);padding:16px;box-shadow:0 1px 3px rgba(0,0,0,.08);margin-top:-26px;position:relative;text-align:center}
+.hero .label{font-size:12px;color:var(--mu)}
+.hero .price{font-size:38px;font-weight:800;color:var(--p);letter-spacing:-1px;margin:2px 0}
+.hero .price small{font-size:16px;font-weight:600}
+.hero .range{font-size:13px;color:var(--mu)}
+.badge{display:inline-block;font-size:11px;padding:2px 10px;border-radius:20px;background:#EBF8FF;color:var(--pa);border:1px solid #BEE3F8;margin-top:6px}
+.sec{margin-top:14px}
+.sec h2{font-size:15px;font-weight:700;margin:0 2px 8px;display:flex;align-items:center;gap:6px}
+.card{background:var(--card);border-radius:var(--r);box-shadow:0 1px 3px rgba(0,0,0,.06);overflow:hidden}
+.room{border-bottom:1px solid var(--bd)}
+.room:last-child{border-bottom:none}
+.room-h{padding:12px 14px;display:flex;justify-content:space-between;align-items:center;gap:8px}
+.room-h .nm{font-weight:700;font-size:14px}
+.room-h .dm{font-size:11px;color:var(--mu)}
+.room-h .tt{font-weight:800;color:var(--p);font-size:15px;white-space:nowrap}
+.room-h .conf{font-size:10px;padding:1px 6px;border-radius:8px;background:#FFFAF0;color:#C05621;border:1px solid #FBD38D;flex-shrink:0}
+.items{padding:0 14px 10px;display:none}
+.room.open .items{display:block}
+.item{display:flex;justify-content:space-between;font-size:12.5px;padding:5px 0;color:#4A5568}
+.item .l span{color:var(--mu);font-size:11px;margin-left:6px}
+.item .v{white-space:nowrap}
+.trow{display:flex;justify-content:space-between;padding:8px 14px;font-size:13px}
+.trow .v{font-weight:700}
+.trow.total{background:#F0FFF4;color:#276749;font-weight:800;font-size:15px}
+.trow.dim{color:var(--mu);font-size:12px}
+.plan-wrap{background:var(--card);border-radius:var(--r);padding:10px;box-shadow:0 1px 3px rgba(0,0,0,.06)}
+.plan-wrap svg,.plan-img{width:100%;height:auto;display:block;border-radius:8px}
+.plan-note{font-size:11px;color:var(--mu);text-align:center;margin-top:6px}
+.selling{background:linear-gradient(135deg,#FFFAF0,#FFF5EB);border:1px solid #FBD38D;border-radius:var(--r);padding:12px 14px}
+.selling li{font-size:13px;margin:6px 0 6px 18px;color:#744210}
+.selling h3{font-size:13px;color:#C05621;margin-bottom:4px}
+.acts .item .v{color:var(--ok)}
+.ctrl{position:fixed;bottom:0;left:0;right:0;background:#fff;border-top:1px solid var(--bd);padding:10px 14px calc(10px + env(safe-area-inset-bottom));display:flex;gap:8px;z-index:99;max-width:640px;margin:0 auto}
+.btn{flex:1;padding:12px;border:none;border-radius:10px;font-size:14px;font-weight:700;cursor:pointer}
+.btn.pri{background:var(--p);color:#fff}
+.btn.sec{background:#EDF2F7;color:var(--tx)}
+details.dims{background:var(--card);border-radius:var(--r);padding:10px 14px;font-size:12px;color:var(--mu)}
+details.dims summary{cursor:pointer;font-weight:600;color:var(--tx)}
+details.dims li{margin:4px 0 4px 18px}
+.drawer{position:fixed;inset:0;background:rgba(0,0,0,.4);z-index:100;display:none}
+.drawer.open{display:block}
+.sheet{position:absolute;bottom:0;left:0;right:0;background:#fff;border-radius:16px 16px 0 0;max-height:82vh;overflow:auto;padding:16px 16px calc(20px + env(safe-area-inset-bottom));max-width:640px;margin:0 auto}
+.sheet h3{font-size:15px;margin-bottom:12px}
+.frow{margin-bottom:14px}
+.frow label{font-size:12px;color:var(--mu);display:block;margin-bottom:4px}
+.frow .opts{display:flex;gap:6px;flex-wrap:wrap}
+.chip{padding:8px 12px;border-radius:8px;border:1.5px solid var(--bd);background:#fff;font-size:13px;cursor:pointer}
+.chip.on{border-color:var(--pa);background:#EBF8FF;color:var(--pa);font-weight:700}
+input[type=range]{width:100%}
+.cabrow{display:flex;align-items:center;gap:8px;padding:8px 0;border-bottom:1px dashed var(--bd);font-size:13px}
+.cabrow input{width:64px;padding:6px;border:1px solid var(--bd);border-radius:6px;font-size:13px}
+.cabrow .del{color:var(--dg);font-size:18px;padding:0 6px;cursor:pointer}
+.cabrow .add{color:var(--ok)}
+@media(min-width:640px){.hero .price{font-size:44px}}
+</style>
+</head>
+<body>
+<header>
+  <h1>PDF链路测试 · 全屋定制报价方案</h1>
+  <div class="sub">标准客户方案 · 生成于 8/31 23:38 · 拉迷全屋定制</div>
+</header>
+<div class="wrap">
+
+  <div class="hero">
+    <div class="label">全屋定制整装价</div>
+    <div class="price" id="finalPrice">¥179,750</div>
+    <div class="range" id="finalRange">谈价参考区间 ¥152,788 ~ ¥206,712</div>
+    <div id="badges">
+      <span class="badge" id="bTier">PET材质</span>
+      <span class="badge" id="bDisc">原价</span>
+      
+    </div>
+  </div>
+
+  
+
+  <div class="sec">
+    <h2>🏠 户型与定制布局</h2>
+    <div class="plan-wrap" id="planWrap"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1870 2330" class="fpc-plan">
+<g id="fpc-rooms">
+<g class="fpc-room" data-room="厨房">
+<rect x="960.0" y="120.0" width="790.0" height="570.0" fill="#F7FAFC" stroke="#2D3748" stroke-width="6"/>
+<line x1="1265.0" y1="690.0" x2="1355.0" y2="690.0" stroke="#fff" stroke-width="8"/>
+<text x="1355.0" y="405.0" text-anchor="middle" dominant-baseline="middle" font-size="52" fill="#2D3748" font-weight="600">厨房</text>
+</g>
+<g class="fpc-room" data-room="阳台(厨房服务阳台)">
+<rect x="960.0" y="120.0" width="790.0" height="570.0" fill="#F7FAFC" stroke="#2D3748" stroke-width="6"/>
+<line x1="1265.0" y1="690.0" x2="1355.0" y2="690.0" stroke="#fff" stroke-width="8"/>
+<text x="1355.0" y="405.0" text-anchor="middle" dominant-baseline="middle" font-size="52" fill="#2D3748" font-weight="600">阳台(厨房服务阳台)</text>
+</g>
+<g class="fpc-room" data-room="玄关">
+<rect x="960.0" y="880.0" width="790.0" height="570.0" fill="#F7FAFC" stroke="#2D3748" stroke-width="6"/>
+<line x1="1265.0" y1="1450.0" x2="1355.0" y2="1450.0" stroke="#fff" stroke-width="8"/>
+<text x="1355.0" y="1165.0" text-anchor="middle" dominant-baseline="middle" font-size="52" fill="#2D3748" font-weight="600">玄关</text>
+</g>
+<g class="fpc-room" data-room="餐厅">
+<rect x="960.0" y="880.0" width="790.0" height="570.0" fill="#F7FAFC" stroke="#2D3748" stroke-width="6"/>
+<line x1="1265.0" y1="1450.0" x2="1355.0" y2="1450.0" stroke="#fff" stroke-width="8"/>
+<text x="1355.0" y="1165.0" text-anchor="middle" dominant-baseline="middle" font-size="52" fill="#2D3748" font-weight="600">餐厅</text>
+</g>
+<g class="fpc-room" data-room="客厅">
+<rect x="960.0" y="880.0" width="790.0" height="570.0" fill="#F7FAFC" stroke="#2D3748" stroke-width="6"/>
+<line x1="1265.0" y1="1450.0" x2="1355.0" y2="1450.0" stroke="#fff" stroke-width="8"/>
+<text x="1355.0" y="1165.0" text-anchor="middle" dominant-baseline="middle" font-size="52" fill="#2D3748" font-weight="600">客厅</text>
+</g>
+<g class="fpc-room" data-room="主卧">
+<rect x="120.0" y="1640.0" width="790.0" height="570.0" fill="#F7FAFC" stroke="#2D3748" stroke-width="6"/>
+<line x1="425.0" y1="2210.0" x2="515.0" y2="2210.0" stroke="#fff" stroke-width="8"/>
+<text x="515.0" y="1925.0" text-anchor="middle" dominant-baseline="middle" font-size="52" fill="#2D3748" font-weight="600">主卧</text>
+</g>
+<g class="fpc-room" data-room="次卧">
+<rect x="960.0" y="1640.0" width="790.0" height="570.0" fill="#F7FAFC" stroke="#2D3748" stroke-width="6"/>
+<line x1="1265.0" y1="2210.0" x2="1355.0" y2="2210.0" stroke="#fff" stroke-width="8"/>
+<text x="1355.0" y="1925.0" text-anchor="middle" dominant-baseline="middle" font-size="52" fill="#2D3748" font-weight="600">次卧</text>
+</g>
+<g class="fpc-room" data-room="书房/第三卧室">
+<rect x="960.0" y="880.0" width="790.0" height="570.0" fill="#F7FAFC" stroke="#2D3748" stroke-width="6"/>
+<line x1="1265.0" y1="1450.0" x2="1355.0" y2="1450.0" stroke="#fff" stroke-width="8"/>
+<text x="1355.0" y="1165.0" text-anchor="middle" dominant-baseline="middle" font-size="52" fill="#2D3748" font-weight="600">书房/第三卧室</text>
+</g>
+<g class="fpc-room" data-room="衣帽间">
+<rect x="960.0" y="880.0" width="790.0" height="570.0" fill="#F7FAFC" stroke="#2D3748" stroke-width="6"/>
+<line x1="1265.0" y1="1450.0" x2="1355.0" y2="1450.0" stroke="#fff" stroke-width="8"/>
+<text x="1355.0" y="1165.0" text-anchor="middle" dominant-baseline="middle" font-size="52" fill="#2D3748" font-weight="600">衣帽间</text>
+</g>
+<g class="fpc-room" data-room="主卫">
+<rect x="960.0" y="880.0" width="790.0" height="570.0" fill="#F7FAFC" stroke="#2D3748" stroke-width="6"/>
+<line x1="1265.0" y1="1450.0" x2="1355.0" y2="1450.0" stroke="#fff" stroke-width="8"/>
+<text x="1355.0" y="1165.0" text-anchor="middle" dominant-baseline="middle" font-size="52" fill="#2D3748" font-weight="600">主卫</text>
+</g>
+<g class="fpc-room" data-room="公卫">
+<rect x="960.0" y="120.0" width="790.0" height="570.0" fill="#F7FAFC" stroke="#2D3748" stroke-width="6"/>
+<line x1="1265.0" y1="690.0" x2="1355.0" y2="690.0" stroke="#fff" stroke-width="8"/>
+<text x="1355.0" y="405.0" text-anchor="middle" dominant-baseline="middle" font-size="52" fill="#2D3748" font-weight="600">公卫</text>
+</g>
+<g class="fpc-room" data-room="生活阳台">
+<rect x="960.0" y="1640.0" width="790.0" height="570.0" fill="#F7FAFC" stroke="#2D3748" stroke-width="6"/>
+<line x1="1265.0" y1="2210.0" x2="1355.0" y2="2210.0" stroke="#fff" stroke-width="8"/>
+<text x="1355.0" y="1925.0" text-anchor="middle" dominant-baseline="middle" font-size="52" fill="#2D3748" font-weight="600">生活阳台</text>
+</g>
+</g>
+<g id="fpc-cabinets">
+<g class="fpc-cab" data-cab="玄关鞋柜">
+<rect x="320.0" y="320.0" width="200.0" height="240.0" rx="8" fill="#BEE3F8" stroke="#1F497D" stroke-width="5" stroke-dasharray="18 8"/>
+<text x="420.0" y="440.0" text-anchor="middle" dominant-baseline="middle" font-size="44" fill="#1F497D" font-weight="700">玄关鞋柜 1×1.2</text>
+</g>
+<g class="fpc-cab" data-cab="厨房橱柜">
+<rect x="320.0" y="320.0" width="960.0" height="170.0" rx="8" fill="#FEEBC8" stroke="#1F497D" stroke-width="5" stroke-dasharray="18 8"/>
+<text x="800.0" y="405.0" text-anchor="middle" dominant-baseline="middle" font-size="44" fill="#1F497D" font-weight="700">厨房橱柜 4.8×0.85</text>
+</g>
+<g class="fpc-cab" data-cab="厨房橱柜">
+<rect x="320.0" y="320.0" width="540.0" height="160.0" rx="8" fill="#FEEBC8" stroke="#1F497D" stroke-width="5" stroke-dasharray="18 8"/>
+<text x="590.0" y="400.0" text-anchor="middle" dominant-baseline="middle" font-size="44" fill="#1F497D" font-weight="700">厨房橱柜 2.7×0.8</text>
+</g>
+<g class="fpc-cab" data-cab="餐厅餐边柜">
+<rect x="320.0" y="320.0" width="300.0" height="240.0" rx="8" fill="#BEE3F8" stroke="#1F497D" stroke-width="5" stroke-dasharray="18 8"/>
+<text x="470.0" y="440.0" text-anchor="middle" dominant-baseline="middle" font-size="44" fill="#1F497D" font-weight="700">餐厅餐边柜 1.5×1.2</text>
+</g>
+<g class="fpc-cab" data-cab="衣帽间">
+<rect x="320.0" y="320.0" width="780.0" height="240.0" rx="8" fill="#BEE3F8" stroke="#1F497D" stroke-width="5" stroke-dasharray="18 8"/>
+<text x="710.0" y="440.0" text-anchor="middle" dominant-baseline="middle" font-size="44" fill="#1F497D" font-weight="700">衣帽间 3.9×1.2</text>
+</g>
+<g class="fpc-cab" data-cab="次卧衣柜">
+<rect x="320.0" y="320.0" width="480.0" height="240.0" rx="8" fill="#BEE3F8" stroke="#1F497D" stroke-width="5" stroke-dasharray="18 8"/>
+<text x="560.0" y="440.0" text-anchor="middle" dominant-baseline="middle" font-size="44" fill="#1F497D" font-weight="700">次卧衣柜 2.4×1.2</text>
+</g>
+<g class="fpc-cab" data-cab="阳台柜">
+<rect x="320.0" y="320.0" width="300.0" height="240.0" rx="8" fill="#BEE3F8" stroke="#1F497D" stroke-width="5" stroke-dasharray="18 8"/>
+<text x="470.0" y="440.0" text-anchor="middle" dominant-baseline="middle" font-size="44" fill="#1F497D" font-weight="700">阳台柜 1.5×1.2</text>
+</g>
+</g>
+<g id="fpc-dims">
+</g>
+<defs><marker id="arr" viewBox="0 0 10 10" refX="5" refY="5" markerWidth="5" markerHeight="5" orient="auto"><path d="M0,0L10,5L0,10z" fill="#C05621"/></marker></defs>
+</svg></div><p class="plan-note">📐 AI 结构化重绘示意图(非施工图,以实际量尺为准)</p>
+    
+  </div>
+
+  <div class="sec">
+    <h2>💰 报价明细 <span style="font-weight:400;font-size:11px;color:var(--mu)">(点击房间展开)</span></h2>
+    <div class="card" id="roomsCard"><div class="room" data-i="0">
+    <div class="room-h"><div><div class="nm">玄关鞋柜</div><div class="dm">1m×2.4m</div></div><span class="tt">¥12,000</span></div>
+    <div class="items"><div class="item"><span class="l">柜体(PET材质)<span>1m×2.4m · 投影2.4m²</span></span><span class="v">¥12,000</span></div></div>
+  </div><div class="room" data-i="1">
+    <div class="room-h"><div><div class="nm">厨房橱柜</div><div class="dm">4.8m×0.85m</div></div><span class="tt">¥26,500</span></div>
+    <div class="items"><div class="item"><span class="l">柜体(PET材质)<span>4.8m×0.85m · 投影5.3m</span></span><span class="v">¥26,500</span></div></div>
+  </div><div class="room" data-i="2">
+    <div class="room-h"><div><div class="nm">厨房橱柜</div><div class="dm">2.7m×0.8m</div></div><span class="tt">¥26,500</span></div>
+    <div class="items"><div class="item"><span class="l">柜体(PET材质)<span>2.7m×0.8m · 投影5.3m</span></span><span class="v">¥26,500</span></div></div>
+  </div><div class="room" data-i="3">
+    <div class="room-h"><div><div class="nm">餐厅餐边柜</div><div class="dm">1.5m×2.4m</div></div><span class="tt">¥18,000</span></div>
+    <div class="items"><div class="item"><span class="l">柜体(PET材质)<span>1.5m×2.4m · 投影3.6m²</span></span><span class="v">¥18,000</span></div></div>
+  </div><div class="room" data-i="4">
+    <div class="room-h"><div><div class="nm">衣帽间</div><div class="dm">3.9m×2.5m</div></div><span class="tt">¥48,750</span></div>
+    <div class="items"><div class="item"><span class="l">柜体(PET材质)<span>3.9m×2.5m · 投影9.75m²</span></span><span class="v">¥48,750</span></div></div>
+  </div><div class="room" data-i="5">
+    <div class="room-h"><div><div class="nm">次卧衣柜</div><div class="dm">2.4m×2.5m</div></div><span class="tt">¥30,000</span></div>
+    <div class="items"><div class="item"><span class="l">柜体(PET材质)<span>2.4m×2.5m · 投影6m²</span></span><span class="v">¥30,000</span></div></div>
+  </div><div class="room" data-i="6">
+    <div class="room-h"><div><div class="nm">阳台柜</div><div class="dm">1.5m×2.4m</div></div><span class="tt">¥18,000</span></div>
+    <div class="items"><div class="item"><span class="l">柜体(PET材质)<span>1.5m×2.4m · 投影3.6m²</span></span><span class="v">¥18,000</span></div></div>
+  </div></div>
+    <div class="card" style="margin-top:8px">
+      <div class="trow"><span>项目小计</span><span class="v" id="tSub">¥179,750</span></div>
+      <div class="trow dim"><span id="tDiscLabel">原价</span><span class="v" id="tAfter">¥179,750</span></div>
+      <div class="trow" id="actRow" style="display:none"><span>活动优惠</span><span class="v" style="color:var(--ok)" id="tAct">-¥0</span></div>
+      <div class="trow total"><span>到手价</span><span class="v" id="tFinal">¥179,750</span></div>
+    </div>
+  </div>
+
+  <div class="sec">
+    <div class="selling">
+      <h3>✨ 为什么选择拉迷(标准客户)</h3>
+      <ul><li>空间利用率拉满:每一面墙都变成收纳</li><li>全国联保5年,铰链五金终身维护</li><li>设计上门量尺,3D效果图确认后再生产</li></ul>
+    </div>
+  </div>
+
+  <div class="sec" style="font-size:11px;color:var(--mu);line-height:1.7">
+    计价说明:柜体按投影面积(宽×高)×材质单价;厨房按计费延米(扣除转角重叠与冰箱位)。本报价为AI辅助估算,实际以现场量尺与正式合同为准。质保与赠送以当期活动政策为准。
+  </div>
+</div>
+
+<div class="ctrl">
+  <button class="btn sec" id="btnAdjust">⚖️ 调整报价</button>
+  <button class="btn pri" id="btnSave">📞 锁定到店礼</button>
+</div>
+
+<div class="drawer" id="drawer">
+  <div class="sheet">
+    <h3>⚖️ 销售操作台(客户不可见)</h3>
+    <div class="frow"><label>客户类型</label><div class="opts" id="optTier"><button class="chip " data-v="price">🏷️ 价格型</button><button class="chip on" data-v="standard">🏠 标准型</button><button class="chip " data-v="quality">💎 品质型</button></div></div>
+    <div class="frow"><label>材质档位</label><div class="opts" id="optMat"><button class="chip " data-v="双饰面">双饰面</button><button class="chip on" data-v="PET">PET</button><button class="chip " data-v="混油">混油</button></div></div>
+    <div class="frow"><label>报价浮动 <span id="floatV">15</span>%</label><input type="range" id="floatR" min="10" max="20" step="1" value="15"></div>
+    <div class="frow"><label>折扣</label><div class="opts" id="optDisc">
+      <button class="chip" data-v="1.00">原价</button>
+      <button class="chip" data-v="0.95">95折</button>
+      <button class="chip" data-v="0.90">9折</button>
+      <button class="chip" data-v="0.85">85折</button>
+      <button class="chip" data-v="0.80">8折</button>
+    </div></div>
+    <div class="frow"><label>柜体清单(宽=投影宽 m / 厨房为计费长)</label><div id="cabList"></div>
+      <div style="margin-top:8px"><button class="chip" id="btnAddCab">+ 增加柜体</button></div>
+    </div>
+    <div class="frow"><label>活动优惠</label><div class="opts" id="optAct"></div></div>
+    <button class="btn pri" style="width:100%" id="btnApply">应用并重算</button>
+  </div>
+</div>
+
+<script>
+// ============================================================
+// 拉迷全屋定制 · 默认报价规则库
+// 数据来源:floorplan-quotation/AI快速报价_Demo2.optimizing.html
+// (DEFAULT_RULES / Q_CUSTOMER_TIERS / DEFAULT_QUOTATION_PLANS /
+//   CABINET_EXTRA_TEMPLATES / EXTRA_METHOD_MAP / 厨房扣减规则)
+// 所有价格单位:元。修改价格请改这里的默认值或运行时覆盖 config。
+// ============================================================
+
+const METHOD = {
+  PROJECTION: '按投影面积', // 元/m²  数量 = 宽(m) × 高(m)
+  METER: '按延米',          // 元/m   数量 = 米数
+  COUNT: '按个数',          // 元/个  数量 = 个数
+};
+
+// —— 材质三档单价区间(AI快速粗略报价核心)——
+const PRICE_TIERS = [
+  { name: '双饰面', min: 3000, max: 3500 },
+  { name: 'PET',   min: 4500, max: 5000 },
+  { name: '混油',   min: 6000, max: 7000 },
+];
+const DEFAULT_TIER = 'PET';
+
+// —— 计价方式 ——
+const METHOD_OPTIONS = [
+  { value: '按投影面积', unit: '元/m²' },
+  { value: '按个数', unit: '元/个' },
+  { value: '按延米', unit: '元/m' },
+];
+
+// —— 柜体→空间 映射(识别命名用)——
+const CABINET_TO_SPACE = {
+  '主卧衣柜': '主卧', '次卧衣柜': '次卧', '儿童房衣柜': '儿童房', '老人房衣柜': '老人房',
+  '衣柜': '卧室', '客厅电视柜': '客厅', '电视柜': '客厅', '厨房橱柜': '厨房', '橱柜': '厨房',
+  '开放式厨房': '开放式厨房', '餐厅餐边柜': '餐厅', '餐边柜': '餐厅', '书房书柜': '书房',
+  '书柜': '书房', '玄关鞋柜': '玄关', '鞋柜': '玄关', '玄关柜': '玄关', '阳台柜': '阳台',
+  '衣帽间': '衣帽间', '储物柜': '储物间', '酒柜': '餐厅', '浴室柜': '卫生间', '榻榻米': '次卧',
+  '展示柜': '客厅', '洗衣机柜': '阳台', '卡座': '餐厅', '飘窗柜': '飘窗',
+};
+
+const SPACE_TO_DEFAULT_CABINET = {
+  '主卧': '主卧衣柜', '次卧': '次卧衣柜', '儿童房': '儿童房衣柜', '老人房': '老人房衣柜',
+  '客厅': '客厅电视柜', '厨房': '厨房橱柜', '开放式厨房': '开放式厨房', '餐厅': '餐厅餐边柜',
+  '客餐厅': '餐厅餐边柜', '书房': '书房书柜', '玄关': '玄关鞋柜', '阳台': '阳台柜',
+  '衣帽间': '衣帽间', '储物间': '储物柜', '卫生间': '浴室柜', '主卫': '浴室柜',
+};
+
+// —— 柜体类型 / 布局 / 门板 预设 ——
+const CUSTOM_TYPES_PRESET = {
+  cabinetType: ['主卧衣柜','次卧衣柜','儿童房衣柜','老人房衣柜','客厅电视柜','厨房橱柜','开放式厨房','餐厅餐边柜','书房书柜','玄关鞋柜','阳台柜','衣帽间','储物柜','酒柜','浴室柜','榻榻米'],
+  layoutForm: ['一字型','L型','U型','二字型','不规则'],
+  doorPanelType: ['普通平板','风影门板','PET门板','实木贴皮','烤漆门板','吸塑门板','玻璃门','百叶门','无门板'],
+};
+
+// —— 柜体默认附件模板(识别到柜型后自动配附件)——
+const CABINET_EXTRA_TEMPLATES = {
+  '玄关': ['灯带','变压器+开关','自弹器','见光板','铰链'],
+  '鞋柜': ['灯带','变压器+开关','自弹器','见光板','铰链','换鞋凳'],
+  '电视柜': ['自弹器','见光板','铰链'],
+  '展示柜': ['灯带','变压器+开关','拉手','见光板','铰链','自弹器'],
+  '酒柜': ['灯带','变压器+开关','拉手','见光板','铰链','自弹器','玻璃门升级'],
+  '洗衣机': ['自弹器','护墙板','见光板','铰链'],
+  '阳台': ['自弹器','见光板','铰链'],
+  '餐边': ['抽屉','拉手','自弹器','金属拉手','灯带','变压器+开关','见光板','铰链'],
+  '次卧': ['衣杆','金属拉手','抽屉','见光板','铰链'],
+  '儿童房': ['衣杆','金属拉手','抽屉','见光板','铰链'],
+  '书房': ['衣杆','金属拉手','抽屉','见光板','铰链'],
+  '主卧': ['金属拉手','抽屉','衣杆','自弹器','圆弧','铰链'],
+  '衣帽间': ['金属拉手','抽屉','衣杆','自弹器','圆弧','铰链','裤架领带格','拉篮'],
+  '厨房': ['见光板','碗篮','锅篮','自弹器'],
+  '_default': ['自弹器','见光板','铰链'],
+};
+
+// —— 附件计价方式映射 ——
+const EXTRA_METHOD_MAP = {
+  '见光板': '按投影面积', '护墙板': '按投影面积', '玻璃门升级': '按投影面积',
+  '灯带': '按延米', '衣杆': '按延米', '石材台面': '按延米',
+  '自弹器': '按个数', '铰链': '按个数', '变压器+开关': '按个数', '拉手': '按个数',
+  '金属拉手': '按个数', '抽屉': '按个数', '裤架领带格': '按个数', '拉篮': '按个数',
+  '碗篮': '按个数', '锅篮': '按个数', '圆弧': '按个数', '换鞋凳': '按个数',
+};
+
+// —— 基础价格表(name → {price, unit, method})——
+const PRICE_BOOK = {
+  // 柜体
+  '柜体投影单价':   { price: 1852, unit: '元/m²', method: '按投影面积' },
+  '生态澳松板柜体': { price: 1852, unit: '元/m²', method: '按投影面积' },
+  '铝格栅板柜体':   { price: 2532, unit: '元/m²', method: '按投影面积' },
+  // 厨房(单件计价模式)
+  '地柜':   { price: 2380, unit: '元/m', method: '按延米' },
+  '吊柜':   { price: 1748, unit: '元/m', method: '按延米' },
+  '冰箱吊柜': { price: 2622, unit: '元/m', method: '按延米' },
+  '高柜':   { price: 5950, unit: '元/m', method: '按延米' },
+  '台面':   { price: 1480, unit: '元/m', method: '按延米' },
+  '台面超出部分': { price: 2480, unit: '元/m', method: '按延米' },
+  '碗篮':   { price: 1268, unit: '元/个', method: '按个数' },
+  '锅篮':   { price: 1628, unit: '元/个', method: '按个数' },
+  '切角':   { price: 500, unit: '元/个', method: '按个数' },
+  '台下盆': { price: 500, unit: '元/个', method: '按个数' },
+  '包管':   { price: 500, unit: '元/个', method: '按个数' },
+  // 门板单价
+  '标配门板': { price: 380, unit: '元/m²', method: '按投影面积' },
+  '烤漆门板': { price: 580, unit: '元/m²', method: '按投影面积' },
+  '吸塑门板': { price: 480, unit: '元/m²', method: '按投影面积' },
+  '实木门板': { price: 880, unit: '元/m²', method: '按投影面积' },
+  '铝格栅门板': { price: 680, unit: '元/m²', method: '按投影面积' },
+  'PET门板':  { price: 450, unit: '元/m²', method: '按投影面积' },
+  // 附件
+  '见光板':   { price: 1138, unit: '元/m²', method: '按投影面积' },
+  '护墙板':   { price: 1852, unit: '元/m²', method: '按投影面积' },
+  '玻璃门升级': { price: 1852, unit: '元/m²', method: '按投影面积' },
+  '灯带':     { price: 386, unit: '元/m', method: '按延米' },
+  '变压器+开关': { price: 536, unit: '元/个', method: '按个数' },
+  '自弹器':   { price: 115, unit: '元/个', method: '按个数' },
+  '铰链':     { price: 40, unit: '元/个', method: '按个数' },
+  '拉手':     { price: 226, unit: '元/个', method: '按个数' },
+  '金属拉手': { price: 225, unit: '元/个', method: '按个数' },
+  '抽屉':     { price: 1300, unit: '元/个', method: '按个数' },
+  '衣杆':     { price: 288, unit: '元/m', method: '按延米' },
+  '裤架领带格': { price: 0, unit: '元/个', method: '按个数' },
+  '拉篮':     { price: 0, unit: '元/个', method: '按个数' },
+  '圆弧':     { price: 1560, unit: '元/个', method: '按个数' },
+  '换鞋凳':   { price: 0, unit: '元/个', method: '按个数' },
+  '石材台面': { price: 1480, unit: '元/m', method: '按延米' },
+};
+
+// —— 厨房计价参数(qGetKitchenBillableMeters 同源)——
+const KITCHEN_RULES = {
+  packageMeters: 3,     // 3米基础套餐(单件计价模式的扣减基准)
+  overlapMm: 600,       // 转角重叠扣减
+  fridgeMm: 1000,       // 冰箱位预留
+  typeThresholdM: 2,    // 最小边 ≤2m 判 L 型,>2m 判 U 型
+};
+
+// —— 客户分型(销售策略核心)——
+const CUSTOMER_TIERS = {
+  price: {
+    label: '看重价格型', icon: '🏷️',
+    strategy: '预算少的客户直接按性价比报,快速赢单',
+    defaultPlan: 'economy', defaultDiscount: 0.85,
+    doorType: '标配', hardware: '经济实用款', service: '标准测量+安装',
+    warranty: '3年质保', gifts: '送基础五金配件', panelLabel: '进口爱格板(加价升级)',
+    autoEnableActivity: true,
+    discounts: [
+      { id: 'p1', label: '投影面积85折', rate: 0.85, deletable: false },
+      { id: 'p2', label: '投影面积8折特惠', rate: 0.80, deletable: true },
+      { id: 'p3', label: '团购78折', rate: 0.78, deletable: true },
+    ],
+  },
+  standard: {
+    label: '标准客户', icon: '🏠',
+    strategy: '标准配置报价,正常谈单空间',
+    defaultPlan: 'standard', defaultDiscount: 1.0,
+    doorType: '标配门板', hardware: '国产优质铰链/拉手', service: '标准测量安装',
+    warranty: '5年质保', gifts: '可参与活动优惠', panelLabel: '国产E0级颗粒板',
+    autoEnableActivity: false,
+    discounts: [
+      { id: 's1', label: '原价', rate: 1.00, deletable: false },
+      { id: 's2', label: '投影面积95折', rate: 0.95, deletable: true },
+      { id: 's3', label: '投影面积9折', rate: 0.90, deletable: true },
+    ],
+  },
+  quality: {
+    label: '品质导向型', icon: '💎',
+    strategy: '有预算的客户报高一点,留谈价空间,突出配置与质感',
+    defaultPlan: 'quality', defaultDiscount: 1.0,
+    doorType: '烤漆/吸塑高端门板', hardware: '进口百隆/海蒂诗五金', service: 'VIP专属设计师+金牌安装',
+    warranty: '10年质保+终身维护', gifts: '赠灯带+抽屉升级包', panelLabel: '进口爱格板/可丽芙(推荐)',
+    premiumRate: 1.15,
+    autoEnableActivity: false,
+    discounts: [
+      { id: 'q1', label: '投影面积95折(保利润)', rate: 0.95, deletable: false },
+      { id: 'q2', label: '原价(高端不打折)', rate: 1.00, deletable: true },
+      { id: 'q3', label: 'VIP专享92折', rate: 0.92, deletable: true },
+    ],
+  },
+};
+
+// —— 报价方案 ——
+const QUOTATION_PLANS = [
+  { id: 'economy',  name: '经济型方案', description: '适合预算敏感型客户,主打性价比', customerTier: 'price',    tierName: '双饰面', floatPct: 10, discountRate: 0.98 },
+  { id: 'standard', name: '标准型方案', description: '默认方案,标准配置,适合大多数客户', customerTier: 'standard', tierName: 'PET',    floatPct: 15, discountRate: 1.00 },
+  { id: 'quality',  name: '品质型方案', description: '高端配置,适合追求品质的客户', customerTier: 'quality',   tierName: '混油',   floatPct: 15, discountRate: 1.00 },
+];
+
+// —— 营销活动模块(可勾选叠加)——
+const ACTIVITY_MODULES = [
+  { id: 'deposit5000', type: 'deposit', name: '定金膨胀(5千档)', desc: '交¥5,000→膨胀¥8,000 + ¥500抵用券', amount: 3500, minOrder: 30000 },
+  { id: 'deposit10000', type: 'deposit', name: '定金膨胀(1万档)', desc: '交¥10,000→膨胀¥13,000 + ¥800抵用券', amount: 3800, minOrder: 30000 },
+  { id: 'coupon20k', type: 'coupon', name: '满减券', desc: '满¥20,000减¥1,000', amount: 1000, threshold: 20000 },
+  { id: 'cashback', type: 'tiered_cashback', name: '阶梯返现', tiers: [
+      { min: 50000, cash: 2000 }, { min: 100000, cash: 5000 }, { min: 150000, cash: 8000 },
+    ] },
+];
+
+// —— 默认层高与房间面积先验(识别失败兜底估算用)——
+const DEFAULTS = {
+  height: 2.8, // 默认柜高(层高)
+  roomAreaPriors: { // 常见房间典型面积范围 [min, max](m²)
+    '主卧': [12, 22], '次卧': [8, 16], '儿童房': [8, 14], '老人房': [9, 15], '书房': [6, 12],
+    '客厅': [18, 35], '餐厅': [8, 18], '厨房': [4.5, 10], '卫生间': [2.5, 6], '主卫': [3, 6],
+    '公卫': [2.5, 6], '阳台': [3, 10], '玄关': [2, 6], '衣帽间': [3, 10], '储物间': [1.5, 5],
+  },
+  cabinetWallRatio: { // 柜体默认占所在墙面比例(无明确尺寸时的估算)
+    '主卧衣柜': 0.55, '次卧衣柜': 0.50, '儿童房衣柜': 0.50, '玄关鞋柜': 0.6,
+    '客厅电视柜': 0.55, '餐厅餐边柜': 0.5, '阳台柜': 0.6, '书房书柜': 0.5,
+  },
+};
+
+// —— 赢单文案素材(业主视角价值描述)——
+const SELLING_POINTS = {
+  price: [
+    '按投影面积计价,明明白白,没有隐形收费',
+    'E0级环保板材,装完即可安心入住',
+    '工厂直供价,同样的预算多做一个柜子',
+  ],
+  standard: [
+    '空间利用率拉满:每一面墙都变成收纳',
+    '全国联保5年,铰链五金终身维护',
+    '设计上门量尺,3D效果图确认后再生产',
+  ],
+  quality: [
+    '进口板材+进口五金,用20年不变形不褪色',
+    'VIP设计师一对一,全屋风格统一设计',
+    '10年质保+终身维护,一次装修终身省心',
+  ],
+};
+
+// ============================================================
+// 拉迷全屋定制 · 报价引擎(纯函数,浏览器/CLI 共用)
+// 两种模式:
+//   quick  — AI快速粗略报价:投影面积/厨房延米 × 三档材质区间单价
+//   detail — 明细报价:Excel 预算表模型(柜体+门板补差+附件逐项)
+// 销售策略:客户分型 → 方案/折扣/浮动 → 活动叠加 → 谈价区间
+// ============================================================
+
+const r2 = (v) => Math.round(v * 100) / 100;
+const num = (v, d = 0) => { const n = parseFloat(v); return Number.isFinite(n) ? n : d; };
+
+// ---------- 材质档位 ----------
+function getTier(name) {
+  const t = PRICE_TIERS.find((x) => x.name === (name || DEFAULT_TIER));
+  return t || PRICE_TIERS[1];
+}
+// 浮动百分比(10~15)在区间内线性插值 → 单价(qGetAiUnitPrice 同源逻辑)
+function tierUnitPrice(tier, floatPct = 15) {
+  const k = Math.min(1, Math.max(0, (num(floatPct, 15) - 10) / 5));
+  return r2(tier.min + k * (tier.max - tier.min));
+}
+
+// ---------- 厨房计费米数(qGetKitchenBillableMeters 同源)----------
+function kitchenBillable(walls = [], rules = {}) {
+  const R = { ...KITCHEN_RULES, ...rules };
+  const ws = (Array.isArray(walls) ? walls : []).map((v) => num(v)).filter((v) => v > 0);
+  if (!ws.length) return null;
+  const total = ws.reduce((a, b) => a + b, 0);
+  const minWall = Math.min(...ws);
+  const maxWall = Math.max(...ws);
+  const type = minWall <= R.typeThresholdM ? 'L' : 'U';
+  const overlapCount = type === 'L' ? 1 : 2;
+  const usedLen = type === 'L' ? maxWall + minWall : maxWall + maxWall + minWall;
+  const billable = Math.max(0, usedLen - (overlapCount * R.overlapMm) / 1000 - R.fridgeMm / 1000);
+  return {
+    walls: ws, total: r2(total), minWall: r2(minWall), maxWall: r2(maxWall),
+    kitchenType: type, overlapCount, fridgeMm: R.fridgeMm, usedLen: r2(usedLen), billable: r2(billable),
+  };
+}
+const isKitchen = (name = '') => /厨房|橱柜/.test(String(name));
+
+// ---------- 附件数量启发式(Excel 模型总结,均可编辑) ----------
+function estimateExtras(cabinet) {
+  const w = num(cabinet.width, 1.5);
+  const h = num(cabinet.height, DEFAULTS.height);
+  const type = String(cabinet.type || '');
+  let tplKey = Object.keys(CABINET_EXTRA_TEMPLATES).find((k) => k !== '_default' && type.includes(k));
+  if (!tplKey && isKitchen(type)) tplKey = '厨房';
+  const names = CABINET_EXTRA_TEMPLATES[tplKey || '_default'] || CABINET_EXTRA_TEMPLATES._default;
+  const sections = h > 1.6 ? 2 : 1;               // 高柜分上下段
+  const doorsPerSection = Math.max(1, Math.round(w / 0.45));
+  const doors = sections * doorsPerSection;
+  const hingePerDoor = h > 2 ? 3 : 2;
+  const out = [];
+  for (const name of names) {
+    const pb = PRICE_BOOK[name];
+    if (!pb) continue;
+    let qty = 0; let note = '';
+    switch (name) {
+      case '铰链': qty = doors * hingePerDoor; note = `${doors}门×${hingePerDoor}铰链`; break;
+      case '自弹器': qty = Math.max(1, doors - 1); note = '反弹门配'; break;
+      case '见光板': qty = 1; note = '单侧见光(0.5m宽)'; break;
+      case '灯带': qty = r2(w * 2); note = '顶底双层'; break;
+      case '变压器+开关': qty = 1; note = 'LED电源'; break;
+      case '衣杆': qty = r2(w); note = '通长挂杆'; break;
+      case '抽屉': qty = type.includes('主卧') || type.includes('衣帽') ? 3 : 2; break;
+      case '金属拉手': case '拉手': qty = doors + 2; break;
+      case '碗篮': case '锅篮': qty = 1; break;
+      default: qty = name === '玻璃门升级' || name === '护墙板' ? 0 : 1;
+    }
+    if (qty > 0) out.push({ name, method: pb.method, unit: pb.unit, unitPrice: pb.price, qty, note });
+  }
+  return out;
+}
+
+// ---------- 单柜体计价 ----------
+// 计价数量:投影面积 = 宽×高;厨房 = 计费延米
+function cabinetQty(cabinet, rules = {}) {
+  if (isKitchen(cabinet.type)) {
+    const kb = kitchenBillable(cabinet.walls || [cabinet.width, cabinet.height, cabinet.width, cabinet.height], rules);
+    return { qty: kb ? kb.billable : num(cabinet.width), unit: 'm', kb };
+  }
+  return { qty: r2(num(cabinet.width) * num(cabinet.height)), unit: 'm²', kb: null };
+}
+
+// 快速模式:单柜体一行
+function quickLine(cabinet, { tierName, floatPct = 15, rules } = {}) {
+  const tier = getTier(tierName);
+  const U = tierUnitPrice(tier, floatPct);
+  const { qty, unit, kb } = cabinetQty(cabinet, rules);
+  return {
+    mode: 'quick', name: cabinet.type, tierName: tier.name,
+    qty, unit, unitPrice: U, total: r2(qty * U), kb,
+    dims: num(cabinet.width) && num(cabinet.height) ? `${num(cabinet.width)}m×${num(cabinet.height)}m` : '',
+  };
+}
+
+// 明细模式:单柜体 → Excel 风格明细行
+function detailLines(cabinet, opts = {}) {
+  const { doorDiffPrice = PRICE_BOOK['柜体投影单价'].price > 0 ? 538 : 538, rules, includeDoorDiff = true } = opts;
+  const lines = [];
+  const isK = isKitchen(cabinet.type);
+  const { qty } = cabinetQty(cabinet, rules);
+  if (isK) {
+    // 厨房单件计价(Excel 模式:地柜+吊柜+台面+功能五金)
+    const walls = cabinet.walls || [cabinet.width, cabinet.height, cabinet.width, cabinet.height];
+    const straight = Math.max(0, ...walls); // 直线段米数近似
+    for (const [name, factor, note] of [
+      ['地柜', 1, '沿墙地柜'], ['吊柜', 1, '沿墙吊柜'], ['台面', 1, '石英石台面'],
+    ]) {
+      const pb = PRICE_BOOK[name];
+      lines.push({ name, qty: r2(qty), unit: 'm', unitPrice: pb.price, total: r2(qty * pb.price), note, method: pb.method });
+    }
+    for (const name of ['碗篮', '锅篮']) {
+      const pb = PRICE_BOOK[name];
+      lines.push({ name, qty: 1, unit: '个', unitPrice: pb.price, total: pb.price, note: '标准配置', method: pb.method });
+    }
+    return { cabinet, isKitchen: true, lines, subtotal: r2(lines.reduce((s, l) => s + l.total, 0)) };
+  }
+  const pb = PRICE_BOOK['生态澳松板柜体'];
+  lines.push({ name: '柜体', qty, unit: 'm²', unitPrice: pb.price, total: r2(qty * pb.price), note: '生态澳松板', method: pb.method });
+  if (includeDoorDiff && doorDiffPrice > 0) {
+    lines.push({ name: '板门补差价', qty, unit: 'm²', unitPrice: doorDiffPrice, total: r2(qty * doorDiffPrice), note: '双饰面门补差', method: pb.method });
+  }
+  for (const ex of estimateExtras(cabinet)) {
+    const total = ex.method === '按投影面积' ? r2(ex.qty * 0.5 * num(cabinet.height) * ex.unitPrice)
+      : ex.method === '按延米' ? r2(ex.qty * ex.unitPrice)
+      : r2(ex.qty * ex.unitPrice);
+    lines.push({ name: ex.name, qty: ex.qty, unit: ex.unit, unitPrice: ex.unitPrice, total, note: ex.note, method: ex.method });
+  }
+  return { cabinet, isKitchen: false, lines, subtotal: r2(lines.reduce((s, l) => s + l.total, 0)) };
+}
+
+// ---------- 报价主入口 ----------
+// inputs: { cabinets:[{type,width,height,walls?,excluded?}], tier, floatPct, discount, tierKey(客户分型), planId, mode }
+function buildQuote(input = {}) {
+  const {
+    cabinets = [], mode = 'quick',
+    tierName = input.tier || DEFAULT_TIER,
+    floatPct = 15, discount = null, tierKey = 'standard', planId = null,
+    rules = {}, extras = [], // extras: [{name, room, qty, unitPrice}] 手动增删项
+    clientName = '', note = '',
+  } = input;
+
+  const plan = QUOTATION_PLANS.find((p) => p.id === (planId || CUSTOMER_TIERS[tierKey]?.defaultPlan)) || QUOTATION_PLANS[1];
+  const custTier = CUSTOMER_TIERS[tierKey] || CUSTOMER_TIERS.standard;
+  const effDiscount = discount != null ? num(discount) : (custTier.defaultDiscount ?? plan.discountRate ?? 1);
+
+  const rooms = cabinets
+    .filter((c) => !c.excluded)
+    .map((c, i) => {
+      const base = mode === 'detail' ? detailLines(c, { rules }) : { ...quickLine(c, { tierName, floatPct, rules }), lines: null };
+      const id = c.id || `cab_${i + 1}`;
+      return { id, type: c.type, dims: base.dims || `${num(c.width)}m×${num(c.height)}m`, ...base };
+    });
+
+  const itemsSubtotal = r2(rooms.reduce((s, r) => s + (r.subtotal ?? r.total), 0));
+  const manualExtras = extras.map((e, i) => ({
+    id: e.id || `ext_${i + 1}`, name: e.name, qty: num(e.qty, 1), unit: e.unit || '项',
+    unitPrice: num(e.unitPrice), total: r2(num(e.qty, 1) * num(e.unitPrice)), kind: e.kind || 'add',
+  }));
+  const extrasTotal = r2(manualExtras.reduce((s, e) => s + (e.kind === 'remove' ? -e.total : e.total), 0));
+
+  const premiumRate = custTier.premiumRate || 1;
+  const afterPremium = r2((itemsSubtotal + extrasTotal) * premiumRate);
+  const afterDiscount = r2(afterPremium * effDiscount);
+
+  // 活动叠加(自动/手动)
+  const acts = (input.activities || (custTier.autoEnableActivity ? ACTIVITY_MODULES.map((m) => m.id) : []))
+    .map((id) => ACTIVITY_MODULES.find((m) => m.id === id)).filter(Boolean);
+  let activityTotal = 0;
+  const activityLines = [];
+  for (const m of acts) {
+    let amt = 0; let desc = m.desc || m.name;
+    if (m.type === 'tiered_cashback') {
+      const hit = [...(m.tiers || [])].sort((a, b) => b.min - a.min).find((t) => afterDiscount >= t.min);
+      if (!hit) continue;
+      amt = hit.cash; desc = `满¥${hit.min.toLocaleString()}返¥${hit.cash.toLocaleString()}`;
+    } else if (m.type === 'coupon' && afterDiscount < (m.threshold || 0)) continue;
+    else if (m.type === 'deposit' && afterDiscount < (m.minOrder || 30000)) continue;
+    else amt = m.amount || 0;
+    activityTotal += amt;
+    activityLines.push({ id: m.id, name: m.name, desc, amount: amt });
+  }
+  const finalPrice = Math.max(0, r2(afterDiscount - activityTotal));
+
+  // 谈价区间(浮动率)
+  const rate = num(floatPct, 15) / 100;
+  const range = { low: Math.round(finalPrice * (1 - rate)), high: Math.round(finalPrice * (1 + rate)) };
+
+  // 业主视角价值点
+  const selling = SELLING_POINTS[tierKey] || SELLING_POINTS.standard;
+
+  return {
+    meta: {
+      clientName, note: note || custTier.strategy, mode,
+      tierName: mode === 'quick' ? getTier(tierName).name : undefined,
+      floatPct, planId: plan.id, planName: plan.name,
+      customerTier: tierKey, customerLabel: custTier.label,
+      discount: effDiscount, premiumRate: premiumRate !== 1 ? premiumRate : undefined,
+      generatedAt: new Date().toISOString(),
+    },
+    rooms, manualExtras, activityLines,
+    totals: {
+      itemsSubtotal, extrasTotal, premiumRate, afterPremium, discount: effDiscount,
+      afterDiscount, activityTotal, finalPrice, range,
+      discountLabel: custTier.discounts?.find((d) => Math.abs(d.rate - effDiscount) < 0.001)?.label || `折扣${Math.round(effDiscount * 100)}%`,
+    },
+    selling,
+    config: { tierKey, tierName, floatPct, discount: effDiscount, planId: plan.id, mode },
+  };
+}
+
+// ---------- 语义化调整应用(销售边聊边改) ----------
+// ops: [{op:'add'|'remove'|'resize'|'rename'|'price', target, ...}]
+function applyOps(cabinets, ops = []) {
+  const out = cabinets.map((c) => ({ ...c }));
+  for (const op of ops) {
+    if (op.op === 'add') {
+      out.push({ type: op.type || op.name || '柜体', width: num(op.width, 1.5), height: num(op.height, DEFAULTS.height), from: '语音新增' });
+    } else if (op.op === 'remove') {
+      const i = out.findIndex((c) => c.type === op.target);
+      if (i >= 0) out.splice(i, 1);
+    } else if (op.op === 'resize') {
+      const c = out.find((c) => c.type === op.target);
+      if (c) { if (op.width) c.width = num(op.width); if (op.height) c.height = num(op.height); c.from = '手动调整'; }
+    } else if (op.op === 'rename') {
+      const c = out.find((c) => c.type === op.target);
+      if (c) c.type = op.to || c.type;
+    } else if (op.op === 'exclude') {
+      const c = out.find((c) => c.type === op.target);
+      if (c) c.excluded = true;
+    } else if (op.op === 'include') {
+      const c = out.find((c) => c.type === op.target);
+      if (c) delete c.excluded;
+    }
+  }
+  return out;
+}
+
+// 授权范围内校验(浮动区间即授权边界)
+function withinAuthority(quote, price) {
+  const { range } = quote.totals;
+  return price >= range.low && price <= range.high;
+}
+
+var __DEFAULT__ = { buildQuote, applyOps, quickLine, detailLines, kitchenBillable, tierUnitPrice, getTier, withinAuthority };
+
+window.__Q__ = {"cabinets":[{"id":"cab_1","type":"玄关鞋柜","width":1,"height":2.6,"rawName":"玄关鞋柜"},{"id":"cab_2","type":"厨房橱柜","height":2.6,"rawName":"厨房橱柜"},{"id":"cab_3","type":"厨房橱柜","height":2.6,"rawName":"厨房橱柜"},{"id":"cab_4","type":"餐厅餐边柜","width":1.5,"height":2.6,"rawName":"餐厅餐边柜"},{"id":"cab_5","type":"衣帽间","width":3.9,"height":2.6,"rawName":"衣帽间"},{"id":"cab_6","type":"次卧衣柜","width":2.4,"height":2.6,"rawName":"次卧衣柜"},{"id":"cab_7","type":"阳台柜","width":1.5,"height":2.6,"rawName":"阳台柜"}],"config":{"tierKey":"standard","tierName":"PET","floatPct":15,"discount":1,"planId":"standard","mode":"quick"},"salesNote":""};
+console.log('fpc engine ready');
+</script>
+<script>
+(function(){
+  var Q = window.__Q__; var S = document; var money = function(n){ return '¥' + Math.round(n||0).toLocaleString('zh-CN'); };
+  var cfg = Object.assign({ tierKey:'standard', tierName:'PET', floatPct:15, discount:1.0, mode:'quick' }, Q.config||{});
+  function calc(){ return buildQuote({ cabinets: Q.cabinets, tierKey: cfg.tierKey, tierName: cfg.tierName, floatPct: cfg.floatPct, discount: cfg.discount, mode: cfg.mode, clientName: "PDF链路测试" }); }
+  function refresh(){
+    var q = calc(); var t = q.totals;
+    S.getElementById('finalPrice').textContent = money(t.finalPrice);
+    S.getElementById('finalRange').textContent = '谈价参考区间 ' + money(t.range.low) + ' ~ ' + money(t.range.high);
+    S.getElementById('tSub').textContent = money(t.itemsSubtotal);
+    S.getElementById('tDiscLabel').textContent = t.discountLabel;
+    S.getElementById('tAfter').textContent = money(t.afterDiscount);
+    S.getElementById('tAct').textContent = '-' + money(t.activityTotal);
+    S.getElementById('tFinal').textContent = money(t.finalPrice);
+    S.getElementById('bTier').textContent = (q.meta.tierName||'') + '材质';
+    S.getElementById('bDisc').textContent = t.discountLabel;
+  }
+  // room expand
+  S.querySelectorAll('.room-h').forEach(function(el){ el.addEventListener('click', function(){ el.parentElement.classList.toggle('open'); }); });
+  // chips
+  function bindChips(id, fn){ var box = S.getElementById(id); if(!box) return; box.addEventListener('click', function(e){ var b = e.target.closest('.chip'); if(!b) return; box.querySelectorAll('.chip').forEach(function(x){x.classList.remove('on');}); b.classList.add('on'); fn(b.dataset.v); }); }
+  bindChips('optTier', function(v){ cfg.tierKey = v; refresh(); });
+  bindChips('optMat', function(v){ cfg.tierName = v; refresh(); });
+  bindChips('optDisc', function(v){ cfg.discount = parseFloat(v); refresh(); });
+  var fr = S.getElementById('floatR'); fr.addEventListener('input', function(){ S.getElementById('floatV').textContent = fr.value; cfg.floatPct = parseInt(fr.value); refresh(); });
+  // drawer
+  var d = S.getElementById('drawer');
+  S.getElementById('btnAdjust').addEventListener('click', function(){ d.classList.add('open'); renderCabs(); });
+  d.addEventListener('click', function(e){ if(e.target === d) d.classList.remove('open'); });
+  function renderCabs(){
+    var box = S.getElementById('cabList'); box.innerHTML = '';
+    Q.cabinets.forEach(function(c, i){
+      var row = S.createElement('div'); row.className = 'cabrow';
+      var dims = String(c.width||'') ? String(c.width) : '';
+      row.innerHTML = '<span style="flex:1">' + c.rawName + '</span>' +
+        '<input type="number" step="0.05" value="' + (c.width||1.5) + '" data-i="' + i + '" data-f="width" title="宽(米)">' +
+        '<span class="del" data-i="' + i + '">✕</span>';
+      box.appendChild(row);
+    });
+    box.onclick = function(e){
+      var del = e.target.closest('.del');
+      if (del) { Q.cabinets.splice(parseInt(del.dataset.i),1); renderCabs(); refresh(); return; }
+      var inp = e.target.closest('input');
+      if (inp) { inp.addEventListener('change', function(){ Q.cabinets[parseInt(inp.dataset.i)].width = parseFloat(inp.value)||1.5; refresh(); }); }
+    };
+  }
+  S.getElementById('btnAddCab').addEventListener('click', function(){ Q.cabinets.push({ type:'储物柜', rawName:'储物柜', width:1.5, height:2.6 }); renderCabs(); refresh(); });
+  S.getElementById('btnSave').addEventListener('click', function(){
+    alert('已记录!销售将带着这份报价与您确认到店时间,到店凭此单领取到店礼。');
+  });
+  refresh();
+})();
+</script>
+</body>
+</html>

+ 757 - 0
skill/out/报价单-PDF链路测试-08312342.html

@@ -0,0 +1,757 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+<head>
+<meta charset="UTF-8">
+<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
+<meta name="format-detection" content="telephone=no">
+<title>PDF链路测试 · 全屋定制报价方案</title>
+<style>
+:root{--p:#1F497D;--pa:#2B6CB0;--bg:#F5F7FA;--card:#fff;--bd:#E2E8F0;--tx:#2D3748;--mu:#718096;--ok:#38A169;--wa:#D69E2E;--dg:#E53E3E;--r:12px}
+*{box-sizing:border-box;margin:0;padding:0;-webkit-tap-highlight-color:transparent}
+body{font-family:-apple-system,BlinkMacSystemFont,"PingFang SC","Microsoft YaHei",sans-serif;background:var(--bg);color:var(--tx);line-height:1.55;padding-bottom:calc(88px + env(safe-area-inset-bottom))}
+header{background:linear-gradient(135deg,#1F497D,#2B6CB0);color:#fff;padding:18px 16px 14px}
+header h1{font-size:17px;font-weight:700}
+header .sub{font-size:12px;opacity:.85;margin-top:2px}
+.wrap{padding:12px;max-width:640px;margin:0 auto}
+.hero{background:var(--card);border-radius:var(--r);padding:16px;box-shadow:0 1px 3px rgba(0,0,0,.08);margin-top:-26px;position:relative;text-align:center}
+.hero .label{font-size:12px;color:var(--mu)}
+.hero .price{font-size:38px;font-weight:800;color:var(--p);letter-spacing:-1px;margin:2px 0}
+.hero .price small{font-size:16px;font-weight:600}
+.hero .range{font-size:13px;color:var(--mu)}
+.badge{display:inline-block;font-size:11px;padding:2px 10px;border-radius:20px;background:#EBF8FF;color:var(--pa);border:1px solid #BEE3F8;margin-top:6px}
+.sec{margin-top:14px}
+.sec h2{font-size:15px;font-weight:700;margin:0 2px 8px;display:flex;align-items:center;gap:6px}
+.card{background:var(--card);border-radius:var(--r);box-shadow:0 1px 3px rgba(0,0,0,.06);overflow:hidden}
+.room{border-bottom:1px solid var(--bd)}
+.room:last-child{border-bottom:none}
+.room-h{padding:12px 14px;display:flex;justify-content:space-between;align-items:center;gap:8px}
+.room-h .nm{font-weight:700;font-size:14px}
+.room-h .dm{font-size:11px;color:var(--mu)}
+.room-h .tt{font-weight:800;color:var(--p);font-size:15px;white-space:nowrap}
+.room-h .conf{font-size:10px;padding:1px 6px;border-radius:8px;background:#FFFAF0;color:#C05621;border:1px solid #FBD38D;flex-shrink:0}
+.items{padding:0 14px 10px;display:none}
+.room.open .items{display:block}
+.item{display:flex;justify-content:space-between;font-size:12.5px;padding:5px 0;color:#4A5568}
+.item .l span{color:var(--mu);font-size:11px;margin-left:6px}
+.item .v{white-space:nowrap}
+.trow{display:flex;justify-content:space-between;padding:8px 14px;font-size:13px}
+.trow .v{font-weight:700}
+.trow.total{background:#F0FFF4;color:#276749;font-weight:800;font-size:15px}
+.trow.dim{color:var(--mu);font-size:12px}
+.plan-wrap{background:var(--card);border-radius:var(--r);padding:10px;box-shadow:0 1px 3px rgba(0,0,0,.06)}
+.plan-wrap svg,.plan-img{width:100%;height:auto;display:block;border-radius:8px}
+.plan-note{font-size:11px;color:var(--mu);text-align:center;margin-top:6px}
+.selling{background:linear-gradient(135deg,#FFFAF0,#FFF5EB);border:1px solid #FBD38D;border-radius:var(--r);padding:12px 14px}
+.selling li{font-size:13px;margin:6px 0 6px 18px;color:#744210}
+.selling h3{font-size:13px;color:#C05621;margin-bottom:4px}
+.acts .item .v{color:var(--ok)}
+.ctrl{position:fixed;bottom:0;left:0;right:0;background:#fff;border-top:1px solid var(--bd);padding:10px 14px calc(10px + env(safe-area-inset-bottom));display:flex;gap:8px;z-index:99;max-width:640px;margin:0 auto}
+.btn{flex:1;padding:12px;border:none;border-radius:10px;font-size:14px;font-weight:700;cursor:pointer}
+.btn.pri{background:var(--p);color:#fff}
+.btn.sec{background:#EDF2F7;color:var(--tx)}
+details.dims{background:var(--card);border-radius:var(--r);padding:10px 14px;font-size:12px;color:var(--mu)}
+details.dims summary{cursor:pointer;font-weight:600;color:var(--tx)}
+details.dims li{margin:4px 0 4px 18px}
+.drawer{position:fixed;inset:0;background:rgba(0,0,0,.4);z-index:100;display:none}
+.drawer.open{display:block}
+.sheet{position:absolute;bottom:0;left:0;right:0;background:#fff;border-radius:16px 16px 0 0;max-height:82vh;overflow:auto;padding:16px 16px calc(20px + env(safe-area-inset-bottom));max-width:640px;margin:0 auto}
+.sheet h3{font-size:15px;margin-bottom:12px}
+.frow{margin-bottom:14px}
+.frow label{font-size:12px;color:var(--mu);display:block;margin-bottom:4px}
+.frow .opts{display:flex;gap:6px;flex-wrap:wrap}
+.chip{padding:8px 12px;border-radius:8px;border:1.5px solid var(--bd);background:#fff;font-size:13px;cursor:pointer}
+.chip.on{border-color:var(--pa);background:#EBF8FF;color:var(--pa);font-weight:700}
+input[type=range]{width:100%}
+.cabrow{display:flex;align-items:center;gap:8px;padding:8px 0;border-bottom:1px dashed var(--bd);font-size:13px}
+.cabrow input{width:64px;padding:6px;border:1px solid var(--bd);border-radius:6px;font-size:13px}
+.cabrow .del{color:var(--dg);font-size:18px;padding:0 6px;cursor:pointer}
+.cabrow .add{color:var(--ok)}
+@media(min-width:640px){.hero .price{font-size:44px}}
+</style>
+</head>
+<body>
+<header>
+  <h1>PDF链路测试 · 全屋定制报价方案</h1>
+  <div class="sub">标准客户方案 · 生成于 8/31 23:42 · 拉迷全屋定制</div>
+</header>
+<div class="wrap">
+
+  <div class="hero">
+    <div class="label">全屋定制整装价</div>
+    <div class="price" id="finalPrice">¥45,400</div>
+    <div class="range" id="finalRange">谈价参考区间 ¥38,590 ~ ¥52,210</div>
+    <div id="badges">
+      <span class="badge" id="bTier">PET材质</span>
+      <span class="badge" id="bDisc">原价</span>
+      
+    </div>
+  </div>
+
+  
+
+  <div class="sec">
+    <h2>🏠 户型与定制布局</h2>
+    <div class="plan-wrap" id="planWrap"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 2710 2330" class="fpc-plan">
+<g id="fpc-rooms">
+<g class="fpc-room" data-room="厨房">
+<rect x="960.0" y="120.0" width="790.0" height="570.0" fill="#F7FAFC" stroke="#2D3748" stroke-width="6"/>
+<line x1="1265.0" y1="690.0" x2="1355.0" y2="690.0" stroke="#fff" stroke-width="8"/>
+<text x="1355.0" y="405.0" text-anchor="middle" dominant-baseline="middle" font-size="52" fill="#2D3748" font-weight="600">厨房</text>
+</g>
+<g class="fpc-room" data-room="玄关">
+<rect x="960.0" y="880.0" width="790.0" height="570.0" fill="#F7FAFC" stroke="#2D3748" stroke-width="6"/>
+<line x1="1265.0" y1="1450.0" x2="1355.0" y2="1450.0" stroke="#fff" stroke-width="8"/>
+<text x="1355.0" y="1165.0" text-anchor="middle" dominant-baseline="middle" font-size="52" fill="#2D3748" font-weight="600">玄关</text>
+</g>
+<g class="fpc-room" data-room="卫生间(公卫)">
+<rect x="960.0" y="880.0" width="790.0" height="570.0" fill="#F7FAFC" stroke="#2D3748" stroke-width="6"/>
+<line x1="1265.0" y1="1450.0" x2="1355.0" y2="1450.0" stroke="#fff" stroke-width="8"/>
+<text x="1355.0" y="1165.0" text-anchor="middle" dominant-baseline="middle" font-size="52" fill="#2D3748" font-weight="600">卫生间(公卫)</text>
+</g>
+<g class="fpc-room" data-room="餐厅">
+<rect x="960.0" y="880.0" width="790.0" height="570.0" fill="#F7FAFC" stroke="#2D3748" stroke-width="6"/>
+<line x1="1265.0" y1="1450.0" x2="1355.0" y2="1450.0" stroke="#fff" stroke-width="8"/>
+<text x="1355.0" y="1165.0" text-anchor="middle" dominant-baseline="middle" font-size="52" fill="#2D3748" font-weight="600">餐厅</text>
+</g>
+<g class="fpc-room" data-room="客厅">
+<rect x="960.0" y="880.0" width="790.0" height="570.0" fill="#F7FAFC" stroke="#2D3748" stroke-width="6"/>
+<line x1="1265.0" y1="1450.0" x2="1355.0" y2="1450.0" stroke="#fff" stroke-width="8"/>
+<text x="1355.0" y="1165.0" text-anchor="middle" dominant-baseline="middle" font-size="52" fill="#2D3748" font-weight="600">客厅</text>
+</g>
+<g class="fpc-room" data-room="主卧">
+<rect x="120.0" y="1640.0" width="790.0" height="570.0" fill="#F7FAFC" stroke="#2D3748" stroke-width="6"/>
+<line x1="425.0" y1="2210.0" x2="515.0" y2="2210.0" stroke="#fff" stroke-width="8"/>
+<text x="515.0" y="1925.0" text-anchor="middle" dominant-baseline="middle" font-size="52" fill="#2D3748" font-weight="600">主卧</text>
+</g>
+<g class="fpc-room" data-room="衣帽间">
+<rect x="960.0" y="880.0" width="790.0" height="570.0" fill="#F7FAFC" stroke="#2D3748" stroke-width="6"/>
+<line x1="1265.0" y1="1450.0" x2="1355.0" y2="1450.0" stroke="#fff" stroke-width="8"/>
+<text x="1355.0" y="1165.0" text-anchor="middle" dominant-baseline="middle" font-size="52" fill="#2D3748" font-weight="600">衣帽间</text>
+</g>
+<g class="fpc-room" data-room="卫生间(主卫)">
+<rect x="960.0" y="880.0" width="790.0" height="570.0" fill="#F7FAFC" stroke="#2D3748" stroke-width="6"/>
+<line x1="1265.0" y1="1450.0" x2="1355.0" y2="1450.0" stroke="#fff" stroke-width="8"/>
+<text x="1355.0" y="1165.0" text-anchor="middle" dominant-baseline="middle" font-size="52" fill="#2D3748" font-weight="600">卫生间(主卫)</text>
+</g>
+<g class="fpc-room" data-room="次卧">
+<rect x="960.0" y="1640.0" width="790.0" height="570.0" fill="#F7FAFC" stroke="#2D3748" stroke-width="6"/>
+<line x1="1265.0" y1="2210.0" x2="1355.0" y2="2210.0" stroke="#fff" stroke-width="8"/>
+<text x="1355.0" y="1925.0" text-anchor="middle" dominant-baseline="middle" font-size="52" fill="#2D3748" font-weight="600">次卧</text>
+</g>
+<g class="fpc-room" data-room="生活阳台">
+<rect x="960.0" y="1640.0" width="790.0" height="570.0" fill="#F7FAFC" stroke="#2D3748" stroke-width="6"/>
+<line x1="1265.0" y1="2210.0" x2="1355.0" y2="2210.0" stroke="#fff" stroke-width="8"/>
+<text x="1355.0" y="1925.0" text-anchor="middle" dominant-baseline="middle" font-size="52" fill="#2D3748" font-weight="600">生活阳台</text>
+</g>
+<g class="fpc-room" data-room="观景阳台">
+<rect x="1800.0" y="1640.0" width="790.0" height="570.0" fill="#F7FAFC" stroke="#2D3748" stroke-width="6"/>
+<line x1="2105.0" y1="2210.0" x2="2195.0" y2="2210.0" stroke="#fff" stroke-width="8"/>
+<text x="2195.0" y="1925.0" text-anchor="middle" dominant-baseline="middle" font-size="52" fill="#2D3748" font-weight="600">观景阳台</text>
+</g>
+</g>
+<g id="fpc-cabinets">
+<g class="fpc-cab" data-cab="玄关鞋柜">
+<rect x="320.0" y="320.0" width="240.0" height="240.0" rx="8" fill="#BEE3F8" stroke="#1F497D" stroke-width="5" stroke-dasharray="18 8"/>
+<text x="440.0" y="440.0" text-anchor="middle" dominant-baseline="middle" font-size="44" fill="#1F497D" font-weight="700">玄关鞋柜 1.2×1.2</text>
+</g>
+<g class="fpc-cab" data-cab="厨房橱柜">
+<rect x="320.0" y="320.0" width="600.0" height="240.0" rx="8" fill="#FEEBC8" stroke="#1F497D" stroke-width="5" stroke-dasharray="18 8"/>
+<text x="620.0" y="440.0" text-anchor="middle" dominant-baseline="middle" font-size="44" fill="#1F497D" font-weight="700">厨房橱柜 3×1.2</text>
+</g>
+</g>
+<g id="fpc-dims">
+</g>
+<defs><marker id="arr" viewBox="0 0 10 10" refX="5" refY="5" markerWidth="5" markerHeight="5" orient="auto"><path d="M0,0L10,5L0,10z" fill="#C05621"/></marker></defs>
+</svg></div><p class="plan-note">📐 AI 结构化重绘示意图(非施工图,以实际量尺为准)</p>
+    <details class="dims"><summary>图面尺寸标注(2 条,用作交叉验证)</summary><ul><li><b>110㎡</b> — 总建筑面积</li><li><b>3房2厅2卫</b> — 户型格局描述</li></ul></details>
+  </div>
+
+  <div class="sec">
+    <h2>💰 报价明细 <span style="font-weight:400;font-size:11px;color:var(--mu)">(点击房间展开)</span></h2>
+    <div class="card" id="roomsCard"><div class="room" data-i="0">
+    <div class="room-h"><div><div class="nm">玄关鞋柜</div><div class="dm">1.2m×2.4m</div></div><span class="tt">¥14,400</span></div>
+    <div class="items"><div class="item"><span class="l">柜体(PET材质)<span>1.2m×2.4m · 投影2.88m²</span></span><span class="v">¥14,400</span></div></div>
+  </div><div class="room" data-i="1">
+    <div class="room-h"><div><div class="nm">厨房橱柜</div><div class="dm">3m×2.8m</div></div><span class="tt">¥31,000</span></div>
+    <div class="items"><div class="item"><span class="l">柜体(PET材质)<span>3m×2.8m · 投影6.2m</span></span><span class="v">¥31,000</span></div></div>
+  </div></div>
+    <div class="card" style="margin-top:8px">
+      <div class="trow"><span>项目小计</span><span class="v" id="tSub">¥45,400</span></div>
+      <div class="trow dim"><span id="tDiscLabel">原价</span><span class="v" id="tAfter">¥45,400</span></div>
+      <div class="trow" id="actRow" style="display:none"><span>活动优惠</span><span class="v" style="color:var(--ok)" id="tAct">-¥0</span></div>
+      <div class="trow total"><span>到手价</span><span class="v" id="tFinal">¥45,400</span></div>
+    </div>
+  </div>
+
+  <div class="sec">
+    <div class="selling">
+      <h3>✨ 为什么选择拉迷(标准客户)</h3>
+      <ul><li>空间利用率拉满:每一面墙都变成收纳</li><li>全国联保5年,铰链五金终身维护</li><li>设计上门量尺,3D效果图确认后再生产</li></ul>
+    </div>
+  </div>
+
+  <div class="sec" style="font-size:11px;color:var(--mu);line-height:1.7">
+    计价说明:柜体按投影面积(宽×高)×材质单价;厨房按计费延米(扣除转角重叠与冰箱位)。本报价为AI辅助估算,实际以现场量尺与正式合同为准。质保与赠送以当期活动政策为准。
+  </div>
+</div>
+
+<div class="ctrl">
+  <button class="btn sec" id="btnAdjust">⚖️ 调整报价</button>
+  <button class="btn pri" id="btnSave">📞 锁定到店礼</button>
+</div>
+
+<div class="drawer" id="drawer">
+  <div class="sheet">
+    <h3>⚖️ 销售操作台(客户不可见)</h3>
+    <div class="frow"><label>客户类型</label><div class="opts" id="optTier"><button class="chip " data-v="price">🏷️ 价格型</button><button class="chip on" data-v="standard">🏠 标准型</button><button class="chip " data-v="quality">💎 品质型</button></div></div>
+    <div class="frow"><label>材质档位</label><div class="opts" id="optMat"><button class="chip " data-v="双饰面">双饰面</button><button class="chip on" data-v="PET">PET</button><button class="chip " data-v="混油">混油</button></div></div>
+    <div class="frow"><label>报价浮动 <span id="floatV">15</span>%</label><input type="range" id="floatR" min="10" max="20" step="1" value="15"></div>
+    <div class="frow"><label>折扣</label><div class="opts" id="optDisc">
+      <button class="chip" data-v="1.00">原价</button>
+      <button class="chip" data-v="0.95">95折</button>
+      <button class="chip" data-v="0.90">9折</button>
+      <button class="chip" data-v="0.85">85折</button>
+      <button class="chip" data-v="0.80">8折</button>
+    </div></div>
+    <div class="frow"><label>柜体清单(宽=投影宽 m / 厨房为计费长)</label><div id="cabList"></div>
+      <div style="margin-top:8px"><button class="chip" id="btnAddCab">+ 增加柜体</button></div>
+    </div>
+    <div class="frow"><label>活动优惠</label><div class="opts" id="optAct"></div></div>
+    <button class="btn pri" style="width:100%" id="btnApply">应用并重算</button>
+  </div>
+</div>
+
+<script>
+// ============================================================
+// 拉迷全屋定制 · 默认报价规则库
+// 数据来源:floorplan-quotation/AI快速报价_Demo2.optimizing.html
+// (DEFAULT_RULES / Q_CUSTOMER_TIERS / DEFAULT_QUOTATION_PLANS /
+//   CABINET_EXTRA_TEMPLATES / EXTRA_METHOD_MAP / 厨房扣减规则)
+// 所有价格单位:元。修改价格请改这里的默认值或运行时覆盖 config。
+// ============================================================
+
+const METHOD = {
+  PROJECTION: '按投影面积', // 元/m²  数量 = 宽(m) × 高(m)
+  METER: '按延米',          // 元/m   数量 = 米数
+  COUNT: '按个数',          // 元/个  数量 = 个数
+};
+
+// —— 材质三档单价区间(AI快速粗略报价核心)——
+const PRICE_TIERS = [
+  { name: '双饰面', min: 3000, max: 3500 },
+  { name: 'PET',   min: 4500, max: 5000 },
+  { name: '混油',   min: 6000, max: 7000 },
+];
+const DEFAULT_TIER = 'PET';
+
+// —— 计价方式 ——
+const METHOD_OPTIONS = [
+  { value: '按投影面积', unit: '元/m²' },
+  { value: '按个数', unit: '元/个' },
+  { value: '按延米', unit: '元/m' },
+];
+
+// —— 柜体→空间 映射(识别命名用)——
+const CABINET_TO_SPACE = {
+  '主卧衣柜': '主卧', '次卧衣柜': '次卧', '儿童房衣柜': '儿童房', '老人房衣柜': '老人房',
+  '衣柜': '卧室', '客厅电视柜': '客厅', '电视柜': '客厅', '厨房橱柜': '厨房', '橱柜': '厨房',
+  '开放式厨房': '开放式厨房', '餐厅餐边柜': '餐厅', '餐边柜': '餐厅', '书房书柜': '书房',
+  '书柜': '书房', '玄关鞋柜': '玄关', '鞋柜': '玄关', '玄关柜': '玄关', '阳台柜': '阳台',
+  '衣帽间': '衣帽间', '储物柜': '储物间', '酒柜': '餐厅', '浴室柜': '卫生间', '榻榻米': '次卧',
+  '展示柜': '客厅', '洗衣机柜': '阳台', '卡座': '餐厅', '飘窗柜': '飘窗',
+};
+
+const SPACE_TO_DEFAULT_CABINET = {
+  '主卧': '主卧衣柜', '次卧': '次卧衣柜', '儿童房': '儿童房衣柜', '老人房': '老人房衣柜',
+  '客厅': '客厅电视柜', '厨房': '厨房橱柜', '开放式厨房': '开放式厨房', '餐厅': '餐厅餐边柜',
+  '客餐厅': '餐厅餐边柜', '书房': '书房书柜', '玄关': '玄关鞋柜', '阳台': '阳台柜',
+  '衣帽间': '衣帽间', '储物间': '储物柜', '卫生间': '浴室柜', '主卫': '浴室柜',
+};
+
+// —— 柜体类型 / 布局 / 门板 预设 ——
+const CUSTOM_TYPES_PRESET = {
+  cabinetType: ['主卧衣柜','次卧衣柜','儿童房衣柜','老人房衣柜','客厅电视柜','厨房橱柜','开放式厨房','餐厅餐边柜','书房书柜','玄关鞋柜','阳台柜','衣帽间','储物柜','酒柜','浴室柜','榻榻米'],
+  layoutForm: ['一字型','L型','U型','二字型','不规则'],
+  doorPanelType: ['普通平板','风影门板','PET门板','实木贴皮','烤漆门板','吸塑门板','玻璃门','百叶门','无门板'],
+};
+
+// —— 柜体默认附件模板(识别到柜型后自动配附件)——
+const CABINET_EXTRA_TEMPLATES = {
+  '玄关': ['灯带','变压器+开关','自弹器','见光板','铰链'],
+  '鞋柜': ['灯带','变压器+开关','自弹器','见光板','铰链','换鞋凳'],
+  '电视柜': ['自弹器','见光板','铰链'],
+  '展示柜': ['灯带','变压器+开关','拉手','见光板','铰链','自弹器'],
+  '酒柜': ['灯带','变压器+开关','拉手','见光板','铰链','自弹器','玻璃门升级'],
+  '洗衣机': ['自弹器','护墙板','见光板','铰链'],
+  '阳台': ['自弹器','见光板','铰链'],
+  '餐边': ['抽屉','拉手','自弹器','金属拉手','灯带','变压器+开关','见光板','铰链'],
+  '次卧': ['衣杆','金属拉手','抽屉','见光板','铰链'],
+  '儿童房': ['衣杆','金属拉手','抽屉','见光板','铰链'],
+  '书房': ['衣杆','金属拉手','抽屉','见光板','铰链'],
+  '主卧': ['金属拉手','抽屉','衣杆','自弹器','圆弧','铰链'],
+  '衣帽间': ['金属拉手','抽屉','衣杆','自弹器','圆弧','铰链','裤架领带格','拉篮'],
+  '厨房': ['见光板','碗篮','锅篮','自弹器'],
+  '_default': ['自弹器','见光板','铰链'],
+};
+
+// —— 附件计价方式映射 ——
+const EXTRA_METHOD_MAP = {
+  '见光板': '按投影面积', '护墙板': '按投影面积', '玻璃门升级': '按投影面积',
+  '灯带': '按延米', '衣杆': '按延米', '石材台面': '按延米',
+  '自弹器': '按个数', '铰链': '按个数', '变压器+开关': '按个数', '拉手': '按个数',
+  '金属拉手': '按个数', '抽屉': '按个数', '裤架领带格': '按个数', '拉篮': '按个数',
+  '碗篮': '按个数', '锅篮': '按个数', '圆弧': '按个数', '换鞋凳': '按个数',
+};
+
+// —— 基础价格表(name → {price, unit, method})——
+const PRICE_BOOK = {
+  // 柜体
+  '柜体投影单价':   { price: 1852, unit: '元/m²', method: '按投影面积' },
+  '生态澳松板柜体': { price: 1852, unit: '元/m²', method: '按投影面积' },
+  '铝格栅板柜体':   { price: 2532, unit: '元/m²', method: '按投影面积' },
+  // 厨房(单件计价模式)
+  '地柜':   { price: 2380, unit: '元/m', method: '按延米' },
+  '吊柜':   { price: 1748, unit: '元/m', method: '按延米' },
+  '冰箱吊柜': { price: 2622, unit: '元/m', method: '按延米' },
+  '高柜':   { price: 5950, unit: '元/m', method: '按延米' },
+  '台面':   { price: 1480, unit: '元/m', method: '按延米' },
+  '台面超出部分': { price: 2480, unit: '元/m', method: '按延米' },
+  '碗篮':   { price: 1268, unit: '元/个', method: '按个数' },
+  '锅篮':   { price: 1628, unit: '元/个', method: '按个数' },
+  '切角':   { price: 500, unit: '元/个', method: '按个数' },
+  '台下盆': { price: 500, unit: '元/个', method: '按个数' },
+  '包管':   { price: 500, unit: '元/个', method: '按个数' },
+  // 门板单价
+  '标配门板': { price: 380, unit: '元/m²', method: '按投影面积' },
+  '烤漆门板': { price: 580, unit: '元/m²', method: '按投影面积' },
+  '吸塑门板': { price: 480, unit: '元/m²', method: '按投影面积' },
+  '实木门板': { price: 880, unit: '元/m²', method: '按投影面积' },
+  '铝格栅门板': { price: 680, unit: '元/m²', method: '按投影面积' },
+  'PET门板':  { price: 450, unit: '元/m²', method: '按投影面积' },
+  // 附件
+  '见光板':   { price: 1138, unit: '元/m²', method: '按投影面积' },
+  '护墙板':   { price: 1852, unit: '元/m²', method: '按投影面积' },
+  '玻璃门升级': { price: 1852, unit: '元/m²', method: '按投影面积' },
+  '灯带':     { price: 386, unit: '元/m', method: '按延米' },
+  '变压器+开关': { price: 536, unit: '元/个', method: '按个数' },
+  '自弹器':   { price: 115, unit: '元/个', method: '按个数' },
+  '铰链':     { price: 40, unit: '元/个', method: '按个数' },
+  '拉手':     { price: 226, unit: '元/个', method: '按个数' },
+  '金属拉手': { price: 225, unit: '元/个', method: '按个数' },
+  '抽屉':     { price: 1300, unit: '元/个', method: '按个数' },
+  '衣杆':     { price: 288, unit: '元/m', method: '按延米' },
+  '裤架领带格': { price: 0, unit: '元/个', method: '按个数' },
+  '拉篮':     { price: 0, unit: '元/个', method: '按个数' },
+  '圆弧':     { price: 1560, unit: '元/个', method: '按个数' },
+  '换鞋凳':   { price: 0, unit: '元/个', method: '按个数' },
+  '石材台面': { price: 1480, unit: '元/m', method: '按延米' },
+};
+
+// —— 厨房计价参数(qGetKitchenBillableMeters 同源)——
+const KITCHEN_RULES = {
+  packageMeters: 3,     // 3米基础套餐(单件计价模式的扣减基准)
+  overlapMm: 600,       // 转角重叠扣减
+  fridgeMm: 1000,       // 冰箱位预留
+  typeThresholdM: 2,    // 最小边 ≤2m 判 L 型,>2m 判 U 型
+};
+
+// —— 客户分型(销售策略核心)——
+const CUSTOMER_TIERS = {
+  price: {
+    label: '看重价格型', icon: '🏷️',
+    strategy: '预算少的客户直接按性价比报,快速赢单',
+    defaultPlan: 'economy', defaultDiscount: 0.85,
+    doorType: '标配', hardware: '经济实用款', service: '标准测量+安装',
+    warranty: '3年质保', gifts: '送基础五金配件', panelLabel: '进口爱格板(加价升级)',
+    autoEnableActivity: true,
+    discounts: [
+      { id: 'p1', label: '投影面积85折', rate: 0.85, deletable: false },
+      { id: 'p2', label: '投影面积8折特惠', rate: 0.80, deletable: true },
+      { id: 'p3', label: '团购78折', rate: 0.78, deletable: true },
+    ],
+  },
+  standard: {
+    label: '标准客户', icon: '🏠',
+    strategy: '标准配置报价,正常谈单空间',
+    defaultPlan: 'standard', defaultDiscount: 1.0,
+    doorType: '标配门板', hardware: '国产优质铰链/拉手', service: '标准测量安装',
+    warranty: '5年质保', gifts: '可参与活动优惠', panelLabel: '国产E0级颗粒板',
+    autoEnableActivity: false,
+    discounts: [
+      { id: 's1', label: '原价', rate: 1.00, deletable: false },
+      { id: 's2', label: '投影面积95折', rate: 0.95, deletable: true },
+      { id: 's3', label: '投影面积9折', rate: 0.90, deletable: true },
+    ],
+  },
+  quality: {
+    label: '品质导向型', icon: '💎',
+    strategy: '有预算的客户报高一点,留谈价空间,突出配置与质感',
+    defaultPlan: 'quality', defaultDiscount: 1.0,
+    doorType: '烤漆/吸塑高端门板', hardware: '进口百隆/海蒂诗五金', service: 'VIP专属设计师+金牌安装',
+    warranty: '10年质保+终身维护', gifts: '赠灯带+抽屉升级包', panelLabel: '进口爱格板/可丽芙(推荐)',
+    premiumRate: 1.15,
+    autoEnableActivity: false,
+    discounts: [
+      { id: 'q1', label: '投影面积95折(保利润)', rate: 0.95, deletable: false },
+      { id: 'q2', label: '原价(高端不打折)', rate: 1.00, deletable: true },
+      { id: 'q3', label: 'VIP专享92折', rate: 0.92, deletable: true },
+    ],
+  },
+};
+
+// —— 报价方案 ——
+const QUOTATION_PLANS = [
+  { id: 'economy',  name: '经济型方案', description: '适合预算敏感型客户,主打性价比', customerTier: 'price',    tierName: '双饰面', floatPct: 10, discountRate: 0.98 },
+  { id: 'standard', name: '标准型方案', description: '默认方案,标准配置,适合大多数客户', customerTier: 'standard', tierName: 'PET',    floatPct: 15, discountRate: 1.00 },
+  { id: 'quality',  name: '品质型方案', description: '高端配置,适合追求品质的客户', customerTier: 'quality',   tierName: '混油',   floatPct: 15, discountRate: 1.00 },
+];
+
+// —— 营销活动模块(可勾选叠加)——
+const ACTIVITY_MODULES = [
+  { id: 'deposit5000', type: 'deposit', name: '定金膨胀(5千档)', desc: '交¥5,000→膨胀¥8,000 + ¥500抵用券', amount: 3500, minOrder: 30000 },
+  { id: 'deposit10000', type: 'deposit', name: '定金膨胀(1万档)', desc: '交¥10,000→膨胀¥13,000 + ¥800抵用券', amount: 3800, minOrder: 30000 },
+  { id: 'coupon20k', type: 'coupon', name: '满减券', desc: '满¥20,000减¥1,000', amount: 1000, threshold: 20000 },
+  { id: 'cashback', type: 'tiered_cashback', name: '阶梯返现', tiers: [
+      { min: 50000, cash: 2000 }, { min: 100000, cash: 5000 }, { min: 150000, cash: 8000 },
+    ] },
+];
+
+// —— 默认层高与房间面积先验(识别失败兜底估算用)——
+const DEFAULTS = {
+  height: 2.8, // 默认柜高(层高)
+  roomAreaPriors: { // 常见房间典型面积范围 [min, max](m²)
+    '主卧': [12, 22], '次卧': [8, 16], '儿童房': [8, 14], '老人房': [9, 15], '书房': [6, 12],
+    '客厅': [18, 35], '餐厅': [8, 18], '厨房': [4.5, 10], '卫生间': [2.5, 6], '主卫': [3, 6],
+    '公卫': [2.5, 6], '阳台': [3, 10], '玄关': [2, 6], '衣帽间': [3, 10], '储物间': [1.5, 5],
+  },
+  cabinetWallRatio: { // 柜体默认占所在墙面比例(无明确尺寸时的估算)
+    '主卧衣柜': 0.55, '次卧衣柜': 0.50, '儿童房衣柜': 0.50, '玄关鞋柜': 0.6,
+    '客厅电视柜': 0.55, '餐厅餐边柜': 0.5, '阳台柜': 0.6, '书房书柜': 0.5,
+  },
+};
+
+// —— 赢单文案素材(业主视角价值描述)——
+const SELLING_POINTS = {
+  price: [
+    '按投影面积计价,明明白白,没有隐形收费',
+    'E0级环保板材,装完即可安心入住',
+    '工厂直供价,同样的预算多做一个柜子',
+  ],
+  standard: [
+    '空间利用率拉满:每一面墙都变成收纳',
+    '全国联保5年,铰链五金终身维护',
+    '设计上门量尺,3D效果图确认后再生产',
+  ],
+  quality: [
+    '进口板材+进口五金,用20年不变形不褪色',
+    'VIP设计师一对一,全屋风格统一设计',
+    '10年质保+终身维护,一次装修终身省心',
+  ],
+};
+
+// ============================================================
+// 拉迷全屋定制 · 报价引擎(纯函数,浏览器/CLI 共用)
+// 两种模式:
+//   quick  — AI快速粗略报价:投影面积/厨房延米 × 三档材质区间单价
+//   detail — 明细报价:Excel 预算表模型(柜体+门板补差+附件逐项)
+// 销售策略:客户分型 → 方案/折扣/浮动 → 活动叠加 → 谈价区间
+// ============================================================
+
+const r2 = (v) => Math.round(v * 100) / 100;
+const num = (v, d = 0) => { const n = parseFloat(v); return Number.isFinite(n) ? n : d; };
+
+// ---------- 材质档位 ----------
+function getTier(name) {
+  const t = PRICE_TIERS.find((x) => x.name === (name || DEFAULT_TIER));
+  return t || PRICE_TIERS[1];
+}
+// 浮动百分比(10~15)在区间内线性插值 → 单价(qGetAiUnitPrice 同源逻辑)
+function tierUnitPrice(tier, floatPct = 15) {
+  const k = Math.min(1, Math.max(0, (num(floatPct, 15) - 10) / 5));
+  return r2(tier.min + k * (tier.max - tier.min));
+}
+
+// ---------- 厨房计费米数(qGetKitchenBillableMeters 同源)----------
+function kitchenBillable(walls = [], rules = {}) {
+  const R = { ...KITCHEN_RULES, ...rules };
+  const ws = (Array.isArray(walls) ? walls : []).map((v) => num(v)).filter((v) => v > 0);
+  if (!ws.length) return null;
+  const total = ws.reduce((a, b) => a + b, 0);
+  const minWall = Math.min(...ws);
+  const maxWall = Math.max(...ws);
+  const type = minWall <= R.typeThresholdM ? 'L' : 'U';
+  const overlapCount = type === 'L' ? 1 : 2;
+  const usedLen = type === 'L' ? maxWall + minWall : maxWall + maxWall + minWall;
+  const billable = Math.max(0, usedLen - (overlapCount * R.overlapMm) / 1000 - R.fridgeMm / 1000);
+  return {
+    walls: ws, total: r2(total), minWall: r2(minWall), maxWall: r2(maxWall),
+    kitchenType: type, overlapCount, fridgeMm: R.fridgeMm, usedLen: r2(usedLen), billable: r2(billable),
+  };
+}
+const isKitchen = (name = '') => /厨房|橱柜/.test(String(name));
+
+// ---------- 附件数量启发式(Excel 模型总结,均可编辑) ----------
+function estimateExtras(cabinet) {
+  const w = num(cabinet.width, 1.5);
+  const h = num(cabinet.height, DEFAULTS.height);
+  const type = String(cabinet.type || '');
+  let tplKey = Object.keys(CABINET_EXTRA_TEMPLATES).find((k) => k !== '_default' && type.includes(k));
+  if (!tplKey && isKitchen(type)) tplKey = '厨房';
+  const names = CABINET_EXTRA_TEMPLATES[tplKey || '_default'] || CABINET_EXTRA_TEMPLATES._default;
+  const sections = h > 1.6 ? 2 : 1;               // 高柜分上下段
+  const doorsPerSection = Math.max(1, Math.round(w / 0.45));
+  const doors = sections * doorsPerSection;
+  const hingePerDoor = h > 2 ? 3 : 2;
+  const out = [];
+  for (const name of names) {
+    const pb = PRICE_BOOK[name];
+    if (!pb) continue;
+    let qty = 0; let note = '';
+    switch (name) {
+      case '铰链': qty = doors * hingePerDoor; note = `${doors}门×${hingePerDoor}铰链`; break;
+      case '自弹器': qty = Math.max(1, doors - 1); note = '反弹门配'; break;
+      case '见光板': qty = 1; note = '单侧见光(0.5m宽)'; break;
+      case '灯带': qty = r2(w * 2); note = '顶底双层'; break;
+      case '变压器+开关': qty = 1; note = 'LED电源'; break;
+      case '衣杆': qty = r2(w); note = '通长挂杆'; break;
+      case '抽屉': qty = type.includes('主卧') || type.includes('衣帽') ? 3 : 2; break;
+      case '金属拉手': case '拉手': qty = doors + 2; break;
+      case '碗篮': case '锅篮': qty = 1; break;
+      default: qty = name === '玻璃门升级' || name === '护墙板' ? 0 : 1;
+    }
+    if (qty > 0) out.push({ name, method: pb.method, unit: pb.unit, unitPrice: pb.price, qty, note });
+  }
+  return out;
+}
+
+// ---------- 单柜体计价 ----------
+// 计价数量:投影面积 = 宽×高;厨房 = 计费延米
+function cabinetQty(cabinet, rules = {}) {
+  if (isKitchen(cabinet.type)) {
+    const kb = kitchenBillable(cabinet.walls || [cabinet.width, cabinet.height, cabinet.width, cabinet.height], rules);
+    return { qty: kb ? kb.billable : num(cabinet.width), unit: 'm', kb };
+  }
+  return { qty: r2(num(cabinet.width) * num(cabinet.height)), unit: 'm²', kb: null };
+}
+
+// 快速模式:单柜体一行
+function quickLine(cabinet, { tierName, floatPct = 15, rules } = {}) {
+  const tier = getTier(tierName);
+  const U = tierUnitPrice(tier, floatPct);
+  const { qty, unit, kb } = cabinetQty(cabinet, rules);
+  return {
+    mode: 'quick', name: cabinet.type, tierName: tier.name,
+    qty, unit, unitPrice: U, total: r2(qty * U), kb,
+    dims: num(cabinet.width) && num(cabinet.height) ? `${num(cabinet.width)}m×${num(cabinet.height)}m` : '',
+  };
+}
+
+// 明细模式:单柜体 → Excel 风格明细行
+function detailLines(cabinet, opts = {}) {
+  const { doorDiffPrice = PRICE_BOOK['柜体投影单价'].price > 0 ? 538 : 538, rules, includeDoorDiff = true } = opts;
+  const lines = [];
+  const isK = isKitchen(cabinet.type);
+  const { qty } = cabinetQty(cabinet, rules);
+  if (isK) {
+    // 厨房单件计价(Excel 模式:地柜+吊柜+台面+功能五金)
+    const walls = cabinet.walls || [cabinet.width, cabinet.height, cabinet.width, cabinet.height];
+    const straight = Math.max(0, ...walls); // 直线段米数近似
+    for (const [name, factor, note] of [
+      ['地柜', 1, '沿墙地柜'], ['吊柜', 1, '沿墙吊柜'], ['台面', 1, '石英石台面'],
+    ]) {
+      const pb = PRICE_BOOK[name];
+      lines.push({ name, qty: r2(qty), unit: 'm', unitPrice: pb.price, total: r2(qty * pb.price), note, method: pb.method });
+    }
+    for (const name of ['碗篮', '锅篮']) {
+      const pb = PRICE_BOOK[name];
+      lines.push({ name, qty: 1, unit: '个', unitPrice: pb.price, total: pb.price, note: '标准配置', method: pb.method });
+    }
+    return { cabinet, isKitchen: true, lines, subtotal: r2(lines.reduce((s, l) => s + l.total, 0)) };
+  }
+  const pb = PRICE_BOOK['生态澳松板柜体'];
+  lines.push({ name: '柜体', qty, unit: 'm²', unitPrice: pb.price, total: r2(qty * pb.price), note: '生态澳松板', method: pb.method });
+  if (includeDoorDiff && doorDiffPrice > 0) {
+    lines.push({ name: '板门补差价', qty, unit: 'm²', unitPrice: doorDiffPrice, total: r2(qty * doorDiffPrice), note: '双饰面门补差', method: pb.method });
+  }
+  for (const ex of estimateExtras(cabinet)) {
+    const total = ex.method === '按投影面积' ? r2(ex.qty * 0.5 * num(cabinet.height) * ex.unitPrice)
+      : ex.method === '按延米' ? r2(ex.qty * ex.unitPrice)
+      : r2(ex.qty * ex.unitPrice);
+    lines.push({ name: ex.name, qty: ex.qty, unit: ex.unit, unitPrice: ex.unitPrice, total, note: ex.note, method: ex.method });
+  }
+  return { cabinet, isKitchen: false, lines, subtotal: r2(lines.reduce((s, l) => s + l.total, 0)) };
+}
+
+// ---------- 报价主入口 ----------
+// inputs: { cabinets:[{type,width,height,walls?,excluded?}], tier, floatPct, discount, tierKey(客户分型), planId, mode }
+function buildQuote(input = {}) {
+  const {
+    cabinets = [], mode = 'quick',
+    tierName = input.tier || DEFAULT_TIER,
+    floatPct = 15, discount = null, tierKey = 'standard', planId = null,
+    rules = {}, extras = [], // extras: [{name, room, qty, unitPrice}] 手动增删项
+    clientName = '', note = '',
+  } = input;
+
+  const plan = QUOTATION_PLANS.find((p) => p.id === (planId || CUSTOMER_TIERS[tierKey]?.defaultPlan)) || QUOTATION_PLANS[1];
+  const custTier = CUSTOMER_TIERS[tierKey] || CUSTOMER_TIERS.standard;
+  const effDiscount = discount != null ? num(discount) : (custTier.defaultDiscount ?? plan.discountRate ?? 1);
+
+  const rooms = cabinets
+    .filter((c) => !c.excluded)
+    .map((c, i) => {
+      const base = mode === 'detail' ? detailLines(c, { rules }) : { ...quickLine(c, { tierName, floatPct, rules }), lines: null };
+      const id = c.id || `cab_${i + 1}`;
+      return { id, type: c.type, dims: base.dims || `${num(c.width)}m×${num(c.height)}m`, ...base };
+    });
+
+  const itemsSubtotal = r2(rooms.reduce((s, r) => s + (r.subtotal ?? r.total), 0));
+  const manualExtras = extras.map((e, i) => ({
+    id: e.id || `ext_${i + 1}`, name: e.name, qty: num(e.qty, 1), unit: e.unit || '项',
+    unitPrice: num(e.unitPrice), total: r2(num(e.qty, 1) * num(e.unitPrice)), kind: e.kind || 'add',
+  }));
+  const extrasTotal = r2(manualExtras.reduce((s, e) => s + (e.kind === 'remove' ? -e.total : e.total), 0));
+
+  const premiumRate = custTier.premiumRate || 1;
+  const afterPremium = r2((itemsSubtotal + extrasTotal) * premiumRate);
+  const afterDiscount = r2(afterPremium * effDiscount);
+
+  // 活动叠加(自动/手动)
+  const acts = (input.activities || (custTier.autoEnableActivity ? ACTIVITY_MODULES.map((m) => m.id) : []))
+    .map((id) => ACTIVITY_MODULES.find((m) => m.id === id)).filter(Boolean);
+  let activityTotal = 0;
+  const activityLines = [];
+  for (const m of acts) {
+    let amt = 0; let desc = m.desc || m.name;
+    if (m.type === 'tiered_cashback') {
+      const hit = [...(m.tiers || [])].sort((a, b) => b.min - a.min).find((t) => afterDiscount >= t.min);
+      if (!hit) continue;
+      amt = hit.cash; desc = `满¥${hit.min.toLocaleString()}返¥${hit.cash.toLocaleString()}`;
+    } else if (m.type === 'coupon' && afterDiscount < (m.threshold || 0)) continue;
+    else if (m.type === 'deposit' && afterDiscount < (m.minOrder || 30000)) continue;
+    else amt = m.amount || 0;
+    activityTotal += amt;
+    activityLines.push({ id: m.id, name: m.name, desc, amount: amt });
+  }
+  const finalPrice = Math.max(0, r2(afterDiscount - activityTotal));
+
+  // 谈价区间(浮动率)
+  const rate = num(floatPct, 15) / 100;
+  const range = { low: Math.round(finalPrice * (1 - rate)), high: Math.round(finalPrice * (1 + rate)) };
+
+  // 业主视角价值点
+  const selling = SELLING_POINTS[tierKey] || SELLING_POINTS.standard;
+
+  return {
+    meta: {
+      clientName, note: note || custTier.strategy, mode,
+      tierName: mode === 'quick' ? getTier(tierName).name : undefined,
+      floatPct, planId: plan.id, planName: plan.name,
+      customerTier: tierKey, customerLabel: custTier.label,
+      discount: effDiscount, premiumRate: premiumRate !== 1 ? premiumRate : undefined,
+      generatedAt: new Date().toISOString(),
+    },
+    rooms, manualExtras, activityLines,
+    totals: {
+      itemsSubtotal, extrasTotal, premiumRate, afterPremium, discount: effDiscount,
+      afterDiscount, activityTotal, finalPrice, range,
+      discountLabel: custTier.discounts?.find((d) => Math.abs(d.rate - effDiscount) < 0.001)?.label || `折扣${Math.round(effDiscount * 100)}%`,
+    },
+    selling,
+    config: { tierKey, tierName, floatPct, discount: effDiscount, planId: plan.id, mode },
+  };
+}
+
+// ---------- 语义化调整应用(销售边聊边改) ----------
+// ops: [{op:'add'|'remove'|'resize'|'rename'|'price', target, ...}]
+function applyOps(cabinets, ops = []) {
+  const out = cabinets.map((c) => ({ ...c }));
+  for (const op of ops) {
+    if (op.op === 'add') {
+      out.push({ type: op.type || op.name || '柜体', width: num(op.width, 1.5), height: num(op.height, DEFAULTS.height), from: '语音新增' });
+    } else if (op.op === 'remove') {
+      const i = out.findIndex((c) => c.type === op.target);
+      if (i >= 0) out.splice(i, 1);
+    } else if (op.op === 'resize') {
+      const c = out.find((c) => c.type === op.target);
+      if (c) { if (op.width) c.width = num(op.width); if (op.height) c.height = num(op.height); c.from = '手动调整'; }
+    } else if (op.op === 'rename') {
+      const c = out.find((c) => c.type === op.target);
+      if (c) c.type = op.to || c.type;
+    } else if (op.op === 'exclude') {
+      const c = out.find((c) => c.type === op.target);
+      if (c) c.excluded = true;
+    } else if (op.op === 'include') {
+      const c = out.find((c) => c.type === op.target);
+      if (c) delete c.excluded;
+    }
+  }
+  return out;
+}
+
+// 授权范围内校验(浮动区间即授权边界)
+function withinAuthority(quote, price) {
+  const { range } = quote.totals;
+  return price >= range.low && price <= range.high;
+}
+
+var __DEFAULT__ = { buildQuote, applyOps, quickLine, detailLines, kitchenBillable, tierUnitPrice, getTier, withinAuthority };
+
+window.__Q__ = {"cabinets":[{"id":"cab_1","type":"玄关鞋柜","width":1.2,"height":2.6,"rawName":"玄关鞋柜"},{"id":"cab_2","type":"厨房橱柜","height":2.6,"rawName":"厨房橱柜"}],"config":{"tierKey":"standard","tierName":"PET","floatPct":15,"discount":1,"planId":"standard","mode":"quick"},"salesNote":""};
+console.log('fpc engine ready');
+</script>
+<script>
+(function(){
+  var Q = window.__Q__; var S = document; var money = function(n){ return '¥' + Math.round(n||0).toLocaleString('zh-CN'); };
+  var cfg = Object.assign({ tierKey:'standard', tierName:'PET', floatPct:15, discount:1.0, mode:'quick' }, Q.config||{});
+  function calc(){ return buildQuote({ cabinets: Q.cabinets, tierKey: cfg.tierKey, tierName: cfg.tierName, floatPct: cfg.floatPct, discount: cfg.discount, mode: cfg.mode, clientName: "PDF链路测试" }); }
+  function refresh(){
+    var q = calc(); var t = q.totals;
+    S.getElementById('finalPrice').textContent = money(t.finalPrice);
+    S.getElementById('finalRange').textContent = '谈价参考区间 ' + money(t.range.low) + ' ~ ' + money(t.range.high);
+    S.getElementById('tSub').textContent = money(t.itemsSubtotal);
+    S.getElementById('tDiscLabel').textContent = t.discountLabel;
+    S.getElementById('tAfter').textContent = money(t.afterDiscount);
+    S.getElementById('tAct').textContent = '-' + money(t.activityTotal);
+    S.getElementById('tFinal').textContent = money(t.finalPrice);
+    S.getElementById('bTier').textContent = (q.meta.tierName||'') + '材质';
+    S.getElementById('bDisc').textContent = t.discountLabel;
+  }
+  // room expand
+  S.querySelectorAll('.room-h').forEach(function(el){ el.addEventListener('click', function(){ el.parentElement.classList.toggle('open'); }); });
+  // chips
+  function bindChips(id, fn){ var box = S.getElementById(id); if(!box) return; box.addEventListener('click', function(e){ var b = e.target.closest('.chip'); if(!b) return; box.querySelectorAll('.chip').forEach(function(x){x.classList.remove('on');}); b.classList.add('on'); fn(b.dataset.v); }); }
+  bindChips('optTier', function(v){ cfg.tierKey = v; refresh(); });
+  bindChips('optMat', function(v){ cfg.tierName = v; refresh(); });
+  bindChips('optDisc', function(v){ cfg.discount = parseFloat(v); refresh(); });
+  var fr = S.getElementById('floatR'); fr.addEventListener('input', function(){ S.getElementById('floatV').textContent = fr.value; cfg.floatPct = parseInt(fr.value); refresh(); });
+  // drawer
+  var d = S.getElementById('drawer');
+  S.getElementById('btnAdjust').addEventListener('click', function(){ d.classList.add('open'); renderCabs(); });
+  d.addEventListener('click', function(e){ if(e.target === d) d.classList.remove('open'); });
+  function renderCabs(){
+    var box = S.getElementById('cabList'); box.innerHTML = '';
+    Q.cabinets.forEach(function(c, i){
+      var row = S.createElement('div'); row.className = 'cabrow';
+      var dims = String(c.width||'') ? String(c.width) : '';
+      row.innerHTML = '<span style="flex:1">' + c.rawName + '</span>' +
+        '<input type="number" step="0.05" value="' + (c.width||1.5) + '" data-i="' + i + '" data-f="width" title="宽(米)">' +
+        '<span class="del" data-i="' + i + '">✕</span>';
+      box.appendChild(row);
+    });
+    box.onclick = function(e){
+      var del = e.target.closest('.del');
+      if (del) { Q.cabinets.splice(parseInt(del.dataset.i),1); renderCabs(); refresh(); return; }
+      var inp = e.target.closest('input');
+      if (inp) { inp.addEventListener('change', function(){ Q.cabinets[parseInt(inp.dataset.i)].width = parseFloat(inp.value)||1.5; refresh(); }); }
+    };
+  }
+  S.getElementById('btnAddCab').addEventListener('click', function(){ Q.cabinets.push({ type:'储物柜', rawName:'储物柜', width:1.5, height:2.6 }); renderCabs(); refresh(); });
+  S.getElementById('btnSave').addEventListener('click', function(){
+    alert('已记录!销售将带着这份报价与您确认到店时间,到店凭此单领取到店礼。');
+  });
+  refresh();
+})();
+</script>
+</body>
+</html>

+ 799 - 0
skill/out/报价单-PDF链路测试-08312349.html

@@ -0,0 +1,799 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+<head>
+<meta charset="UTF-8">
+<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
+<meta name="format-detection" content="telephone=no">
+<title>PDF链路测试 · 全屋定制报价方案</title>
+<style>
+:root{--p:#1F497D;--pa:#2B6CB0;--bg:#F5F7FA;--card:#fff;--bd:#E2E8F0;--tx:#2D3748;--mu:#718096;--ok:#38A169;--wa:#D69E2E;--dg:#E53E3E;--r:12px}
+*{box-sizing:border-box;margin:0;padding:0;-webkit-tap-highlight-color:transparent}
+body{font-family:-apple-system,BlinkMacSystemFont,"PingFang SC","Microsoft YaHei",sans-serif;background:var(--bg);color:var(--tx);line-height:1.55;padding-bottom:calc(88px + env(safe-area-inset-bottom))}
+header{background:linear-gradient(135deg,#1F497D,#2B6CB0);color:#fff;padding:18px 16px 14px}
+header h1{font-size:17px;font-weight:700}
+header .sub{font-size:12px;opacity:.85;margin-top:2px}
+.wrap{padding:12px;max-width:640px;margin:0 auto}
+.hero{background:var(--card);border-radius:var(--r);padding:16px;box-shadow:0 1px 3px rgba(0,0,0,.08);margin-top:-26px;position:relative;text-align:center}
+.hero .label{font-size:12px;color:var(--mu)}
+.hero .price{font-size:38px;font-weight:800;color:var(--p);letter-spacing:-1px;margin:2px 0}
+.hero .price small{font-size:16px;font-weight:600}
+.hero .range{font-size:13px;color:var(--mu)}
+.badge{display:inline-block;font-size:11px;padding:2px 10px;border-radius:20px;background:#EBF8FF;color:var(--pa);border:1px solid #BEE3F8;margin-top:6px}
+.sec{margin-top:14px}
+.sec h2{font-size:15px;font-weight:700;margin:0 2px 8px;display:flex;align-items:center;gap:6px}
+.card{background:var(--card);border-radius:var(--r);box-shadow:0 1px 3px rgba(0,0,0,.06);overflow:hidden}
+.room{border-bottom:1px solid var(--bd)}
+.room:last-child{border-bottom:none}
+.room-h{padding:12px 14px;display:flex;justify-content:space-between;align-items:center;gap:8px}
+.room-h .nm{font-weight:700;font-size:14px}
+.room-h .dm{font-size:11px;color:var(--mu)}
+.room-h .tt{font-weight:800;color:var(--p);font-size:15px;white-space:nowrap}
+.room-h .conf{font-size:10px;padding:1px 6px;border-radius:8px;background:#FFFAF0;color:#C05621;border:1px solid #FBD38D;flex-shrink:0}
+.items{padding:0 14px 10px;display:none}
+.room.open .items{display:block}
+.item{display:flex;justify-content:space-between;font-size:12.5px;padding:5px 0;color:#4A5568}
+.item .l span{color:var(--mu);font-size:11px;margin-left:6px}
+.item .v{white-space:nowrap}
+.trow{display:flex;justify-content:space-between;padding:8px 14px;font-size:13px}
+.trow .v{font-weight:700}
+.trow.total{background:#F0FFF4;color:#276749;font-weight:800;font-size:15px}
+.trow.dim{color:var(--mu);font-size:12px}
+.plan-wrap{background:var(--card);border-radius:var(--r);padding:10px;box-shadow:0 1px 3px rgba(0,0,0,.06)}
+.plan-wrap svg,.plan-img{width:100%;height:auto;display:block;border-radius:8px}
+.plan-note{font-size:11px;color:var(--mu);text-align:center;margin-top:6px}
+.selling{background:linear-gradient(135deg,#FFFAF0,#FFF5EB);border:1px solid #FBD38D;border-radius:var(--r);padding:12px 14px}
+.selling li{font-size:13px;margin:6px 0 6px 18px;color:#744210}
+.selling h3{font-size:13px;color:#C05621;margin-bottom:4px}
+.acts .item .v{color:var(--ok)}
+.ctrl{position:fixed;bottom:0;left:0;right:0;background:#fff;border-top:1px solid var(--bd);padding:10px 14px calc(10px + env(safe-area-inset-bottom));display:flex;gap:8px;z-index:99;max-width:640px;margin:0 auto}
+.btn{flex:1;padding:12px;border:none;border-radius:10px;font-size:14px;font-weight:700;cursor:pointer}
+.btn.pri{background:var(--p);color:#fff}
+.btn.sec{background:#EDF2F7;color:var(--tx)}
+details.dims{background:var(--card);border-radius:var(--r);padding:10px 14px;font-size:12px;color:var(--mu)}
+details.dims summary{cursor:pointer;font-weight:600;color:var(--tx)}
+details.dims li{margin:4px 0 4px 18px}
+.drawer{position:fixed;inset:0;background:rgba(0,0,0,.4);z-index:100;display:none}
+.drawer.open{display:block}
+.sheet{position:absolute;bottom:0;left:0;right:0;background:#fff;border-radius:16px 16px 0 0;max-height:82vh;overflow:auto;padding:16px 16px calc(20px + env(safe-area-inset-bottom));max-width:640px;margin:0 auto}
+.sheet h3{font-size:15px;margin-bottom:12px}
+.frow{margin-bottom:14px}
+.frow label{font-size:12px;color:var(--mu);display:block;margin-bottom:4px}
+.frow .opts{display:flex;gap:6px;flex-wrap:wrap}
+.chip{padding:8px 12px;border-radius:8px;border:1.5px solid var(--bd);background:#fff;font-size:13px;cursor:pointer}
+.chip.on{border-color:var(--pa);background:#EBF8FF;color:var(--pa);font-weight:700}
+input[type=range]{width:100%}
+.cabrow{display:flex;align-items:center;gap:8px;padding:8px 0;border-bottom:1px dashed var(--bd);font-size:13px}
+.cabrow input{width:64px;padding:6px;border:1px solid var(--bd);border-radius:6px;font-size:13px}
+.cabrow .del{color:var(--dg);font-size:18px;padding:0 6px;cursor:pointer}
+.cabrow .add{color:var(--ok)}
+@media(min-width:640px){.hero .price{font-size:44px}}
+</style>
+</head>
+<body>
+<header>
+  <h1>PDF链路测试 · 全屋定制报价方案</h1>
+  <div class="sub">标准客户方案 · 生成于 8/31 23:49 · 拉迷全屋定制</div>
+</header>
+<div class="wrap">
+
+  <div class="hero">
+    <div class="label">全屋定制整装价</div>
+    <div class="price" id="finalPrice">¥165,500</div>
+    <div class="range" id="finalRange">谈价参考区间 ¥140,675 ~ ¥190,325</div>
+    <div id="badges">
+      <span class="badge" id="bTier">PET材质</span>
+      <span class="badge" id="bDisc">原价</span>
+      
+    </div>
+  </div>
+
+  
+
+  <div class="sec">
+    <h2>🏠 户型与定制布局</h2>
+    <div class="plan-wrap" id="planWrap"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 2710 2330" class="fpc-plan">
+<g id="fpc-rooms">
+<g class="fpc-room" data-room="厨房">
+<rect x="960.0" y="120.0" width="790.0" height="570.0" fill="#F7FAFC" stroke="#2D3748" stroke-width="6"/>
+<line x1="1265.0" y1="690.0" x2="1355.0" y2="690.0" stroke="#fff" stroke-width="8"/>
+<text x="1355.0" y="405.0" text-anchor="middle" dominant-baseline="middle" font-size="52" fill="#2D3748" font-weight="600">厨房</text>
+</g>
+<g class="fpc-room" data-room="玄关">
+<rect x="960.0" y="880.0" width="790.0" height="570.0" fill="#F7FAFC" stroke="#2D3748" stroke-width="6"/>
+<line x1="1265.0" y1="1450.0" x2="1355.0" y2="1450.0" stroke="#fff" stroke-width="8"/>
+<text x="1355.0" y="1165.0" text-anchor="middle" dominant-baseline="middle" font-size="52" fill="#2D3748" font-weight="600">玄关</text>
+</g>
+<g class="fpc-room" data-room="餐厅">
+<rect x="960.0" y="880.0" width="790.0" height="570.0" fill="#F7FAFC" stroke="#2D3748" stroke-width="6"/>
+<line x1="1265.0" y1="1450.0" x2="1355.0" y2="1450.0" stroke="#fff" stroke-width="8"/>
+<text x="1355.0" y="1165.0" text-anchor="middle" dominant-baseline="middle" font-size="52" fill="#2D3748" font-weight="600">餐厅</text>
+</g>
+<g class="fpc-room" data-room="客厅">
+<rect x="960.0" y="880.0" width="790.0" height="570.0" fill="#F7FAFC" stroke="#2D3748" stroke-width="6"/>
+<line x1="1265.0" y1="1450.0" x2="1355.0" y2="1450.0" stroke="#fff" stroke-width="8"/>
+<text x="1355.0" y="1165.0" text-anchor="middle" dominant-baseline="middle" font-size="52" fill="#2D3748" font-weight="600">客厅</text>
+</g>
+<g class="fpc-room" data-room="主卧">
+<rect x="120.0" y="1640.0" width="790.0" height="570.0" fill="#F7FAFC" stroke="#2D3748" stroke-width="6"/>
+<line x1="425.0" y1="2210.0" x2="515.0" y2="2210.0" stroke="#fff" stroke-width="8"/>
+<text x="515.0" y="1925.0" text-anchor="middle" dominant-baseline="middle" font-size="52" fill="#2D3748" font-weight="600">主卧</text>
+</g>
+<g class="fpc-room" data-room="次卧">
+<rect x="960.0" y="1640.0" width="790.0" height="570.0" fill="#F7FAFC" stroke="#2D3748" stroke-width="6"/>
+<line x1="1265.0" y1="2210.0" x2="1355.0" y2="2210.0" stroke="#fff" stroke-width="8"/>
+<text x="1355.0" y="1925.0" text-anchor="middle" dominant-baseline="middle" font-size="52" fill="#2D3748" font-weight="600">次卧</text>
+</g>
+<g class="fpc-room" data-room="卫生间(公卫)">
+<rect x="960.0" y="880.0" width="790.0" height="570.0" fill="#F7FAFC" stroke="#2D3748" stroke-width="6"/>
+<line x1="1265.0" y1="1450.0" x2="1355.0" y2="1450.0" stroke="#fff" stroke-width="8"/>
+<text x="1355.0" y="1165.0" text-anchor="middle" dominant-baseline="middle" font-size="52" fill="#2D3748" font-weight="600">卫生间(公卫)</text>
+</g>
+<g class="fpc-room" data-room="卫生间(主卫)">
+<rect x="960.0" y="120.0" width="790.0" height="570.0" fill="#F7FAFC" stroke="#2D3748" stroke-width="6"/>
+<line x1="1265.0" y1="690.0" x2="1355.0" y2="690.0" stroke="#fff" stroke-width="8"/>
+<text x="1355.0" y="405.0" text-anchor="middle" dominant-baseline="middle" font-size="52" fill="#2D3748" font-weight="600">卫生间(主卫)</text>
+</g>
+<g class="fpc-room" data-room="衣帽间">
+<rect x="960.0" y="880.0" width="790.0" height="570.0" fill="#F7FAFC" stroke="#2D3748" stroke-width="6"/>
+<line x1="1265.0" y1="1450.0" x2="1355.0" y2="1450.0" stroke="#fff" stroke-width="8"/>
+<text x="1355.0" y="1165.0" text-anchor="middle" dominant-baseline="middle" font-size="52" fill="#2D3748" font-weight="600">衣帽间</text>
+</g>
+<g class="fpc-room" data-room="生活阳台">
+<rect x="960.0" y="1640.0" width="790.0" height="570.0" fill="#F7FAFC" stroke="#2D3748" stroke-width="6"/>
+<line x1="1265.0" y1="2210.0" x2="1355.0" y2="2210.0" stroke="#fff" stroke-width="8"/>
+<text x="1355.0" y="1925.0" text-anchor="middle" dominant-baseline="middle" font-size="52" fill="#2D3748" font-weight="600">生活阳台</text>
+</g>
+<g class="fpc-room" data-room="观景阳台">
+<rect x="1800.0" y="1640.0" width="790.0" height="570.0" fill="#F7FAFC" stroke="#2D3748" stroke-width="6"/>
+<line x1="2105.0" y1="2210.0" x2="2195.0" y2="2210.0" stroke="#fff" stroke-width="8"/>
+<text x="2195.0" y="1925.0" text-anchor="middle" dominant-baseline="middle" font-size="52" fill="#2D3748" font-weight="600">观景阳台</text>
+</g>
+</g>
+<g id="fpc-cabinets">
+<g class="fpc-cab" data-cab="次卧衣柜">
+<rect x="320.0" y="320.0" width="520.0" height="240.0" rx="8" fill="#BEE3F8" stroke="#1F497D" stroke-width="5" stroke-dasharray="18 8"/>
+<text x="580.0" y="440.0" text-anchor="middle" dominant-baseline="middle" font-size="44" fill="#1F497D" font-weight="700">次卧衣柜 2.6×1.2</text>
+</g>
+<g class="fpc-cab" data-cab="玄关鞋柜">
+<rect x="320.0" y="320.0" width="240.0" height="240.0" rx="8" fill="#BEE3F8" stroke="#1F497D" stroke-width="5" stroke-dasharray="18 8"/>
+<text x="440.0" y="440.0" text-anchor="middle" dominant-baseline="middle" font-size="44" fill="#1F497D" font-weight="700">玄关鞋柜 1.2×1.2</text>
+</g>
+<g class="fpc-cab" data-cab="阳台柜">
+<rect x="320.0" y="320.0" width="240.0" height="240.0" rx="8" fill="#BEE3F8" stroke="#1F497D" stroke-width="5" stroke-dasharray="18 8"/>
+<text x="440.0" y="440.0" text-anchor="middle" dominant-baseline="middle" font-size="44" fill="#1F497D" font-weight="700">阳台柜 1.2×1.2</text>
+</g>
+<g class="fpc-cab" data-cab="餐厅餐边柜">
+<rect x="320.0" y="320.0" width="320.0" height="240.0" rx="8" fill="#BEE3F8" stroke="#1F497D" stroke-width="5" stroke-dasharray="18 8"/>
+<text x="480.0" y="440.0" text-anchor="middle" dominant-baseline="middle" font-size="44" fill="#1F497D" font-weight="700">餐厅餐边柜 1.6×1.2</text>
+</g>
+<g class="fpc-cab" data-cab="厨房橱柜">
+<rect x="320.0" y="320.0" width="300.0" height="240.0" rx="8" fill="#FEEBC8" stroke="#1F497D" stroke-width="5" stroke-dasharray="18 8"/>
+<text x="470.0" y="440.0" text-anchor="middle" dominant-baseline="middle" font-size="44" fill="#1F497D" font-weight="700">厨房橱柜 1.5×1.2</text>
+</g>
+<g class="fpc-cab" data-cab="衣帽间">
+<rect x="320.0" y="320.0" width="300.0" height="240.0" rx="8" fill="#BEE3F8" stroke="#1F497D" stroke-width="5" stroke-dasharray="18 8"/>
+<text x="470.0" y="440.0" text-anchor="middle" dominant-baseline="middle" font-size="44" fill="#1F497D" font-weight="700">衣帽间 1.5×1.2</text>
+</g>
+<g class="fpc-cab" data-cab="主卧衣柜">
+<rect x="320.0" y="320.0" width="300.0" height="240.0" rx="8" fill="#BEE3F8" stroke="#1F497D" stroke-width="5" stroke-dasharray="18 8"/>
+<text x="470.0" y="440.0" text-anchor="middle" dominant-baseline="middle" font-size="44" fill="#1F497D" font-weight="700">主卧衣柜 1.5×1.2</text>
+</g>
+<g class="fpc-cab" data-cab="浴室柜">
+<rect x="320.0" y="320.0" width="300.0" height="240.0" rx="8" fill="#BEE3F8" stroke="#1F497D" stroke-width="5" stroke-dasharray="18 8"/>
+<text x="470.0" y="440.0" text-anchor="middle" dominant-baseline="middle" font-size="44" fill="#1F497D" font-weight="700">浴室柜 1.5×1.2</text>
+</g>
+</g>
+<g id="fpc-dims">
+</g>
+<defs><marker id="arr" viewBox="0 0 10 10" refX="5" refY="5" markerWidth="5" markerHeight="5" orient="auto"><path d="M0,0L10,5L0,10z" fill="#C05621"/></marker></defs>
+</svg></div><p class="plan-note">📐 AI 结构化重绘示意图(非施工图,以实际量尺为准)</p>
+    <details class="dims"><summary>图面尺寸标注(1 条,用作交叉验证)</summary><ul><li><b>110㎡</b> — 总建筑面积</li></ul></details>
+  </div>
+
+  <div class="sec">
+    <h2>💰 报价明细 <span style="font-weight:400;font-size:11px;color:var(--mu)">(点击房间展开)</span></h2>
+    <div class="card" id="roomsCard"><div class="room" data-i="0">
+    <div class="room-h"><div><div class="nm">次卧衣柜</div><div class="dm">2.6m×2.5m</div></div><span class="tt">¥32,500</span></div>
+    <div class="items"><div class="item"><span class="l">柜体(PET材质)<span>2.6m×2.5m · 投影6.5m²</span></span><span class="v">¥32,500</span></div></div>
+  </div><div class="room" data-i="1">
+    <div class="room-h"><div><div class="nm">玄关鞋柜</div><div class="dm">1.2m×2.4m</div></div><span class="tt">¥14,400</span></div>
+    <div class="items"><div class="item"><span class="l">柜体(PET材质)<span>1.2m×2.4m · 投影2.88m²</span></span><span class="v">¥14,400</span></div></div>
+  </div><div class="room" data-i="2">
+    <div class="room-h"><div><div class="nm">阳台柜</div><div class="dm">1.2m×2.4m</div></div><span class="tt">¥14,400</span></div>
+    <div class="items"><div class="item"><span class="l">柜体(PET材质)<span>1.2m×2.4m · 投影2.88m²</span></span><span class="v">¥14,400</span></div></div>
+  </div><div class="room" data-i="3">
+    <div class="room-h"><div><div class="nm">餐厅餐边柜</div><div class="dm">1.6m×2.4m</div></div><span class="tt">¥19,200</span></div>
+    <div class="items"><div class="item"><span class="l">柜体(PET材质)<span>1.6m×2.4m · 投影3.84m²</span></span><span class="v">¥19,200</span></div></div>
+  </div><div class="room" data-i="4">
+    <div class="room-h"><div><div class="nm">厨房橱柜</div><div class="dm">1.5m×2.6m</div></div><span class="tt">¥26,500</span></div>
+    <div class="items"><div class="item"><span class="l">柜体(PET材质)<span>1.5m×2.6m · 投影5.3m</span></span><span class="v">¥26,500</span></div></div>
+  </div><div class="room" data-i="5">
+    <div class="room-h"><div><div class="nm">衣帽间</div><div class="dm">1.5m×2.6m</div></div><span class="tt">¥19,500</span></div>
+    <div class="items"><div class="item"><span class="l">柜体(PET材质)<span>1.5m×2.6m · 投影3.9m²</span></span><span class="v">¥19,500</span></div></div>
+  </div><div class="room" data-i="6">
+    <div class="room-h"><div><div class="nm">主卧衣柜</div><div class="dm">1.5m×2.6m</div></div><span class="tt">¥19,500</span></div>
+    <div class="items"><div class="item"><span class="l">柜体(PET材质)<span>1.5m×2.6m · 投影3.9m²</span></span><span class="v">¥19,500</span></div></div>
+  </div><div class="room" data-i="7">
+    <div class="room-h"><div><div class="nm">浴室柜</div><div class="dm">1.5m×2.6m</div></div><span class="tt">¥19,500</span></div>
+    <div class="items"><div class="item"><span class="l">柜体(PET材质)<span>1.5m×2.6m · 投影3.9m²</span></span><span class="v">¥19,500</span></div></div>
+  </div></div>
+    <div class="card" style="margin-top:8px">
+      <div class="trow"><span>项目小计</span><span class="v" id="tSub">¥165,500</span></div>
+      <div class="trow dim"><span id="tDiscLabel">原价</span><span class="v" id="tAfter">¥165,500</span></div>
+      <div class="trow" id="actRow" style="display:none"><span>活动优惠</span><span class="v" style="color:var(--ok)" id="tAct">-¥0</span></div>
+      <div class="trow total"><span>到手价</span><span class="v" id="tFinal">¥165,500</span></div>
+    </div>
+  </div>
+
+  <div class="sec">
+    <div class="selling">
+      <h3>✨ 为什么选择拉迷(标准客户)</h3>
+      <ul><li>空间利用率拉满:每一面墙都变成收纳</li><li>全国联保5年,铰链五金终身维护</li><li>设计上门量尺,3D效果图确认后再生产</li></ul>
+    </div>
+  </div>
+
+  <div class="sec" style="font-size:11px;color:var(--mu);line-height:1.7">
+    计价说明:柜体按投影面积(宽×高)×材质单价;厨房按计费延米(扣除转角重叠与冰箱位)。本报价为AI辅助估算,实际以现场量尺与正式合同为准。质保与赠送以当期活动政策为准。
+  </div>
+</div>
+
+<div class="ctrl">
+  <button class="btn sec" id="btnAdjust">⚖️ 调整报价</button>
+  <button class="btn pri" id="btnSave">📞 锁定到店礼</button>
+</div>
+
+<div class="drawer" id="drawer">
+  <div class="sheet">
+    <h3>⚖️ 销售操作台(客户不可见)</h3>
+    <div class="frow"><label>客户类型</label><div class="opts" id="optTier"><button class="chip " data-v="price">🏷️ 价格型</button><button class="chip on" data-v="standard">🏠 标准型</button><button class="chip " data-v="quality">💎 品质型</button></div></div>
+    <div class="frow"><label>材质档位</label><div class="opts" id="optMat"><button class="chip " data-v="双饰面">双饰面</button><button class="chip on" data-v="PET">PET</button><button class="chip " data-v="混油">混油</button></div></div>
+    <div class="frow"><label>报价浮动 <span id="floatV">15</span>%</label><input type="range" id="floatR" min="10" max="20" step="1" value="15"></div>
+    <div class="frow"><label>折扣</label><div class="opts" id="optDisc">
+      <button class="chip" data-v="1.00">原价</button>
+      <button class="chip" data-v="0.95">95折</button>
+      <button class="chip" data-v="0.90">9折</button>
+      <button class="chip" data-v="0.85">85折</button>
+      <button class="chip" data-v="0.80">8折</button>
+    </div></div>
+    <div class="frow"><label>柜体清单(宽=投影宽 m / 厨房为计费长)</label><div id="cabList"></div>
+      <div style="margin-top:8px"><button class="chip" id="btnAddCab">+ 增加柜体</button></div>
+    </div>
+    <div class="frow"><label>活动优惠</label><div class="opts" id="optAct"></div></div>
+    <button class="btn pri" style="width:100%" id="btnApply">应用并重算</button>
+  </div>
+</div>
+
+<script>
+// ============================================================
+// 拉迷全屋定制 · 默认报价规则库
+// 数据来源:floorplan-quotation/AI快速报价_Demo2.optimizing.html
+// (DEFAULT_RULES / Q_CUSTOMER_TIERS / DEFAULT_QUOTATION_PLANS /
+//   CABINET_EXTRA_TEMPLATES / EXTRA_METHOD_MAP / 厨房扣减规则)
+// 所有价格单位:元。修改价格请改这里的默认值或运行时覆盖 config。
+// ============================================================
+
+const METHOD = {
+  PROJECTION: '按投影面积', // 元/m²  数量 = 宽(m) × 高(m)
+  METER: '按延米',          // 元/m   数量 = 米数
+  COUNT: '按个数',          // 元/个  数量 = 个数
+};
+
+// —— 材质三档单价区间(AI快速粗略报价核心)——
+const PRICE_TIERS = [
+  { name: '双饰面', min: 3000, max: 3500 },
+  { name: 'PET',   min: 4500, max: 5000 },
+  { name: '混油',   min: 6000, max: 7000 },
+];
+const DEFAULT_TIER = 'PET';
+
+// —— 计价方式 ——
+const METHOD_OPTIONS = [
+  { value: '按投影面积', unit: '元/m²' },
+  { value: '按个数', unit: '元/个' },
+  { value: '按延米', unit: '元/m' },
+];
+
+// —— 柜体→空间 映射(识别命名用)——
+const CABINET_TO_SPACE = {
+  '主卧衣柜': '主卧', '次卧衣柜': '次卧', '儿童房衣柜': '儿童房', '老人房衣柜': '老人房',
+  '衣柜': '卧室', '客厅电视柜': '客厅', '电视柜': '客厅', '厨房橱柜': '厨房', '橱柜': '厨房',
+  '开放式厨房': '开放式厨房', '餐厅餐边柜': '餐厅', '餐边柜': '餐厅', '书房书柜': '书房',
+  '书柜': '书房', '玄关鞋柜': '玄关', '鞋柜': '玄关', '玄关柜': '玄关', '阳台柜': '阳台',
+  '衣帽间': '衣帽间', '储物柜': '储物间', '酒柜': '餐厅', '浴室柜': '卫生间', '榻榻米': '次卧',
+  '展示柜': '客厅', '洗衣机柜': '阳台', '卡座': '餐厅', '飘窗柜': '飘窗',
+};
+
+const SPACE_TO_DEFAULT_CABINET = {
+  '主卧': '主卧衣柜', '次卧': '次卧衣柜', '儿童房': '儿童房衣柜', '老人房': '老人房衣柜',
+  '客厅': '客厅电视柜', '厨房': '厨房橱柜', '开放式厨房': '开放式厨房', '餐厅': '餐厅餐边柜',
+  '客餐厅': '餐厅餐边柜', '书房': '书房书柜', '玄关': '玄关鞋柜', '阳台': '阳台柜',
+  '衣帽间': '衣帽间', '储物间': '储物柜', '卫生间': '浴室柜', '主卫': '浴室柜',
+};
+
+// —— 柜体类型 / 布局 / 门板 预设 ——
+const CUSTOM_TYPES_PRESET = {
+  cabinetType: ['主卧衣柜','次卧衣柜','儿童房衣柜','老人房衣柜','客厅电视柜','厨房橱柜','开放式厨房','餐厅餐边柜','书房书柜','玄关鞋柜','阳台柜','衣帽间','储物柜','酒柜','浴室柜','榻榻米'],
+  layoutForm: ['一字型','L型','U型','二字型','不规则'],
+  doorPanelType: ['普通平板','风影门板','PET门板','实木贴皮','烤漆门板','吸塑门板','玻璃门','百叶门','无门板'],
+};
+
+// —— 柜体默认附件模板(识别到柜型后自动配附件)——
+const CABINET_EXTRA_TEMPLATES = {
+  '玄关': ['灯带','变压器+开关','自弹器','见光板','铰链'],
+  '鞋柜': ['灯带','变压器+开关','自弹器','见光板','铰链','换鞋凳'],
+  '电视柜': ['自弹器','见光板','铰链'],
+  '展示柜': ['灯带','变压器+开关','拉手','见光板','铰链','自弹器'],
+  '酒柜': ['灯带','变压器+开关','拉手','见光板','铰链','自弹器','玻璃门升级'],
+  '洗衣机': ['自弹器','护墙板','见光板','铰链'],
+  '阳台': ['自弹器','见光板','铰链'],
+  '餐边': ['抽屉','拉手','自弹器','金属拉手','灯带','变压器+开关','见光板','铰链'],
+  '次卧': ['衣杆','金属拉手','抽屉','见光板','铰链'],
+  '儿童房': ['衣杆','金属拉手','抽屉','见光板','铰链'],
+  '书房': ['衣杆','金属拉手','抽屉','见光板','铰链'],
+  '主卧': ['金属拉手','抽屉','衣杆','自弹器','圆弧','铰链'],
+  '衣帽间': ['金属拉手','抽屉','衣杆','自弹器','圆弧','铰链','裤架领带格','拉篮'],
+  '厨房': ['见光板','碗篮','锅篮','自弹器'],
+  '_default': ['自弹器','见光板','铰链'],
+};
+
+// —— 附件计价方式映射 ——
+const EXTRA_METHOD_MAP = {
+  '见光板': '按投影面积', '护墙板': '按投影面积', '玻璃门升级': '按投影面积',
+  '灯带': '按延米', '衣杆': '按延米', '石材台面': '按延米',
+  '自弹器': '按个数', '铰链': '按个数', '变压器+开关': '按个数', '拉手': '按个数',
+  '金属拉手': '按个数', '抽屉': '按个数', '裤架领带格': '按个数', '拉篮': '按个数',
+  '碗篮': '按个数', '锅篮': '按个数', '圆弧': '按个数', '换鞋凳': '按个数',
+};
+
+// —— 基础价格表(name → {price, unit, method})——
+const PRICE_BOOK = {
+  // 柜体
+  '柜体投影单价':   { price: 1852, unit: '元/m²', method: '按投影面积' },
+  '生态澳松板柜体': { price: 1852, unit: '元/m²', method: '按投影面积' },
+  '铝格栅板柜体':   { price: 2532, unit: '元/m²', method: '按投影面积' },
+  // 厨房(单件计价模式)
+  '地柜':   { price: 2380, unit: '元/m', method: '按延米' },
+  '吊柜':   { price: 1748, unit: '元/m', method: '按延米' },
+  '冰箱吊柜': { price: 2622, unit: '元/m', method: '按延米' },
+  '高柜':   { price: 5950, unit: '元/m', method: '按延米' },
+  '台面':   { price: 1480, unit: '元/m', method: '按延米' },
+  '台面超出部分': { price: 2480, unit: '元/m', method: '按延米' },
+  '碗篮':   { price: 1268, unit: '元/个', method: '按个数' },
+  '锅篮':   { price: 1628, unit: '元/个', method: '按个数' },
+  '切角':   { price: 500, unit: '元/个', method: '按个数' },
+  '台下盆': { price: 500, unit: '元/个', method: '按个数' },
+  '包管':   { price: 500, unit: '元/个', method: '按个数' },
+  // 门板单价
+  '标配门板': { price: 380, unit: '元/m²', method: '按投影面积' },
+  '烤漆门板': { price: 580, unit: '元/m²', method: '按投影面积' },
+  '吸塑门板': { price: 480, unit: '元/m²', method: '按投影面积' },
+  '实木门板': { price: 880, unit: '元/m²', method: '按投影面积' },
+  '铝格栅门板': { price: 680, unit: '元/m²', method: '按投影面积' },
+  'PET门板':  { price: 450, unit: '元/m²', method: '按投影面积' },
+  // 附件
+  '见光板':   { price: 1138, unit: '元/m²', method: '按投影面积' },
+  '护墙板':   { price: 1852, unit: '元/m²', method: '按投影面积' },
+  '玻璃门升级': { price: 1852, unit: '元/m²', method: '按投影面积' },
+  '灯带':     { price: 386, unit: '元/m', method: '按延米' },
+  '变压器+开关': { price: 536, unit: '元/个', method: '按个数' },
+  '自弹器':   { price: 115, unit: '元/个', method: '按个数' },
+  '铰链':     { price: 40, unit: '元/个', method: '按个数' },
+  '拉手':     { price: 226, unit: '元/个', method: '按个数' },
+  '金属拉手': { price: 225, unit: '元/个', method: '按个数' },
+  '抽屉':     { price: 1300, unit: '元/个', method: '按个数' },
+  '衣杆':     { price: 288, unit: '元/m', method: '按延米' },
+  '裤架领带格': { price: 0, unit: '元/个', method: '按个数' },
+  '拉篮':     { price: 0, unit: '元/个', method: '按个数' },
+  '圆弧':     { price: 1560, unit: '元/个', method: '按个数' },
+  '换鞋凳':   { price: 0, unit: '元/个', method: '按个数' },
+  '石材台面': { price: 1480, unit: '元/m', method: '按延米' },
+};
+
+// —— 厨房计价参数(qGetKitchenBillableMeters 同源)——
+const KITCHEN_RULES = {
+  packageMeters: 3,     // 3米基础套餐(单件计价模式的扣减基准)
+  overlapMm: 600,       // 转角重叠扣减
+  fridgeMm: 1000,       // 冰箱位预留
+  typeThresholdM: 2,    // 最小边 ≤2m 判 L 型,>2m 判 U 型
+};
+
+// —— 客户分型(销售策略核心)——
+const CUSTOMER_TIERS = {
+  price: {
+    label: '看重价格型', icon: '🏷️',
+    strategy: '预算少的客户直接按性价比报,快速赢单',
+    defaultPlan: 'economy', defaultDiscount: 0.85,
+    doorType: '标配', hardware: '经济实用款', service: '标准测量+安装',
+    warranty: '3年质保', gifts: '送基础五金配件', panelLabel: '进口爱格板(加价升级)',
+    autoEnableActivity: true,
+    discounts: [
+      { id: 'p1', label: '投影面积85折', rate: 0.85, deletable: false },
+      { id: 'p2', label: '投影面积8折特惠', rate: 0.80, deletable: true },
+      { id: 'p3', label: '团购78折', rate: 0.78, deletable: true },
+    ],
+  },
+  standard: {
+    label: '标准客户', icon: '🏠',
+    strategy: '标准配置报价,正常谈单空间',
+    defaultPlan: 'standard', defaultDiscount: 1.0,
+    doorType: '标配门板', hardware: '国产优质铰链/拉手', service: '标准测量安装',
+    warranty: '5年质保', gifts: '可参与活动优惠', panelLabel: '国产E0级颗粒板',
+    autoEnableActivity: false,
+    discounts: [
+      { id: 's1', label: '原价', rate: 1.00, deletable: false },
+      { id: 's2', label: '投影面积95折', rate: 0.95, deletable: true },
+      { id: 's3', label: '投影面积9折', rate: 0.90, deletable: true },
+    ],
+  },
+  quality: {
+    label: '品质导向型', icon: '💎',
+    strategy: '有预算的客户报高一点,留谈价空间,突出配置与质感',
+    defaultPlan: 'quality', defaultDiscount: 1.0,
+    doorType: '烤漆/吸塑高端门板', hardware: '进口百隆/海蒂诗五金', service: 'VIP专属设计师+金牌安装',
+    warranty: '10年质保+终身维护', gifts: '赠灯带+抽屉升级包', panelLabel: '进口爱格板/可丽芙(推荐)',
+    premiumRate: 1.15,
+    autoEnableActivity: false,
+    discounts: [
+      { id: 'q1', label: '投影面积95折(保利润)', rate: 0.95, deletable: false },
+      { id: 'q2', label: '原价(高端不打折)', rate: 1.00, deletable: true },
+      { id: 'q3', label: 'VIP专享92折', rate: 0.92, deletable: true },
+    ],
+  },
+};
+
+// —— 报价方案 ——
+const QUOTATION_PLANS = [
+  { id: 'economy',  name: '经济型方案', description: '适合预算敏感型客户,主打性价比', customerTier: 'price',    tierName: '双饰面', floatPct: 10, discountRate: 0.98 },
+  { id: 'standard', name: '标准型方案', description: '默认方案,标准配置,适合大多数客户', customerTier: 'standard', tierName: 'PET',    floatPct: 15, discountRate: 1.00 },
+  { id: 'quality',  name: '品质型方案', description: '高端配置,适合追求品质的客户', customerTier: 'quality',   tierName: '混油',   floatPct: 15, discountRate: 1.00 },
+];
+
+// —— 营销活动模块(可勾选叠加)——
+const ACTIVITY_MODULES = [
+  { id: 'deposit5000', type: 'deposit', name: '定金膨胀(5千档)', desc: '交¥5,000→膨胀¥8,000 + ¥500抵用券', amount: 3500, minOrder: 30000 },
+  { id: 'deposit10000', type: 'deposit', name: '定金膨胀(1万档)', desc: '交¥10,000→膨胀¥13,000 + ¥800抵用券', amount: 3800, minOrder: 30000 },
+  { id: 'coupon20k', type: 'coupon', name: '满减券', desc: '满¥20,000减¥1,000', amount: 1000, threshold: 20000 },
+  { id: 'cashback', type: 'tiered_cashback', name: '阶梯返现', tiers: [
+      { min: 50000, cash: 2000 }, { min: 100000, cash: 5000 }, { min: 150000, cash: 8000 },
+    ] },
+];
+
+// —— 默认层高与房间面积先验(识别失败兜底估算用)——
+const DEFAULTS = {
+  height: 2.8, // 默认柜高(层高)
+  roomAreaPriors: { // 常见房间典型面积范围 [min, max](m²)
+    '主卧': [12, 22], '次卧': [8, 16], '儿童房': [8, 14], '老人房': [9, 15], '书房': [6, 12],
+    '客厅': [18, 35], '餐厅': [8, 18], '厨房': [4.5, 10], '卫生间': [2.5, 6], '主卫': [3, 6],
+    '公卫': [2.5, 6], '阳台': [3, 10], '玄关': [2, 6], '衣帽间': [3, 10], '储物间': [1.5, 5],
+  },
+  cabinetWallRatio: { // 柜体默认占所在墙面比例(无明确尺寸时的估算)
+    '主卧衣柜': 0.55, '次卧衣柜': 0.50, '儿童房衣柜': 0.50, '玄关鞋柜': 0.6,
+    '客厅电视柜': 0.55, '餐厅餐边柜': 0.5, '阳台柜': 0.6, '书房书柜': 0.5,
+  },
+};
+
+// —— 赢单文案素材(业主视角价值描述)——
+const SELLING_POINTS = {
+  price: [
+    '按投影面积计价,明明白白,没有隐形收费',
+    'E0级环保板材,装完即可安心入住',
+    '工厂直供价,同样的预算多做一个柜子',
+  ],
+  standard: [
+    '空间利用率拉满:每一面墙都变成收纳',
+    '全国联保5年,铰链五金终身维护',
+    '设计上门量尺,3D效果图确认后再生产',
+  ],
+  quality: [
+    '进口板材+进口五金,用20年不变形不褪色',
+    'VIP设计师一对一,全屋风格统一设计',
+    '10年质保+终身维护,一次装修终身省心',
+  ],
+};
+
+// ============================================================
+// 拉迷全屋定制 · 报价引擎(纯函数,浏览器/CLI 共用)
+// 两种模式:
+//   quick  — AI快速粗略报价:投影面积/厨房延米 × 三档材质区间单价
+//   detail — 明细报价:Excel 预算表模型(柜体+门板补差+附件逐项)
+// 销售策略:客户分型 → 方案/折扣/浮动 → 活动叠加 → 谈价区间
+// ============================================================
+
+const r2 = (v) => Math.round(v * 100) / 100;
+const num = (v, d = 0) => { const n = parseFloat(v); return Number.isFinite(n) ? n : d; };
+
+// ---------- 材质档位 ----------
+function getTier(name) {
+  const t = PRICE_TIERS.find((x) => x.name === (name || DEFAULT_TIER));
+  return t || PRICE_TIERS[1];
+}
+// 浮动百分比(10~15)在区间内线性插值 → 单价(qGetAiUnitPrice 同源逻辑)
+function tierUnitPrice(tier, floatPct = 15) {
+  const k = Math.min(1, Math.max(0, (num(floatPct, 15) - 10) / 5));
+  return r2(tier.min + k * (tier.max - tier.min));
+}
+
+// ---------- 厨房计费米数(qGetKitchenBillableMeters 同源)----------
+function kitchenBillable(walls = [], rules = {}) {
+  const R = { ...KITCHEN_RULES, ...rules };
+  const ws = (Array.isArray(walls) ? walls : []).map((v) => num(v)).filter((v) => v > 0);
+  if (!ws.length) return null;
+  const total = ws.reduce((a, b) => a + b, 0);
+  const minWall = Math.min(...ws);
+  const maxWall = Math.max(...ws);
+  const type = minWall <= R.typeThresholdM ? 'L' : 'U';
+  const overlapCount = type === 'L' ? 1 : 2;
+  const usedLen = type === 'L' ? maxWall + minWall : maxWall + maxWall + minWall;
+  const billable = Math.max(0, usedLen - (overlapCount * R.overlapMm) / 1000 - R.fridgeMm / 1000);
+  return {
+    walls: ws, total: r2(total), minWall: r2(minWall), maxWall: r2(maxWall),
+    kitchenType: type, overlapCount, fridgeMm: R.fridgeMm, usedLen: r2(usedLen), billable: r2(billable),
+  };
+}
+const isKitchen = (name = '') => /厨房|橱柜/.test(String(name));
+
+// ---------- 附件数量启发式(Excel 模型总结,均可编辑) ----------
+function estimateExtras(cabinet) {
+  const w = num(cabinet.width, 1.5);
+  const h = num(cabinet.height, DEFAULTS.height);
+  const type = String(cabinet.type || '');
+  let tplKey = Object.keys(CABINET_EXTRA_TEMPLATES).find((k) => k !== '_default' && type.includes(k));
+  if (!tplKey && isKitchen(type)) tplKey = '厨房';
+  const names = CABINET_EXTRA_TEMPLATES[tplKey || '_default'] || CABINET_EXTRA_TEMPLATES._default;
+  const sections = h > 1.6 ? 2 : 1;               // 高柜分上下段
+  const doorsPerSection = Math.max(1, Math.round(w / 0.45));
+  const doors = sections * doorsPerSection;
+  const hingePerDoor = h > 2 ? 3 : 2;
+  const out = [];
+  for (const name of names) {
+    const pb = PRICE_BOOK[name];
+    if (!pb) continue;
+    let qty = 0; let note = '';
+    switch (name) {
+      case '铰链': qty = doors * hingePerDoor; note = `${doors}门×${hingePerDoor}铰链`; break;
+      case '自弹器': qty = Math.max(1, doors - 1); note = '反弹门配'; break;
+      case '见光板': qty = 1; note = '单侧见光(0.5m宽)'; break;
+      case '灯带': qty = r2(w * 2); note = '顶底双层'; break;
+      case '变压器+开关': qty = 1; note = 'LED电源'; break;
+      case '衣杆': qty = r2(w); note = '通长挂杆'; break;
+      case '抽屉': qty = type.includes('主卧') || type.includes('衣帽') ? 3 : 2; break;
+      case '金属拉手': case '拉手': qty = doors + 2; break;
+      case '碗篮': case '锅篮': qty = 1; break;
+      default: qty = name === '玻璃门升级' || name === '护墙板' ? 0 : 1;
+    }
+    if (qty > 0) out.push({ name, method: pb.method, unit: pb.unit, unitPrice: pb.price, qty, note });
+  }
+  return out;
+}
+
+// ---------- 单柜体计价 ----------
+// 计价数量:投影面积 = 宽×高;厨房 = 计费延米
+function cabinetQty(cabinet, rules = {}) {
+  if (isKitchen(cabinet.type)) {
+    const kb = kitchenBillable(cabinet.walls || [cabinet.width, cabinet.height, cabinet.width, cabinet.height], rules);
+    return { qty: kb ? kb.billable : num(cabinet.width), unit: 'm', kb };
+  }
+  return { qty: r2(num(cabinet.width) * num(cabinet.height)), unit: 'm²', kb: null };
+}
+
+// 快速模式:单柜体一行
+function quickLine(cabinet, { tierName, floatPct = 15, rules } = {}) {
+  const tier = getTier(tierName);
+  const U = tierUnitPrice(tier, floatPct);
+  const { qty, unit, kb } = cabinetQty(cabinet, rules);
+  return {
+    mode: 'quick', name: cabinet.type, tierName: tier.name,
+    qty, unit, unitPrice: U, total: r2(qty * U), kb,
+    dims: num(cabinet.width) && num(cabinet.height) ? `${num(cabinet.width)}m×${num(cabinet.height)}m` : '',
+  };
+}
+
+// 明细模式:单柜体 → Excel 风格明细行
+function detailLines(cabinet, opts = {}) {
+  const { doorDiffPrice = PRICE_BOOK['柜体投影单价'].price > 0 ? 538 : 538, rules, includeDoorDiff = true } = opts;
+  const lines = [];
+  const isK = isKitchen(cabinet.type);
+  const { qty } = cabinetQty(cabinet, rules);
+  if (isK) {
+    // 厨房单件计价(Excel 模式:地柜+吊柜+台面+功能五金)
+    const walls = cabinet.walls || [cabinet.width, cabinet.height, cabinet.width, cabinet.height];
+    const straight = Math.max(0, ...walls); // 直线段米数近似
+    for (const [name, factor, note] of [
+      ['地柜', 1, '沿墙地柜'], ['吊柜', 1, '沿墙吊柜'], ['台面', 1, '石英石台面'],
+    ]) {
+      const pb = PRICE_BOOK[name];
+      lines.push({ name, qty: r2(qty), unit: 'm', unitPrice: pb.price, total: r2(qty * pb.price), note, method: pb.method });
+    }
+    for (const name of ['碗篮', '锅篮']) {
+      const pb = PRICE_BOOK[name];
+      lines.push({ name, qty: 1, unit: '个', unitPrice: pb.price, total: pb.price, note: '标准配置', method: pb.method });
+    }
+    return { cabinet, isKitchen: true, lines, subtotal: r2(lines.reduce((s, l) => s + l.total, 0)) };
+  }
+  const pb = PRICE_BOOK['生态澳松板柜体'];
+  lines.push({ name: '柜体', qty, unit: 'm²', unitPrice: pb.price, total: r2(qty * pb.price), note: '生态澳松板', method: pb.method });
+  if (includeDoorDiff && doorDiffPrice > 0) {
+    lines.push({ name: '板门补差价', qty, unit: 'm²', unitPrice: doorDiffPrice, total: r2(qty * doorDiffPrice), note: '双饰面门补差', method: pb.method });
+  }
+  for (const ex of estimateExtras(cabinet)) {
+    const total = ex.method === '按投影面积' ? r2(ex.qty * 0.5 * num(cabinet.height) * ex.unitPrice)
+      : ex.method === '按延米' ? r2(ex.qty * ex.unitPrice)
+      : r2(ex.qty * ex.unitPrice);
+    lines.push({ name: ex.name, qty: ex.qty, unit: ex.unit, unitPrice: ex.unitPrice, total, note: ex.note, method: ex.method });
+  }
+  return { cabinet, isKitchen: false, lines, subtotal: r2(lines.reduce((s, l) => s + l.total, 0)) };
+}
+
+// ---------- 报价主入口 ----------
+// inputs: { cabinets:[{type,width,height,walls?,excluded?}], tier, floatPct, discount, tierKey(客户分型), planId, mode }
+function buildQuote(input = {}) {
+  const {
+    cabinets = [], mode = 'quick',
+    tierName = input.tier || DEFAULT_TIER,
+    floatPct = 15, discount = null, tierKey = 'standard', planId = null,
+    rules = {}, extras = [], // extras: [{name, room, qty, unitPrice}] 手动增删项
+    clientName = '', note = '',
+  } = input;
+
+  const plan = QUOTATION_PLANS.find((p) => p.id === (planId || CUSTOMER_TIERS[tierKey]?.defaultPlan)) || QUOTATION_PLANS[1];
+  const custTier = CUSTOMER_TIERS[tierKey] || CUSTOMER_TIERS.standard;
+  const effDiscount = discount != null ? num(discount) : (custTier.defaultDiscount ?? plan.discountRate ?? 1);
+
+  const rooms = cabinets
+    .filter((c) => !c.excluded)
+    .map((c, i) => {
+      const base = mode === 'detail' ? detailLines(c, { rules }) : { ...quickLine(c, { tierName, floatPct, rules }), lines: null };
+      const id = c.id || `cab_${i + 1}`;
+      return { id, type: c.type, dims: base.dims || `${num(c.width)}m×${num(c.height)}m`, ...base };
+    });
+
+  const itemsSubtotal = r2(rooms.reduce((s, r) => s + (r.subtotal ?? r.total), 0));
+  const manualExtras = extras.map((e, i) => ({
+    id: e.id || `ext_${i + 1}`, name: e.name, qty: num(e.qty, 1), unit: e.unit || '项',
+    unitPrice: num(e.unitPrice), total: r2(num(e.qty, 1) * num(e.unitPrice)), kind: e.kind || 'add',
+  }));
+  const extrasTotal = r2(manualExtras.reduce((s, e) => s + (e.kind === 'remove' ? -e.total : e.total), 0));
+
+  const premiumRate = custTier.premiumRate || 1;
+  const afterPremium = r2((itemsSubtotal + extrasTotal) * premiumRate);
+  const afterDiscount = r2(afterPremium * effDiscount);
+
+  // 活动叠加(自动/手动)
+  const acts = (input.activities || (custTier.autoEnableActivity ? ACTIVITY_MODULES.map((m) => m.id) : []))
+    .map((id) => ACTIVITY_MODULES.find((m) => m.id === id)).filter(Boolean);
+  let activityTotal = 0;
+  const activityLines = [];
+  for (const m of acts) {
+    let amt = 0; let desc = m.desc || m.name;
+    if (m.type === 'tiered_cashback') {
+      const hit = [...(m.tiers || [])].sort((a, b) => b.min - a.min).find((t) => afterDiscount >= t.min);
+      if (!hit) continue;
+      amt = hit.cash; desc = `满¥${hit.min.toLocaleString()}返¥${hit.cash.toLocaleString()}`;
+    } else if (m.type === 'coupon' && afterDiscount < (m.threshold || 0)) continue;
+    else if (m.type === 'deposit' && afterDiscount < (m.minOrder || 30000)) continue;
+    else amt = m.amount || 0;
+    activityTotal += amt;
+    activityLines.push({ id: m.id, name: m.name, desc, amount: amt });
+  }
+  const finalPrice = Math.max(0, r2(afterDiscount - activityTotal));
+
+  // 谈价区间(浮动率)
+  const rate = num(floatPct, 15) / 100;
+  const range = { low: Math.round(finalPrice * (1 - rate)), high: Math.round(finalPrice * (1 + rate)) };
+
+  // 业主视角价值点
+  const selling = SELLING_POINTS[tierKey] || SELLING_POINTS.standard;
+
+  return {
+    meta: {
+      clientName, note: note || custTier.strategy, mode,
+      tierName: mode === 'quick' ? getTier(tierName).name : undefined,
+      floatPct, planId: plan.id, planName: plan.name,
+      customerTier: tierKey, customerLabel: custTier.label,
+      discount: effDiscount, premiumRate: premiumRate !== 1 ? premiumRate : undefined,
+      generatedAt: new Date().toISOString(),
+    },
+    rooms, manualExtras, activityLines,
+    totals: {
+      itemsSubtotal, extrasTotal, premiumRate, afterPremium, discount: effDiscount,
+      afterDiscount, activityTotal, finalPrice, range,
+      discountLabel: custTier.discounts?.find((d) => Math.abs(d.rate - effDiscount) < 0.001)?.label || `折扣${Math.round(effDiscount * 100)}%`,
+    },
+    selling,
+    config: { tierKey, tierName, floatPct, discount: effDiscount, planId: plan.id, mode },
+  };
+}
+
+// ---------- 语义化调整应用(销售边聊边改) ----------
+// ops: [{op:'add'|'remove'|'resize'|'rename'|'price', target, ...}]
+function applyOps(cabinets, ops = []) {
+  const out = cabinets.map((c) => ({ ...c }));
+  for (const op of ops) {
+    if (op.op === 'add') {
+      out.push({ type: op.type || op.name || '柜体', width: num(op.width, 1.5), height: num(op.height, DEFAULTS.height), from: '语音新增' });
+    } else if (op.op === 'remove') {
+      const i = out.findIndex((c) => c.type === op.target);
+      if (i >= 0) out.splice(i, 1);
+    } else if (op.op === 'resize') {
+      const c = out.find((c) => c.type === op.target);
+      if (c) { if (op.width) c.width = num(op.width); if (op.height) c.height = num(op.height); c.from = '手动调整'; }
+    } else if (op.op === 'rename') {
+      const c = out.find((c) => c.type === op.target);
+      if (c) c.type = op.to || c.type;
+    } else if (op.op === 'exclude') {
+      const c = out.find((c) => c.type === op.target);
+      if (c) c.excluded = true;
+    } else if (op.op === 'include') {
+      const c = out.find((c) => c.type === op.target);
+      if (c) delete c.excluded;
+    }
+  }
+  return out;
+}
+
+// 授权范围内校验(浮动区间即授权边界)
+function withinAuthority(quote, price) {
+  const { range } = quote.totals;
+  return price >= range.low && price <= range.high;
+}
+
+var __DEFAULT__ = { buildQuote, applyOps, quickLine, detailLines, kitchenBillable, tierUnitPrice, getTier, withinAuthority };
+
+window.__Q__ = {"cabinets":[{"id":"cab_1","type":"次卧衣柜","width":2.6,"height":2.6,"rawName":"次卧衣柜"},{"id":"cab_2","type":"玄关鞋柜","width":1.2,"height":2.6,"rawName":"玄关鞋柜"},{"id":"cab_3","type":"阳台柜","width":1.2,"height":2.6,"rawName":"阳台柜"},{"id":"cab_4","type":"餐厅餐边柜","width":1.6,"height":2.6,"rawName":"餐厅餐边柜"},{"id":"cab_5","type":"厨房橱柜","height":2.6,"rawName":"厨房橱柜"},{"id":"cab_6","type":"衣帽间","width":1.5,"height":2.6,"rawName":"衣帽间"},{"id":"cab_7","type":"主卧衣柜","width":1.5,"height":2.6,"rawName":"主卧衣柜"},{"id":"cab_8","type":"浴室柜","width":1.5,"height":2.6,"rawName":"浴室柜"}],"config":{"tierKey":"standard","tierName":"PET","floatPct":15,"discount":1,"planId":"standard","mode":"quick"},"salesNote":""};
+console.log('fpc engine ready');
+</script>
+<script>
+(function(){
+  var Q = window.__Q__; var S = document; var money = function(n){ return '¥' + Math.round(n||0).toLocaleString('zh-CN'); };
+  var cfg = Object.assign({ tierKey:'standard', tierName:'PET', floatPct:15, discount:1.0, mode:'quick' }, Q.config||{});
+  function calc(){ return buildQuote({ cabinets: Q.cabinets, tierKey: cfg.tierKey, tierName: cfg.tierName, floatPct: cfg.floatPct, discount: cfg.discount, mode: cfg.mode, clientName: "PDF链路测试" }); }
+  function refresh(){
+    var q = calc(); var t = q.totals;
+    S.getElementById('finalPrice').textContent = money(t.finalPrice);
+    S.getElementById('finalRange').textContent = '谈价参考区间 ' + money(t.range.low) + ' ~ ' + money(t.range.high);
+    S.getElementById('tSub').textContent = money(t.itemsSubtotal);
+    S.getElementById('tDiscLabel').textContent = t.discountLabel;
+    S.getElementById('tAfter').textContent = money(t.afterDiscount);
+    S.getElementById('tAct').textContent = '-' + money(t.activityTotal);
+    S.getElementById('tFinal').textContent = money(t.finalPrice);
+    S.getElementById('bTier').textContent = (q.meta.tierName||'') + '材质';
+    S.getElementById('bDisc').textContent = t.discountLabel;
+  }
+  // room expand
+  S.querySelectorAll('.room-h').forEach(function(el){ el.addEventListener('click', function(){ el.parentElement.classList.toggle('open'); }); });
+  // chips
+  function bindChips(id, fn){ var box = S.getElementById(id); if(!box) return; box.addEventListener('click', function(e){ var b = e.target.closest('.chip'); if(!b) return; box.querySelectorAll('.chip').forEach(function(x){x.classList.remove('on');}); b.classList.add('on'); fn(b.dataset.v); }); }
+  bindChips('optTier', function(v){ cfg.tierKey = v; refresh(); });
+  bindChips('optMat', function(v){ cfg.tierName = v; refresh(); });
+  bindChips('optDisc', function(v){ cfg.discount = parseFloat(v); refresh(); });
+  var fr = S.getElementById('floatR'); fr.addEventListener('input', function(){ S.getElementById('floatV').textContent = fr.value; cfg.floatPct = parseInt(fr.value); refresh(); });
+  // drawer
+  var d = S.getElementById('drawer');
+  S.getElementById('btnAdjust').addEventListener('click', function(){ d.classList.add('open'); renderCabs(); });
+  d.addEventListener('click', function(e){ if(e.target === d) d.classList.remove('open'); });
+  function renderCabs(){
+    var box = S.getElementById('cabList'); box.innerHTML = '';
+    Q.cabinets.forEach(function(c, i){
+      var row = S.createElement('div'); row.className = 'cabrow';
+      var dims = String(c.width||'') ? String(c.width) : '';
+      row.innerHTML = '<span style="flex:1">' + c.rawName + '</span>' +
+        '<input type="number" step="0.05" value="' + (c.width||1.5) + '" data-i="' + i + '" data-f="width" title="宽(米)">' +
+        '<span class="del" data-i="' + i + '">✕</span>';
+      box.appendChild(row);
+    });
+    box.onclick = function(e){
+      var del = e.target.closest('.del');
+      if (del) { Q.cabinets.splice(parseInt(del.dataset.i),1); renderCabs(); refresh(); return; }
+      var inp = e.target.closest('input');
+      if (inp) { inp.addEventListener('change', function(){ Q.cabinets[parseInt(inp.dataset.i)].width = parseFloat(inp.value)||1.5; refresh(); }); }
+    };
+  }
+  S.getElementById('btnAddCab').addEventListener('click', function(){ Q.cabinets.push({ type:'储物柜', rawName:'储物柜', width:1.5, height:2.6 }); renderCabs(); refresh(); });
+  S.getElementById('btnSave').addEventListener('click', function(){
+    alert('已记录!销售将带着这份报价与您确认到店时间,到店凭此单领取到店礼。');
+  });
+  refresh();
+})();
+</script>
+</body>
+</html>

+ 737 - 0
skill/out/报价单-缪女士(时代之城)-08312312.html

@@ -0,0 +1,737 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+<head>
+<meta charset="UTF-8">
+<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
+<meta name="format-detection" content="telephone=no">
+<title>缪女士(时代之城) · 全屋定制报价方案</title>
+<style>
+:root{--p:#1F497D;--pa:#2B6CB0;--bg:#F5F7FA;--card:#fff;--bd:#E2E8F0;--tx:#2D3748;--mu:#718096;--ok:#38A169;--wa:#D69E2E;--dg:#E53E3E;--r:12px}
+*{box-sizing:border-box;margin:0;padding:0;-webkit-tap-highlight-color:transparent}
+body{font-family:-apple-system,BlinkMacSystemFont,"PingFang SC","Microsoft YaHei",sans-serif;background:var(--bg);color:var(--tx);line-height:1.55;padding-bottom:calc(88px + env(safe-area-inset-bottom))}
+header{background:linear-gradient(135deg,#1F497D,#2B6CB0);color:#fff;padding:18px 16px 14px}
+header h1{font-size:17px;font-weight:700}
+header .sub{font-size:12px;opacity:.85;margin-top:2px}
+.wrap{padding:12px;max-width:640px;margin:0 auto}
+.hero{background:var(--card);border-radius:var(--r);padding:16px;box-shadow:0 1px 3px rgba(0,0,0,.08);margin-top:-26px;position:relative;text-align:center}
+.hero .label{font-size:12px;color:var(--mu)}
+.hero .price{font-size:38px;font-weight:800;color:var(--p);letter-spacing:-1px;margin:2px 0}
+.hero .price small{font-size:16px;font-weight:600}
+.hero .range{font-size:13px;color:var(--mu)}
+.badge{display:inline-block;font-size:11px;padding:2px 10px;border-radius:20px;background:#EBF8FF;color:var(--pa);border:1px solid #BEE3F8;margin-top:6px}
+.sec{margin-top:14px}
+.sec h2{font-size:15px;font-weight:700;margin:0 2px 8px;display:flex;align-items:center;gap:6px}
+.card{background:var(--card);border-radius:var(--r);box-shadow:0 1px 3px rgba(0,0,0,.06);overflow:hidden}
+.room{border-bottom:1px solid var(--bd)}
+.room:last-child{border-bottom:none}
+.room-h{padding:12px 14px;display:flex;justify-content:space-between;align-items:center;gap:8px}
+.room-h .nm{font-weight:700;font-size:14px}
+.room-h .dm{font-size:11px;color:var(--mu)}
+.room-h .tt{font-weight:800;color:var(--p);font-size:15px;white-space:nowrap}
+.room-h .conf{font-size:10px;padding:1px 6px;border-radius:8px;background:#FFFAF0;color:#C05621;border:1px solid #FBD38D;flex-shrink:0}
+.items{padding:0 14px 10px;display:none}
+.room.open .items{display:block}
+.item{display:flex;justify-content:space-between;font-size:12.5px;padding:5px 0;color:#4A5568}
+.item .l span{color:var(--mu);font-size:11px;margin-left:6px}
+.item .v{white-space:nowrap}
+.trow{display:flex;justify-content:space-between;padding:8px 14px;font-size:13px}
+.trow .v{font-weight:700}
+.trow.total{background:#F0FFF4;color:#276749;font-weight:800;font-size:15px}
+.trow.dim{color:var(--mu);font-size:12px}
+.plan-wrap{background:var(--card);border-radius:var(--r);padding:10px;box-shadow:0 1px 3px rgba(0,0,0,.06)}
+.plan-wrap svg,.plan-img{width:100%;height:auto;display:block;border-radius:8px}
+.plan-note{font-size:11px;color:var(--mu);text-align:center;margin-top:6px}
+.selling{background:linear-gradient(135deg,#FFFAF0,#FFF5EB);border:1px solid #FBD38D;border-radius:var(--r);padding:12px 14px}
+.selling li{font-size:13px;margin:6px 0 6px 18px;color:#744210}
+.selling h3{font-size:13px;color:#C05621;margin-bottom:4px}
+.acts .item .v{color:var(--ok)}
+.ctrl{position:fixed;bottom:0;left:0;right:0;background:#fff;border-top:1px solid var(--bd);padding:10px 14px calc(10px + env(safe-area-inset-bottom));display:flex;gap:8px;z-index:99;max-width:640px;margin:0 auto}
+.btn{flex:1;padding:12px;border:none;border-radius:10px;font-size:14px;font-weight:700;cursor:pointer}
+.btn.pri{background:var(--p);color:#fff}
+.btn.sec{background:#EDF2F7;color:var(--tx)}
+details.dims{background:var(--card);border-radius:var(--r);padding:10px 14px;font-size:12px;color:var(--mu)}
+details.dims summary{cursor:pointer;font-weight:600;color:var(--tx)}
+details.dims li{margin:4px 0 4px 18px}
+.drawer{position:fixed;inset:0;background:rgba(0,0,0,.4);z-index:100;display:none}
+.drawer.open{display:block}
+.sheet{position:absolute;bottom:0;left:0;right:0;background:#fff;border-radius:16px 16px 0 0;max-height:82vh;overflow:auto;padding:16px 16px calc(20px + env(safe-area-inset-bottom));max-width:640px;margin:0 auto}
+.sheet h3{font-size:15px;margin-bottom:12px}
+.frow{margin-bottom:14px}
+.frow label{font-size:12px;color:var(--mu);display:block;margin-bottom:4px}
+.frow .opts{display:flex;gap:6px;flex-wrap:wrap}
+.chip{padding:8px 12px;border-radius:8px;border:1.5px solid var(--bd);background:#fff;font-size:13px;cursor:pointer}
+.chip.on{border-color:var(--pa);background:#EBF8FF;color:var(--pa);font-weight:700}
+input[type=range]{width:100%}
+.cabrow{display:flex;align-items:center;gap:8px;padding:8px 0;border-bottom:1px dashed var(--bd);font-size:13px}
+.cabrow input{width:64px;padding:6px;border:1px solid var(--bd);border-radius:6px;font-size:13px}
+.cabrow .del{color:var(--dg);font-size:18px;padding:0 6px;cursor:pointer}
+.cabrow .add{color:var(--ok)}
+@media(min-width:640px){.hero .price{font-size:44px}}
+</style>
+</head>
+<body>
+<header>
+  <h1>缪女士(时代之城) · 全屋定制报价方案</h1>
+  <div class="sub">标准客户方案 · 生成于 8/31 23:12 · 拉迷全屋定制</div>
+</header>
+<div class="wrap">
+
+  <div class="hero">
+    <div class="label">全屋定制整装价</div>
+    <div class="price" id="finalPrice">¥152,600</div>
+    <div class="range" id="finalRange">谈价参考区间 ¥129,710 ~ ¥175,490</div>
+    <div id="badges">
+      <span class="badge" id="bTier">PET材质</span>
+      <span class="badge" id="bDisc">原价</span>
+      
+    </div>
+  </div>
+
+  <div class="sec"><div class="selling"><h3>💡 给销售的建议(标准客户)</h3><ul><li>三房两厅新居,孩子明年上小学,看重收纳和环保,预算25万内想多谈点优惠</li></ul></div></div>
+
+  <div class="sec">
+    <h2>🏠 户型与定制布局</h2>
+    <div class="plan-wrap" id="planWrap"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 760 480" class="fpc-plan">
+<g id="fpc-rooms">
+</g>
+<g id="fpc-cabinets">
+<g class="fpc-cab" data-cab="玄关鞋柜">
+<rect x="120.0" y="120.0" width="240.0" height="240.0" rx="8" fill="#BEE3F8" stroke="#1F497D" stroke-width="5" stroke-dasharray="18 8"/>
+<text x="240.0" y="240.0" text-anchor="middle" dominant-baseline="middle" font-size="44" fill="#1F497D" font-weight="700">玄关鞋柜 1.2×1.2</text>
+</g>
+<g class="fpc-cab" data-cab="主卧衣柜">
+<rect x="120.0" y="120.0" width="520.0" height="240.0" rx="8" fill="#BEE3F8" stroke="#1F497D" stroke-width="5" stroke-dasharray="18 8"/>
+<text x="380.0" y="240.0" text-anchor="middle" dominant-baseline="middle" font-size="44" fill="#1F497D" font-weight="700">主卧衣柜 2.6×1.2</text>
+</g>
+<g class="fpc-cab" data-cab="次卧衣柜">
+<rect x="120.0" y="120.0" width="440.0" height="240.0" rx="8" fill="#BEE3F8" stroke="#1F497D" stroke-width="5" stroke-dasharray="18 8"/>
+<text x="340.0" y="240.0" text-anchor="middle" dominant-baseline="middle" font-size="44" fill="#1F497D" font-weight="700">次卧衣柜 2.2×1.2</text>
+</g>
+<g class="fpc-cab" data-cab="书房书柜">
+<rect x="120.0" y="120.0" width="360.0" height="240.0" rx="8" fill="#BEE3F8" stroke="#1F497D" stroke-width="5" stroke-dasharray="18 8"/>
+<text x="300.0" y="240.0" text-anchor="middle" dominant-baseline="middle" font-size="44" fill="#1F497D" font-weight="700">书房书柜 1.8×1.2</text>
+</g>
+<g class="fpc-cab" data-cab="阳台柜">
+<rect x="120.0" y="120.0" width="300.0" height="240.0" rx="8" fill="#BEE3F8" stroke="#1F497D" stroke-width="5" stroke-dasharray="18 8"/>
+<text x="270.0" y="240.0" text-anchor="middle" dominant-baseline="middle" font-size="44" fill="#1F497D" font-weight="700">阳台柜 1.5×1.2</text>
+</g>
+<g class="fpc-cab" data-cab="餐厅餐边柜">
+<rect x="120.0" y="120.0" width="320.0" height="240.0" rx="8" fill="#BEE3F8" stroke="#1F497D" stroke-width="5" stroke-dasharray="18 8"/>
+<text x="280.0" y="240.0" text-anchor="middle" dominant-baseline="middle" font-size="44" fill="#1F497D" font-weight="700">餐厅餐边柜 1.6×1.2</text>
+</g>
+<g class="fpc-cab" data-cab="厨房吊柜">
+<rect x="120.0" y="120.0" width="480.0" height="160.0" rx="8" fill="#FEEBC8" stroke="#1F497D" stroke-width="5" stroke-dasharray="18 8"/>
+<text x="360.0" y="200.0" text-anchor="middle" dominant-baseline="middle" font-size="44" fill="#1F497D" font-weight="700">厨房吊柜 2.4×0.8</text>
+</g>
+</g>
+<g id="fpc-dims">
+</g>
+<defs><marker id="arr" viewBox="0 0 10 10" refX="5" refY="5" markerWidth="5" markerHeight="5" orient="auto"><path d="M0,0L10,5L0,10z" fill="#C05621"/></marker></defs>
+</svg></div><p class="plan-note">📐 AI 结构化重绘示意图(非施工图,以实际量尺为准)</p>
+    
+  </div>
+
+  <div class="sec">
+    <h2>💰 报价明细 <span style="font-weight:400;font-size:11px;color:var(--mu)">(点击房间展开)</span></h2>
+    <div class="card" id="roomsCard"><div class="room" data-i="0">
+    <div class="room-h"><div><div class="nm">玄关鞋柜</div><div class="dm">1.2m×2.4m</div></div><span class="tt">¥14,400</span></div>
+    <div class="items"><div class="item"><span class="l">柜体(PET材质)<span>1.2m×2.4m · 投影2.88m²</span></span><span class="v">¥14,400</span></div></div>
+  </div><div class="room" data-i="1">
+    <div class="room-h"><div><div class="nm">主卧衣柜</div><div class="dm">2.6m×2.6m</div></div><span class="tt">¥33,800</span></div>
+    <div class="items"><div class="item"><span class="l">柜体(PET材质)<span>2.6m×2.6m · 投影6.76m²</span></span><span class="v">¥33,800</span></div></div>
+  </div><div class="room" data-i="2">
+    <div class="room-h"><div><div class="nm">次卧衣柜</div><div class="dm">2.2m×2.6m</div></div><span class="tt">¥28,600</span></div>
+    <div class="items"><div class="item"><span class="l">柜体(PET材质)<span>2.2m×2.6m · 投影5.72m²</span></span><span class="v">¥28,600</span></div></div>
+  </div><div class="room" data-i="3">
+    <div class="room-h"><div><div class="nm">书房书柜</div><div class="dm">1.8m×2.4m</div></div><span class="tt">¥21,600</span></div>
+    <div class="items"><div class="item"><span class="l">柜体(PET材质)<span>1.8m×2.4m · 投影4.32m²</span></span><span class="v">¥21,600</span></div></div>
+  </div><div class="room" data-i="4">
+    <div class="room-h"><div><div class="nm">阳台柜</div><div class="dm">1.5m×2.4m</div></div><span class="tt">¥18,000</span></div>
+    <div class="items"><div class="item"><span class="l">柜体(PET材质)<span>1.5m×2.4m · 投影3.6m²</span></span><span class="v">¥18,000</span></div></div>
+  </div><div class="room" data-i="5">
+    <div class="room-h"><div><div class="nm">餐厅餐边柜</div><div class="dm">1.6m×2.4m</div></div><span class="tt">¥19,200</span></div>
+    <div class="items"><div class="item"><span class="l">柜体(PET材质)<span>1.6m×2.4m · 投影3.84m²</span></span><span class="v">¥19,200</span></div></div>
+  </div><div class="room" data-i="6">
+    <div class="room-h"><div><div class="nm">厨房吊柜</div><div class="dm">2.4m×0.8m</div></div><span class="tt">¥17,000</span></div>
+    <div class="items"><div class="item"><span class="l">柜体(PET材质)<span>2.4m×0.8m · 投影3.4m</span></span><span class="v">¥17,000</span></div></div>
+  </div></div>
+    <div class="card" style="margin-top:8px">
+      <div class="trow"><span>项目小计</span><span class="v" id="tSub">¥152,600</span></div>
+      <div class="trow dim"><span id="tDiscLabel">原价</span><span class="v" id="tAfter">¥152,600</span></div>
+      <div class="trow" id="actRow" style="display:none"><span>活动优惠</span><span class="v" style="color:var(--ok)" id="tAct">-¥0</span></div>
+      <div class="trow total"><span>到手价</span><span class="v" id="tFinal">¥152,600</span></div>
+    </div>
+  </div>
+
+  <div class="sec">
+    <div class="selling">
+      <h3>✨ 为什么选择拉迷(标准客户)</h3>
+      <ul><li>空间利用率拉满:每一面墙都变成收纳</li><li>全国联保5年,铰链五金终身维护</li><li>设计上门量尺,3D效果图确认后再生产</li></ul>
+    </div>
+  </div>
+
+  <div class="sec" style="font-size:11px;color:var(--mu);line-height:1.7">
+    计价说明:柜体按投影面积(宽×高)×材质单价;厨房按计费延米(扣除转角重叠与冰箱位)。本报价为AI辅助估算,实际以现场量尺与正式合同为准。质保与赠送以当期活动政策为准。
+  </div>
+</div>
+
+<div class="ctrl">
+  <button class="btn sec" id="btnAdjust">⚖️ 调整报价</button>
+  <button class="btn pri" id="btnSave">📞 锁定到店礼</button>
+</div>
+
+<div class="drawer" id="drawer">
+  <div class="sheet">
+    <h3>⚖️ 销售操作台(客户不可见)</h3>
+    <div class="frow"><label>客户类型</label><div class="opts" id="optTier"><button class="chip " data-v="price">🏷️ 价格型</button><button class="chip on" data-v="standard">🏠 标准型</button><button class="chip " data-v="quality">💎 品质型</button></div></div>
+    <div class="frow"><label>材质档位</label><div class="opts" id="optMat"><button class="chip " data-v="双饰面">双饰面</button><button class="chip on" data-v="PET">PET</button><button class="chip " data-v="混油">混油</button></div></div>
+    <div class="frow"><label>报价浮动 <span id="floatV">15</span>%</label><input type="range" id="floatR" min="10" max="20" step="1" value="15"></div>
+    <div class="frow"><label>折扣</label><div class="opts" id="optDisc">
+      <button class="chip" data-v="1.00">原价</button>
+      <button class="chip" data-v="0.95">95折</button>
+      <button class="chip" data-v="0.90">9折</button>
+      <button class="chip" data-v="0.85">85折</button>
+      <button class="chip" data-v="0.80">8折</button>
+    </div></div>
+    <div class="frow"><label>柜体清单(宽=投影宽 m / 厨房为计费长)</label><div id="cabList"></div>
+      <div style="margin-top:8px"><button class="chip" id="btnAddCab">+ 增加柜体</button></div>
+    </div>
+    <div class="frow"><label>活动优惠</label><div class="opts" id="optAct"></div></div>
+    <button class="btn pri" style="width:100%" id="btnApply">应用并重算</button>
+  </div>
+</div>
+
+<script>
+// ============================================================
+// 拉迷全屋定制 · 默认报价规则库
+// 数据来源:floorplan-quotation/AI快速报价_Demo2.optimizing.html
+// (DEFAULT_RULES / Q_CUSTOMER_TIERS / DEFAULT_QUOTATION_PLANS /
+//   CABINET_EXTRA_TEMPLATES / EXTRA_METHOD_MAP / 厨房扣减规则)
+// 所有价格单位:元。修改价格请改这里的默认值或运行时覆盖 config。
+// ============================================================
+
+const METHOD = {
+  PROJECTION: '按投影面积', // 元/m²  数量 = 宽(m) × 高(m)
+  METER: '按延米',          // 元/m   数量 = 米数
+  COUNT: '按个数',          // 元/个  数量 = 个数
+};
+
+// —— 材质三档单价区间(AI快速粗略报价核心)——
+const PRICE_TIERS = [
+  { name: '双饰面', min: 3000, max: 3500 },
+  { name: 'PET',   min: 4500, max: 5000 },
+  { name: '混油',   min: 6000, max: 7000 },
+];
+const DEFAULT_TIER = 'PET';
+
+// —— 计价方式 ——
+const METHOD_OPTIONS = [
+  { value: '按投影面积', unit: '元/m²' },
+  { value: '按个数', unit: '元/个' },
+  { value: '按延米', unit: '元/m' },
+];
+
+// —— 柜体→空间 映射(识别命名用)——
+const CABINET_TO_SPACE = {
+  '主卧衣柜': '主卧', '次卧衣柜': '次卧', '儿童房衣柜': '儿童房', '老人房衣柜': '老人房',
+  '衣柜': '卧室', '客厅电视柜': '客厅', '电视柜': '客厅', '厨房橱柜': '厨房', '橱柜': '厨房',
+  '开放式厨房': '开放式厨房', '餐厅餐边柜': '餐厅', '餐边柜': '餐厅', '书房书柜': '书房',
+  '书柜': '书房', '玄关鞋柜': '玄关', '鞋柜': '玄关', '玄关柜': '玄关', '阳台柜': '阳台',
+  '衣帽间': '衣帽间', '储物柜': '储物间', '酒柜': '餐厅', '浴室柜': '卫生间', '榻榻米': '次卧',
+  '展示柜': '客厅', '洗衣机柜': '阳台', '卡座': '餐厅', '飘窗柜': '飘窗',
+};
+
+const SPACE_TO_DEFAULT_CABINET = {
+  '主卧': '主卧衣柜', '次卧': '次卧衣柜', '儿童房': '儿童房衣柜', '老人房': '老人房衣柜',
+  '客厅': '客厅电视柜', '厨房': '厨房橱柜', '开放式厨房': '开放式厨房', '餐厅': '餐厅餐边柜',
+  '客餐厅': '餐厅餐边柜', '书房': '书房书柜', '玄关': '玄关鞋柜', '阳台': '阳台柜',
+  '衣帽间': '衣帽间', '储物间': '储物柜', '卫生间': '浴室柜', '主卫': '浴室柜',
+};
+
+// —— 柜体类型 / 布局 / 门板 预设 ——
+const CUSTOM_TYPES_PRESET = {
+  cabinetType: ['主卧衣柜','次卧衣柜','儿童房衣柜','老人房衣柜','客厅电视柜','厨房橱柜','开放式厨房','餐厅餐边柜','书房书柜','玄关鞋柜','阳台柜','衣帽间','储物柜','酒柜','浴室柜','榻榻米'],
+  layoutForm: ['一字型','L型','U型','二字型','不规则'],
+  doorPanelType: ['普通平板','风影门板','PET门板','实木贴皮','烤漆门板','吸塑门板','玻璃门','百叶门','无门板'],
+};
+
+// —— 柜体默认附件模板(识别到柜型后自动配附件)——
+const CABINET_EXTRA_TEMPLATES = {
+  '玄关': ['灯带','变压器+开关','自弹器','见光板','铰链'],
+  '鞋柜': ['灯带','变压器+开关','自弹器','见光板','铰链','换鞋凳'],
+  '电视柜': ['自弹器','见光板','铰链'],
+  '展示柜': ['灯带','变压器+开关','拉手','见光板','铰链','自弹器'],
+  '酒柜': ['灯带','变压器+开关','拉手','见光板','铰链','自弹器','玻璃门升级'],
+  '洗衣机': ['自弹器','护墙板','见光板','铰链'],
+  '阳台': ['自弹器','见光板','铰链'],
+  '餐边': ['抽屉','拉手','自弹器','金属拉手','灯带','变压器+开关','见光板','铰链'],
+  '次卧': ['衣杆','金属拉手','抽屉','见光板','铰链'],
+  '儿童房': ['衣杆','金属拉手','抽屉','见光板','铰链'],
+  '书房': ['衣杆','金属拉手','抽屉','见光板','铰链'],
+  '主卧': ['金属拉手','抽屉','衣杆','自弹器','圆弧','铰链'],
+  '衣帽间': ['金属拉手','抽屉','衣杆','自弹器','圆弧','铰链','裤架领带格','拉篮'],
+  '厨房': ['见光板','碗篮','锅篮','自弹器'],
+  '_default': ['自弹器','见光板','铰链'],
+};
+
+// —— 附件计价方式映射 ——
+const EXTRA_METHOD_MAP = {
+  '见光板': '按投影面积', '护墙板': '按投影面积', '玻璃门升级': '按投影面积',
+  '灯带': '按延米', '衣杆': '按延米', '石材台面': '按延米',
+  '自弹器': '按个数', '铰链': '按个数', '变压器+开关': '按个数', '拉手': '按个数',
+  '金属拉手': '按个数', '抽屉': '按个数', '裤架领带格': '按个数', '拉篮': '按个数',
+  '碗篮': '按个数', '锅篮': '按个数', '圆弧': '按个数', '换鞋凳': '按个数',
+};
+
+// —— 基础价格表(name → {price, unit, method})——
+const PRICE_BOOK = {
+  // 柜体
+  '柜体投影单价':   { price: 1852, unit: '元/m²', method: '按投影面积' },
+  '生态澳松板柜体': { price: 1852, unit: '元/m²', method: '按投影面积' },
+  '铝格栅板柜体':   { price: 2532, unit: '元/m²', method: '按投影面积' },
+  // 厨房(单件计价模式)
+  '地柜':   { price: 2380, unit: '元/m', method: '按延米' },
+  '吊柜':   { price: 1748, unit: '元/m', method: '按延米' },
+  '冰箱吊柜': { price: 2622, unit: '元/m', method: '按延米' },
+  '高柜':   { price: 5950, unit: '元/m', method: '按延米' },
+  '台面':   { price: 1480, unit: '元/m', method: '按延米' },
+  '台面超出部分': { price: 2480, unit: '元/m', method: '按延米' },
+  '碗篮':   { price: 1268, unit: '元/个', method: '按个数' },
+  '锅篮':   { price: 1628, unit: '元/个', method: '按个数' },
+  '切角':   { price: 500, unit: '元/个', method: '按个数' },
+  '台下盆': { price: 500, unit: '元/个', method: '按个数' },
+  '包管':   { price: 500, unit: '元/个', method: '按个数' },
+  // 门板单价
+  '标配门板': { price: 380, unit: '元/m²', method: '按投影面积' },
+  '烤漆门板': { price: 580, unit: '元/m²', method: '按投影面积' },
+  '吸塑门板': { price: 480, unit: '元/m²', method: '按投影面积' },
+  '实木门板': { price: 880, unit: '元/m²', method: '按投影面积' },
+  '铝格栅门板': { price: 680, unit: '元/m²', method: '按投影面积' },
+  'PET门板':  { price: 450, unit: '元/m²', method: '按投影面积' },
+  // 附件
+  '见光板':   { price: 1138, unit: '元/m²', method: '按投影面积' },
+  '护墙板':   { price: 1852, unit: '元/m²', method: '按投影面积' },
+  '玻璃门升级': { price: 1852, unit: '元/m²', method: '按投影面积' },
+  '灯带':     { price: 386, unit: '元/m', method: '按延米' },
+  '变压器+开关': { price: 536, unit: '元/个', method: '按个数' },
+  '自弹器':   { price: 115, unit: '元/个', method: '按个数' },
+  '铰链':     { price: 40, unit: '元/个', method: '按个数' },
+  '拉手':     { price: 226, unit: '元/个', method: '按个数' },
+  '金属拉手': { price: 225, unit: '元/个', method: '按个数' },
+  '抽屉':     { price: 1300, unit: '元/个', method: '按个数' },
+  '衣杆':     { price: 288, unit: '元/m', method: '按延米' },
+  '裤架领带格': { price: 0, unit: '元/个', method: '按个数' },
+  '拉篮':     { price: 0, unit: '元/个', method: '按个数' },
+  '圆弧':     { price: 1560, unit: '元/个', method: '按个数' },
+  '换鞋凳':   { price: 0, unit: '元/个', method: '按个数' },
+  '石材台面': { price: 1480, unit: '元/m', method: '按延米' },
+};
+
+// —— 厨房计价参数(qGetKitchenBillableMeters 同源)——
+const KITCHEN_RULES = {
+  packageMeters: 3,     // 3米基础套餐(单件计价模式的扣减基准)
+  overlapMm: 600,       // 转角重叠扣减
+  fridgeMm: 1000,       // 冰箱位预留
+  typeThresholdM: 2,    // 最小边 ≤2m 判 L 型,>2m 判 U 型
+};
+
+// —— 客户分型(销售策略核心)——
+const CUSTOMER_TIERS = {
+  price: {
+    label: '看重价格型', icon: '🏷️',
+    strategy: '预算少的客户直接按性价比报,快速赢单',
+    defaultPlan: 'economy', defaultDiscount: 0.85,
+    doorType: '标配', hardware: '经济实用款', service: '标准测量+安装',
+    warranty: '3年质保', gifts: '送基础五金配件', panelLabel: '进口爱格板(加价升级)',
+    autoEnableActivity: true,
+    discounts: [
+      { id: 'p1', label: '投影面积85折', rate: 0.85, deletable: false },
+      { id: 'p2', label: '投影面积8折特惠', rate: 0.80, deletable: true },
+      { id: 'p3', label: '团购78折', rate: 0.78, deletable: true },
+    ],
+  },
+  standard: {
+    label: '标准客户', icon: '🏠',
+    strategy: '标准配置报价,正常谈单空间',
+    defaultPlan: 'standard', defaultDiscount: 1.0,
+    doorType: '标配门板', hardware: '国产优质铰链/拉手', service: '标准测量安装',
+    warranty: '5年质保', gifts: '可参与活动优惠', panelLabel: '国产E0级颗粒板',
+    autoEnableActivity: false,
+    discounts: [
+      { id: 's1', label: '原价', rate: 1.00, deletable: false },
+      { id: 's2', label: '投影面积95折', rate: 0.95, deletable: true },
+      { id: 's3', label: '投影面积9折', rate: 0.90, deletable: true },
+    ],
+  },
+  quality: {
+    label: '品质导向型', icon: '💎',
+    strategy: '有预算的客户报高一点,留谈价空间,突出配置与质感',
+    defaultPlan: 'quality', defaultDiscount: 1.0,
+    doorType: '烤漆/吸塑高端门板', hardware: '进口百隆/海蒂诗五金', service: 'VIP专属设计师+金牌安装',
+    warranty: '10年质保+终身维护', gifts: '赠灯带+抽屉升级包', panelLabel: '进口爱格板/可丽芙(推荐)',
+    premiumRate: 1.15,
+    autoEnableActivity: false,
+    discounts: [
+      { id: 'q1', label: '投影面积95折(保利润)', rate: 0.95, deletable: false },
+      { id: 'q2', label: '原价(高端不打折)', rate: 1.00, deletable: true },
+      { id: 'q3', label: 'VIP专享92折', rate: 0.92, deletable: true },
+    ],
+  },
+};
+
+// —— 报价方案 ——
+const QUOTATION_PLANS = [
+  { id: 'economy',  name: '经济型方案', description: '适合预算敏感型客户,主打性价比', customerTier: 'price',    tierName: '双饰面', floatPct: 10, discountRate: 0.98 },
+  { id: 'standard', name: '标准型方案', description: '默认方案,标准配置,适合大多数客户', customerTier: 'standard', tierName: 'PET',    floatPct: 15, discountRate: 1.00 },
+  { id: 'quality',  name: '品质型方案', description: '高端配置,适合追求品质的客户', customerTier: 'quality',   tierName: '混油',   floatPct: 15, discountRate: 1.00 },
+];
+
+// —— 营销活动模块(可勾选叠加)——
+const ACTIVITY_MODULES = [
+  { id: 'deposit5000', type: 'deposit', name: '定金膨胀(5千档)', desc: '交¥5,000→膨胀¥8,000 + ¥500抵用券', amount: 3500, minOrder: 30000 },
+  { id: 'deposit10000', type: 'deposit', name: '定金膨胀(1万档)', desc: '交¥10,000→膨胀¥13,000 + ¥800抵用券', amount: 3800, minOrder: 30000 },
+  { id: 'coupon20k', type: 'coupon', name: '满减券', desc: '满¥20,000减¥1,000', amount: 1000, threshold: 20000 },
+  { id: 'cashback', type: 'tiered_cashback', name: '阶梯返现', tiers: [
+      { min: 50000, cash: 2000 }, { min: 100000, cash: 5000 }, { min: 150000, cash: 8000 },
+    ] },
+];
+
+// —— 默认层高与房间面积先验(识别失败兜底估算用)——
+const DEFAULTS = {
+  height: 2.8, // 默认柜高(层高)
+  roomAreaPriors: { // 常见房间典型面积范围 [min, max](m²)
+    '主卧': [12, 22], '次卧': [8, 16], '儿童房': [8, 14], '老人房': [9, 15], '书房': [6, 12],
+    '客厅': [18, 35], '餐厅': [8, 18], '厨房': [4.5, 10], '卫生间': [2.5, 6], '主卫': [3, 6],
+    '公卫': [2.5, 6], '阳台': [3, 10], '玄关': [2, 6], '衣帽间': [3, 10], '储物间': [1.5, 5],
+  },
+  cabinetWallRatio: { // 柜体默认占所在墙面比例(无明确尺寸时的估算)
+    '主卧衣柜': 0.55, '次卧衣柜': 0.50, '儿童房衣柜': 0.50, '玄关鞋柜': 0.6,
+    '客厅电视柜': 0.55, '餐厅餐边柜': 0.5, '阳台柜': 0.6, '书房书柜': 0.5,
+  },
+};
+
+// —— 赢单文案素材(业主视角价值描述)——
+const SELLING_POINTS = {
+  price: [
+    '按投影面积计价,明明白白,没有隐形收费',
+    'E0级环保板材,装完即可安心入住',
+    '工厂直供价,同样的预算多做一个柜子',
+  ],
+  standard: [
+    '空间利用率拉满:每一面墙都变成收纳',
+    '全国联保5年,铰链五金终身维护',
+    '设计上门量尺,3D效果图确认后再生产',
+  ],
+  quality: [
+    '进口板材+进口五金,用20年不变形不褪色',
+    'VIP设计师一对一,全屋风格统一设计',
+    '10年质保+终身维护,一次装修终身省心',
+  ],
+};
+
+// ============================================================
+// 拉迷全屋定制 · 报价引擎(纯函数,浏览器/CLI 共用)
+// 两种模式:
+//   quick  — AI快速粗略报价:投影面积/厨房延米 × 三档材质区间单价
+//   detail — 明细报价:Excel 预算表模型(柜体+门板补差+附件逐项)
+// 销售策略:客户分型 → 方案/折扣/浮动 → 活动叠加 → 谈价区间
+// ============================================================
+
+const r2 = (v) => Math.round(v * 100) / 100;
+const num = (v, d = 0) => { const n = parseFloat(v); return Number.isFinite(n) ? n : d; };
+
+// ---------- 材质档位 ----------
+function getTier(name) {
+  const t = PRICE_TIERS.find((x) => x.name === (name || DEFAULT_TIER));
+  return t || PRICE_TIERS[1];
+}
+// 浮动百分比(10~15)在区间内线性插值 → 单价(qGetAiUnitPrice 同源逻辑)
+function tierUnitPrice(tier, floatPct = 15) {
+  const k = Math.min(1, Math.max(0, (num(floatPct, 15) - 10) / 5));
+  return r2(tier.min + k * (tier.max - tier.min));
+}
+
+// ---------- 厨房计费米数(qGetKitchenBillableMeters 同源)----------
+function kitchenBillable(walls = [], rules = {}) {
+  const R = { ...KITCHEN_RULES, ...rules };
+  const ws = (Array.isArray(walls) ? walls : []).map((v) => num(v)).filter((v) => v > 0);
+  if (!ws.length) return null;
+  const total = ws.reduce((a, b) => a + b, 0);
+  const minWall = Math.min(...ws);
+  const maxWall = Math.max(...ws);
+  const type = minWall <= R.typeThresholdM ? 'L' : 'U';
+  const overlapCount = type === 'L' ? 1 : 2;
+  const usedLen = type === 'L' ? maxWall + minWall : maxWall + maxWall + minWall;
+  const billable = Math.max(0, usedLen - (overlapCount * R.overlapMm) / 1000 - R.fridgeMm / 1000);
+  return {
+    walls: ws, total: r2(total), minWall: r2(minWall), maxWall: r2(maxWall),
+    kitchenType: type, overlapCount, fridgeMm: R.fridgeMm, usedLen: r2(usedLen), billable: r2(billable),
+  };
+}
+const isKitchen = (name = '') => /厨房|橱柜/.test(String(name));
+
+// ---------- 附件数量启发式(Excel 模型总结,均可编辑) ----------
+function estimateExtras(cabinet) {
+  const w = num(cabinet.width, 1.5);
+  const h = num(cabinet.height, DEFAULTS.height);
+  const type = String(cabinet.type || '');
+  let tplKey = Object.keys(CABINET_EXTRA_TEMPLATES).find((k) => k !== '_default' && type.includes(k));
+  if (!tplKey && isKitchen(type)) tplKey = '厨房';
+  const names = CABINET_EXTRA_TEMPLATES[tplKey || '_default'] || CABINET_EXTRA_TEMPLATES._default;
+  const sections = h > 1.6 ? 2 : 1;               // 高柜分上下段
+  const doorsPerSection = Math.max(1, Math.round(w / 0.45));
+  const doors = sections * doorsPerSection;
+  const hingePerDoor = h > 2 ? 3 : 2;
+  const out = [];
+  for (const name of names) {
+    const pb = PRICE_BOOK[name];
+    if (!pb) continue;
+    let qty = 0; let note = '';
+    switch (name) {
+      case '铰链': qty = doors * hingePerDoor; note = `${doors}门×${hingePerDoor}铰链`; break;
+      case '自弹器': qty = Math.max(1, doors - 1); note = '反弹门配'; break;
+      case '见光板': qty = 1; note = '单侧见光(0.5m宽)'; break;
+      case '灯带': qty = r2(w * 2); note = '顶底双层'; break;
+      case '变压器+开关': qty = 1; note = 'LED电源'; break;
+      case '衣杆': qty = r2(w); note = '通长挂杆'; break;
+      case '抽屉': qty = type.includes('主卧') || type.includes('衣帽') ? 3 : 2; break;
+      case '金属拉手': case '拉手': qty = doors + 2; break;
+      case '碗篮': case '锅篮': qty = 1; break;
+      default: qty = name === '玻璃门升级' || name === '护墙板' ? 0 : 1;
+    }
+    if (qty > 0) out.push({ name, method: pb.method, unit: pb.unit, unitPrice: pb.price, qty, note });
+  }
+  return out;
+}
+
+// ---------- 单柜体计价 ----------
+// 计价数量:投影面积 = 宽×高;厨房 = 计费延米
+function cabinetQty(cabinet, rules = {}) {
+  if (isKitchen(cabinet.type)) {
+    const kb = kitchenBillable(cabinet.walls || [cabinet.width, cabinet.height, cabinet.width, cabinet.height], rules);
+    return { qty: kb ? kb.billable : num(cabinet.width), unit: 'm', kb };
+  }
+  return { qty: r2(num(cabinet.width) * num(cabinet.height)), unit: 'm²', kb: null };
+}
+
+// 快速模式:单柜体一行
+function quickLine(cabinet, { tierName, floatPct = 15, rules } = {}) {
+  const tier = getTier(tierName);
+  const U = tierUnitPrice(tier, floatPct);
+  const { qty, unit, kb } = cabinetQty(cabinet, rules);
+  return {
+    mode: 'quick', name: cabinet.type, tierName: tier.name,
+    qty, unit, unitPrice: U, total: r2(qty * U), kb,
+    dims: num(cabinet.width) && num(cabinet.height) ? `${num(cabinet.width)}m×${num(cabinet.height)}m` : '',
+  };
+}
+
+// 明细模式:单柜体 → Excel 风格明细行
+function detailLines(cabinet, opts = {}) {
+  const { doorDiffPrice = PRICE_BOOK['柜体投影单价'].price > 0 ? 538 : 538, rules, includeDoorDiff = true } = opts;
+  const lines = [];
+  const isK = isKitchen(cabinet.type);
+  const { qty } = cabinetQty(cabinet, rules);
+  if (isK) {
+    // 厨房单件计价(Excel 模式:地柜+吊柜+台面+功能五金)
+    const walls = cabinet.walls || [cabinet.width, cabinet.height, cabinet.width, cabinet.height];
+    const straight = Math.max(0, ...walls); // 直线段米数近似
+    for (const [name, factor, note] of [
+      ['地柜', 1, '沿墙地柜'], ['吊柜', 1, '沿墙吊柜'], ['台面', 1, '石英石台面'],
+    ]) {
+      const pb = PRICE_BOOK[name];
+      lines.push({ name, qty: r2(qty), unit: 'm', unitPrice: pb.price, total: r2(qty * pb.price), note, method: pb.method });
+    }
+    for (const name of ['碗篮', '锅篮']) {
+      const pb = PRICE_BOOK[name];
+      lines.push({ name, qty: 1, unit: '个', unitPrice: pb.price, total: pb.price, note: '标准配置', method: pb.method });
+    }
+    return { cabinet, isKitchen: true, lines, subtotal: r2(lines.reduce((s, l) => s + l.total, 0)) };
+  }
+  const pb = PRICE_BOOK['生态澳松板柜体'];
+  lines.push({ name: '柜体', qty, unit: 'm²', unitPrice: pb.price, total: r2(qty * pb.price), note: '生态澳松板', method: pb.method });
+  if (includeDoorDiff && doorDiffPrice > 0) {
+    lines.push({ name: '板门补差价', qty, unit: 'm²', unitPrice: doorDiffPrice, total: r2(qty * doorDiffPrice), note: '双饰面门补差', method: pb.method });
+  }
+  for (const ex of estimateExtras(cabinet)) {
+    const total = ex.method === '按投影面积' ? r2(ex.qty * 0.5 * num(cabinet.height) * ex.unitPrice)
+      : ex.method === '按延米' ? r2(ex.qty * ex.unitPrice)
+      : r2(ex.qty * ex.unitPrice);
+    lines.push({ name: ex.name, qty: ex.qty, unit: ex.unit, unitPrice: ex.unitPrice, total, note: ex.note, method: ex.method });
+  }
+  return { cabinet, isKitchen: false, lines, subtotal: r2(lines.reduce((s, l) => s + l.total, 0)) };
+}
+
+// ---------- 报价主入口 ----------
+// inputs: { cabinets:[{type,width,height,walls?,excluded?}], tier, floatPct, discount, tierKey(客户分型), planId, mode }
+function buildQuote(input = {}) {
+  const {
+    cabinets = [], mode = 'quick',
+    tierName = input.tier || DEFAULT_TIER,
+    floatPct = 15, discount = null, tierKey = 'standard', planId = null,
+    rules = {}, extras = [], // extras: [{name, room, qty, unitPrice}] 手动增删项
+    clientName = '', note = '',
+  } = input;
+
+  const plan = QUOTATION_PLANS.find((p) => p.id === (planId || CUSTOMER_TIERS[tierKey]?.defaultPlan)) || QUOTATION_PLANS[1];
+  const custTier = CUSTOMER_TIERS[tierKey] || CUSTOMER_TIERS.standard;
+  const effDiscount = discount != null ? num(discount) : (custTier.defaultDiscount ?? plan.discountRate ?? 1);
+
+  const rooms = cabinets
+    .filter((c) => !c.excluded)
+    .map((c, i) => {
+      const base = mode === 'detail' ? detailLines(c, { rules }) : { ...quickLine(c, { tierName, floatPct, rules }), lines: null };
+      const id = c.id || `cab_${i + 1}`;
+      return { id, type: c.type, dims: base.dims || `${num(c.width)}m×${num(c.height)}m`, ...base };
+    });
+
+  const itemsSubtotal = r2(rooms.reduce((s, r) => s + (r.subtotal ?? r.total), 0));
+  const manualExtras = extras.map((e, i) => ({
+    id: e.id || `ext_${i + 1}`, name: e.name, qty: num(e.qty, 1), unit: e.unit || '项',
+    unitPrice: num(e.unitPrice), total: r2(num(e.qty, 1) * num(e.unitPrice)), kind: e.kind || 'add',
+  }));
+  const extrasTotal = r2(manualExtras.reduce((s, e) => s + (e.kind === 'remove' ? -e.total : e.total), 0));
+
+  const premiumRate = custTier.premiumRate || 1;
+  const afterPremium = r2((itemsSubtotal + extrasTotal) * premiumRate);
+  const afterDiscount = r2(afterPremium * effDiscount);
+
+  // 活动叠加(自动/手动)
+  const acts = (input.activities || (custTier.autoEnableActivity ? ACTIVITY_MODULES.map((m) => m.id) : []))
+    .map((id) => ACTIVITY_MODULES.find((m) => m.id === id)).filter(Boolean);
+  let activityTotal = 0;
+  const activityLines = [];
+  for (const m of acts) {
+    let amt = 0; let desc = m.desc || m.name;
+    if (m.type === 'tiered_cashback') {
+      const hit = [...(m.tiers || [])].sort((a, b) => b.min - a.min).find((t) => afterDiscount >= t.min);
+      if (!hit) continue;
+      amt = hit.cash; desc = `满¥${hit.min.toLocaleString()}返¥${hit.cash.toLocaleString()}`;
+    } else if (m.type === 'coupon' && afterDiscount < (m.threshold || 0)) continue;
+    else if (m.type === 'deposit' && afterDiscount < (m.minOrder || 30000)) continue;
+    else amt = m.amount || 0;
+    activityTotal += amt;
+    activityLines.push({ id: m.id, name: m.name, desc, amount: amt });
+  }
+  const finalPrice = Math.max(0, r2(afterDiscount - activityTotal));
+
+  // 谈价区间(浮动率)
+  const rate = num(floatPct, 15) / 100;
+  const range = { low: Math.round(finalPrice * (1 - rate)), high: Math.round(finalPrice * (1 + rate)) };
+
+  // 业主视角价值点
+  const selling = SELLING_POINTS[tierKey] || SELLING_POINTS.standard;
+
+  return {
+    meta: {
+      clientName, note: note || custTier.strategy, mode,
+      tierName: mode === 'quick' ? getTier(tierName).name : undefined,
+      floatPct, planId: plan.id, planName: plan.name,
+      customerTier: tierKey, customerLabel: custTier.label,
+      discount: effDiscount, premiumRate: premiumRate !== 1 ? premiumRate : undefined,
+      generatedAt: new Date().toISOString(),
+    },
+    rooms, manualExtras, activityLines,
+    totals: {
+      itemsSubtotal, extrasTotal, premiumRate, afterPremium, discount: effDiscount,
+      afterDiscount, activityTotal, finalPrice, range,
+      discountLabel: custTier.discounts?.find((d) => Math.abs(d.rate - effDiscount) < 0.001)?.label || `折扣${Math.round(effDiscount * 100)}%`,
+    },
+    selling,
+    config: { tierKey, tierName, floatPct, discount: effDiscount, planId: plan.id, mode },
+  };
+}
+
+// ---------- 语义化调整应用(销售边聊边改) ----------
+// ops: [{op:'add'|'remove'|'resize'|'rename'|'price', target, ...}]
+function applyOps(cabinets, ops = []) {
+  const out = cabinets.map((c) => ({ ...c }));
+  for (const op of ops) {
+    if (op.op === 'add') {
+      out.push({ type: op.type || op.name || '柜体', width: num(op.width, 1.5), height: num(op.height, DEFAULTS.height), from: '语音新增' });
+    } else if (op.op === 'remove') {
+      const i = out.findIndex((c) => c.type === op.target);
+      if (i >= 0) out.splice(i, 1);
+    } else if (op.op === 'resize') {
+      const c = out.find((c) => c.type === op.target);
+      if (c) { if (op.width) c.width = num(op.width); if (op.height) c.height = num(op.height); c.from = '手动调整'; }
+    } else if (op.op === 'rename') {
+      const c = out.find((c) => c.type === op.target);
+      if (c) c.type = op.to || c.type;
+    } else if (op.op === 'exclude') {
+      const c = out.find((c) => c.type === op.target);
+      if (c) c.excluded = true;
+    } else if (op.op === 'include') {
+      const c = out.find((c) => c.type === op.target);
+      if (c) delete c.excluded;
+    }
+  }
+  return out;
+}
+
+// 授权范围内校验(浮动区间即授权边界)
+function withinAuthority(quote, price) {
+  const { range } = quote.totals;
+  return price >= range.low && price <= range.high;
+}
+
+var __DEFAULT__ = { buildQuote, applyOps, quickLine, detailLines, kitchenBillable, tierUnitPrice, getTier, withinAuthority };
+
+window.__Q__ = {"cabinets":[{"id":"cab_1","type":"玄关鞋柜","width":1.2,"height":2.6,"rawName":"玄关鞋柜"},{"id":"cab_2","type":"主卧衣柜","width":2.6,"height":2.6,"rawName":"主卧衣柜"},{"id":"cab_3","type":"次卧衣柜","width":2.2,"height":2.6,"rawName":"次卧衣柜"},{"id":"cab_4","type":"书房书柜","width":1.8,"height":2.6,"rawName":"书房书柜"},{"id":"cab_5","type":"阳台柜","width":1.5,"height":2.6,"rawName":"阳台柜"},{"id":"cab_6","type":"餐厅餐边柜","width":1.6,"height":2.6,"rawName":"餐厅餐边柜"},{"id":"cab_7","type":"厨房吊柜","height":2.6,"rawName":"厨房吊柜"}],"config":{"tierKey":"standard","tierName":"PET","floatPct":15,"discount":1,"planId":"standard","mode":"quick"},"salesNote":"三房两厅新居,孩子明年上小学,看重收纳和环保,预算25万内想多谈点优惠"};
+console.log('fpc engine ready');
+</script>
+<script>
+(function(){
+  var Q = window.__Q__; var S = document; var money = function(n){ return '¥' + Math.round(n||0).toLocaleString('zh-CN'); };
+  var cfg = Object.assign({ tierKey:'standard', tierName:'PET', floatPct:15, discount:1.0, mode:'quick' }, Q.config||{});
+  function calc(){ return buildQuote({ cabinets: Q.cabinets, tierKey: cfg.tierKey, tierName: cfg.tierName, floatPct: cfg.floatPct, discount: cfg.discount, mode: cfg.mode, clientName: "缪女士(时代之城)" }); }
+  function refresh(){
+    var q = calc(); var t = q.totals;
+    S.getElementById('finalPrice').textContent = money(t.finalPrice);
+    S.getElementById('finalRange').textContent = '谈价参考区间 ' + money(t.range.low) + ' ~ ' + money(t.range.high);
+    S.getElementById('tSub').textContent = money(t.itemsSubtotal);
+    S.getElementById('tDiscLabel').textContent = t.discountLabel;
+    S.getElementById('tAfter').textContent = money(t.afterDiscount);
+    S.getElementById('tAct').textContent = '-' + money(t.activityTotal);
+    S.getElementById('tFinal').textContent = money(t.finalPrice);
+    S.getElementById('bTier').textContent = (q.meta.tierName||'') + '材质';
+    S.getElementById('bDisc').textContent = t.discountLabel;
+  }
+  // room expand
+  S.querySelectorAll('.room-h').forEach(function(el){ el.addEventListener('click', function(){ el.parentElement.classList.toggle('open'); }); });
+  // chips
+  function bindChips(id, fn){ var box = S.getElementById(id); if(!box) return; box.addEventListener('click', function(e){ var b = e.target.closest('.chip'); if(!b) return; box.querySelectorAll('.chip').forEach(function(x){x.classList.remove('on');}); b.classList.add('on'); fn(b.dataset.v); }); }
+  bindChips('optTier', function(v){ cfg.tierKey = v; refresh(); });
+  bindChips('optMat', function(v){ cfg.tierName = v; refresh(); });
+  bindChips('optDisc', function(v){ cfg.discount = parseFloat(v); refresh(); });
+  var fr = S.getElementById('floatR'); fr.addEventListener('input', function(){ S.getElementById('floatV').textContent = fr.value; cfg.floatPct = parseInt(fr.value); refresh(); });
+  // drawer
+  var d = S.getElementById('drawer');
+  S.getElementById('btnAdjust').addEventListener('click', function(){ d.classList.add('open'); renderCabs(); });
+  d.addEventListener('click', function(e){ if(e.target === d) d.classList.remove('open'); });
+  function renderCabs(){
+    var box = S.getElementById('cabList'); box.innerHTML = '';
+    Q.cabinets.forEach(function(c, i){
+      var row = S.createElement('div'); row.className = 'cabrow';
+      var dims = String(c.width||'') ? String(c.width) : '';
+      row.innerHTML = '<span style="flex:1">' + c.rawName + '</span>' +
+        '<input type="number" step="0.05" value="' + (c.width||1.5) + '" data-i="' + i + '" data-f="width" title="宽(米)">' +
+        '<span class="del" data-i="' + i + '">✕</span>';
+      box.appendChild(row);
+    });
+    box.onclick = function(e){
+      var del = e.target.closest('.del');
+      if (del) { Q.cabinets.splice(parseInt(del.dataset.i),1); renderCabs(); refresh(); return; }
+      var inp = e.target.closest('input');
+      if (inp) { inp.addEventListener('change', function(){ Q.cabinets[parseInt(inp.dataset.i)].width = parseFloat(inp.value)||1.5; refresh(); }); }
+    };
+  }
+  S.getElementById('btnAddCab').addEventListener('click', function(){ Q.cabinets.push({ type:'储物柜', rawName:'储物柜', width:1.5, height:2.6 }); renderCabs(); refresh(); });
+  S.getElementById('btnSave').addEventListener('click', function(){
+    alert('已记录!销售将带着这份报价与您确认到店时间,到店凭此单领取到店礼。');
+  });
+  refresh();
+})();
+</script>
+</body>
+</html>

+ 792 - 0
skill/out/报价单-缪女士(时代之城)-08312321.html

@@ -0,0 +1,792 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+<head>
+<meta charset="UTF-8">
+<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
+<meta name="format-detection" content="telephone=no">
+<title>缪女士(时代之城) · 全屋定制报价方案</title>
+<style>
+:root{--p:#1F497D;--pa:#2B6CB0;--bg:#F5F7FA;--card:#fff;--bd:#E2E8F0;--tx:#2D3748;--mu:#718096;--ok:#38A169;--wa:#D69E2E;--dg:#E53E3E;--r:12px}
+*{box-sizing:border-box;margin:0;padding:0;-webkit-tap-highlight-color:transparent}
+body{font-family:-apple-system,BlinkMacSystemFont,"PingFang SC","Microsoft YaHei",sans-serif;background:var(--bg);color:var(--tx);line-height:1.55;padding-bottom:calc(88px + env(safe-area-inset-bottom))}
+header{background:linear-gradient(135deg,#1F497D,#2B6CB0);color:#fff;padding:18px 16px 14px}
+header h1{font-size:17px;font-weight:700}
+header .sub{font-size:12px;opacity:.85;margin-top:2px}
+.wrap{padding:12px;max-width:640px;margin:0 auto}
+.hero{background:var(--card);border-radius:var(--r);padding:16px;box-shadow:0 1px 3px rgba(0,0,0,.08);margin-top:-26px;position:relative;text-align:center}
+.hero .label{font-size:12px;color:var(--mu)}
+.hero .price{font-size:38px;font-weight:800;color:var(--p);letter-spacing:-1px;margin:2px 0}
+.hero .price small{font-size:16px;font-weight:600}
+.hero .range{font-size:13px;color:var(--mu)}
+.badge{display:inline-block;font-size:11px;padding:2px 10px;border-radius:20px;background:#EBF8FF;color:var(--pa);border:1px solid #BEE3F8;margin-top:6px}
+.sec{margin-top:14px}
+.sec h2{font-size:15px;font-weight:700;margin:0 2px 8px;display:flex;align-items:center;gap:6px}
+.card{background:var(--card);border-radius:var(--r);box-shadow:0 1px 3px rgba(0,0,0,.06);overflow:hidden}
+.room{border-bottom:1px solid var(--bd)}
+.room:last-child{border-bottom:none}
+.room-h{padding:12px 14px;display:flex;justify-content:space-between;align-items:center;gap:8px}
+.room-h .nm{font-weight:700;font-size:14px}
+.room-h .dm{font-size:11px;color:var(--mu)}
+.room-h .tt{font-weight:800;color:var(--p);font-size:15px;white-space:nowrap}
+.room-h .conf{font-size:10px;padding:1px 6px;border-radius:8px;background:#FFFAF0;color:#C05621;border:1px solid #FBD38D;flex-shrink:0}
+.items{padding:0 14px 10px;display:none}
+.room.open .items{display:block}
+.item{display:flex;justify-content:space-between;font-size:12.5px;padding:5px 0;color:#4A5568}
+.item .l span{color:var(--mu);font-size:11px;margin-left:6px}
+.item .v{white-space:nowrap}
+.trow{display:flex;justify-content:space-between;padding:8px 14px;font-size:13px}
+.trow .v{font-weight:700}
+.trow.total{background:#F0FFF4;color:#276749;font-weight:800;font-size:15px}
+.trow.dim{color:var(--mu);font-size:12px}
+.plan-wrap{background:var(--card);border-radius:var(--r);padding:10px;box-shadow:0 1px 3px rgba(0,0,0,.06)}
+.plan-wrap svg,.plan-img{width:100%;height:auto;display:block;border-radius:8px}
+.plan-note{font-size:11px;color:var(--mu);text-align:center;margin-top:6px}
+.selling{background:linear-gradient(135deg,#FFFAF0,#FFF5EB);border:1px solid #FBD38D;border-radius:var(--r);padding:12px 14px}
+.selling li{font-size:13px;margin:6px 0 6px 18px;color:#744210}
+.selling h3{font-size:13px;color:#C05621;margin-bottom:4px}
+.acts .item .v{color:var(--ok)}
+.ctrl{position:fixed;bottom:0;left:0;right:0;background:#fff;border-top:1px solid var(--bd);padding:10px 14px calc(10px + env(safe-area-inset-bottom));display:flex;gap:8px;z-index:99;max-width:640px;margin:0 auto}
+.btn{flex:1;padding:12px;border:none;border-radius:10px;font-size:14px;font-weight:700;cursor:pointer}
+.btn.pri{background:var(--p);color:#fff}
+.btn.sec{background:#EDF2F7;color:var(--tx)}
+details.dims{background:var(--card);border-radius:var(--r);padding:10px 14px;font-size:12px;color:var(--mu)}
+details.dims summary{cursor:pointer;font-weight:600;color:var(--tx)}
+details.dims li{margin:4px 0 4px 18px}
+.drawer{position:fixed;inset:0;background:rgba(0,0,0,.4);z-index:100;display:none}
+.drawer.open{display:block}
+.sheet{position:absolute;bottom:0;left:0;right:0;background:#fff;border-radius:16px 16px 0 0;max-height:82vh;overflow:auto;padding:16px 16px calc(20px + env(safe-area-inset-bottom));max-width:640px;margin:0 auto}
+.sheet h3{font-size:15px;margin-bottom:12px}
+.frow{margin-bottom:14px}
+.frow label{font-size:12px;color:var(--mu);display:block;margin-bottom:4px}
+.frow .opts{display:flex;gap:6px;flex-wrap:wrap}
+.chip{padding:8px 12px;border-radius:8px;border:1.5px solid var(--bd);background:#fff;font-size:13px;cursor:pointer}
+.chip.on{border-color:var(--pa);background:#EBF8FF;color:var(--pa);font-weight:700}
+input[type=range]{width:100%}
+.cabrow{display:flex;align-items:center;gap:8px;padding:8px 0;border-bottom:1px dashed var(--bd);font-size:13px}
+.cabrow input{width:64px;padding:6px;border:1px solid var(--bd);border-radius:6px;font-size:13px}
+.cabrow .del{color:var(--dg);font-size:18px;padding:0 6px;cursor:pointer}
+.cabrow .add{color:var(--ok)}
+@media(min-width:640px){.hero .price{font-size:44px}}
+</style>
+</head>
+<body>
+<header>
+  <h1>缪女士(时代之城) · 全屋定制报价方案</h1>
+  <div class="sub">标准客户方案 · 生成于 8/31 23:21 · 拉迷全屋定制</div>
+</header>
+<div class="wrap">
+
+  <div class="hero">
+    <div class="label">全屋定制整装价</div>
+    <div class="price" id="finalPrice">¥144,000</div>
+    <div class="range" id="finalRange">谈价参考区间 ¥122,400 ~ ¥165,600</div>
+    <div id="badges">
+      <span class="badge" id="bTier">PET材质</span>
+      <span class="badge" id="bDisc">原价</span>
+      
+    </div>
+  </div>
+
+  <div class="sec"><div class="selling"><h3>💡 给销售的建议(标准客户)</h3><ul><li>三房两厅新居,孩子明年上小学,看重收纳和环保,预算25万内想多谈点优惠</li></ul></div></div>
+
+  <div class="sec">
+    <h2>🏠 户型与定制布局</h2>
+    <div class="plan-wrap" id="planWrap"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 2888 2458" class="fpc-plan">
+<g id="fpc-rooms">
+<g class="fpc-room" data-room="玄关">
+<rect x="960.0" y="880.0" width="500.0" height="360.0" fill="#F7FAFC" stroke="#2D3748" stroke-width="6"/>
+<line x1="1120.0" y1="1240.0" x2="1210.0" y2="1240.0" stroke="#fff" stroke-width="8"/>
+<text x="1210.0" y="1060.0" text-anchor="middle" dominant-baseline="middle" font-size="52" fill="#2D3748" font-weight="600">玄关<tspan x="1210.0" dy="60" font-size="40" fill="#718096">4m²</tspan></text>
+</g>
+<g class="fpc-room" data-room="厨房">
+<rect x="960.0" y="120.0" width="612.0" height="440.0" fill="#F7FAFC" stroke="#2D3748" stroke-width="6"/>
+<line x1="1176.0" y1="560.0" x2="1266.0" y2="560.0" stroke="#fff" stroke-width="8"/>
+<text x="1266.0" y="340.0" text-anchor="middle" dominant-baseline="middle" font-size="52" fill="#2D3748" font-weight="600">厨房<tspan x="1266.0" dy="60" font-size="40" fill="#718096">6m²</tspan></text>
+</g>
+<g class="fpc-room" data-room="餐厅">
+<rect x="960.0" y="880.0" width="790.0" height="570.0" fill="#F7FAFC" stroke="#2D3748" stroke-width="6"/>
+<line x1="1265.0" y1="1450.0" x2="1355.0" y2="1450.0" stroke="#fff" stroke-width="8"/>
+<text x="1355.0" y="1165.0" text-anchor="middle" dominant-baseline="middle" font-size="52" fill="#2D3748" font-weight="600">餐厅<tspan x="1355.0" dy="60" font-size="40" fill="#718096">10m²</tspan></text>
+</g>
+<g class="fpc-room" data-room="客厅">
+<rect x="1800.0" y="1640.0" width="968.0" height="698.0" fill="#F7FAFC" stroke="#2D3748" stroke-width="6"/>
+<line x1="2194.0" y1="2338.0" x2="2284.0" y2="2338.0" stroke="#fff" stroke-width="8"/>
+<text x="2284.0" y="1989.0" text-anchor="middle" dominant-baseline="middle" font-size="52" fill="#2D3748" font-weight="600">客厅<tspan x="2284.0" dy="60" font-size="40" fill="#718096">15m²</tspan></text>
+</g>
+<g class="fpc-room" data-room="主卧">
+<rect x="120.0" y="1640.0" width="936.0" height="674.0" fill="#F7FAFC" stroke="#2D3748" stroke-width="6"/>
+<line x1="498.0" y1="2314.0" x2="588.0" y2="2314.0" stroke="#fff" stroke-width="8"/>
+<text x="588.0" y="1977.0" text-anchor="middle" dominant-baseline="middle" font-size="52" fill="#2D3748" font-weight="600">主卧<tspan x="588.0" dy="60" font-size="40" fill="#718096">14m²</tspan></text>
+</g>
+<g class="fpc-room" data-room="书房">
+<rect x="960.0" y="1640.0" width="708.0" height="510.0" fill="#F7FAFC" stroke="#2D3748" stroke-width="6"/>
+<line x1="1224.0" y1="2150.0" x2="1314.0" y2="2150.0" stroke="#fff" stroke-width="8"/>
+<text x="1314.0" y="1895.0" text-anchor="middle" dominant-baseline="middle" font-size="52" fill="#2D3748" font-weight="600">书房<tspan x="1314.0" dy="60" font-size="40" fill="#718096">8m²</tspan></text>
+</g>
+<g class="fpc-room" data-room="老人房">
+<rect x="960.0" y="880.0" width="708.0" height="510.0" fill="#F7FAFC" stroke="#2D3748" stroke-width="6"/>
+<line x1="1224.0" y1="1390.0" x2="1314.0" y2="1390.0" stroke="#fff" stroke-width="8"/>
+<text x="1314.0" y="1135.0" text-anchor="middle" dominant-baseline="middle" font-size="52" fill="#2D3748" font-weight="600">老人房<tspan x="1314.0" dy="60" font-size="40" fill="#718096">8m²</tspan></text>
+</g>
+<g class="fpc-room" data-room="卫生间(公卫)">
+<rect x="960.0" y="120.0" width="500.0" height="360.0" fill="#F7FAFC" stroke="#2D3748" stroke-width="6"/>
+<line x1="1120.0" y1="480.0" x2="1210.0" y2="480.0" stroke="#fff" stroke-width="8"/>
+<text x="1210.0" y="300.0" text-anchor="middle" dominant-baseline="middle" font-size="52" fill="#2D3748" font-weight="600">卫生间(公卫)<tspan x="1210.0" dy="60" font-size="40" fill="#718096">4m²</tspan></text>
+</g>
+<g class="fpc-room" data-room="卫生间(主卫)">
+<rect x="960.0" y="880.0" width="500.0" height="360.0" fill="#F7FAFC" stroke="#2D3748" stroke-width="6"/>
+<line x1="1120.0" y1="1240.0" x2="1210.0" y2="1240.0" stroke="#fff" stroke-width="8"/>
+<text x="1210.0" y="1060.0" text-anchor="middle" dominant-baseline="middle" font-size="52" fill="#2D3748" font-weight="600">卫生间(主卫)<tspan x="1210.0" dy="60" font-size="40" fill="#718096">4m²</tspan></text>
+</g>
+<g class="fpc-room" data-room="生活阳台">
+<rect x="960.0" y="1640.0" width="500.0" height="360.0" fill="#F7FAFC" stroke="#2D3748" stroke-width="6"/>
+<line x1="1120.0" y1="2000.0" x2="1210.0" y2="2000.0" stroke="#fff" stroke-width="8"/>
+<text x="1210.0" y="1820.0" text-anchor="middle" dominant-baseline="middle" font-size="52" fill="#2D3748" font-weight="600">生活阳台<tspan x="1210.0" dy="60" font-size="40" fill="#718096">4m²</tspan></text>
+</g>
+<g class="fpc-room" data-room="景观阳台">
+<rect x="1800.0" y="1640.0" width="560.0" height="402.0" fill="#F7FAFC" stroke="#2D3748" stroke-width="6"/>
+<line x1="1990.0" y1="2042.0" x2="2080.0" y2="2042.0" stroke="#fff" stroke-width="8"/>
+<text x="2080.0" y="1841.0" text-anchor="middle" dominant-baseline="middle" font-size="52" fill="#2D3748" font-weight="600">景观阳台<tspan x="2080.0" dy="60" font-size="40" fill="#718096">5m²</tspan></text>
+</g>
+</g>
+<g id="fpc-cabinets">
+<g class="fpc-cab" data-cab="主卧衣柜">
+<rect x="320.0" y="320.0" width="480.0" height="240.0" rx="8" fill="#BEE3F8" stroke="#1F497D" stroke-width="5" stroke-dasharray="18 8"/>
+<text x="560.0" y="440.0" text-anchor="middle" dominant-baseline="middle" font-size="44" fill="#1F497D" font-weight="700">主卧衣柜 2.4×1.2</text>
+</g>
+<g class="fpc-cab" data-cab="次卧衣柜">
+<rect x="320.0" y="320.0" width="400.0" height="240.0" rx="8" fill="#BEE3F8" stroke="#1F497D" stroke-width="5" stroke-dasharray="18 8"/>
+<text x="520.0" y="440.0" text-anchor="middle" dominant-baseline="middle" font-size="44" fill="#1F497D" font-weight="700">次卧衣柜 2×1.2</text>
+</g>
+<g class="fpc-cab" data-cab="书房书柜">
+<rect x="320.0" y="320.0" width="320.0" height="240.0" rx="8" fill="#BEE3F8" stroke="#1F497D" stroke-width="5" stroke-dasharray="18 8"/>
+<text x="480.0" y="440.0" text-anchor="middle" dominant-baseline="middle" font-size="44" fill="#1F497D" font-weight="700">书房书柜 1.6×1.2</text>
+</g>
+<g class="fpc-cab" data-cab="玄关鞋柜">
+<rect x="320.0" y="320.0" width="240.0" height="240.0" rx="8" fill="#BEE3F8" stroke="#1F497D" stroke-width="5" stroke-dasharray="18 8"/>
+<text x="440.0" y="440.0" text-anchor="middle" dominant-baseline="middle" font-size="44" fill="#1F497D" font-weight="700">玄关鞋柜 1.2×1.2</text>
+</g>
+<g class="fpc-cab" data-cab="阳台柜">
+<rect x="320.0" y="320.0" width="300.0" height="240.0" rx="8" fill="#BEE3F8" stroke="#1F497D" stroke-width="5" stroke-dasharray="18 8"/>
+<text x="470.0" y="440.0" text-anchor="middle" dominant-baseline="middle" font-size="44" fill="#1F497D" font-weight="700">阳台柜 1.5×1.2</text>
+</g>
+<g class="fpc-cab" data-cab="餐厅餐边柜">
+<rect x="320.0" y="320.0" width="360.0" height="240.0" rx="8" fill="#BEE3F8" stroke="#1F497D" stroke-width="5" stroke-dasharray="18 8"/>
+<text x="500.0" y="440.0" text-anchor="middle" dominant-baseline="middle" font-size="44" fill="#1F497D" font-weight="700">餐厅餐边柜 1.8×1.2</text>
+</g>
+<g class="fpc-cab" data-cab="厨房橱柜">
+<rect x="320.0" y="320.0" width="560.0" height="160.0" rx="8" fill="#FEEBC8" stroke="#1F497D" stroke-width="5" stroke-dasharray="18 8"/>
+<text x="600.0" y="400.0" text-anchor="middle" dominant-baseline="middle" font-size="44" fill="#1F497D" font-weight="700">厨房橱柜 2.8×0.8</text>
+</g>
+</g>
+<g id="fpc-dims">
+</g>
+<defs><marker id="arr" viewBox="0 0 10 10" refX="5" refY="5" markerWidth="5" markerHeight="5" orient="auto"><path d="M0,0L10,5L0,10z" fill="#C05621"/></marker></defs>
+</svg></div><p class="plan-note">📐 AI 结构化重绘示意图(非施工图,以实际量尺为准)</p>
+    <details class="dims"><summary>图面尺寸标注(4 条,用作交叉验证)</summary><ul><li><b>110㎡</b> — 总建筑面积</li><li><b>3房2厅2卫</b> — 户型房间配置</li><li><b>Q-1</b> — 户型编号</li><li><b>D6#14号702室</b> — 户型所在楼栋及房号</li></ul></details>
+  </div>
+
+  <div class="sec">
+    <h2>💰 报价明细 <span style="font-weight:400;font-size:11px;color:var(--mu)">(点击房间展开)</span></h2>
+    <div class="card" id="roomsCard"><div class="room" data-i="0">
+    <div class="room-h"><div><div class="nm">主卧衣柜</div><div class="dm">2.4m×2.5m</div></div><span class="tt">¥30,000</span></div>
+    <div class="items"><div class="item"><span class="l">柜体(PET材质)<span>2.4m×2.5m · 投影6m²</span></span><span class="v">¥30,000</span></div></div>
+  </div><div class="room" data-i="1">
+    <div class="room-h"><div><div class="nm">次卧衣柜</div><div class="dm">2m×2.5m</div></div><span class="tt">¥25,000</span></div>
+    <div class="items"><div class="item"><span class="l">柜体(PET材质)<span>2m×2.5m · 投影5m²</span></span><span class="v">¥25,000</span></div></div>
+  </div><div class="room" data-i="2">
+    <div class="room-h"><div><div class="nm">书房书柜</div><div class="dm">1.6m×2.5m</div></div><span class="tt">¥20,000</span></div>
+    <div class="items"><div class="item"><span class="l">柜体(PET材质)<span>1.6m×2.5m · 投影4m²</span></span><span class="v">¥20,000</span></div></div>
+  </div><div class="room" data-i="3">
+    <div class="room-h"><div><div class="nm">玄关鞋柜</div><div class="dm">1.2m×2.4m</div></div><span class="tt">¥14,400</span></div>
+    <div class="items"><div class="item"><span class="l">柜体(PET材质)<span>1.2m×2.4m · 投影2.88m²</span></span><span class="v">¥14,400</span></div></div>
+  </div><div class="room" data-i="4">
+    <div class="room-h"><div><div class="nm">阳台柜</div><div class="dm">1.5m×2.4m</div></div><span class="tt">¥18,000</span></div>
+    <div class="items"><div class="item"><span class="l">柜体(PET材质)<span>1.5m×2.4m · 投影3.6m²</span></span><span class="v">¥18,000</span></div></div>
+  </div><div class="room" data-i="5">
+    <div class="room-h"><div><div class="nm">餐厅餐边柜</div><div class="dm">1.8m×2.4m</div></div><span class="tt">¥21,600</span></div>
+    <div class="items"><div class="item"><span class="l">柜体(PET材质)<span>1.8m×2.4m · 投影4.32m²</span></span><span class="v">¥21,600</span></div></div>
+  </div><div class="room" data-i="6">
+    <div class="room-h"><div><div class="nm">厨房橱柜</div><div class="dm">2.8m×0.8m</div></div><span class="tt">¥15,000</span></div>
+    <div class="items"><div class="item"><span class="l">柜体(PET材质)<span>2.8m×0.8m · 投影3m</span></span><span class="v">¥15,000</span></div></div>
+  </div></div>
+    <div class="card" style="margin-top:8px">
+      <div class="trow"><span>项目小计</span><span class="v" id="tSub">¥144,000</span></div>
+      <div class="trow dim"><span id="tDiscLabel">原价</span><span class="v" id="tAfter">¥144,000</span></div>
+      <div class="trow" id="actRow" style="display:none"><span>活动优惠</span><span class="v" style="color:var(--ok)" id="tAct">-¥0</span></div>
+      <div class="trow total"><span>到手价</span><span class="v" id="tFinal">¥144,000</span></div>
+    </div>
+  </div>
+
+  <div class="sec">
+    <div class="selling">
+      <h3>✨ 为什么选择拉迷(标准客户)</h3>
+      <ul><li>空间利用率拉满:每一面墙都变成收纳</li><li>全国联保5年,铰链五金终身维护</li><li>设计上门量尺,3D效果图确认后再生产</li></ul>
+    </div>
+  </div>
+
+  <div class="sec" style="font-size:11px;color:var(--mu);line-height:1.7">
+    计价说明:柜体按投影面积(宽×高)×材质单价;厨房按计费延米(扣除转角重叠与冰箱位)。本报价为AI辅助估算,实际以现场量尺与正式合同为准。质保与赠送以当期活动政策为准。
+  </div>
+</div>
+
+<div class="ctrl">
+  <button class="btn sec" id="btnAdjust">⚖️ 调整报价</button>
+  <button class="btn pri" id="btnSave">📞 锁定到店礼</button>
+</div>
+
+<div class="drawer" id="drawer">
+  <div class="sheet">
+    <h3>⚖️ 销售操作台(客户不可见)</h3>
+    <div class="frow"><label>客户类型</label><div class="opts" id="optTier"><button class="chip " data-v="price">🏷️ 价格型</button><button class="chip on" data-v="standard">🏠 标准型</button><button class="chip " data-v="quality">💎 品质型</button></div></div>
+    <div class="frow"><label>材质档位</label><div class="opts" id="optMat"><button class="chip " data-v="双饰面">双饰面</button><button class="chip on" data-v="PET">PET</button><button class="chip " data-v="混油">混油</button></div></div>
+    <div class="frow"><label>报价浮动 <span id="floatV">15</span>%</label><input type="range" id="floatR" min="10" max="20" step="1" value="15"></div>
+    <div class="frow"><label>折扣</label><div class="opts" id="optDisc">
+      <button class="chip" data-v="1.00">原价</button>
+      <button class="chip" data-v="0.95">95折</button>
+      <button class="chip" data-v="0.90">9折</button>
+      <button class="chip" data-v="0.85">85折</button>
+      <button class="chip" data-v="0.80">8折</button>
+    </div></div>
+    <div class="frow"><label>柜体清单(宽=投影宽 m / 厨房为计费长)</label><div id="cabList"></div>
+      <div style="margin-top:8px"><button class="chip" id="btnAddCab">+ 增加柜体</button></div>
+    </div>
+    <div class="frow"><label>活动优惠</label><div class="opts" id="optAct"></div></div>
+    <button class="btn pri" style="width:100%" id="btnApply">应用并重算</button>
+  </div>
+</div>
+
+<script>
+// ============================================================
+// 拉迷全屋定制 · 默认报价规则库
+// 数据来源:floorplan-quotation/AI快速报价_Demo2.optimizing.html
+// (DEFAULT_RULES / Q_CUSTOMER_TIERS / DEFAULT_QUOTATION_PLANS /
+//   CABINET_EXTRA_TEMPLATES / EXTRA_METHOD_MAP / 厨房扣减规则)
+// 所有价格单位:元。修改价格请改这里的默认值或运行时覆盖 config。
+// ============================================================
+
+const METHOD = {
+  PROJECTION: '按投影面积', // 元/m²  数量 = 宽(m) × 高(m)
+  METER: '按延米',          // 元/m   数量 = 米数
+  COUNT: '按个数',          // 元/个  数量 = 个数
+};
+
+// —— 材质三档单价区间(AI快速粗略报价核心)——
+const PRICE_TIERS = [
+  { name: '双饰面', min: 3000, max: 3500 },
+  { name: 'PET',   min: 4500, max: 5000 },
+  { name: '混油',   min: 6000, max: 7000 },
+];
+const DEFAULT_TIER = 'PET';
+
+// —— 计价方式 ——
+const METHOD_OPTIONS = [
+  { value: '按投影面积', unit: '元/m²' },
+  { value: '按个数', unit: '元/个' },
+  { value: '按延米', unit: '元/m' },
+];
+
+// —— 柜体→空间 映射(识别命名用)——
+const CABINET_TO_SPACE = {
+  '主卧衣柜': '主卧', '次卧衣柜': '次卧', '儿童房衣柜': '儿童房', '老人房衣柜': '老人房',
+  '衣柜': '卧室', '客厅电视柜': '客厅', '电视柜': '客厅', '厨房橱柜': '厨房', '橱柜': '厨房',
+  '开放式厨房': '开放式厨房', '餐厅餐边柜': '餐厅', '餐边柜': '餐厅', '书房书柜': '书房',
+  '书柜': '书房', '玄关鞋柜': '玄关', '鞋柜': '玄关', '玄关柜': '玄关', '阳台柜': '阳台',
+  '衣帽间': '衣帽间', '储物柜': '储物间', '酒柜': '餐厅', '浴室柜': '卫生间', '榻榻米': '次卧',
+  '展示柜': '客厅', '洗衣机柜': '阳台', '卡座': '餐厅', '飘窗柜': '飘窗',
+};
+
+const SPACE_TO_DEFAULT_CABINET = {
+  '主卧': '主卧衣柜', '次卧': '次卧衣柜', '儿童房': '儿童房衣柜', '老人房': '老人房衣柜',
+  '客厅': '客厅电视柜', '厨房': '厨房橱柜', '开放式厨房': '开放式厨房', '餐厅': '餐厅餐边柜',
+  '客餐厅': '餐厅餐边柜', '书房': '书房书柜', '玄关': '玄关鞋柜', '阳台': '阳台柜',
+  '衣帽间': '衣帽间', '储物间': '储物柜', '卫生间': '浴室柜', '主卫': '浴室柜',
+};
+
+// —— 柜体类型 / 布局 / 门板 预设 ——
+const CUSTOM_TYPES_PRESET = {
+  cabinetType: ['主卧衣柜','次卧衣柜','儿童房衣柜','老人房衣柜','客厅电视柜','厨房橱柜','开放式厨房','餐厅餐边柜','书房书柜','玄关鞋柜','阳台柜','衣帽间','储物柜','酒柜','浴室柜','榻榻米'],
+  layoutForm: ['一字型','L型','U型','二字型','不规则'],
+  doorPanelType: ['普通平板','风影门板','PET门板','实木贴皮','烤漆门板','吸塑门板','玻璃门','百叶门','无门板'],
+};
+
+// —— 柜体默认附件模板(识别到柜型后自动配附件)——
+const CABINET_EXTRA_TEMPLATES = {
+  '玄关': ['灯带','变压器+开关','自弹器','见光板','铰链'],
+  '鞋柜': ['灯带','变压器+开关','自弹器','见光板','铰链','换鞋凳'],
+  '电视柜': ['自弹器','见光板','铰链'],
+  '展示柜': ['灯带','变压器+开关','拉手','见光板','铰链','自弹器'],
+  '酒柜': ['灯带','变压器+开关','拉手','见光板','铰链','自弹器','玻璃门升级'],
+  '洗衣机': ['自弹器','护墙板','见光板','铰链'],
+  '阳台': ['自弹器','见光板','铰链'],
+  '餐边': ['抽屉','拉手','自弹器','金属拉手','灯带','变压器+开关','见光板','铰链'],
+  '次卧': ['衣杆','金属拉手','抽屉','见光板','铰链'],
+  '儿童房': ['衣杆','金属拉手','抽屉','见光板','铰链'],
+  '书房': ['衣杆','金属拉手','抽屉','见光板','铰链'],
+  '主卧': ['金属拉手','抽屉','衣杆','自弹器','圆弧','铰链'],
+  '衣帽间': ['金属拉手','抽屉','衣杆','自弹器','圆弧','铰链','裤架领带格','拉篮'],
+  '厨房': ['见光板','碗篮','锅篮','自弹器'],
+  '_default': ['自弹器','见光板','铰链'],
+};
+
+// —— 附件计价方式映射 ——
+const EXTRA_METHOD_MAP = {
+  '见光板': '按投影面积', '护墙板': '按投影面积', '玻璃门升级': '按投影面积',
+  '灯带': '按延米', '衣杆': '按延米', '石材台面': '按延米',
+  '自弹器': '按个数', '铰链': '按个数', '变压器+开关': '按个数', '拉手': '按个数',
+  '金属拉手': '按个数', '抽屉': '按个数', '裤架领带格': '按个数', '拉篮': '按个数',
+  '碗篮': '按个数', '锅篮': '按个数', '圆弧': '按个数', '换鞋凳': '按个数',
+};
+
+// —— 基础价格表(name → {price, unit, method})——
+const PRICE_BOOK = {
+  // 柜体
+  '柜体投影单价':   { price: 1852, unit: '元/m²', method: '按投影面积' },
+  '生态澳松板柜体': { price: 1852, unit: '元/m²', method: '按投影面积' },
+  '铝格栅板柜体':   { price: 2532, unit: '元/m²', method: '按投影面积' },
+  // 厨房(单件计价模式)
+  '地柜':   { price: 2380, unit: '元/m', method: '按延米' },
+  '吊柜':   { price: 1748, unit: '元/m', method: '按延米' },
+  '冰箱吊柜': { price: 2622, unit: '元/m', method: '按延米' },
+  '高柜':   { price: 5950, unit: '元/m', method: '按延米' },
+  '台面':   { price: 1480, unit: '元/m', method: '按延米' },
+  '台面超出部分': { price: 2480, unit: '元/m', method: '按延米' },
+  '碗篮':   { price: 1268, unit: '元/个', method: '按个数' },
+  '锅篮':   { price: 1628, unit: '元/个', method: '按个数' },
+  '切角':   { price: 500, unit: '元/个', method: '按个数' },
+  '台下盆': { price: 500, unit: '元/个', method: '按个数' },
+  '包管':   { price: 500, unit: '元/个', method: '按个数' },
+  // 门板单价
+  '标配门板': { price: 380, unit: '元/m²', method: '按投影面积' },
+  '烤漆门板': { price: 580, unit: '元/m²', method: '按投影面积' },
+  '吸塑门板': { price: 480, unit: '元/m²', method: '按投影面积' },
+  '实木门板': { price: 880, unit: '元/m²', method: '按投影面积' },
+  '铝格栅门板': { price: 680, unit: '元/m²', method: '按投影面积' },
+  'PET门板':  { price: 450, unit: '元/m²', method: '按投影面积' },
+  // 附件
+  '见光板':   { price: 1138, unit: '元/m²', method: '按投影面积' },
+  '护墙板':   { price: 1852, unit: '元/m²', method: '按投影面积' },
+  '玻璃门升级': { price: 1852, unit: '元/m²', method: '按投影面积' },
+  '灯带':     { price: 386, unit: '元/m', method: '按延米' },
+  '变压器+开关': { price: 536, unit: '元/个', method: '按个数' },
+  '自弹器':   { price: 115, unit: '元/个', method: '按个数' },
+  '铰链':     { price: 40, unit: '元/个', method: '按个数' },
+  '拉手':     { price: 226, unit: '元/个', method: '按个数' },
+  '金属拉手': { price: 225, unit: '元/个', method: '按个数' },
+  '抽屉':     { price: 1300, unit: '元/个', method: '按个数' },
+  '衣杆':     { price: 288, unit: '元/m', method: '按延米' },
+  '裤架领带格': { price: 0, unit: '元/个', method: '按个数' },
+  '拉篮':     { price: 0, unit: '元/个', method: '按个数' },
+  '圆弧':     { price: 1560, unit: '元/个', method: '按个数' },
+  '换鞋凳':   { price: 0, unit: '元/个', method: '按个数' },
+  '石材台面': { price: 1480, unit: '元/m', method: '按延米' },
+};
+
+// —— 厨房计价参数(qGetKitchenBillableMeters 同源)——
+const KITCHEN_RULES = {
+  packageMeters: 3,     // 3米基础套餐(单件计价模式的扣减基准)
+  overlapMm: 600,       // 转角重叠扣减
+  fridgeMm: 1000,       // 冰箱位预留
+  typeThresholdM: 2,    // 最小边 ≤2m 判 L 型,>2m 判 U 型
+};
+
+// —— 客户分型(销售策略核心)——
+const CUSTOMER_TIERS = {
+  price: {
+    label: '看重价格型', icon: '🏷️',
+    strategy: '预算少的客户直接按性价比报,快速赢单',
+    defaultPlan: 'economy', defaultDiscount: 0.85,
+    doorType: '标配', hardware: '经济实用款', service: '标准测量+安装',
+    warranty: '3年质保', gifts: '送基础五金配件', panelLabel: '进口爱格板(加价升级)',
+    autoEnableActivity: true,
+    discounts: [
+      { id: 'p1', label: '投影面积85折', rate: 0.85, deletable: false },
+      { id: 'p2', label: '投影面积8折特惠', rate: 0.80, deletable: true },
+      { id: 'p3', label: '团购78折', rate: 0.78, deletable: true },
+    ],
+  },
+  standard: {
+    label: '标准客户', icon: '🏠',
+    strategy: '标准配置报价,正常谈单空间',
+    defaultPlan: 'standard', defaultDiscount: 1.0,
+    doorType: '标配门板', hardware: '国产优质铰链/拉手', service: '标准测量安装',
+    warranty: '5年质保', gifts: '可参与活动优惠', panelLabel: '国产E0级颗粒板',
+    autoEnableActivity: false,
+    discounts: [
+      { id: 's1', label: '原价', rate: 1.00, deletable: false },
+      { id: 's2', label: '投影面积95折', rate: 0.95, deletable: true },
+      { id: 's3', label: '投影面积9折', rate: 0.90, deletable: true },
+    ],
+  },
+  quality: {
+    label: '品质导向型', icon: '💎',
+    strategy: '有预算的客户报高一点,留谈价空间,突出配置与质感',
+    defaultPlan: 'quality', defaultDiscount: 1.0,
+    doorType: '烤漆/吸塑高端门板', hardware: '进口百隆/海蒂诗五金', service: 'VIP专属设计师+金牌安装',
+    warranty: '10年质保+终身维护', gifts: '赠灯带+抽屉升级包', panelLabel: '进口爱格板/可丽芙(推荐)',
+    premiumRate: 1.15,
+    autoEnableActivity: false,
+    discounts: [
+      { id: 'q1', label: '投影面积95折(保利润)', rate: 0.95, deletable: false },
+      { id: 'q2', label: '原价(高端不打折)', rate: 1.00, deletable: true },
+      { id: 'q3', label: 'VIP专享92折', rate: 0.92, deletable: true },
+    ],
+  },
+};
+
+// —— 报价方案 ——
+const QUOTATION_PLANS = [
+  { id: 'economy',  name: '经济型方案', description: '适合预算敏感型客户,主打性价比', customerTier: 'price',    tierName: '双饰面', floatPct: 10, discountRate: 0.98 },
+  { id: 'standard', name: '标准型方案', description: '默认方案,标准配置,适合大多数客户', customerTier: 'standard', tierName: 'PET',    floatPct: 15, discountRate: 1.00 },
+  { id: 'quality',  name: '品质型方案', description: '高端配置,适合追求品质的客户', customerTier: 'quality',   tierName: '混油',   floatPct: 15, discountRate: 1.00 },
+];
+
+// —— 营销活动模块(可勾选叠加)——
+const ACTIVITY_MODULES = [
+  { id: 'deposit5000', type: 'deposit', name: '定金膨胀(5千档)', desc: '交¥5,000→膨胀¥8,000 + ¥500抵用券', amount: 3500, minOrder: 30000 },
+  { id: 'deposit10000', type: 'deposit', name: '定金膨胀(1万档)', desc: '交¥10,000→膨胀¥13,000 + ¥800抵用券', amount: 3800, minOrder: 30000 },
+  { id: 'coupon20k', type: 'coupon', name: '满减券', desc: '满¥20,000减¥1,000', amount: 1000, threshold: 20000 },
+  { id: 'cashback', type: 'tiered_cashback', name: '阶梯返现', tiers: [
+      { min: 50000, cash: 2000 }, { min: 100000, cash: 5000 }, { min: 150000, cash: 8000 },
+    ] },
+];
+
+// —— 默认层高与房间面积先验(识别失败兜底估算用)——
+const DEFAULTS = {
+  height: 2.8, // 默认柜高(层高)
+  roomAreaPriors: { // 常见房间典型面积范围 [min, max](m²)
+    '主卧': [12, 22], '次卧': [8, 16], '儿童房': [8, 14], '老人房': [9, 15], '书房': [6, 12],
+    '客厅': [18, 35], '餐厅': [8, 18], '厨房': [4.5, 10], '卫生间': [2.5, 6], '主卫': [3, 6],
+    '公卫': [2.5, 6], '阳台': [3, 10], '玄关': [2, 6], '衣帽间': [3, 10], '储物间': [1.5, 5],
+  },
+  cabinetWallRatio: { // 柜体默认占所在墙面比例(无明确尺寸时的估算)
+    '主卧衣柜': 0.55, '次卧衣柜': 0.50, '儿童房衣柜': 0.50, '玄关鞋柜': 0.6,
+    '客厅电视柜': 0.55, '餐厅餐边柜': 0.5, '阳台柜': 0.6, '书房书柜': 0.5,
+  },
+};
+
+// —— 赢单文案素材(业主视角价值描述)——
+const SELLING_POINTS = {
+  price: [
+    '按投影面积计价,明明白白,没有隐形收费',
+    'E0级环保板材,装完即可安心入住',
+    '工厂直供价,同样的预算多做一个柜子',
+  ],
+  standard: [
+    '空间利用率拉满:每一面墙都变成收纳',
+    '全国联保5年,铰链五金终身维护',
+    '设计上门量尺,3D效果图确认后再生产',
+  ],
+  quality: [
+    '进口板材+进口五金,用20年不变形不褪色',
+    'VIP设计师一对一,全屋风格统一设计',
+    '10年质保+终身维护,一次装修终身省心',
+  ],
+};
+
+// ============================================================
+// 拉迷全屋定制 · 报价引擎(纯函数,浏览器/CLI 共用)
+// 两种模式:
+//   quick  — AI快速粗略报价:投影面积/厨房延米 × 三档材质区间单价
+//   detail — 明细报价:Excel 预算表模型(柜体+门板补差+附件逐项)
+// 销售策略:客户分型 → 方案/折扣/浮动 → 活动叠加 → 谈价区间
+// ============================================================
+
+const r2 = (v) => Math.round(v * 100) / 100;
+const num = (v, d = 0) => { const n = parseFloat(v); return Number.isFinite(n) ? n : d; };
+
+// ---------- 材质档位 ----------
+function getTier(name) {
+  const t = PRICE_TIERS.find((x) => x.name === (name || DEFAULT_TIER));
+  return t || PRICE_TIERS[1];
+}
+// 浮动百分比(10~15)在区间内线性插值 → 单价(qGetAiUnitPrice 同源逻辑)
+function tierUnitPrice(tier, floatPct = 15) {
+  const k = Math.min(1, Math.max(0, (num(floatPct, 15) - 10) / 5));
+  return r2(tier.min + k * (tier.max - tier.min));
+}
+
+// ---------- 厨房计费米数(qGetKitchenBillableMeters 同源)----------
+function kitchenBillable(walls = [], rules = {}) {
+  const R = { ...KITCHEN_RULES, ...rules };
+  const ws = (Array.isArray(walls) ? walls : []).map((v) => num(v)).filter((v) => v > 0);
+  if (!ws.length) return null;
+  const total = ws.reduce((a, b) => a + b, 0);
+  const minWall = Math.min(...ws);
+  const maxWall = Math.max(...ws);
+  const type = minWall <= R.typeThresholdM ? 'L' : 'U';
+  const overlapCount = type === 'L' ? 1 : 2;
+  const usedLen = type === 'L' ? maxWall + minWall : maxWall + maxWall + minWall;
+  const billable = Math.max(0, usedLen - (overlapCount * R.overlapMm) / 1000 - R.fridgeMm / 1000);
+  return {
+    walls: ws, total: r2(total), minWall: r2(minWall), maxWall: r2(maxWall),
+    kitchenType: type, overlapCount, fridgeMm: R.fridgeMm, usedLen: r2(usedLen), billable: r2(billable),
+  };
+}
+const isKitchen = (name = '') => /厨房|橱柜/.test(String(name));
+
+// ---------- 附件数量启发式(Excel 模型总结,均可编辑) ----------
+function estimateExtras(cabinet) {
+  const w = num(cabinet.width, 1.5);
+  const h = num(cabinet.height, DEFAULTS.height);
+  const type = String(cabinet.type || '');
+  let tplKey = Object.keys(CABINET_EXTRA_TEMPLATES).find((k) => k !== '_default' && type.includes(k));
+  if (!tplKey && isKitchen(type)) tplKey = '厨房';
+  const names = CABINET_EXTRA_TEMPLATES[tplKey || '_default'] || CABINET_EXTRA_TEMPLATES._default;
+  const sections = h > 1.6 ? 2 : 1;               // 高柜分上下段
+  const doorsPerSection = Math.max(1, Math.round(w / 0.45));
+  const doors = sections * doorsPerSection;
+  const hingePerDoor = h > 2 ? 3 : 2;
+  const out = [];
+  for (const name of names) {
+    const pb = PRICE_BOOK[name];
+    if (!pb) continue;
+    let qty = 0; let note = '';
+    switch (name) {
+      case '铰链': qty = doors * hingePerDoor; note = `${doors}门×${hingePerDoor}铰链`; break;
+      case '自弹器': qty = Math.max(1, doors - 1); note = '反弹门配'; break;
+      case '见光板': qty = 1; note = '单侧见光(0.5m宽)'; break;
+      case '灯带': qty = r2(w * 2); note = '顶底双层'; break;
+      case '变压器+开关': qty = 1; note = 'LED电源'; break;
+      case '衣杆': qty = r2(w); note = '通长挂杆'; break;
+      case '抽屉': qty = type.includes('主卧') || type.includes('衣帽') ? 3 : 2; break;
+      case '金属拉手': case '拉手': qty = doors + 2; break;
+      case '碗篮': case '锅篮': qty = 1; break;
+      default: qty = name === '玻璃门升级' || name === '护墙板' ? 0 : 1;
+    }
+    if (qty > 0) out.push({ name, method: pb.method, unit: pb.unit, unitPrice: pb.price, qty, note });
+  }
+  return out;
+}
+
+// ---------- 单柜体计价 ----------
+// 计价数量:投影面积 = 宽×高;厨房 = 计费延米
+function cabinetQty(cabinet, rules = {}) {
+  if (isKitchen(cabinet.type)) {
+    const kb = kitchenBillable(cabinet.walls || [cabinet.width, cabinet.height, cabinet.width, cabinet.height], rules);
+    return { qty: kb ? kb.billable : num(cabinet.width), unit: 'm', kb };
+  }
+  return { qty: r2(num(cabinet.width) * num(cabinet.height)), unit: 'm²', kb: null };
+}
+
+// 快速模式:单柜体一行
+function quickLine(cabinet, { tierName, floatPct = 15, rules } = {}) {
+  const tier = getTier(tierName);
+  const U = tierUnitPrice(tier, floatPct);
+  const { qty, unit, kb } = cabinetQty(cabinet, rules);
+  return {
+    mode: 'quick', name: cabinet.type, tierName: tier.name,
+    qty, unit, unitPrice: U, total: r2(qty * U), kb,
+    dims: num(cabinet.width) && num(cabinet.height) ? `${num(cabinet.width)}m×${num(cabinet.height)}m` : '',
+  };
+}
+
+// 明细模式:单柜体 → Excel 风格明细行
+function detailLines(cabinet, opts = {}) {
+  const { doorDiffPrice = PRICE_BOOK['柜体投影单价'].price > 0 ? 538 : 538, rules, includeDoorDiff = true } = opts;
+  const lines = [];
+  const isK = isKitchen(cabinet.type);
+  const { qty } = cabinetQty(cabinet, rules);
+  if (isK) {
+    // 厨房单件计价(Excel 模式:地柜+吊柜+台面+功能五金)
+    const walls = cabinet.walls || [cabinet.width, cabinet.height, cabinet.width, cabinet.height];
+    const straight = Math.max(0, ...walls); // 直线段米数近似
+    for (const [name, factor, note] of [
+      ['地柜', 1, '沿墙地柜'], ['吊柜', 1, '沿墙吊柜'], ['台面', 1, '石英石台面'],
+    ]) {
+      const pb = PRICE_BOOK[name];
+      lines.push({ name, qty: r2(qty), unit: 'm', unitPrice: pb.price, total: r2(qty * pb.price), note, method: pb.method });
+    }
+    for (const name of ['碗篮', '锅篮']) {
+      const pb = PRICE_BOOK[name];
+      lines.push({ name, qty: 1, unit: '个', unitPrice: pb.price, total: pb.price, note: '标准配置', method: pb.method });
+    }
+    return { cabinet, isKitchen: true, lines, subtotal: r2(lines.reduce((s, l) => s + l.total, 0)) };
+  }
+  const pb = PRICE_BOOK['生态澳松板柜体'];
+  lines.push({ name: '柜体', qty, unit: 'm²', unitPrice: pb.price, total: r2(qty * pb.price), note: '生态澳松板', method: pb.method });
+  if (includeDoorDiff && doorDiffPrice > 0) {
+    lines.push({ name: '板门补差价', qty, unit: 'm²', unitPrice: doorDiffPrice, total: r2(qty * doorDiffPrice), note: '双饰面门补差', method: pb.method });
+  }
+  for (const ex of estimateExtras(cabinet)) {
+    const total = ex.method === '按投影面积' ? r2(ex.qty * 0.5 * num(cabinet.height) * ex.unitPrice)
+      : ex.method === '按延米' ? r2(ex.qty * ex.unitPrice)
+      : r2(ex.qty * ex.unitPrice);
+    lines.push({ name: ex.name, qty: ex.qty, unit: ex.unit, unitPrice: ex.unitPrice, total, note: ex.note, method: ex.method });
+  }
+  return { cabinet, isKitchen: false, lines, subtotal: r2(lines.reduce((s, l) => s + l.total, 0)) };
+}
+
+// ---------- 报价主入口 ----------
+// inputs: { cabinets:[{type,width,height,walls?,excluded?}], tier, floatPct, discount, tierKey(客户分型), planId, mode }
+function buildQuote(input = {}) {
+  const {
+    cabinets = [], mode = 'quick',
+    tierName = input.tier || DEFAULT_TIER,
+    floatPct = 15, discount = null, tierKey = 'standard', planId = null,
+    rules = {}, extras = [], // extras: [{name, room, qty, unitPrice}] 手动增删项
+    clientName = '', note = '',
+  } = input;
+
+  const plan = QUOTATION_PLANS.find((p) => p.id === (planId || CUSTOMER_TIERS[tierKey]?.defaultPlan)) || QUOTATION_PLANS[1];
+  const custTier = CUSTOMER_TIERS[tierKey] || CUSTOMER_TIERS.standard;
+  const effDiscount = discount != null ? num(discount) : (custTier.defaultDiscount ?? plan.discountRate ?? 1);
+
+  const rooms = cabinets
+    .filter((c) => !c.excluded)
+    .map((c, i) => {
+      const base = mode === 'detail' ? detailLines(c, { rules }) : { ...quickLine(c, { tierName, floatPct, rules }), lines: null };
+      const id = c.id || `cab_${i + 1}`;
+      return { id, type: c.type, dims: base.dims || `${num(c.width)}m×${num(c.height)}m`, ...base };
+    });
+
+  const itemsSubtotal = r2(rooms.reduce((s, r) => s + (r.subtotal ?? r.total), 0));
+  const manualExtras = extras.map((e, i) => ({
+    id: e.id || `ext_${i + 1}`, name: e.name, qty: num(e.qty, 1), unit: e.unit || '项',
+    unitPrice: num(e.unitPrice), total: r2(num(e.qty, 1) * num(e.unitPrice)), kind: e.kind || 'add',
+  }));
+  const extrasTotal = r2(manualExtras.reduce((s, e) => s + (e.kind === 'remove' ? -e.total : e.total), 0));
+
+  const premiumRate = custTier.premiumRate || 1;
+  const afterPremium = r2((itemsSubtotal + extrasTotal) * premiumRate);
+  const afterDiscount = r2(afterPremium * effDiscount);
+
+  // 活动叠加(自动/手动)
+  const acts = (input.activities || (custTier.autoEnableActivity ? ACTIVITY_MODULES.map((m) => m.id) : []))
+    .map((id) => ACTIVITY_MODULES.find((m) => m.id === id)).filter(Boolean);
+  let activityTotal = 0;
+  const activityLines = [];
+  for (const m of acts) {
+    let amt = 0; let desc = m.desc || m.name;
+    if (m.type === 'tiered_cashback') {
+      const hit = [...(m.tiers || [])].sort((a, b) => b.min - a.min).find((t) => afterDiscount >= t.min);
+      if (!hit) continue;
+      amt = hit.cash; desc = `满¥${hit.min.toLocaleString()}返¥${hit.cash.toLocaleString()}`;
+    } else if (m.type === 'coupon' && afterDiscount < (m.threshold || 0)) continue;
+    else if (m.type === 'deposit' && afterDiscount < (m.minOrder || 30000)) continue;
+    else amt = m.amount || 0;
+    activityTotal += amt;
+    activityLines.push({ id: m.id, name: m.name, desc, amount: amt });
+  }
+  const finalPrice = Math.max(0, r2(afterDiscount - activityTotal));
+
+  // 谈价区间(浮动率)
+  const rate = num(floatPct, 15) / 100;
+  const range = { low: Math.round(finalPrice * (1 - rate)), high: Math.round(finalPrice * (1 + rate)) };
+
+  // 业主视角价值点
+  const selling = SELLING_POINTS[tierKey] || SELLING_POINTS.standard;
+
+  return {
+    meta: {
+      clientName, note: note || custTier.strategy, mode,
+      tierName: mode === 'quick' ? getTier(tierName).name : undefined,
+      floatPct, planId: plan.id, planName: plan.name,
+      customerTier: tierKey, customerLabel: custTier.label,
+      discount: effDiscount, premiumRate: premiumRate !== 1 ? premiumRate : undefined,
+      generatedAt: new Date().toISOString(),
+    },
+    rooms, manualExtras, activityLines,
+    totals: {
+      itemsSubtotal, extrasTotal, premiumRate, afterPremium, discount: effDiscount,
+      afterDiscount, activityTotal, finalPrice, range,
+      discountLabel: custTier.discounts?.find((d) => Math.abs(d.rate - effDiscount) < 0.001)?.label || `折扣${Math.round(effDiscount * 100)}%`,
+    },
+    selling,
+    config: { tierKey, tierName, floatPct, discount: effDiscount, planId: plan.id, mode },
+  };
+}
+
+// ---------- 语义化调整应用(销售边聊边改) ----------
+// ops: [{op:'add'|'remove'|'resize'|'rename'|'price', target, ...}]
+function applyOps(cabinets, ops = []) {
+  const out = cabinets.map((c) => ({ ...c }));
+  for (const op of ops) {
+    if (op.op === 'add') {
+      out.push({ type: op.type || op.name || '柜体', width: num(op.width, 1.5), height: num(op.height, DEFAULTS.height), from: '语音新增' });
+    } else if (op.op === 'remove') {
+      const i = out.findIndex((c) => c.type === op.target);
+      if (i >= 0) out.splice(i, 1);
+    } else if (op.op === 'resize') {
+      const c = out.find((c) => c.type === op.target);
+      if (c) { if (op.width) c.width = num(op.width); if (op.height) c.height = num(op.height); c.from = '手动调整'; }
+    } else if (op.op === 'rename') {
+      const c = out.find((c) => c.type === op.target);
+      if (c) c.type = op.to || c.type;
+    } else if (op.op === 'exclude') {
+      const c = out.find((c) => c.type === op.target);
+      if (c) c.excluded = true;
+    } else if (op.op === 'include') {
+      const c = out.find((c) => c.type === op.target);
+      if (c) delete c.excluded;
+    }
+  }
+  return out;
+}
+
+// 授权范围内校验(浮动区间即授权边界)
+function withinAuthority(quote, price) {
+  const { range } = quote.totals;
+  return price >= range.low && price <= range.high;
+}
+
+var __DEFAULT__ = { buildQuote, applyOps, quickLine, detailLines, kitchenBillable, tierUnitPrice, getTier, withinAuthority };
+
+window.__Q__ = {"cabinets":[{"id":"cab_1","type":"主卧衣柜","width":2.4,"height":2.6,"rawName":"主卧衣柜"},{"id":"cab_2","type":"次卧衣柜","width":2,"height":2.6,"rawName":"次卧衣柜"},{"id":"cab_3","type":"书房书柜","width":1.6,"height":2.6,"rawName":"书房书柜"},{"id":"cab_4","type":"玄关鞋柜","width":1.2,"height":2.6,"rawName":"玄关鞋柜"},{"id":"cab_5","type":"阳台柜","width":1.5,"height":2.6,"rawName":"阳台柜"},{"id":"cab_6","type":"餐厅餐边柜","width":1.8,"height":2.6,"rawName":"餐厅餐边柜"},{"id":"cab_7","type":"厨房橱柜","height":2.6,"rawName":"厨房橱柜"}],"config":{"tierKey":"standard","tierName":"PET","floatPct":15,"discount":1,"planId":"standard","mode":"quick"},"salesNote":"三房两厅新居,孩子明年上小学,看重收纳和环保,预算25万内想多谈点优惠"};
+console.log('fpc engine ready');
+</script>
+<script>
+(function(){
+  var Q = window.__Q__; var S = document; var money = function(n){ return '¥' + Math.round(n||0).toLocaleString('zh-CN'); };
+  var cfg = Object.assign({ tierKey:'standard', tierName:'PET', floatPct:15, discount:1.0, mode:'quick' }, Q.config||{});
+  function calc(){ return buildQuote({ cabinets: Q.cabinets, tierKey: cfg.tierKey, tierName: cfg.tierName, floatPct: cfg.floatPct, discount: cfg.discount, mode: cfg.mode, clientName: "缪女士(时代之城)" }); }
+  function refresh(){
+    var q = calc(); var t = q.totals;
+    S.getElementById('finalPrice').textContent = money(t.finalPrice);
+    S.getElementById('finalRange').textContent = '谈价参考区间 ' + money(t.range.low) + ' ~ ' + money(t.range.high);
+    S.getElementById('tSub').textContent = money(t.itemsSubtotal);
+    S.getElementById('tDiscLabel').textContent = t.discountLabel;
+    S.getElementById('tAfter').textContent = money(t.afterDiscount);
+    S.getElementById('tAct').textContent = '-' + money(t.activityTotal);
+    S.getElementById('tFinal').textContent = money(t.finalPrice);
+    S.getElementById('bTier').textContent = (q.meta.tierName||'') + '材质';
+    S.getElementById('bDisc').textContent = t.discountLabel;
+  }
+  // room expand
+  S.querySelectorAll('.room-h').forEach(function(el){ el.addEventListener('click', function(){ el.parentElement.classList.toggle('open'); }); });
+  // chips
+  function bindChips(id, fn){ var box = S.getElementById(id); if(!box) return; box.addEventListener('click', function(e){ var b = e.target.closest('.chip'); if(!b) return; box.querySelectorAll('.chip').forEach(function(x){x.classList.remove('on');}); b.classList.add('on'); fn(b.dataset.v); }); }
+  bindChips('optTier', function(v){ cfg.tierKey = v; refresh(); });
+  bindChips('optMat', function(v){ cfg.tierName = v; refresh(); });
+  bindChips('optDisc', function(v){ cfg.discount = parseFloat(v); refresh(); });
+  var fr = S.getElementById('floatR'); fr.addEventListener('input', function(){ S.getElementById('floatV').textContent = fr.value; cfg.floatPct = parseInt(fr.value); refresh(); });
+  // drawer
+  var d = S.getElementById('drawer');
+  S.getElementById('btnAdjust').addEventListener('click', function(){ d.classList.add('open'); renderCabs(); });
+  d.addEventListener('click', function(e){ if(e.target === d) d.classList.remove('open'); });
+  function renderCabs(){
+    var box = S.getElementById('cabList'); box.innerHTML = '';
+    Q.cabinets.forEach(function(c, i){
+      var row = S.createElement('div'); row.className = 'cabrow';
+      var dims = String(c.width||'') ? String(c.width) : '';
+      row.innerHTML = '<span style="flex:1">' + c.rawName + '</span>' +
+        '<input type="number" step="0.05" value="' + (c.width||1.5) + '" data-i="' + i + '" data-f="width" title="宽(米)">' +
+        '<span class="del" data-i="' + i + '">✕</span>';
+      box.appendChild(row);
+    });
+    box.onclick = function(e){
+      var del = e.target.closest('.del');
+      if (del) { Q.cabinets.splice(parseInt(del.dataset.i),1); renderCabs(); refresh(); return; }
+      var inp = e.target.closest('input');
+      if (inp) { inp.addEventListener('change', function(){ Q.cabinets[parseInt(inp.dataset.i)].width = parseFloat(inp.value)||1.5; refresh(); }); }
+    };
+  }
+  S.getElementById('btnAddCab').addEventListener('click', function(){ Q.cabinets.push({ type:'储物柜', rawName:'储物柜', width:1.5, height:2.6 }); renderCabs(); refresh(); });
+  S.getElementById('btnSave').addEventListener('click', function(){
+    alert('已记录!销售将带着这份报价与您确认到店时间,到店凭此单领取到店礼。');
+  });
+  refresh();
+})();
+</script>
+</body>
+</html>

+ 792 - 0
skill/out/报价单-缪女士(时代之城)-08312347.html

@@ -0,0 +1,792 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+<head>
+<meta charset="UTF-8">
+<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
+<meta name="format-detection" content="telephone=no">
+<title>缪女士(时代之城) · 全屋定制报价方案</title>
+<style>
+:root{--p:#1F497D;--pa:#2B6CB0;--bg:#F5F7FA;--card:#fff;--bd:#E2E8F0;--tx:#2D3748;--mu:#718096;--ok:#38A169;--wa:#D69E2E;--dg:#E53E3E;--r:12px}
+*{box-sizing:border-box;margin:0;padding:0;-webkit-tap-highlight-color:transparent}
+body{font-family:-apple-system,BlinkMacSystemFont,"PingFang SC","Microsoft YaHei",sans-serif;background:var(--bg);color:var(--tx);line-height:1.55;padding-bottom:calc(88px + env(safe-area-inset-bottom))}
+header{background:linear-gradient(135deg,#1F497D,#2B6CB0);color:#fff;padding:18px 16px 14px}
+header h1{font-size:17px;font-weight:700}
+header .sub{font-size:12px;opacity:.85;margin-top:2px}
+.wrap{padding:12px;max-width:640px;margin:0 auto}
+.hero{background:var(--card);border-radius:var(--r);padding:16px;box-shadow:0 1px 3px rgba(0,0,0,.08);margin-top:-26px;position:relative;text-align:center}
+.hero .label{font-size:12px;color:var(--mu)}
+.hero .price{font-size:38px;font-weight:800;color:var(--p);letter-spacing:-1px;margin:2px 0}
+.hero .price small{font-size:16px;font-weight:600}
+.hero .range{font-size:13px;color:var(--mu)}
+.badge{display:inline-block;font-size:11px;padding:2px 10px;border-radius:20px;background:#EBF8FF;color:var(--pa);border:1px solid #BEE3F8;margin-top:6px}
+.sec{margin-top:14px}
+.sec h2{font-size:15px;font-weight:700;margin:0 2px 8px;display:flex;align-items:center;gap:6px}
+.card{background:var(--card);border-radius:var(--r);box-shadow:0 1px 3px rgba(0,0,0,.06);overflow:hidden}
+.room{border-bottom:1px solid var(--bd)}
+.room:last-child{border-bottom:none}
+.room-h{padding:12px 14px;display:flex;justify-content:space-between;align-items:center;gap:8px}
+.room-h .nm{font-weight:700;font-size:14px}
+.room-h .dm{font-size:11px;color:var(--mu)}
+.room-h .tt{font-weight:800;color:var(--p);font-size:15px;white-space:nowrap}
+.room-h .conf{font-size:10px;padding:1px 6px;border-radius:8px;background:#FFFAF0;color:#C05621;border:1px solid #FBD38D;flex-shrink:0}
+.items{padding:0 14px 10px;display:none}
+.room.open .items{display:block}
+.item{display:flex;justify-content:space-between;font-size:12.5px;padding:5px 0;color:#4A5568}
+.item .l span{color:var(--mu);font-size:11px;margin-left:6px}
+.item .v{white-space:nowrap}
+.trow{display:flex;justify-content:space-between;padding:8px 14px;font-size:13px}
+.trow .v{font-weight:700}
+.trow.total{background:#F0FFF4;color:#276749;font-weight:800;font-size:15px}
+.trow.dim{color:var(--mu);font-size:12px}
+.plan-wrap{background:var(--card);border-radius:var(--r);padding:10px;box-shadow:0 1px 3px rgba(0,0,0,.06)}
+.plan-wrap svg,.plan-img{width:100%;height:auto;display:block;border-radius:8px}
+.plan-note{font-size:11px;color:var(--mu);text-align:center;margin-top:6px}
+.selling{background:linear-gradient(135deg,#FFFAF0,#FFF5EB);border:1px solid #FBD38D;border-radius:var(--r);padding:12px 14px}
+.selling li{font-size:13px;margin:6px 0 6px 18px;color:#744210}
+.selling h3{font-size:13px;color:#C05621;margin-bottom:4px}
+.acts .item .v{color:var(--ok)}
+.ctrl{position:fixed;bottom:0;left:0;right:0;background:#fff;border-top:1px solid var(--bd);padding:10px 14px calc(10px + env(safe-area-inset-bottom));display:flex;gap:8px;z-index:99;max-width:640px;margin:0 auto}
+.btn{flex:1;padding:12px;border:none;border-radius:10px;font-size:14px;font-weight:700;cursor:pointer}
+.btn.pri{background:var(--p);color:#fff}
+.btn.sec{background:#EDF2F7;color:var(--tx)}
+details.dims{background:var(--card);border-radius:var(--r);padding:10px 14px;font-size:12px;color:var(--mu)}
+details.dims summary{cursor:pointer;font-weight:600;color:var(--tx)}
+details.dims li{margin:4px 0 4px 18px}
+.drawer{position:fixed;inset:0;background:rgba(0,0,0,.4);z-index:100;display:none}
+.drawer.open{display:block}
+.sheet{position:absolute;bottom:0;left:0;right:0;background:#fff;border-radius:16px 16px 0 0;max-height:82vh;overflow:auto;padding:16px 16px calc(20px + env(safe-area-inset-bottom));max-width:640px;margin:0 auto}
+.sheet h3{font-size:15px;margin-bottom:12px}
+.frow{margin-bottom:14px}
+.frow label{font-size:12px;color:var(--mu);display:block;margin-bottom:4px}
+.frow .opts{display:flex;gap:6px;flex-wrap:wrap}
+.chip{padding:8px 12px;border-radius:8px;border:1.5px solid var(--bd);background:#fff;font-size:13px;cursor:pointer}
+.chip.on{border-color:var(--pa);background:#EBF8FF;color:var(--pa);font-weight:700}
+input[type=range]{width:100%}
+.cabrow{display:flex;align-items:center;gap:8px;padding:8px 0;border-bottom:1px dashed var(--bd);font-size:13px}
+.cabrow input{width:64px;padding:6px;border:1px solid var(--bd);border-radius:6px;font-size:13px}
+.cabrow .del{color:var(--dg);font-size:18px;padding:0 6px;cursor:pointer}
+.cabrow .add{color:var(--ok)}
+@media(min-width:640px){.hero .price{font-size:44px}}
+</style>
+</head>
+<body>
+<header>
+  <h1>缪女士(时代之城) · 全屋定制报价方案</h1>
+  <div class="sub">标准客户方案 · 生成于 8/31 23:47 · 拉迷全屋定制</div>
+</header>
+<div class="wrap">
+
+  <div class="hero">
+    <div class="label">全屋定制整装价</div>
+    <div class="price" id="finalPrice">¥150,900</div>
+    <div class="range" id="finalRange">谈价参考区间 ¥128,265 ~ ¥173,535</div>
+    <div id="badges">
+      <span class="badge" id="bTier">PET材质</span>
+      <span class="badge" id="bDisc">原价</span>
+      
+    </div>
+  </div>
+
+  
+
+  <div class="sec">
+    <h2>🏠 户型与定制布局</h2>
+    <div class="plan-wrap" id="planWrap"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 2710 2330" class="fpc-plan">
+<g id="fpc-rooms">
+<g class="fpc-room" data-room="玄关">
+<rect x="960.0" y="120.0" width="790.0" height="570.0" fill="#F7FAFC" stroke="#2D3748" stroke-width="6"/>
+<line x1="1265.0" y1="690.0" x2="1355.0" y2="690.0" stroke="#fff" stroke-width="8"/>
+<text x="1355.0" y="405.0" text-anchor="middle" dominant-baseline="middle" font-size="52" fill="#2D3748" font-weight="600">玄关</text>
+</g>
+<g class="fpc-room" data-room="厨房">
+<rect x="960.0" y="120.0" width="790.0" height="570.0" fill="#F7FAFC" stroke="#2D3748" stroke-width="6"/>
+<line x1="1265.0" y1="690.0" x2="1355.0" y2="690.0" stroke="#fff" stroke-width="8"/>
+<text x="1355.0" y="405.0" text-anchor="middle" dominant-baseline="middle" font-size="52" fill="#2D3748" font-weight="600">厨房</text>
+</g>
+<g class="fpc-room" data-room="餐厅">
+<rect x="960.0" y="880.0" width="790.0" height="570.0" fill="#F7FAFC" stroke="#2D3748" stroke-width="6"/>
+<line x1="1265.0" y1="1450.0" x2="1355.0" y2="1450.0" stroke="#fff" stroke-width="8"/>
+<text x="1355.0" y="1165.0" text-anchor="middle" dominant-baseline="middle" font-size="52" fill="#2D3748" font-weight="600">餐厅</text>
+</g>
+<g class="fpc-room" data-room="客厅">
+<rect x="960.0" y="880.0" width="790.0" height="570.0" fill="#F7FAFC" stroke="#2D3748" stroke-width="6"/>
+<line x1="1265.0" y1="1450.0" x2="1355.0" y2="1450.0" stroke="#fff" stroke-width="8"/>
+<text x="1355.0" y="1165.0" text-anchor="middle" dominant-baseline="middle" font-size="52" fill="#2D3748" font-weight="600">客厅</text>
+</g>
+<g class="fpc-room" data-room="主卧">
+<rect x="960.0" y="880.0" width="790.0" height="570.0" fill="#F7FAFC" stroke="#2D3748" stroke-width="6"/>
+<line x1="1265.0" y1="1450.0" x2="1355.0" y2="1450.0" stroke="#fff" stroke-width="8"/>
+<text x="1355.0" y="1165.0" text-anchor="middle" dominant-baseline="middle" font-size="52" fill="#2D3748" font-weight="600">主卧</text>
+</g>
+<g class="fpc-room" data-room="次卧">
+<rect x="960.0" y="1640.0" width="790.0" height="570.0" fill="#F7FAFC" stroke="#2D3748" stroke-width="6"/>
+<line x1="1265.0" y1="2210.0" x2="1355.0" y2="2210.0" stroke="#fff" stroke-width="8"/>
+<text x="1355.0" y="1925.0" text-anchor="middle" dominant-baseline="middle" font-size="52" fill="#2D3748" font-weight="600">次卧</text>
+</g>
+<g class="fpc-room" data-room="书房/第三卧室">
+<rect x="120.0" y="120.0" width="790.0" height="570.0" fill="#F7FAFC" stroke="#2D3748" stroke-width="6"/>
+<line x1="425.0" y1="690.0" x2="515.0" y2="690.0" stroke="#fff" stroke-width="8"/>
+<text x="515.0" y="405.0" text-anchor="middle" dominant-baseline="middle" font-size="52" fill="#2D3748" font-weight="600">书房/第三卧室</text>
+</g>
+<g class="fpc-room" data-room="卫生间(公卫)">
+<rect x="960.0" y="120.0" width="790.0" height="570.0" fill="#F7FAFC" stroke="#2D3748" stroke-width="6"/>
+<line x1="1265.0" y1="690.0" x2="1355.0" y2="690.0" stroke="#fff" stroke-width="8"/>
+<text x="1355.0" y="405.0" text-anchor="middle" dominant-baseline="middle" font-size="52" fill="#2D3748" font-weight="600">卫生间(公卫)</text>
+</g>
+<g class="fpc-room" data-room="卫生间(主卫)">
+<rect x="960.0" y="880.0" width="790.0" height="570.0" fill="#F7FAFC" stroke="#2D3748" stroke-width="6"/>
+<line x1="1265.0" y1="1450.0" x2="1355.0" y2="1450.0" stroke="#fff" stroke-width="8"/>
+<text x="1355.0" y="1165.0" text-anchor="middle" dominant-baseline="middle" font-size="52" fill="#2D3748" font-weight="600">卫生间(主卫)</text>
+</g>
+<g class="fpc-room" data-room="生活阳台">
+<rect x="1800.0" y="1640.0" width="790.0" height="570.0" fill="#F7FAFC" stroke="#2D3748" stroke-width="6"/>
+<line x1="2105.0" y1="2210.0" x2="2195.0" y2="2210.0" stroke="#fff" stroke-width="8"/>
+<text x="2195.0" y="1925.0" text-anchor="middle" dominant-baseline="middle" font-size="52" fill="#2D3748" font-weight="600">生活阳台</text>
+</g>
+<g class="fpc-room" data-room="阳台(双景阳台)">
+<rect x="120.0" y="1640.0" width="790.0" height="570.0" fill="#F7FAFC" stroke="#2D3748" stroke-width="6"/>
+<line x1="425.0" y1="2210.0" x2="515.0" y2="2210.0" stroke="#fff" stroke-width="8"/>
+<text x="515.0" y="1925.0" text-anchor="middle" dominant-baseline="middle" font-size="52" fill="#2D3748" font-weight="600">阳台(双景阳台)</text>
+</g>
+</g>
+<g id="fpc-cabinets">
+<g class="fpc-cab" data-cab="玄关鞋柜">
+<rect x="320.0" y="320.0" width="240.0" height="240.0" rx="8" fill="#BEE3F8" stroke="#1F497D" stroke-width="5" stroke-dasharray="18 8"/>
+<text x="440.0" y="440.0" text-anchor="middle" dominant-baseline="middle" font-size="44" fill="#1F497D" font-weight="700">玄关鞋柜 1.2×1.2</text>
+</g>
+<g class="fpc-cab" data-cab="厨房橱柜">
+<rect x="320.0" y="320.0" width="920.0" height="170.0" rx="8" fill="#FEEBC8" stroke="#1F497D" stroke-width="5" stroke-dasharray="18 8"/>
+<text x="780.0" y="405.0" text-anchor="middle" dominant-baseline="middle" font-size="44" fill="#1F497D" font-weight="700">厨房橱柜 4.6×0.85</text>
+</g>
+<g class="fpc-cab" data-cab="主卧衣柜">
+<rect x="320.0" y="320.0" width="520.0" height="240.0" rx="8" fill="#BEE3F8" stroke="#1F497D" stroke-width="5" stroke-dasharray="18 8"/>
+<text x="580.0" y="440.0" text-anchor="middle" dominant-baseline="middle" font-size="44" fill="#1F497D" font-weight="700">主卧衣柜 2.6×1.2</text>
+</g>
+<g class="fpc-cab" data-cab="次卧衣柜">
+<rect x="320.0" y="320.0" width="360.0" height="240.0" rx="8" fill="#BEE3F8" stroke="#1F497D" stroke-width="5" stroke-dasharray="18 8"/>
+<text x="500.0" y="440.0" text-anchor="middle" dominant-baseline="middle" font-size="44" fill="#1F497D" font-weight="700">次卧衣柜 1.8×1.2</text>
+</g>
+<g class="fpc-cab" data-cab="阳台柜">
+<rect x="320.0" y="320.0" width="300.0" height="240.0" rx="8" fill="#BEE3F8" stroke="#1F497D" stroke-width="5" stroke-dasharray="18 8"/>
+<text x="470.0" y="440.0" text-anchor="middle" dominant-baseline="middle" font-size="44" fill="#1F497D" font-weight="700">阳台柜 1.5×1.2</text>
+</g>
+<g class="fpc-cab" data-cab="浴室柜">
+<rect x="320.0" y="320.0" width="300.0" height="240.0" rx="8" fill="#BEE3F8" stroke="#1F497D" stroke-width="5" stroke-dasharray="18 8"/>
+<text x="470.0" y="440.0" text-anchor="middle" dominant-baseline="middle" font-size="44" fill="#1F497D" font-weight="700">浴室柜 1.5×1.2</text>
+</g>
+<g class="fpc-cab" data-cab="衣帽间">
+<rect x="320.0" y="320.0" width="300.0" height="240.0" rx="8" fill="#BEE3F8" stroke="#1F497D" stroke-width="5" stroke-dasharray="18 8"/>
+<text x="470.0" y="440.0" text-anchor="middle" dominant-baseline="middle" font-size="44" fill="#1F497D" font-weight="700">衣帽间 1.5×1.2</text>
+</g>
+</g>
+<g id="fpc-dims">
+</g>
+<defs><marker id="arr" viewBox="0 0 10 10" refX="5" refY="5" markerWidth="5" markerHeight="5" orient="auto"><path d="M0,0L10,5L0,10z" fill="#C05621"/></marker></defs>
+</svg></div><p class="plan-note">📐 AI 结构化重绘示意图(非施工图,以实际量尺为准)</p>
+    <details class="dims"><summary>图面尺寸标注(2 条,用作交叉验证)</summary><ul><li><b>110㎡</b> — 总建筑面积(户型建筑面积)</li><li><b>3房2厅2卫</b> — 户型格局(非尺寸)</li></ul></details>
+  </div>
+
+  <div class="sec">
+    <h2>💰 报价明细 <span style="font-weight:400;font-size:11px;color:var(--mu)">(点击房间展开)</span></h2>
+    <div class="card" id="roomsCard"><div class="room" data-i="0">
+    <div class="room-h"><div><div class="nm">玄关鞋柜</div><div class="dm">1.2m×2.4m</div></div><span class="tt">¥14,400</span></div>
+    <div class="items"><div class="item"><span class="l">柜体(PET材质)<span>1.2m×2.4m · 投影2.88m²</span></span><span class="v">¥14,400</span></div></div>
+  </div><div class="room" data-i="1">
+    <div class="room-h"><div><div class="nm">厨房橱柜</div><div class="dm">4.6m×0.85m</div></div><span class="tt">¥24,500</span></div>
+    <div class="items"><div class="item"><span class="l">柜体(PET材质)<span>4.6m×0.85m · 投影4.9m</span></span><span class="v">¥24,500</span></div></div>
+  </div><div class="room" data-i="2">
+    <div class="room-h"><div><div class="nm">主卧衣柜</div><div class="dm">2.6m×2.5m</div></div><span class="tt">¥32,500</span></div>
+    <div class="items"><div class="item"><span class="l">柜体(PET材质)<span>2.6m×2.5m · 投影6.5m²</span></span><span class="v">¥32,500</span></div></div>
+  </div><div class="room" data-i="3">
+    <div class="room-h"><div><div class="nm">次卧衣柜</div><div class="dm">1.8m×2.5m</div></div><span class="tt">¥22,500</span></div>
+    <div class="items"><div class="item"><span class="l">柜体(PET材质)<span>1.8m×2.5m · 投影4.5m²</span></span><span class="v">¥22,500</span></div></div>
+  </div><div class="room" data-i="4">
+    <div class="room-h"><div><div class="nm">阳台柜</div><div class="dm">1.5m×2.4m</div></div><span class="tt">¥18,000</span></div>
+    <div class="items"><div class="item"><span class="l">柜体(PET材质)<span>1.5m×2.4m · 投影3.6m²</span></span><span class="v">¥18,000</span></div></div>
+  </div><div class="room" data-i="5">
+    <div class="room-h"><div><div class="nm">浴室柜</div><div class="dm">1.5m×2.6m</div></div><span class="tt">¥19,500</span></div>
+    <div class="items"><div class="item"><span class="l">柜体(PET材质)<span>1.5m×2.6m · 投影3.9m²</span></span><span class="v">¥19,500</span></div></div>
+  </div><div class="room" data-i="6">
+    <div class="room-h"><div><div class="nm">衣帽间</div><div class="dm">1.5m×2.6m</div></div><span class="tt">¥19,500</span></div>
+    <div class="items"><div class="item"><span class="l">柜体(PET材质)<span>1.5m×2.6m · 投影3.9m²</span></span><span class="v">¥19,500</span></div></div>
+  </div></div>
+    <div class="card" style="margin-top:8px">
+      <div class="trow"><span>项目小计</span><span class="v" id="tSub">¥150,900</span></div>
+      <div class="trow dim"><span id="tDiscLabel">原价</span><span class="v" id="tAfter">¥150,900</span></div>
+      <div class="trow" id="actRow" style="display:none"><span>活动优惠</span><span class="v" style="color:var(--ok)" id="tAct">-¥0</span></div>
+      <div class="trow total"><span>到手价</span><span class="v" id="tFinal">¥150,900</span></div>
+    </div>
+  </div>
+
+  <div class="sec">
+    <div class="selling">
+      <h3>✨ 为什么选择拉迷(标准客户)</h3>
+      <ul><li>空间利用率拉满:每一面墙都变成收纳</li><li>全国联保5年,铰链五金终身维护</li><li>设计上门量尺,3D效果图确认后再生产</li></ul>
+    </div>
+  </div>
+
+  <div class="sec" style="font-size:11px;color:var(--mu);line-height:1.7">
+    计价说明:柜体按投影面积(宽×高)×材质单价;厨房按计费延米(扣除转角重叠与冰箱位)。本报价为AI辅助估算,实际以现场量尺与正式合同为准。质保与赠送以当期活动政策为准。
+  </div>
+</div>
+
+<div class="ctrl">
+  <button class="btn sec" id="btnAdjust">⚖️ 调整报价</button>
+  <button class="btn pri" id="btnSave">📞 锁定到店礼</button>
+</div>
+
+<div class="drawer" id="drawer">
+  <div class="sheet">
+    <h3>⚖️ 销售操作台(客户不可见)</h3>
+    <div class="frow"><label>客户类型</label><div class="opts" id="optTier"><button class="chip " data-v="price">🏷️ 价格型</button><button class="chip on" data-v="standard">🏠 标准型</button><button class="chip " data-v="quality">💎 品质型</button></div></div>
+    <div class="frow"><label>材质档位</label><div class="opts" id="optMat"><button class="chip " data-v="双饰面">双饰面</button><button class="chip on" data-v="PET">PET</button><button class="chip " data-v="混油">混油</button></div></div>
+    <div class="frow"><label>报价浮动 <span id="floatV">15</span>%</label><input type="range" id="floatR" min="10" max="20" step="1" value="15"></div>
+    <div class="frow"><label>折扣</label><div class="opts" id="optDisc">
+      <button class="chip" data-v="1.00">原价</button>
+      <button class="chip" data-v="0.95">95折</button>
+      <button class="chip" data-v="0.90">9折</button>
+      <button class="chip" data-v="0.85">85折</button>
+      <button class="chip" data-v="0.80">8折</button>
+    </div></div>
+    <div class="frow"><label>柜体清单(宽=投影宽 m / 厨房为计费长)</label><div id="cabList"></div>
+      <div style="margin-top:8px"><button class="chip" id="btnAddCab">+ 增加柜体</button></div>
+    </div>
+    <div class="frow"><label>活动优惠</label><div class="opts" id="optAct"></div></div>
+    <button class="btn pri" style="width:100%" id="btnApply">应用并重算</button>
+  </div>
+</div>
+
+<script>
+// ============================================================
+// 拉迷全屋定制 · 默认报价规则库
+// 数据来源:floorplan-quotation/AI快速报价_Demo2.optimizing.html
+// (DEFAULT_RULES / Q_CUSTOMER_TIERS / DEFAULT_QUOTATION_PLANS /
+//   CABINET_EXTRA_TEMPLATES / EXTRA_METHOD_MAP / 厨房扣减规则)
+// 所有价格单位:元。修改价格请改这里的默认值或运行时覆盖 config。
+// ============================================================
+
+const METHOD = {
+  PROJECTION: '按投影面积', // 元/m²  数量 = 宽(m) × 高(m)
+  METER: '按延米',          // 元/m   数量 = 米数
+  COUNT: '按个数',          // 元/个  数量 = 个数
+};
+
+// —— 材质三档单价区间(AI快速粗略报价核心)——
+const PRICE_TIERS = [
+  { name: '双饰面', min: 3000, max: 3500 },
+  { name: 'PET',   min: 4500, max: 5000 },
+  { name: '混油',   min: 6000, max: 7000 },
+];
+const DEFAULT_TIER = 'PET';
+
+// —— 计价方式 ——
+const METHOD_OPTIONS = [
+  { value: '按投影面积', unit: '元/m²' },
+  { value: '按个数', unit: '元/个' },
+  { value: '按延米', unit: '元/m' },
+];
+
+// —— 柜体→空间 映射(识别命名用)——
+const CABINET_TO_SPACE = {
+  '主卧衣柜': '主卧', '次卧衣柜': '次卧', '儿童房衣柜': '儿童房', '老人房衣柜': '老人房',
+  '衣柜': '卧室', '客厅电视柜': '客厅', '电视柜': '客厅', '厨房橱柜': '厨房', '橱柜': '厨房',
+  '开放式厨房': '开放式厨房', '餐厅餐边柜': '餐厅', '餐边柜': '餐厅', '书房书柜': '书房',
+  '书柜': '书房', '玄关鞋柜': '玄关', '鞋柜': '玄关', '玄关柜': '玄关', '阳台柜': '阳台',
+  '衣帽间': '衣帽间', '储物柜': '储物间', '酒柜': '餐厅', '浴室柜': '卫生间', '榻榻米': '次卧',
+  '展示柜': '客厅', '洗衣机柜': '阳台', '卡座': '餐厅', '飘窗柜': '飘窗',
+};
+
+const SPACE_TO_DEFAULT_CABINET = {
+  '主卧': '主卧衣柜', '次卧': '次卧衣柜', '儿童房': '儿童房衣柜', '老人房': '老人房衣柜',
+  '客厅': '客厅电视柜', '厨房': '厨房橱柜', '开放式厨房': '开放式厨房', '餐厅': '餐厅餐边柜',
+  '客餐厅': '餐厅餐边柜', '书房': '书房书柜', '玄关': '玄关鞋柜', '阳台': '阳台柜',
+  '衣帽间': '衣帽间', '储物间': '储物柜', '卫生间': '浴室柜', '主卫': '浴室柜',
+};
+
+// —— 柜体类型 / 布局 / 门板 预设 ——
+const CUSTOM_TYPES_PRESET = {
+  cabinetType: ['主卧衣柜','次卧衣柜','儿童房衣柜','老人房衣柜','客厅电视柜','厨房橱柜','开放式厨房','餐厅餐边柜','书房书柜','玄关鞋柜','阳台柜','衣帽间','储物柜','酒柜','浴室柜','榻榻米'],
+  layoutForm: ['一字型','L型','U型','二字型','不规则'],
+  doorPanelType: ['普通平板','风影门板','PET门板','实木贴皮','烤漆门板','吸塑门板','玻璃门','百叶门','无门板'],
+};
+
+// —— 柜体默认附件模板(识别到柜型后自动配附件)——
+const CABINET_EXTRA_TEMPLATES = {
+  '玄关': ['灯带','变压器+开关','自弹器','见光板','铰链'],
+  '鞋柜': ['灯带','变压器+开关','自弹器','见光板','铰链','换鞋凳'],
+  '电视柜': ['自弹器','见光板','铰链'],
+  '展示柜': ['灯带','变压器+开关','拉手','见光板','铰链','自弹器'],
+  '酒柜': ['灯带','变压器+开关','拉手','见光板','铰链','自弹器','玻璃门升级'],
+  '洗衣机': ['自弹器','护墙板','见光板','铰链'],
+  '阳台': ['自弹器','见光板','铰链'],
+  '餐边': ['抽屉','拉手','自弹器','金属拉手','灯带','变压器+开关','见光板','铰链'],
+  '次卧': ['衣杆','金属拉手','抽屉','见光板','铰链'],
+  '儿童房': ['衣杆','金属拉手','抽屉','见光板','铰链'],
+  '书房': ['衣杆','金属拉手','抽屉','见光板','铰链'],
+  '主卧': ['金属拉手','抽屉','衣杆','自弹器','圆弧','铰链'],
+  '衣帽间': ['金属拉手','抽屉','衣杆','自弹器','圆弧','铰链','裤架领带格','拉篮'],
+  '厨房': ['见光板','碗篮','锅篮','自弹器'],
+  '_default': ['自弹器','见光板','铰链'],
+};
+
+// —— 附件计价方式映射 ——
+const EXTRA_METHOD_MAP = {
+  '见光板': '按投影面积', '护墙板': '按投影面积', '玻璃门升级': '按投影面积',
+  '灯带': '按延米', '衣杆': '按延米', '石材台面': '按延米',
+  '自弹器': '按个数', '铰链': '按个数', '变压器+开关': '按个数', '拉手': '按个数',
+  '金属拉手': '按个数', '抽屉': '按个数', '裤架领带格': '按个数', '拉篮': '按个数',
+  '碗篮': '按个数', '锅篮': '按个数', '圆弧': '按个数', '换鞋凳': '按个数',
+};
+
+// —— 基础价格表(name → {price, unit, method})——
+const PRICE_BOOK = {
+  // 柜体
+  '柜体投影单价':   { price: 1852, unit: '元/m²', method: '按投影面积' },
+  '生态澳松板柜体': { price: 1852, unit: '元/m²', method: '按投影面积' },
+  '铝格栅板柜体':   { price: 2532, unit: '元/m²', method: '按投影面积' },
+  // 厨房(单件计价模式)
+  '地柜':   { price: 2380, unit: '元/m', method: '按延米' },
+  '吊柜':   { price: 1748, unit: '元/m', method: '按延米' },
+  '冰箱吊柜': { price: 2622, unit: '元/m', method: '按延米' },
+  '高柜':   { price: 5950, unit: '元/m', method: '按延米' },
+  '台面':   { price: 1480, unit: '元/m', method: '按延米' },
+  '台面超出部分': { price: 2480, unit: '元/m', method: '按延米' },
+  '碗篮':   { price: 1268, unit: '元/个', method: '按个数' },
+  '锅篮':   { price: 1628, unit: '元/个', method: '按个数' },
+  '切角':   { price: 500, unit: '元/个', method: '按个数' },
+  '台下盆': { price: 500, unit: '元/个', method: '按个数' },
+  '包管':   { price: 500, unit: '元/个', method: '按个数' },
+  // 门板单价
+  '标配门板': { price: 380, unit: '元/m²', method: '按投影面积' },
+  '烤漆门板': { price: 580, unit: '元/m²', method: '按投影面积' },
+  '吸塑门板': { price: 480, unit: '元/m²', method: '按投影面积' },
+  '实木门板': { price: 880, unit: '元/m²', method: '按投影面积' },
+  '铝格栅门板': { price: 680, unit: '元/m²', method: '按投影面积' },
+  'PET门板':  { price: 450, unit: '元/m²', method: '按投影面积' },
+  // 附件
+  '见光板':   { price: 1138, unit: '元/m²', method: '按投影面积' },
+  '护墙板':   { price: 1852, unit: '元/m²', method: '按投影面积' },
+  '玻璃门升级': { price: 1852, unit: '元/m²', method: '按投影面积' },
+  '灯带':     { price: 386, unit: '元/m', method: '按延米' },
+  '变压器+开关': { price: 536, unit: '元/个', method: '按个数' },
+  '自弹器':   { price: 115, unit: '元/个', method: '按个数' },
+  '铰链':     { price: 40, unit: '元/个', method: '按个数' },
+  '拉手':     { price: 226, unit: '元/个', method: '按个数' },
+  '金属拉手': { price: 225, unit: '元/个', method: '按个数' },
+  '抽屉':     { price: 1300, unit: '元/个', method: '按个数' },
+  '衣杆':     { price: 288, unit: '元/m', method: '按延米' },
+  '裤架领带格': { price: 0, unit: '元/个', method: '按个数' },
+  '拉篮':     { price: 0, unit: '元/个', method: '按个数' },
+  '圆弧':     { price: 1560, unit: '元/个', method: '按个数' },
+  '换鞋凳':   { price: 0, unit: '元/个', method: '按个数' },
+  '石材台面': { price: 1480, unit: '元/m', method: '按延米' },
+};
+
+// —— 厨房计价参数(qGetKitchenBillableMeters 同源)——
+const KITCHEN_RULES = {
+  packageMeters: 3,     // 3米基础套餐(单件计价模式的扣减基准)
+  overlapMm: 600,       // 转角重叠扣减
+  fridgeMm: 1000,       // 冰箱位预留
+  typeThresholdM: 2,    // 最小边 ≤2m 判 L 型,>2m 判 U 型
+};
+
+// —— 客户分型(销售策略核心)——
+const CUSTOMER_TIERS = {
+  price: {
+    label: '看重价格型', icon: '🏷️',
+    strategy: '预算少的客户直接按性价比报,快速赢单',
+    defaultPlan: 'economy', defaultDiscount: 0.85,
+    doorType: '标配', hardware: '经济实用款', service: '标准测量+安装',
+    warranty: '3年质保', gifts: '送基础五金配件', panelLabel: '进口爱格板(加价升级)',
+    autoEnableActivity: true,
+    discounts: [
+      { id: 'p1', label: '投影面积85折', rate: 0.85, deletable: false },
+      { id: 'p2', label: '投影面积8折特惠', rate: 0.80, deletable: true },
+      { id: 'p3', label: '团购78折', rate: 0.78, deletable: true },
+    ],
+  },
+  standard: {
+    label: '标准客户', icon: '🏠',
+    strategy: '标准配置报价,正常谈单空间',
+    defaultPlan: 'standard', defaultDiscount: 1.0,
+    doorType: '标配门板', hardware: '国产优质铰链/拉手', service: '标准测量安装',
+    warranty: '5年质保', gifts: '可参与活动优惠', panelLabel: '国产E0级颗粒板',
+    autoEnableActivity: false,
+    discounts: [
+      { id: 's1', label: '原价', rate: 1.00, deletable: false },
+      { id: 's2', label: '投影面积95折', rate: 0.95, deletable: true },
+      { id: 's3', label: '投影面积9折', rate: 0.90, deletable: true },
+    ],
+  },
+  quality: {
+    label: '品质导向型', icon: '💎',
+    strategy: '有预算的客户报高一点,留谈价空间,突出配置与质感',
+    defaultPlan: 'quality', defaultDiscount: 1.0,
+    doorType: '烤漆/吸塑高端门板', hardware: '进口百隆/海蒂诗五金', service: 'VIP专属设计师+金牌安装',
+    warranty: '10年质保+终身维护', gifts: '赠灯带+抽屉升级包', panelLabel: '进口爱格板/可丽芙(推荐)',
+    premiumRate: 1.15,
+    autoEnableActivity: false,
+    discounts: [
+      { id: 'q1', label: '投影面积95折(保利润)', rate: 0.95, deletable: false },
+      { id: 'q2', label: '原价(高端不打折)', rate: 1.00, deletable: true },
+      { id: 'q3', label: 'VIP专享92折', rate: 0.92, deletable: true },
+    ],
+  },
+};
+
+// —— 报价方案 ——
+const QUOTATION_PLANS = [
+  { id: 'economy',  name: '经济型方案', description: '适合预算敏感型客户,主打性价比', customerTier: 'price',    tierName: '双饰面', floatPct: 10, discountRate: 0.98 },
+  { id: 'standard', name: '标准型方案', description: '默认方案,标准配置,适合大多数客户', customerTier: 'standard', tierName: 'PET',    floatPct: 15, discountRate: 1.00 },
+  { id: 'quality',  name: '品质型方案', description: '高端配置,适合追求品质的客户', customerTier: 'quality',   tierName: '混油',   floatPct: 15, discountRate: 1.00 },
+];
+
+// —— 营销活动模块(可勾选叠加)——
+const ACTIVITY_MODULES = [
+  { id: 'deposit5000', type: 'deposit', name: '定金膨胀(5千档)', desc: '交¥5,000→膨胀¥8,000 + ¥500抵用券', amount: 3500, minOrder: 30000 },
+  { id: 'deposit10000', type: 'deposit', name: '定金膨胀(1万档)', desc: '交¥10,000→膨胀¥13,000 + ¥800抵用券', amount: 3800, minOrder: 30000 },
+  { id: 'coupon20k', type: 'coupon', name: '满减券', desc: '满¥20,000减¥1,000', amount: 1000, threshold: 20000 },
+  { id: 'cashback', type: 'tiered_cashback', name: '阶梯返现', tiers: [
+      { min: 50000, cash: 2000 }, { min: 100000, cash: 5000 }, { min: 150000, cash: 8000 },
+    ] },
+];
+
+// —— 默认层高与房间面积先验(识别失败兜底估算用)——
+const DEFAULTS = {
+  height: 2.8, // 默认柜高(层高)
+  roomAreaPriors: { // 常见房间典型面积范围 [min, max](m²)
+    '主卧': [12, 22], '次卧': [8, 16], '儿童房': [8, 14], '老人房': [9, 15], '书房': [6, 12],
+    '客厅': [18, 35], '餐厅': [8, 18], '厨房': [4.5, 10], '卫生间': [2.5, 6], '主卫': [3, 6],
+    '公卫': [2.5, 6], '阳台': [3, 10], '玄关': [2, 6], '衣帽间': [3, 10], '储物间': [1.5, 5],
+  },
+  cabinetWallRatio: { // 柜体默认占所在墙面比例(无明确尺寸时的估算)
+    '主卧衣柜': 0.55, '次卧衣柜': 0.50, '儿童房衣柜': 0.50, '玄关鞋柜': 0.6,
+    '客厅电视柜': 0.55, '餐厅餐边柜': 0.5, '阳台柜': 0.6, '书房书柜': 0.5,
+  },
+};
+
+// —— 赢单文案素材(业主视角价值描述)——
+const SELLING_POINTS = {
+  price: [
+    '按投影面积计价,明明白白,没有隐形收费',
+    'E0级环保板材,装完即可安心入住',
+    '工厂直供价,同样的预算多做一个柜子',
+  ],
+  standard: [
+    '空间利用率拉满:每一面墙都变成收纳',
+    '全国联保5年,铰链五金终身维护',
+    '设计上门量尺,3D效果图确认后再生产',
+  ],
+  quality: [
+    '进口板材+进口五金,用20年不变形不褪色',
+    'VIP设计师一对一,全屋风格统一设计',
+    '10年质保+终身维护,一次装修终身省心',
+  ],
+};
+
+// ============================================================
+// 拉迷全屋定制 · 报价引擎(纯函数,浏览器/CLI 共用)
+// 两种模式:
+//   quick  — AI快速粗略报价:投影面积/厨房延米 × 三档材质区间单价
+//   detail — 明细报价:Excel 预算表模型(柜体+门板补差+附件逐项)
+// 销售策略:客户分型 → 方案/折扣/浮动 → 活动叠加 → 谈价区间
+// ============================================================
+
+const r2 = (v) => Math.round(v * 100) / 100;
+const num = (v, d = 0) => { const n = parseFloat(v); return Number.isFinite(n) ? n : d; };
+
+// ---------- 材质档位 ----------
+function getTier(name) {
+  const t = PRICE_TIERS.find((x) => x.name === (name || DEFAULT_TIER));
+  return t || PRICE_TIERS[1];
+}
+// 浮动百分比(10~15)在区间内线性插值 → 单价(qGetAiUnitPrice 同源逻辑)
+function tierUnitPrice(tier, floatPct = 15) {
+  const k = Math.min(1, Math.max(0, (num(floatPct, 15) - 10) / 5));
+  return r2(tier.min + k * (tier.max - tier.min));
+}
+
+// ---------- 厨房计费米数(qGetKitchenBillableMeters 同源)----------
+function kitchenBillable(walls = [], rules = {}) {
+  const R = { ...KITCHEN_RULES, ...rules };
+  const ws = (Array.isArray(walls) ? walls : []).map((v) => num(v)).filter((v) => v > 0);
+  if (!ws.length) return null;
+  const total = ws.reduce((a, b) => a + b, 0);
+  const minWall = Math.min(...ws);
+  const maxWall = Math.max(...ws);
+  const type = minWall <= R.typeThresholdM ? 'L' : 'U';
+  const overlapCount = type === 'L' ? 1 : 2;
+  const usedLen = type === 'L' ? maxWall + minWall : maxWall + maxWall + minWall;
+  const billable = Math.max(0, usedLen - (overlapCount * R.overlapMm) / 1000 - R.fridgeMm / 1000);
+  return {
+    walls: ws, total: r2(total), minWall: r2(minWall), maxWall: r2(maxWall),
+    kitchenType: type, overlapCount, fridgeMm: R.fridgeMm, usedLen: r2(usedLen), billable: r2(billable),
+  };
+}
+const isKitchen = (name = '') => /厨房|橱柜/.test(String(name));
+
+// ---------- 附件数量启发式(Excel 模型总结,均可编辑) ----------
+function estimateExtras(cabinet) {
+  const w = num(cabinet.width, 1.5);
+  const h = num(cabinet.height, DEFAULTS.height);
+  const type = String(cabinet.type || '');
+  let tplKey = Object.keys(CABINET_EXTRA_TEMPLATES).find((k) => k !== '_default' && type.includes(k));
+  if (!tplKey && isKitchen(type)) tplKey = '厨房';
+  const names = CABINET_EXTRA_TEMPLATES[tplKey || '_default'] || CABINET_EXTRA_TEMPLATES._default;
+  const sections = h > 1.6 ? 2 : 1;               // 高柜分上下段
+  const doorsPerSection = Math.max(1, Math.round(w / 0.45));
+  const doors = sections * doorsPerSection;
+  const hingePerDoor = h > 2 ? 3 : 2;
+  const out = [];
+  for (const name of names) {
+    const pb = PRICE_BOOK[name];
+    if (!pb) continue;
+    let qty = 0; let note = '';
+    switch (name) {
+      case '铰链': qty = doors * hingePerDoor; note = `${doors}门×${hingePerDoor}铰链`; break;
+      case '自弹器': qty = Math.max(1, doors - 1); note = '反弹门配'; break;
+      case '见光板': qty = 1; note = '单侧见光(0.5m宽)'; break;
+      case '灯带': qty = r2(w * 2); note = '顶底双层'; break;
+      case '变压器+开关': qty = 1; note = 'LED电源'; break;
+      case '衣杆': qty = r2(w); note = '通长挂杆'; break;
+      case '抽屉': qty = type.includes('主卧') || type.includes('衣帽') ? 3 : 2; break;
+      case '金属拉手': case '拉手': qty = doors + 2; break;
+      case '碗篮': case '锅篮': qty = 1; break;
+      default: qty = name === '玻璃门升级' || name === '护墙板' ? 0 : 1;
+    }
+    if (qty > 0) out.push({ name, method: pb.method, unit: pb.unit, unitPrice: pb.price, qty, note });
+  }
+  return out;
+}
+
+// ---------- 单柜体计价 ----------
+// 计价数量:投影面积 = 宽×高;厨房 = 计费延米
+function cabinetQty(cabinet, rules = {}) {
+  if (isKitchen(cabinet.type)) {
+    const kb = kitchenBillable(cabinet.walls || [cabinet.width, cabinet.height, cabinet.width, cabinet.height], rules);
+    return { qty: kb ? kb.billable : num(cabinet.width), unit: 'm', kb };
+  }
+  return { qty: r2(num(cabinet.width) * num(cabinet.height)), unit: 'm²', kb: null };
+}
+
+// 快速模式:单柜体一行
+function quickLine(cabinet, { tierName, floatPct = 15, rules } = {}) {
+  const tier = getTier(tierName);
+  const U = tierUnitPrice(tier, floatPct);
+  const { qty, unit, kb } = cabinetQty(cabinet, rules);
+  return {
+    mode: 'quick', name: cabinet.type, tierName: tier.name,
+    qty, unit, unitPrice: U, total: r2(qty * U), kb,
+    dims: num(cabinet.width) && num(cabinet.height) ? `${num(cabinet.width)}m×${num(cabinet.height)}m` : '',
+  };
+}
+
+// 明细模式:单柜体 → Excel 风格明细行
+function detailLines(cabinet, opts = {}) {
+  const { doorDiffPrice = PRICE_BOOK['柜体投影单价'].price > 0 ? 538 : 538, rules, includeDoorDiff = true } = opts;
+  const lines = [];
+  const isK = isKitchen(cabinet.type);
+  const { qty } = cabinetQty(cabinet, rules);
+  if (isK) {
+    // 厨房单件计价(Excel 模式:地柜+吊柜+台面+功能五金)
+    const walls = cabinet.walls || [cabinet.width, cabinet.height, cabinet.width, cabinet.height];
+    const straight = Math.max(0, ...walls); // 直线段米数近似
+    for (const [name, factor, note] of [
+      ['地柜', 1, '沿墙地柜'], ['吊柜', 1, '沿墙吊柜'], ['台面', 1, '石英石台面'],
+    ]) {
+      const pb = PRICE_BOOK[name];
+      lines.push({ name, qty: r2(qty), unit: 'm', unitPrice: pb.price, total: r2(qty * pb.price), note, method: pb.method });
+    }
+    for (const name of ['碗篮', '锅篮']) {
+      const pb = PRICE_BOOK[name];
+      lines.push({ name, qty: 1, unit: '个', unitPrice: pb.price, total: pb.price, note: '标准配置', method: pb.method });
+    }
+    return { cabinet, isKitchen: true, lines, subtotal: r2(lines.reduce((s, l) => s + l.total, 0)) };
+  }
+  const pb = PRICE_BOOK['生态澳松板柜体'];
+  lines.push({ name: '柜体', qty, unit: 'm²', unitPrice: pb.price, total: r2(qty * pb.price), note: '生态澳松板', method: pb.method });
+  if (includeDoorDiff && doorDiffPrice > 0) {
+    lines.push({ name: '板门补差价', qty, unit: 'm²', unitPrice: doorDiffPrice, total: r2(qty * doorDiffPrice), note: '双饰面门补差', method: pb.method });
+  }
+  for (const ex of estimateExtras(cabinet)) {
+    const total = ex.method === '按投影面积' ? r2(ex.qty * 0.5 * num(cabinet.height) * ex.unitPrice)
+      : ex.method === '按延米' ? r2(ex.qty * ex.unitPrice)
+      : r2(ex.qty * ex.unitPrice);
+    lines.push({ name: ex.name, qty: ex.qty, unit: ex.unit, unitPrice: ex.unitPrice, total, note: ex.note, method: ex.method });
+  }
+  return { cabinet, isKitchen: false, lines, subtotal: r2(lines.reduce((s, l) => s + l.total, 0)) };
+}
+
+// ---------- 报价主入口 ----------
+// inputs: { cabinets:[{type,width,height,walls?,excluded?}], tier, floatPct, discount, tierKey(客户分型), planId, mode }
+function buildQuote(input = {}) {
+  const {
+    cabinets = [], mode = 'quick',
+    tierName = input.tier || DEFAULT_TIER,
+    floatPct = 15, discount = null, tierKey = 'standard', planId = null,
+    rules = {}, extras = [], // extras: [{name, room, qty, unitPrice}] 手动增删项
+    clientName = '', note = '',
+  } = input;
+
+  const plan = QUOTATION_PLANS.find((p) => p.id === (planId || CUSTOMER_TIERS[tierKey]?.defaultPlan)) || QUOTATION_PLANS[1];
+  const custTier = CUSTOMER_TIERS[tierKey] || CUSTOMER_TIERS.standard;
+  const effDiscount = discount != null ? num(discount) : (custTier.defaultDiscount ?? plan.discountRate ?? 1);
+
+  const rooms = cabinets
+    .filter((c) => !c.excluded)
+    .map((c, i) => {
+      const base = mode === 'detail' ? detailLines(c, { rules }) : { ...quickLine(c, { tierName, floatPct, rules }), lines: null };
+      const id = c.id || `cab_${i + 1}`;
+      return { id, type: c.type, dims: base.dims || `${num(c.width)}m×${num(c.height)}m`, ...base };
+    });
+
+  const itemsSubtotal = r2(rooms.reduce((s, r) => s + (r.subtotal ?? r.total), 0));
+  const manualExtras = extras.map((e, i) => ({
+    id: e.id || `ext_${i + 1}`, name: e.name, qty: num(e.qty, 1), unit: e.unit || '项',
+    unitPrice: num(e.unitPrice), total: r2(num(e.qty, 1) * num(e.unitPrice)), kind: e.kind || 'add',
+  }));
+  const extrasTotal = r2(manualExtras.reduce((s, e) => s + (e.kind === 'remove' ? -e.total : e.total), 0));
+
+  const premiumRate = custTier.premiumRate || 1;
+  const afterPremium = r2((itemsSubtotal + extrasTotal) * premiumRate);
+  const afterDiscount = r2(afterPremium * effDiscount);
+
+  // 活动叠加(自动/手动)
+  const acts = (input.activities || (custTier.autoEnableActivity ? ACTIVITY_MODULES.map((m) => m.id) : []))
+    .map((id) => ACTIVITY_MODULES.find((m) => m.id === id)).filter(Boolean);
+  let activityTotal = 0;
+  const activityLines = [];
+  for (const m of acts) {
+    let amt = 0; let desc = m.desc || m.name;
+    if (m.type === 'tiered_cashback') {
+      const hit = [...(m.tiers || [])].sort((a, b) => b.min - a.min).find((t) => afterDiscount >= t.min);
+      if (!hit) continue;
+      amt = hit.cash; desc = `满¥${hit.min.toLocaleString()}返¥${hit.cash.toLocaleString()}`;
+    } else if (m.type === 'coupon' && afterDiscount < (m.threshold || 0)) continue;
+    else if (m.type === 'deposit' && afterDiscount < (m.minOrder || 30000)) continue;
+    else amt = m.amount || 0;
+    activityTotal += amt;
+    activityLines.push({ id: m.id, name: m.name, desc, amount: amt });
+  }
+  const finalPrice = Math.max(0, r2(afterDiscount - activityTotal));
+
+  // 谈价区间(浮动率)
+  const rate = num(floatPct, 15) / 100;
+  const range = { low: Math.round(finalPrice * (1 - rate)), high: Math.round(finalPrice * (1 + rate)) };
+
+  // 业主视角价值点
+  const selling = SELLING_POINTS[tierKey] || SELLING_POINTS.standard;
+
+  return {
+    meta: {
+      clientName, note: note || custTier.strategy, mode,
+      tierName: mode === 'quick' ? getTier(tierName).name : undefined,
+      floatPct, planId: plan.id, planName: plan.name,
+      customerTier: tierKey, customerLabel: custTier.label,
+      discount: effDiscount, premiumRate: premiumRate !== 1 ? premiumRate : undefined,
+      generatedAt: new Date().toISOString(),
+    },
+    rooms, manualExtras, activityLines,
+    totals: {
+      itemsSubtotal, extrasTotal, premiumRate, afterPremium, discount: effDiscount,
+      afterDiscount, activityTotal, finalPrice, range,
+      discountLabel: custTier.discounts?.find((d) => Math.abs(d.rate - effDiscount) < 0.001)?.label || `折扣${Math.round(effDiscount * 100)}%`,
+    },
+    selling,
+    config: { tierKey, tierName, floatPct, discount: effDiscount, planId: plan.id, mode },
+  };
+}
+
+// ---------- 语义化调整应用(销售边聊边改) ----------
+// ops: [{op:'add'|'remove'|'resize'|'rename'|'price', target, ...}]
+function applyOps(cabinets, ops = []) {
+  const out = cabinets.map((c) => ({ ...c }));
+  for (const op of ops) {
+    if (op.op === 'add') {
+      out.push({ type: op.type || op.name || '柜体', width: num(op.width, 1.5), height: num(op.height, DEFAULTS.height), from: '语音新增' });
+    } else if (op.op === 'remove') {
+      const i = out.findIndex((c) => c.type === op.target);
+      if (i >= 0) out.splice(i, 1);
+    } else if (op.op === 'resize') {
+      const c = out.find((c) => c.type === op.target);
+      if (c) { if (op.width) c.width = num(op.width); if (op.height) c.height = num(op.height); c.from = '手动调整'; }
+    } else if (op.op === 'rename') {
+      const c = out.find((c) => c.type === op.target);
+      if (c) c.type = op.to || c.type;
+    } else if (op.op === 'exclude') {
+      const c = out.find((c) => c.type === op.target);
+      if (c) c.excluded = true;
+    } else if (op.op === 'include') {
+      const c = out.find((c) => c.type === op.target);
+      if (c) delete c.excluded;
+    }
+  }
+  return out;
+}
+
+// 授权范围内校验(浮动区间即授权边界)
+function withinAuthority(quote, price) {
+  const { range } = quote.totals;
+  return price >= range.low && price <= range.high;
+}
+
+var __DEFAULT__ = { buildQuote, applyOps, quickLine, detailLines, kitchenBillable, tierUnitPrice, getTier, withinAuthority };
+
+window.__Q__ = {"cabinets":[{"id":"cab_1","type":"玄关鞋柜","width":1.2,"height":2.6,"rawName":"玄关鞋柜"},{"id":"cab_2","type":"厨房橱柜","height":2.6,"rawName":"厨房橱柜"},{"id":"cab_3","type":"主卧衣柜","width":2.6,"height":2.6,"rawName":"主卧衣柜"},{"id":"cab_4","type":"次卧衣柜","width":1.8,"height":2.6,"rawName":"次卧衣柜"},{"id":"cab_5","type":"阳台柜","width":1.5,"height":2.6,"rawName":"阳台柜"},{"id":"cab_6","type":"浴室柜","width":1.5,"height":2.6,"rawName":"浴室柜"},{"id":"cab_7","type":"衣帽间","width":1.5,"height":2.6,"rawName":"衣帽间"}],"config":{"tierKey":"standard","tierName":"PET","floatPct":15,"discount":1,"planId":"standard","mode":"quick"},"salesNote":""};
+console.log('fpc engine ready');
+</script>
+<script>
+(function(){
+  var Q = window.__Q__; var S = document; var money = function(n){ return '¥' + Math.round(n||0).toLocaleString('zh-CN'); };
+  var cfg = Object.assign({ tierKey:'standard', tierName:'PET', floatPct:15, discount:1.0, mode:'quick' }, Q.config||{});
+  function calc(){ return buildQuote({ cabinets: Q.cabinets, tierKey: cfg.tierKey, tierName: cfg.tierName, floatPct: cfg.floatPct, discount: cfg.discount, mode: cfg.mode, clientName: "缪女士(时代之城)" }); }
+  function refresh(){
+    var q = calc(); var t = q.totals;
+    S.getElementById('finalPrice').textContent = money(t.finalPrice);
+    S.getElementById('finalRange').textContent = '谈价参考区间 ' + money(t.range.low) + ' ~ ' + money(t.range.high);
+    S.getElementById('tSub').textContent = money(t.itemsSubtotal);
+    S.getElementById('tDiscLabel').textContent = t.discountLabel;
+    S.getElementById('tAfter').textContent = money(t.afterDiscount);
+    S.getElementById('tAct').textContent = '-' + money(t.activityTotal);
+    S.getElementById('tFinal').textContent = money(t.finalPrice);
+    S.getElementById('bTier').textContent = (q.meta.tierName||'') + '材质';
+    S.getElementById('bDisc').textContent = t.discountLabel;
+  }
+  // room expand
+  S.querySelectorAll('.room-h').forEach(function(el){ el.addEventListener('click', function(){ el.parentElement.classList.toggle('open'); }); });
+  // chips
+  function bindChips(id, fn){ var box = S.getElementById(id); if(!box) return; box.addEventListener('click', function(e){ var b = e.target.closest('.chip'); if(!b) return; box.querySelectorAll('.chip').forEach(function(x){x.classList.remove('on');}); b.classList.add('on'); fn(b.dataset.v); }); }
+  bindChips('optTier', function(v){ cfg.tierKey = v; refresh(); });
+  bindChips('optMat', function(v){ cfg.tierName = v; refresh(); });
+  bindChips('optDisc', function(v){ cfg.discount = parseFloat(v); refresh(); });
+  var fr = S.getElementById('floatR'); fr.addEventListener('input', function(){ S.getElementById('floatV').textContent = fr.value; cfg.floatPct = parseInt(fr.value); refresh(); });
+  // drawer
+  var d = S.getElementById('drawer');
+  S.getElementById('btnAdjust').addEventListener('click', function(){ d.classList.add('open'); renderCabs(); });
+  d.addEventListener('click', function(e){ if(e.target === d) d.classList.remove('open'); });
+  function renderCabs(){
+    var box = S.getElementById('cabList'); box.innerHTML = '';
+    Q.cabinets.forEach(function(c, i){
+      var row = S.createElement('div'); row.className = 'cabrow';
+      var dims = String(c.width||'') ? String(c.width) : '';
+      row.innerHTML = '<span style="flex:1">' + c.rawName + '</span>' +
+        '<input type="number" step="0.05" value="' + (c.width||1.5) + '" data-i="' + i + '" data-f="width" title="宽(米)">' +
+        '<span class="del" data-i="' + i + '">✕</span>';
+      box.appendChild(row);
+    });
+    box.onclick = function(e){
+      var del = e.target.closest('.del');
+      if (del) { Q.cabinets.splice(parseInt(del.dataset.i),1); renderCabs(); refresh(); return; }
+      var inp = e.target.closest('input');
+      if (inp) { inp.addEventListener('change', function(){ Q.cabinets[parseInt(inp.dataset.i)].width = parseFloat(inp.value)||1.5; refresh(); }); }
+    };
+  }
+  S.getElementById('btnAddCab').addEventListener('click', function(){ Q.cabinets.push({ type:'储物柜', rawName:'储物柜', width:1.5, height:2.6 }); renderCabs(); refresh(); });
+  S.getElementById('btnSave').addEventListener('click', function(){
+    alert('已记录!销售将带着这份报价与您确认到店时间,到店凭此单领取到店礼。');
+  });
+  refresh();
+})();
+</script>
+</body>
+</html>

+ 301 - 0
skill/package-lock.json

@@ -0,0 +1,301 @@
+{
+  "name": "@lami/floorplan-quote-skill",
+  "version": "0.1.0",
+  "lockfileVersion": 3,
+  "requires": true,
+  "packages": {
+    "": {
+      "name": "@lami/floorplan-quote-skill",
+      "version": "0.1.0",
+      "dependencies": {
+        "@napi-rs/canvas": "^0.1.100",
+        "pdfjs-dist": "^4.10.38"
+      },
+      "bin": {
+        "fpc": "bin/fpc.js"
+      },
+      "optionalDependencies": {
+        "@napi-rs/canvas": "^0.1.100",
+        "pdfjs-dist": "^4.10.38"
+      }
+    },
+    "node_modules/@napi-rs/canvas": {
+      "version": "0.1.100",
+      "resolved": "https://registry.npmjs.org/@napi-rs/canvas/-/canvas-0.1.100.tgz",
+      "integrity": "sha512-xglYA6q3XO5P3BNJYxVZ1IV7DLVjp1Py6nwag88YntrS+3vKHyYcMqXVS4ZztJmwz2uGvz1FWhI/4LgbR5uQDA==",
+      "license": "MIT",
+      "optional": true,
+      "workspaces": [
+        "e2e/*"
+      ],
+      "engines": {
+        "node": ">= 10"
+      },
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/Brooooooklyn"
+      },
+      "optionalDependencies": {
+        "@napi-rs/canvas-android-arm64": "0.1.100",
+        "@napi-rs/canvas-darwin-arm64": "0.1.100",
+        "@napi-rs/canvas-darwin-x64": "0.1.100",
+        "@napi-rs/canvas-linux-arm-gnueabihf": "0.1.100",
+        "@napi-rs/canvas-linux-arm64-gnu": "0.1.100",
+        "@napi-rs/canvas-linux-arm64-musl": "0.1.100",
+        "@napi-rs/canvas-linux-riscv64-gnu": "0.1.100",
+        "@napi-rs/canvas-linux-x64-gnu": "0.1.100",
+        "@napi-rs/canvas-linux-x64-musl": "0.1.100",
+        "@napi-rs/canvas-win32-arm64-msvc": "0.1.100",
+        "@napi-rs/canvas-win32-x64-msvc": "0.1.100"
+      }
+    },
+    "node_modules/@napi-rs/canvas-android-arm64": {
+      "version": "0.1.100",
+      "resolved": "https://registry.npmjs.org/@napi-rs/canvas-android-arm64/-/canvas-android-arm64-0.1.100.tgz",
+      "integrity": "sha512-hjhCKhntPv9+t4ckHymdx0phYNcVW+GKQR6Lzw2zE+pOVjOplSmtx9nNNknTjbEDLcuLZqA1y8ufKg1XfgftzQ==",
+      "cpu": [
+        "arm64"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": ">= 10"
+      },
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/Brooooooklyn"
+      }
+    },
+    "node_modules/@napi-rs/canvas-darwin-arm64": {
+      "version": "0.1.100",
+      "resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-arm64/-/canvas-darwin-arm64-0.1.100.tgz",
+      "integrity": "sha512-2PcswRaC7Ly645DGt88///zuFDhJxJYdKAs1uU3mfk1atYkXufgcgLfBpk6Tm12nCQBaNt1wpybuPZ4qOhTo8A==",
+      "cpu": [
+        "arm64"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": ">= 10"
+      },
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/Brooooooklyn"
+      }
+    },
+    "node_modules/@napi-rs/canvas-darwin-x64": {
+      "version": "0.1.100",
+      "resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-x64/-/canvas-darwin-x64-0.1.100.tgz",
+      "integrity": "sha512-ePNZtj7pNIva/siZMg+HmbeozkIjqUIYdoymH8HaA3qK7LfzFN4WMBM8G6HQ9ZC+H3+Dnn5pqtiXpgLykaPOhw==",
+      "cpu": [
+        "x64"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": ">= 10"
+      },
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/Brooooooklyn"
+      }
+    },
+    "node_modules/@napi-rs/canvas-linux-arm-gnueabihf": {
+      "version": "0.1.100",
+      "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm-gnueabihf/-/canvas-linux-arm-gnueabihf-0.1.100.tgz",
+      "integrity": "sha512-d5cDB48oWFGU8/XPhUOFAlySgb/VAu7D+s8fi55K1Pcfg8aPplHWqMgibhVLU8ky7Pyg/fuiVLz4Nf3JrSTuUA==",
+      "cpu": [
+        "arm"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">= 10"
+      },
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/Brooooooklyn"
+      }
+    },
+    "node_modules/@napi-rs/canvas-linux-arm64-gnu": {
+      "version": "0.1.100",
+      "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-gnu/-/canvas-linux-arm64-gnu-0.1.100.tgz",
+      "integrity": "sha512-rDxgxRu69RvDlX/bh9o22DxLsGr8EqsNgotL9+RwQE1S0b0cqeatqsw6aW45mukm0B42DIAaAacKaYQ8cqS1nw==",
+      "cpu": [
+        "arm64"
+      ],
+      "libc": [
+        "glibc"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">= 10"
+      },
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/Brooooooklyn"
+      }
+    },
+    "node_modules/@napi-rs/canvas-linux-arm64-musl": {
+      "version": "0.1.100",
+      "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-musl/-/canvas-linux-arm64-musl-0.1.100.tgz",
+      "integrity": "sha512-K3mDW66N+xT2/V439u1alFANiBUjdEx2gLiNYnCmUsva5jZMxWTjafBYwTzYK+EMFMHrUoabuU+T1BIP5CgbYQ==",
+      "cpu": [
+        "arm64"
+      ],
+      "libc": [
+        "musl"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">= 10"
+      },
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/Brooooooklyn"
+      }
+    },
+    "node_modules/@napi-rs/canvas-linux-riscv64-gnu": {
+      "version": "0.1.100",
+      "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-riscv64-gnu/-/canvas-linux-riscv64-gnu-0.1.100.tgz",
+      "integrity": "sha512-mooqUBTIsccZpnoQC4NgrC1v6C1vof39etLNMnBwCY+p0gajWJvAHLGQ6g/gGyS5YrpDW+GefSN4+Cvcr08UWw==",
+      "cpu": [
+        "riscv64"
+      ],
+      "libc": [
+        "glibc"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">= 10"
+      },
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/Brooooooklyn"
+      }
+    },
+    "node_modules/@napi-rs/canvas-linux-x64-gnu": {
+      "version": "0.1.100",
+      "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-gnu/-/canvas-linux-x64-gnu-0.1.100.tgz",
+      "integrity": "sha512-1eCvkDCazm7FFhsT7DfGOdSaHgZVK3bt/dSBl5EWHOWmnz+I7j8tPseJqqD81NF+MH21jKUK4wQSDjN0mdhnTg==",
+      "cpu": [
+        "x64"
+      ],
+      "libc": [
+        "glibc"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">= 10"
+      },
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/Brooooooklyn"
+      }
+    },
+    "node_modules/@napi-rs/canvas-linux-x64-musl": {
+      "version": "0.1.100",
+      "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-musl/-/canvas-linux-x64-musl-0.1.100.tgz",
+      "integrity": "sha512-20arT6lnI19S68qNlii73TSEDbECNgzMz2EpldC1V3mZFuRkeujXkcebRk0LRJe9SEUAooYiLokfMViY8IX7yA==",
+      "cpu": [
+        "x64"
+      ],
+      "libc": [
+        "musl"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">= 10"
+      },
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/Brooooooklyn"
+      }
+    },
+    "node_modules/@napi-rs/canvas-win32-arm64-msvc": {
+      "version": "0.1.100",
+      "resolved": "https://registry.npmjs.org/@napi-rs/canvas-win32-arm64-msvc/-/canvas-win32-arm64-msvc-0.1.100.tgz",
+      "integrity": "sha512-DZFFT1wIAg37LJw37yhMRFfjATd3vTQzjZ1Yki8u2vhO6Hi5VE6BVaGQ1aaDu7xb4iMErz+9EOwjpS7xcxFeBw==",
+      "cpu": [
+        "arm64"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">= 10"
+      },
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/Brooooooklyn"
+      }
+    },
+    "node_modules/@napi-rs/canvas-win32-x64-msvc": {
+      "version": "0.1.100",
+      "resolved": "https://registry.npmjs.org/@napi-rs/canvas-win32-x64-msvc/-/canvas-win32-x64-msvc-0.1.100.tgz",
+      "integrity": "sha512-MyT1j3mHC2+Lu4pBi9mKyMJhtP6U7k7EldY7sj/uS5gJA65gTXt8MefJQXLJo5d/vZbuWmfxzkEUNc/urV3pHA==",
+      "cpu": [
+        "x64"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">= 10"
+      },
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/Brooooooklyn"
+      }
+    },
+    "node_modules/pdfjs-dist": {
+      "version": "4.10.38",
+      "resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-4.10.38.tgz",
+      "integrity": "sha512-/Y3fcFrXEAsMjJXeL9J8+ZG9U01LbuWaYypvDW2ycW1jL269L3js3DVBjDJ0Up9Np1uqDXsDrRihHANhZOlwdQ==",
+      "license": "Apache-2.0",
+      "optional": true,
+      "engines": {
+        "node": ">=20"
+      },
+      "optionalDependencies": {
+        "@napi-rs/canvas": "^0.1.65"
+      }
+    }
+  }
+}

+ 27 - 0
skill/package.json

@@ -0,0 +1,27 @@
+{
+  "name": "@lami/floorplan-quote-skill",
+  "version": "0.1.0",
+  "type": "module",
+  "description": "拉迷全屋定制 · 户型图智能报价技能(JS ESM + GLM 视觉,浏览器/CLI 共用)",
+  "bin": {
+    "fpc": "bin/fpc.js"
+  },
+  "exports": {
+    ".": "./src/index.js",
+    "./quote": "./src/quote.js",
+    "./defaults": "./src/defaults.js",
+    "./vision": "./src/vision.js",
+    "./geometry": "./src/geometry.js",
+    "./redraw": "./src/redraw.js",
+    "./report": "./src/report.js",
+    "./publish": "./src/publish.js",
+    "./parse": "./src/parse.js"
+  },
+  "scripts": {
+    "test": "node test/run.js"
+  },
+  "optionalDependencies": {
+    "@napi-rs/canvas": "^0.1.100",
+    "pdfjs-dist": "^4.10.38"
+  }
+}

+ 226 - 0
skill/src/defaults.js

@@ -0,0 +1,226 @@
+// ============================================================
+// 拉迷全屋定制 · 默认报价规则库
+// 数据来源:floorplan-quotation/AI快速报价_Demo2.optimizing.html
+// (DEFAULT_RULES / Q_CUSTOMER_TIERS / DEFAULT_QUOTATION_PLANS /
+//   CABINET_EXTRA_TEMPLATES / EXTRA_METHOD_MAP / 厨房扣减规则)
+// 所有价格单位:元。修改价格请改这里的默认值或运行时覆盖 config。
+// ============================================================
+
+export const METHOD = {
+  PROJECTION: '按投影面积', // 元/m²  数量 = 宽(m) × 高(m)
+  METER: '按延米',          // 元/m   数量 = 米数
+  COUNT: '按个数',          // 元/个  数量 = 个数
+};
+
+// —— 材质三档单价区间(AI快速粗略报价核心)——
+export const PRICE_TIERS = [
+  { name: '双饰面', min: 3000, max: 3500 },
+  { name: 'PET',   min: 4500, max: 5000 },
+  { name: '混油',   min: 6000, max: 7000 },
+];
+export const DEFAULT_TIER = 'PET';
+
+// —— 计价方式 ——
+export const METHOD_OPTIONS = [
+  { value: '按投影面积', unit: '元/m²' },
+  { value: '按个数', unit: '元/个' },
+  { value: '按延米', unit: '元/m' },
+];
+
+// —— 柜体→空间 映射(识别命名用)——
+export const CABINET_TO_SPACE = {
+  '主卧衣柜': '主卧', '次卧衣柜': '次卧', '儿童房衣柜': '儿童房', '老人房衣柜': '老人房',
+  '衣柜': '卧室', '客厅电视柜': '客厅', '电视柜': '客厅', '厨房橱柜': '厨房', '橱柜': '厨房',
+  '开放式厨房': '开放式厨房', '餐厅餐边柜': '餐厅', '餐边柜': '餐厅', '书房书柜': '书房',
+  '书柜': '书房', '玄关鞋柜': '玄关', '鞋柜': '玄关', '玄关柜': '玄关', '阳台柜': '阳台',
+  '衣帽间': '衣帽间', '储物柜': '储物间', '酒柜': '餐厅', '浴室柜': '卫生间', '榻榻米': '次卧',
+  '展示柜': '客厅', '洗衣机柜': '阳台', '卡座': '餐厅', '飘窗柜': '飘窗',
+};
+
+export const SPACE_TO_DEFAULT_CABINET = {
+  '主卧': '主卧衣柜', '次卧': '次卧衣柜', '儿童房': '儿童房衣柜', '老人房': '老人房衣柜',
+  '客厅': '客厅电视柜', '厨房': '厨房橱柜', '开放式厨房': '开放式厨房', '餐厅': '餐厅餐边柜',
+  '客餐厅': '餐厅餐边柜', '书房': '书房书柜', '玄关': '玄关鞋柜', '阳台': '阳台柜',
+  '衣帽间': '衣帽间', '储物间': '储物柜', '卫生间': '浴室柜', '主卫': '浴室柜',
+};
+
+// —— 柜体类型 / 布局 / 门板 预设 ——
+export const CUSTOM_TYPES_PRESET = {
+  cabinetType: ['主卧衣柜','次卧衣柜','儿童房衣柜','老人房衣柜','客厅电视柜','厨房橱柜','开放式厨房','餐厅餐边柜','书房书柜','玄关鞋柜','阳台柜','衣帽间','储物柜','酒柜','浴室柜','榻榻米'],
+  layoutForm: ['一字型','L型','U型','二字型','不规则'],
+  doorPanelType: ['普通平板','风影门板','PET门板','实木贴皮','烤漆门板','吸塑门板','玻璃门','百叶门','无门板'],
+};
+
+// —— 柜体默认附件模板(识别到柜型后自动配附件)——
+export const CABINET_EXTRA_TEMPLATES = {
+  '玄关': ['灯带','变压器+开关','自弹器','见光板','铰链'],
+  '鞋柜': ['灯带','变压器+开关','自弹器','见光板','铰链','换鞋凳'],
+  '电视柜': ['自弹器','见光板','铰链'],
+  '展示柜': ['灯带','变压器+开关','拉手','见光板','铰链','自弹器'],
+  '酒柜': ['灯带','变压器+开关','拉手','见光板','铰链','自弹器','玻璃门升级'],
+  '洗衣机': ['自弹器','护墙板','见光板','铰链'],
+  '阳台': ['自弹器','见光板','铰链'],
+  '餐边': ['抽屉','拉手','自弹器','金属拉手','灯带','变压器+开关','见光板','铰链'],
+  '次卧': ['衣杆','金属拉手','抽屉','见光板','铰链'],
+  '儿童房': ['衣杆','金属拉手','抽屉','见光板','铰链'],
+  '书房': ['衣杆','金属拉手','抽屉','见光板','铰链'],
+  '主卧': ['金属拉手','抽屉','衣杆','自弹器','圆弧','铰链'],
+  '衣帽间': ['金属拉手','抽屉','衣杆','自弹器','圆弧','铰链','裤架领带格','拉篮'],
+  '厨房': ['见光板','碗篮','锅篮','自弹器'],
+  '_default': ['自弹器','见光板','铰链'],
+};
+
+// —— 附件计价方式映射 ——
+export const EXTRA_METHOD_MAP = {
+  '见光板': '按投影面积', '护墙板': '按投影面积', '玻璃门升级': '按投影面积',
+  '灯带': '按延米', '衣杆': '按延米', '石材台面': '按延米',
+  '自弹器': '按个数', '铰链': '按个数', '变压器+开关': '按个数', '拉手': '按个数',
+  '金属拉手': '按个数', '抽屉': '按个数', '裤架领带格': '按个数', '拉篮': '按个数',
+  '碗篮': '按个数', '锅篮': '按个数', '圆弧': '按个数', '换鞋凳': '按个数',
+};
+
+// —— 基础价格表(name → {price, unit, method})——
+export const PRICE_BOOK = {
+  // 柜体
+  '柜体投影单价':   { price: 1852, unit: '元/m²', method: '按投影面积' },
+  '生态澳松板柜体': { price: 1852, unit: '元/m²', method: '按投影面积' },
+  '铝格栅板柜体':   { price: 2532, unit: '元/m²', method: '按投影面积' },
+  // 厨房(单件计价模式)
+  '地柜':   { price: 2380, unit: '元/m', method: '按延米' },
+  '吊柜':   { price: 1748, unit: '元/m', method: '按延米' },
+  '冰箱吊柜': { price: 2622, unit: '元/m', method: '按延米' },
+  '高柜':   { price: 5950, unit: '元/m', method: '按延米' },
+  '台面':   { price: 1480, unit: '元/m', method: '按延米' },
+  '台面超出部分': { price: 2480, unit: '元/m', method: '按延米' },
+  '碗篮':   { price: 1268, unit: '元/个', method: '按个数' },
+  '锅篮':   { price: 1628, unit: '元/个', method: '按个数' },
+  '切角':   { price: 500, unit: '元/个', method: '按个数' },
+  '台下盆': { price: 500, unit: '元/个', method: '按个数' },
+  '包管':   { price: 500, unit: '元/个', method: '按个数' },
+  // 门板单价
+  '标配门板': { price: 380, unit: '元/m²', method: '按投影面积' },
+  '烤漆门板': { price: 580, unit: '元/m²', method: '按投影面积' },
+  '吸塑门板': { price: 480, unit: '元/m²', method: '按投影面积' },
+  '实木门板': { price: 880, unit: '元/m²', method: '按投影面积' },
+  '铝格栅门板': { price: 680, unit: '元/m²', method: '按投影面积' },
+  'PET门板':  { price: 450, unit: '元/m²', method: '按投影面积' },
+  // 附件
+  '见光板':   { price: 1138, unit: '元/m²', method: '按投影面积' },
+  '护墙板':   { price: 1852, unit: '元/m²', method: '按投影面积' },
+  '玻璃门升级': { price: 1852, unit: '元/m²', method: '按投影面积' },
+  '灯带':     { price: 386, unit: '元/m', method: '按延米' },
+  '变压器+开关': { price: 536, unit: '元/个', method: '按个数' },
+  '自弹器':   { price: 115, unit: '元/个', method: '按个数' },
+  '铰链':     { price: 40, unit: '元/个', method: '按个数' },
+  '拉手':     { price: 226, unit: '元/个', method: '按个数' },
+  '金属拉手': { price: 225, unit: '元/个', method: '按个数' },
+  '抽屉':     { price: 1300, unit: '元/个', method: '按个数' },
+  '衣杆':     { price: 288, unit: '元/m', method: '按延米' },
+  '裤架领带格': { price: 0, unit: '元/个', method: '按个数' },
+  '拉篮':     { price: 0, unit: '元/个', method: '按个数' },
+  '圆弧':     { price: 1560, unit: '元/个', method: '按个数' },
+  '换鞋凳':   { price: 0, unit: '元/个', method: '按个数' },
+  '石材台面': { price: 1480, unit: '元/m', method: '按延米' },
+};
+
+// —— 厨房计价参数(qGetKitchenBillableMeters 同源)——
+export const KITCHEN_RULES = {
+  packageMeters: 3,     // 3米基础套餐(单件计价模式的扣减基准)
+  overlapMm: 600,       // 转角重叠扣减
+  fridgeMm: 1000,       // 冰箱位预留
+  typeThresholdM: 2,    // 最小边 ≤2m 判 L 型,>2m 判 U 型
+};
+
+// —— 客户分型(销售策略核心)——
+export const CUSTOMER_TIERS = {
+  price: {
+    label: '看重价格型', icon: '🏷️',
+    strategy: '预算少的客户直接按性价比报,快速赢单',
+    defaultPlan: 'economy', defaultDiscount: 0.85,
+    doorType: '标配', hardware: '经济实用款', service: '标准测量+安装',
+    warranty: '3年质保', gifts: '送基础五金配件', panelLabel: '进口爱格板(加价升级)',
+    autoEnableActivity: true,
+    discounts: [
+      { id: 'p1', label: '投影面积85折', rate: 0.85, deletable: false },
+      { id: 'p2', label: '投影面积8折特惠', rate: 0.80, deletable: true },
+      { id: 'p3', label: '团购78折', rate: 0.78, deletable: true },
+    ],
+  },
+  standard: {
+    label: '标准客户', icon: '🏠',
+    strategy: '标准配置报价,正常谈单空间',
+    defaultPlan: 'standard', defaultDiscount: 1.0,
+    doorType: '标配门板', hardware: '国产优质铰链/拉手', service: '标准测量安装',
+    warranty: '5年质保', gifts: '可参与活动优惠', panelLabel: '国产E0级颗粒板',
+    autoEnableActivity: false,
+    discounts: [
+      { id: 's1', label: '原价', rate: 1.00, deletable: false },
+      { id: 's2', label: '投影面积95折', rate: 0.95, deletable: true },
+      { id: 's3', label: '投影面积9折', rate: 0.90, deletable: true },
+    ],
+  },
+  quality: {
+    label: '品质导向型', icon: '💎',
+    strategy: '有预算的客户报高一点,留谈价空间,突出配置与质感',
+    defaultPlan: 'quality', defaultDiscount: 1.0,
+    doorType: '烤漆/吸塑高端门板', hardware: '进口百隆/海蒂诗五金', service: 'VIP专属设计师+金牌安装',
+    warranty: '10年质保+终身维护', gifts: '赠灯带+抽屉升级包', panelLabel: '进口爱格板/可丽芙(推荐)',
+    premiumRate: 1.15,
+    autoEnableActivity: false,
+    discounts: [
+      { id: 'q1', label: '投影面积95折(保利润)', rate: 0.95, deletable: false },
+      { id: 'q2', label: '原价(高端不打折)', rate: 1.00, deletable: true },
+      { id: 'q3', label: 'VIP专享92折', rate: 0.92, deletable: true },
+    ],
+  },
+};
+
+// —— 报价方案 ——
+export const QUOTATION_PLANS = [
+  { id: 'economy',  name: '经济型方案', description: '适合预算敏感型客户,主打性价比', customerTier: 'price',    tierName: '双饰面', floatPct: 10, discountRate: 0.98 },
+  { id: 'standard', name: '标准型方案', description: '默认方案,标准配置,适合大多数客户', customerTier: 'standard', tierName: 'PET',    floatPct: 15, discountRate: 1.00 },
+  { id: 'quality',  name: '品质型方案', description: '高端配置,适合追求品质的客户', customerTier: 'quality',   tierName: '混油',   floatPct: 15, discountRate: 1.00 },
+];
+
+// —— 营销活动模块(可勾选叠加)——
+export const ACTIVITY_MODULES = [
+  { id: 'deposit5000', type: 'deposit', name: '定金膨胀(5千档)', desc: '交¥5,000→膨胀¥8,000 + ¥500抵用券', amount: 3500, minOrder: 30000 },
+  { id: 'deposit10000', type: 'deposit', name: '定金膨胀(1万档)', desc: '交¥10,000→膨胀¥13,000 + ¥800抵用券', amount: 3800, minOrder: 30000 },
+  { id: 'coupon20k', type: 'coupon', name: '满减券', desc: '满¥20,000减¥1,000', amount: 1000, threshold: 20000 },
+  { id: 'cashback', type: 'tiered_cashback', name: '阶梯返现', tiers: [
+      { min: 50000, cash: 2000 }, { min: 100000, cash: 5000 }, { min: 150000, cash: 8000 },
+    ] },
+];
+
+// —— 默认层高与房间面积先验(识别失败兜底估算用)——
+export const DEFAULTS = {
+  height: 2.8, // 默认柜高(层高)
+  roomAreaPriors: { // 常见房间典型面积范围 [min, max](m²)
+    '主卧': [12, 22], '次卧': [8, 16], '儿童房': [8, 14], '老人房': [9, 15], '书房': [6, 12],
+    '客厅': [18, 35], '餐厅': [8, 18], '厨房': [4.5, 10], '卫生间': [2.5, 6], '主卫': [3, 6],
+    '公卫': [2.5, 6], '阳台': [3, 10], '玄关': [2, 6], '衣帽间': [3, 10], '储物间': [1.5, 5],
+  },
+  cabinetWallRatio: { // 柜体默认占所在墙面比例(无明确尺寸时的估算)
+    '主卧衣柜': 0.55, '次卧衣柜': 0.50, '儿童房衣柜': 0.50, '玄关鞋柜': 0.6,
+    '客厅电视柜': 0.55, '餐厅餐边柜': 0.5, '阳台柜': 0.6, '书房书柜': 0.5,
+  },
+};
+
+// —— 赢单文案素材(业主视角价值描述)——
+export const SELLING_POINTS = {
+  price: [
+    '按投影面积计价,明明白白,没有隐形收费',
+    'E0级环保板材,装完即可安心入住',
+    '工厂直供价,同样的预算多做一个柜子',
+  ],
+  standard: [
+    '空间利用率拉满:每一面墙都变成收纳',
+    '全国联保5年,铰链五金终身维护',
+    '设计上门量尺,3D效果图确认后再生产',
+  ],
+  quality: [
+    '进口板材+进口五金,用20年不变形不褪色',
+    'VIP设计师一对一,全屋风格统一设计',
+    '10年质保+终身维护,一次装修终身省心',
+  ],
+};

+ 69 - 0
skill/src/geometry.js

@@ -0,0 +1,69 @@
+// ============================================================
+// 拉迷全屋定制 · 几何与单位工具(浏览器/CLI 共用)
+// ============================================================
+
+export const isKitchenName = (name = '') => /厨房|橱柜|厨柜/.test(String(name));
+
+// mm/m 归一:>=100 视为毫米,转米
+export function toMeters(v) {
+  const n = parseFloat(v);
+  if (!Number.isFinite(n) || n <= 0) return 0;
+  return n >= 100 ? n / 1000 : n;
+}
+
+export function clamp(v, a, b) { return Math.min(b, Math.max(a, v)); }
+export function r2(v) { return Math.round(v * 100) / 100; }
+export function r3(v) { return Math.round(v * 1000) / 1000; }
+
+// 从一段文本中抽取所有数字(支持 3600、3.6m、3米6 等写法的简单情形)
+export function extractNumbers(text = '') {
+  return String(text).match(/\d+(?:\.\d+)?/g)?.map(Number) || [];
+}
+
+// 判断一组标注数字最可能是"毫米尺寸线"还是"米"
+export function likelyMeters(nums = []) {
+  const valid = nums.filter((n) => n > 0);
+  if (!valid.length) return null;
+  const max = Math.max(...valid);
+  return max >= 100 ? 'mm' : 'm';
+}
+
+// 像素比例推算:用一条已知真实长度的标注(米)和它覆盖的像素长度求 米/像素
+export function metersPerPixel({ realMeters, pixelLength }) {
+  if (!realMeters || !pixelLength) return null;
+  return realMeters / pixelLength;
+}
+
+// 投影面积
+export function projectionArea(w, h) { return r2(Math.max(0, toMeters(w) || w) * Math.max(0, toMeters(h) || h)); }
+
+// 两点距离(像素坐标)
+export function dist(x1, y1, x2, y2) { return Math.hypot(x2 - x1, y2 - y1); }
+
+// 矩形重叠率(假框抑制:与文字簇大面积重叠的色块剔除,同源原项目思路)
+export function overlapRatio(a, b) {
+  const ox = Math.max(0, Math.min(a.x + a.w, b.x + b.w) - Math.max(a.x, b.x));
+  const oy = Math.max(0, Math.min(a.y + a.h, b.y + b.h) - Math.max(a.y, b.y));
+  const inter = ox * oy;
+  const aArea = a.w * a.h; const bArea = b.w * b.h;
+  if (!aArea || !bArea) return 0;
+  return inter / Math.min(aArea, bArea);
+}
+
+// 同名柜体编号(阳台柜1/阳台柜2)
+export function dedupeNames(cabinets = []) {
+  const cnt = {};
+  const seq = {};
+  return cabinets.map((c) => {
+    const name = c.type || c.name || '柜体';
+    cnt[name] = (cnt[name] || 0) + 1;
+    if (cnt[name] > 1) {
+      seq[name] = (seq[name] || 1);
+      const n = ++seq[name] - 1;
+      return { ...c, type: `${name}${n}` };
+    }
+    return c;
+  });
+}
+
+export default { isKitchenName, toMeters, clamp, r2, r3, extractNumbers, likelyMeters, metersPerPixel, projectionArea, dist, overlapRatio, dedupeNames };

+ 156 - 0
skill/src/index.js

@@ -0,0 +1,156 @@
+// ============================================================
+// 拉迷全屋定制 · 户型图智能报价技能 · 主入口
+// 流水线:文件 → 解析 → 视觉识别 → 交叉验证 → 报价 → 报告 → 发布
+// 用法(CLI):
+//   fpc quote <文件> [--client 客户名] [--note 销售备注] [--tier price|standard|quality]
+//            [--mode quick|detail] [--material 双饰面|PET|混油] [--float 15] [--discount 0.9]
+//            [--publish] [--out out/report.html] [--debug]
+// 用法(ESM):
+//   import { quoteFromFile } from '@lami/floorplan-quote-skill'
+// ============================================================
+import fs from 'node:fs';
+import path from 'node:path';
+import { loadDocument } from './parse.js';
+import { recognizeFloorplan } from './vision.js';
+import { buildQuote, applyOps } from './quote.js';
+import { renderSVG, buildPlan, fallbackImage } from './redraw.js';
+import { renderReport, writeReport } from './report.js';
+import { publish } from './publish.js';
+
+export { buildQuote, applyOps, kitchenBillable, tierUnitPrice } from './quote.js';
+export { recognizeFloorplan, analyzeFloorplan, measureCabinets, normalizeCabinetName } from './vision.js';
+export { loadDocument, imageToDataUrl, pdfToImagePages } from './parse.js';
+export { renderReport, writeReport } from './report.js';
+export { renderSVG, buildPlan } from './redraw.js';
+export { publish } from './publish.js';
+export * from './defaults.js';
+
+// ---------- 完整流水线 ----------
+export async function quoteFromFile(file, opts = {}) {
+  const {
+    clientName = '', salesNote = '', tierKey = 'standard', mode = 'quick',
+    tierName, floatPct = 15, discount, ops = [], floorHeight, publish: doPublish = false,
+    out, debug = false, apiKey, model, downscale = 1600,
+  } = opts;
+
+  // 1. 解析文件
+  const doc = await loadDocument(file, { downscale });
+  const page = doc.pages[0];
+  if (debug) console.error(`[fpc] 解析: ${doc.kind}, ${doc.numPages || 1} 页`);
+
+  // 2. 视觉识别(两阶段+交叉验证)
+  const rec = await recognizeFloorplan({
+    imageDataUrl: page.dataUrl, floorHeight, knownRooms: opts.knownRooms || {},
+    apiKey, model, debug,
+  });
+  if (debug) {
+    console.error('[fpc] 识别:', JSON.stringify(rec.cabinets, null, 1));
+    console.error('[fpc] usage:', JSON.stringify(rec._usage));
+  }
+
+  // 3. 应用销售语音调整指令(增删改)
+  let cabinets = rec.cabinets;
+  if (ops.length) cabinets = applyOps(cabinets, ops);
+
+  // 4. 报价
+  const quote = buildQuote({
+    cabinets, mode, tierKey, tierName: tierName || (tierKey === 'price' ? '双饰面' : tierKey === 'quality' ? '混油' : 'PET'),
+    floatPct, discount, clientName, note: salesNote,
+  });
+
+  // 5. 重绘(优先结构化 SVG,效果不佳可降级原图)
+  let planSVG = null; let imageDataUrl = page.dataUrl;
+  try {
+    const plan = buildPlan({ analysis: rec.analysis, cabinets, floorHeight: rec.floorHeight });
+    planSVG = renderSVG(plan);
+  } catch (e) { if (debug) console.error('[fpc] 重绘失败,降级原图:', e.message); planSVG = null; }
+
+  // 6. 渲染报告
+  const html = renderReport({ quote, planSVG, imageDataUrl, analysis: rec.analysis, clientName, salesNote });
+
+  // 7. 输出/发布
+  let published = null;
+  const outPath = out || path.join(process.cwd(), 'out', `报价单-${(clientName || '客户')}-${stamp()}.html`);
+  writeReport(html, outPath);
+  if (doPublish) {
+    published = await publish({ html, filename: path.basename(outPath) });
+  }
+  return { quote, rec, htmlPath: path.resolve(outPath), published, doc: { kind: doc.kind, numPages: doc.numPages || 1 } };
+}
+
+function stamp() { return new Date().toISOString().slice(5, 16).replace(/[-T:]/g, ''); }
+
+// ---------- CLI ----------
+export async function main(argv = process.argv.slice(2)) {
+  if (!argv.length || argv[0] === 'help' || argv[0] === '--help') {
+    console.log(`拉迷户型图智能报价 (fpc)
+
+用法:
+  fpc quote <图片/PDF路径> [选项]
+  fpc test <图片路径>              # 只跑识别,看结构化结果,不生成报告
+
+选项:
+  --client <名称>    客户名
+  --note "<一句话>"  销售对客户情况的描述(如"预算20万内,两孩家庭")
+  --tier <key>       price|standard|quality (客户分型,默认 standard)
+  --mode <m>         quick|detail (默认 quick)
+  --material <名>    双饰面|PET|混油
+  --float <10-20>    报价浮动百分比
+  --discount <0-1>   折扣(如 0.9)
+  --add "<类型>,宽"  增加柜体,如 --add "阳台柜,1.8"
+  --remove "<类型>"  删除柜体
+  --publish          同时发布(需已配置 ~/.fpc/publish.json)
+  --out <路径>       报告输出路径
+  --debug            输出识别与token用量`);
+    process.exit(0);
+  }
+  const cmd = argv[0];
+  const rest = argv.slice(1);
+  const get = (flag, def = null) => { const i = rest.indexOf(flag); return i >= 0 ? rest[i + 1] : def; };
+  const has = (f) => rest.includes(f);
+
+  if (cmd === 'test') {
+    const file = rest[0];
+    const doc = await loadDocument(file);
+    const rec = await recognizeFloorplan({ imageDataUrl: doc.pages[0].dataUrl, debug: true, apiKey: process.env.FMODE_API_KEY });
+    console.log(JSON.stringify({ analysis: rec.analysis, cabinets: rec.cabinets }, null, 2));
+    return;
+  }
+  if (cmd !== 'quote') { console.error('未知命令:', cmd); process.exit(1); }
+
+  const file = rest[0];
+  if (!file || !fs.existsSync(file)) { console.error('文件不存在:', file); process.exit(1); }
+  const ops = [];
+  const adds = rest.filter((a) => a === '--add');
+  for (const a of adds) { const v = get('--add'); if (v) { const [t, w] = v.split(','); ops.push({ op: 'add', type: t, width: parseFloat(w) || 1.5 }); } }
+  const removes = rest.filter((a) => a === '--remove');
+  for (const r of removes) { const v = get('--remove'); if (v) ops.push({ op: 'remove', target: v }); }
+
+  const result = await quoteFromFile(file, {
+    clientName: get('--client', ''),
+    salesNote: get('--note', ''),
+    tierKey: get('--tier', 'standard'),
+    mode: get('--mode', 'quick'),
+    tierName: get('--material'),
+    floatPct: parseFloat(get('--float', '15')),
+    discount: get('--discount') ? parseFloat(get('--discount')) : null,
+    ops, publish: has('--publish'), out: get('--out'), debug: has('--debug'),
+    apiKey: process.env.FMODE_API_KEY,
+  });
+  const { quote } = result;
+  const money = (n) => '¥' + Math.round(n).toLocaleString();
+  console.log('\n========== 报价完成 ==========');
+  console.log(`客户: ${quote.meta.clientName || '未命名'} | ${quote.meta.customerLabel} | ${quote.meta.tierName || quote.meta.mode === 'detail' ? '明细' : ''}`);
+  console.log(`柜体 ${quote.rooms.length} 组:`);
+  for (const r of quote.rooms) console.log(`  · ${r.type.padEnd(8)} ${String(r.dims || '').padEnd(12)} ${money(r.subtotal ?? r.total)}`);
+  console.log(`小计 ${money(quote.totals.itemsSubtotal)} → 折后 ${money(quote.totals.afterDiscount)} → 到手 ${money(quote.totals.finalPrice)}`);
+  console.log(`谈价区间: ${money(quote.totals.range.low)} ~ ${money(quote.totals.range.high)}`);
+  if (result.published?.url) console.log(`已发布: ${result.published.url}`);
+  console.log(`报告: ${result.htmlPath}`);
+  return result;
+}
+
+// 直接执行时走 CLI
+if (process.argv[1] && process.argv[1].endsWith('fpc.js') === false && import.meta.url === `file://${process.argv[1]}`) {
+  main().catch((e) => { console.error('执行失败:', e.message); process.exit(1); });
+}

+ 81 - 0
skill/src/parse.js

@@ -0,0 +1,81 @@
+// ============================================================
+// 拉迷全屋定制 · 文档解析(PDF/图片 → 视觉模型输入)
+// PDF:可选依赖 pdfjs-dist(页→PNG dataURL,含版面文字提取)
+// 图片:jpg/png/webp → dataURL(>1.5MB 自动压缩到宽 1600)
+// CLI 环境无依赖时,PDF 提取失败会给出明确指引
+// ============================================================
+import fs from 'node:fs';
+import path from 'node:path';
+
+const MIME = { '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.png': 'image/png', '.webp': 'image/webp', '.gif': 'image/gif', '.bmp': 'image/bmp' };
+
+export function isPDF(file) { return /\.pdf$/i.test(file) || sniffPDF(file); }
+function sniffPDF(file) { try { const b = fs.readFileSync(file).subarray(0, 5).toString(); return b.startsWith('%PDF'); } catch { return false; } }
+function mimeOf(file) { return MIME[path.extname(file).toLowerCase()] || 'application/octet-stream'; }
+
+export function imageToDataUrl(file, { maxWidth = 1600 } = {}) {
+  const buf = fs.readFileSync(file);
+  const mime = mimeOf(file);
+  const b64 = buf.toString('base64');
+  const dataUrl = `data:${mime};base64,${b64}`;
+  // 尺寸检查:数据体积 > 1.9MB(原始约1.4MB) 时尝试压缩(需要 sharp/@napi-rs/canvas,失败则原样返回)
+  if (buf.length > 1_400_000) {
+    try { return compressDataUrl(dataUrl, maxWidth) || dataUrl; } catch { return dataUrl; }
+  }
+  return dataUrl;
+}
+
+async function compressDataUrl(dataUrl, maxWidth) {
+  try {
+    const { createCanvas, loadImage } = await import('@napi-rs/canvas');
+    const img = await loadImage(dataUrl);
+    if (img.width <= maxWidth) return null;
+    const scale = maxWidth / img.width;
+    const canvas = createCanvas(maxWidth, Math.round(img.height * scale));
+    const ctx = canvas.getContext('2d');
+    ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
+    return canvas.toDataURL('image/jpeg', 0.85);
+  } catch {
+    return null;
+  }
+}
+
+// ---------- PDF → 页面图片 ----------
+export async function pdfToImagePages(file, { pages = null, scale = 2.0 } = {}) {
+  let pdfjs;
+  try { pdfjs = await import('pdfjs-dist/legacy/build/pdf.mjs'); }
+  catch {
+    throw new Error('PDF 解析需要 pdfjs-dist:请在 skill 目录执行 `npm i pdfjs-dist @napi-rs/canvas` 或将 PDF 转为图片后再发');
+  }
+  const data = new Uint8Array(fs.readFileSync(file));
+  const doc = await pdfjs.getDocument({ data, useSystemFonts: true }).promise;
+  const n = doc.numPages;
+  const want = pages && pages.length ? pages : Array.from({ length: n }, (_, i) => i + 1);
+  const out = [];
+  for (const pno of want) {
+    if (pno > n) continue;
+    const page = await doc.getPage(pno);
+    const viewport = page.getViewport({ scale });
+    const { createCanvas } = await import('@napi-rs/canvas');
+    const canvas = createCanvas(Math.ceil(viewport.width), Math.ceil(viewport.height));
+    const ctx = canvas.getContext('2d');
+    await page.render({ canvasContext: ctx, viewport, canvas }).promise;
+    // 页内文字一并提取(图纸标注常是矢量文本)
+    const textContent = await page.getTextContent();
+    const text = textContent.items.map((it) => it.str).join(' ').slice(0, 4000);
+    out.push({ page: pno, dataUrl: canvas.toDataURL('image/jpeg', 0.9), text });
+  }
+  return { pages: out, numPages: n };
+}
+
+// ---------- 端到端:任意文件 → 视觉输入 ----------
+export async function loadDocument(file, opts = {}) {
+  const abs = path.resolve(file);
+  if (!fs.existsSync(abs)) throw new Error(`文件不存在: ${abs}`);
+  if (isPDF(abs)) {
+    const { pages, numPages } = await pdfToImagePages(abs, opts);
+    return { kind: 'pdf', numPages, pages };
+  }
+  return { kind: 'image', pages: [{ page: 1, dataUrl: imageToDataUrl(abs), text: '' }] };
+}
+export default { loadDocument, pdfToImagePages, imageToDataUrl, isPDF };

+ 91 - 0
skill/src/publish.js

@@ -0,0 +1,91 @@
+// ============================================================
+// 拉迷全屋定制 · 报告发布(个人 S3 兼容存储,零依赖 SigV4)
+// 配置(环境变量或 ~/.fpc/publish.json):
+//   endpoint  如 https://s3.cn-north-1.amazonaws.com.cn 或自建 MinIO
+//   region / bucket / accessKeyId / secretAccessKey / prefix
+// 兜底:未配置时写入本地 out/ 目录,返回 file:// 路径
+// ============================================================
+import fs from 'node:fs';
+import path from 'node:path';
+import os from 'node:os';
+import crypto from 'node:crypto';
+
+const CONFIG_FILE = path.join(os.homedir(), '.fpc', 'publish.json');
+
+export function loadConfig(override = {}) {
+  let fileCfg = {};
+  try { fileCfg = JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8')); } catch { /* none */ }
+  const cfg = {
+    endpoint: process.env.FPC_S3_ENDPOINT || fileCfg.endpoint,
+    region: process.env.FPC_S3_REGION || fileCfg.region || 'us-east-1',
+    bucket: process.env.FPC_S3_BUCKET || fileCfg.bucket,
+    accessKeyId: process.env.FPC_S3_AK || fileCfg.accessKeyId,
+    secretAccessKey: process.env.FPC_S3_SK || fileCfg.secretAccessKey,
+    prefix: process.env.FPC_S3_PREFIX || fileCfg.prefix || 'fpc-quotes/',
+    publicBase: process.env.FPC_S3_PUBLIC_BASE || fileCfg.publicBase, // 自定义域名/CDN
+    ...override,
+  };
+  return cfg;
+}
+export function isConfigured(cfg) {
+  return !!(cfg?.endpoint && cfg?.bucket && cfg?.accessKeyId && cfg?.secretAccessKey);
+}
+
+// ---------- SigV4 ----------
+function hmac(key, data) { return crypto.createHmac('sha256', key).update(data).digest(); }
+function sha256hex(data) { return crypto.createHash('sha256').update(data).digest('hex'); }
+
+function signRequest({ method, host, path: p, query = '', headers, payload, region, accessKeyId, secretAccessKey }) {
+  const now = new Date();
+  const amzDate = now.toISOString().replace(/[:-]|\.\d{3}/g, '');
+  const dateStamp = amzDate.slice(0, 8);
+  const payloadHash = sha256hex(payload);
+  const canonicalHeaders = Object.keys(headers).sort().map((k) => `${k.toLowerCase()}:${String(headers[k]).trim()}\n`).join('');
+  const signedHeaders = Object.keys(headers).map((k) => k.toLowerCase()).sort().join(';');
+  const canonicalRequest = [method, p, query, canonicalHeaders, signedHeaders, payloadHash].join('\n');
+  const scope = `${dateStamp}/${region}/s3/aws4_request`;
+  const stringToSign = ['AWS4-HMAC-SHA256', amzDate, scope, sha256hex(canonicalRequest)].join('\n');
+  const kDate = hmac(`AWS4${secretAccessKey}`, dateStamp);
+  const kRegion = hmac(kDate, region);
+  const kService = hmac(kRegion, 's3');
+  const kSigning = hmac(kService, 'aws4_request');
+  const signature = crypto.createHmac('sha256', kSigning).update(stringToSign).digest('hex');
+  return `AWS4-HMAC-SHA256 Credential=${accessKeyId}/${scope}, SignedHeaders=${signedHeaders}, Signature=${signature}`;
+}
+
+export async function s3Put({ key, body, contentType = 'text/html', cfg }) {
+  const url = new URL(`${cfg.endpoint.replace(/\/$/, '')}/${cfg.bucket}/${key}`);
+  const host = url.host;
+  const payload = Buffer.from(body);
+  const headers = { host, 'x-amz-date': '', 'x-amz-content-sha256': sha256hex(payload), 'content-type': contentType };
+  const amzDate = new Date().toISOString().replace(/[:-]|\.\d{3}/g, '');
+  headers['x-amz-date'] = amzDate;
+  const auth = signRequest({
+    method: 'PUT', host, path: `/${cfg.bucket}/${key}`, query: '',
+    headers, payload, region: cfg.region, accessKeyId: cfg.accessKeyId, secretAccessKey: cfg.secretAccessKey,
+  });
+  const resp = await fetch(url, {
+    method: 'PUT', headers: { ...headers, Authorization: auth }, body: payload,
+  });
+  if (!resp.ok) throw new Error(`S3 上传失败 ${resp.status}: ${(await resp.text().catch(() => '')).slice(0, 200)}`);
+  const base = cfg.publicBase ? `${cfg.publicBase.replace(/\/$/, '')}/${key}` : `${cfg.endpoint.replace(/\/$/, '')}/${cfg.bucket}/${key}`;
+  return base;
+}
+
+// ---------- 发布主入口 ----------
+export async function publish({ html, filename, cfg: override } = {}) {
+  const cfg = loadConfig(override);
+  const stamp = new Date().toISOString().slice(0, 10).replace(/-/g, '');
+  const key = `${cfg.prefix}${stamp}-${filename || `quote-${Date.now()}.html`}`;
+  if (isConfigured(cfg)) {
+    const url = await s3Put({ key, body: html, contentType: 'text/html; charset=utf-8', cfg });
+    return { url, local: null, mode: 's3' };
+  }
+  // 本地兜底
+  const outDir = path.resolve(process.cwd(), 'out');
+  const local = path.join(outDir, path.basename(key));
+  fs.mkdirSync(outDir, { recursive: true });
+  fs.writeFileSync(local, html);
+  return { url: null, local, mode: 'local' };
+}
+export default { publish, loadConfig, isConfigured, s3Put };

+ 252 - 0
skill/src/quote.js

@@ -0,0 +1,252 @@
+// ============================================================
+// 拉迷全屋定制 · 报价引擎(纯函数,浏览器/CLI 共用)
+// 两种模式:
+//   quick  — AI快速粗略报价:投影面积/厨房延米 × 三档材质区间单价
+//   detail — 明细报价:Excel 预算表模型(柜体+门板补差+附件逐项)
+// 销售策略:客户分型 → 方案/折扣/浮动 → 活动叠加 → 谈价区间
+// ============================================================
+import {
+  PRICE_TIERS, DEFAULT_TIER, CUSTOMER_TIERS, QUOTATION_PLANS,
+  CABINET_EXTRA_TEMPLATES, EXTRA_METHOD_MAP, PRICE_BOOK,
+  KITCHEN_RULES, DEFAULTS, ACTIVITY_MODULES, SELLING_POINTS,
+} from './defaults.js';
+
+const r2 = (v) => Math.round(v * 100) / 100;
+const num = (v, d = 0) => { const n = parseFloat(v); return Number.isFinite(n) ? n : d; };
+
+// ---------- 材质档位 ----------
+export function getTier(name) {
+  const t = PRICE_TIERS.find((x) => x.name === (name || DEFAULT_TIER));
+  return t || PRICE_TIERS[1];
+}
+// 浮动百分比(10~15)在区间内线性插值 → 单价(qGetAiUnitPrice 同源逻辑)
+export function tierUnitPrice(tier, floatPct = 15) {
+  const k = Math.min(1, Math.max(0, (num(floatPct, 15) - 10) / 5));
+  return r2(tier.min + k * (tier.max - tier.min));
+}
+
+// ---------- 厨房计费米数(qGetKitchenBillableMeters 同源)----------
+export function kitchenBillable(walls = [], rules = {}) {
+  const R = { ...KITCHEN_RULES, ...rules };
+  const ws = (Array.isArray(walls) ? walls : []).map((v) => num(v)).filter((v) => v > 0);
+  if (!ws.length) return null;
+  const total = ws.reduce((a, b) => a + b, 0);
+  const minWall = Math.min(...ws);
+  const maxWall = Math.max(...ws);
+  const type = minWall <= R.typeThresholdM ? 'L' : 'U';
+  const overlapCount = type === 'L' ? 1 : 2;
+  const usedLen = type === 'L' ? maxWall + minWall : maxWall + maxWall + minWall;
+  const billable = Math.max(0, usedLen - (overlapCount * R.overlapMm) / 1000 - R.fridgeMm / 1000);
+  return {
+    walls: ws, total: r2(total), minWall: r2(minWall), maxWall: r2(maxWall),
+    kitchenType: type, overlapCount, fridgeMm: R.fridgeMm, usedLen: r2(usedLen), billable: r2(billable),
+  };
+}
+export const isKitchen = (name = '') => /厨房|橱柜/.test(String(name));
+
+// ---------- 附件数量启发式(Excel 模型总结,均可编辑) ----------
+export function estimateExtras(cabinet) {
+  const w = num(cabinet.width, 1.5);
+  const h = num(cabinet.height, DEFAULTS.height);
+  const type = String(cabinet.type || '');
+  let tplKey = Object.keys(CABINET_EXTRA_TEMPLATES).find((k) => k !== '_default' && type.includes(k));
+  if (!tplKey && isKitchen(type)) tplKey = '厨房';
+  const names = CABINET_EXTRA_TEMPLATES[tplKey || '_default'] || CABINET_EXTRA_TEMPLATES._default;
+  const sections = h > 1.6 ? 2 : 1;               // 高柜分上下段
+  const doorsPerSection = Math.max(1, Math.round(w / 0.45));
+  const doors = sections * doorsPerSection;
+  const hingePerDoor = h > 2 ? 3 : 2;
+  const out = [];
+  for (const name of names) {
+    const pb = PRICE_BOOK[name];
+    if (!pb) continue;
+    let qty = 0; let note = '';
+    switch (name) {
+      case '铰链': qty = doors * hingePerDoor; note = `${doors}门×${hingePerDoor}铰链`; break;
+      case '自弹器': qty = Math.max(1, doors - 1); note = '反弹门配'; break;
+      case '见光板': qty = 1; note = '单侧见光(0.5m宽)'; break;
+      case '灯带': qty = r2(w * 2); note = '顶底双层'; break;
+      case '变压器+开关': qty = 1; note = 'LED电源'; break;
+      case '衣杆': qty = r2(w); note = '通长挂杆'; break;
+      case '抽屉': qty = type.includes('主卧') || type.includes('衣帽') ? 3 : 2; break;
+      case '金属拉手': case '拉手': qty = doors + 2; break;
+      case '碗篮': case '锅篮': qty = 1; break;
+      default: qty = name === '玻璃门升级' || name === '护墙板' ? 0 : 1;
+    }
+    if (qty > 0) out.push({ name, method: pb.method, unit: pb.unit, unitPrice: pb.price, qty, note });
+  }
+  return out;
+}
+
+// ---------- 单柜体计价 ----------
+// 计价数量:投影面积 = 宽×高;厨房 = 计费延米
+export function cabinetQty(cabinet, rules = {}) {
+  if (isKitchen(cabinet.type)) {
+    const kb = kitchenBillable(cabinet.walls || [cabinet.width, cabinet.height, cabinet.width, cabinet.height], rules);
+    return { qty: kb ? kb.billable : num(cabinet.width), unit: 'm', kb };
+  }
+  return { qty: r2(num(cabinet.width) * num(cabinet.height)), unit: 'm²', kb: null };
+}
+
+// 快速模式:单柜体一行
+export function quickLine(cabinet, { tierName, floatPct = 15, rules } = {}) {
+  const tier = getTier(tierName);
+  const U = tierUnitPrice(tier, floatPct);
+  const { qty, unit, kb } = cabinetQty(cabinet, rules);
+  return {
+    mode: 'quick', name: cabinet.type, tierName: tier.name,
+    qty, unit, unitPrice: U, total: r2(qty * U), kb,
+    dims: num(cabinet.width) && num(cabinet.height) ? `${num(cabinet.width)}m×${num(cabinet.height)}m` : '',
+  };
+}
+
+// 明细模式:单柜体 → Excel 风格明细行
+export function detailLines(cabinet, opts = {}) {
+  const { doorDiffPrice = PRICE_BOOK['柜体投影单价'].price > 0 ? 538 : 538, rules, includeDoorDiff = true } = opts;
+  const lines = [];
+  const isK = isKitchen(cabinet.type);
+  const { qty } = cabinetQty(cabinet, rules);
+  if (isK) {
+    // 厨房单件计价(Excel 模式:地柜+吊柜+台面+功能五金)
+    const walls = cabinet.walls || [cabinet.width, cabinet.height, cabinet.width, cabinet.height];
+    const straight = Math.max(0, ...walls); // 直线段米数近似
+    for (const [name, factor, note] of [
+      ['地柜', 1, '沿墙地柜'], ['吊柜', 1, '沿墙吊柜'], ['台面', 1, '石英石台面'],
+    ]) {
+      const pb = PRICE_BOOK[name];
+      lines.push({ name, qty: r2(qty), unit: 'm', unitPrice: pb.price, total: r2(qty * pb.price), note, method: pb.method });
+    }
+    for (const name of ['碗篮', '锅篮']) {
+      const pb = PRICE_BOOK[name];
+      lines.push({ name, qty: 1, unit: '个', unitPrice: pb.price, total: pb.price, note: '标准配置', method: pb.method });
+    }
+    return { cabinet, isKitchen: true, lines, subtotal: r2(lines.reduce((s, l) => s + l.total, 0)) };
+  }
+  const pb = PRICE_BOOK['生态澳松板柜体'];
+  lines.push({ name: '柜体', qty, unit: 'm²', unitPrice: pb.price, total: r2(qty * pb.price), note: '生态澳松板', method: pb.method });
+  if (includeDoorDiff && doorDiffPrice > 0) {
+    lines.push({ name: '板门补差价', qty, unit: 'm²', unitPrice: doorDiffPrice, total: r2(qty * doorDiffPrice), note: '双饰面门补差', method: pb.method });
+  }
+  for (const ex of estimateExtras(cabinet)) {
+    const total = ex.method === '按投影面积' ? r2(ex.qty * 0.5 * num(cabinet.height) * ex.unitPrice)
+      : ex.method === '按延米' ? r2(ex.qty * ex.unitPrice)
+      : r2(ex.qty * ex.unitPrice);
+    lines.push({ name: ex.name, qty: ex.qty, unit: ex.unit, unitPrice: ex.unitPrice, total, note: ex.note, method: ex.method });
+  }
+  return { cabinet, isKitchen: false, lines, subtotal: r2(lines.reduce((s, l) => s + l.total, 0)) };
+}
+
+// ---------- 报价主入口 ----------
+// inputs: { cabinets:[{type,width,height,walls?,excluded?}], tier, floatPct, discount, tierKey(客户分型), planId, mode }
+export function buildQuote(input = {}) {
+  const {
+    cabinets = [], mode = 'quick',
+    tierName = input.tier || DEFAULT_TIER,
+    floatPct = 15, discount = null, tierKey = 'standard', planId = null,
+    rules = {}, extras = [], // extras: [{name, room, qty, unitPrice}] 手动增删项
+    clientName = '', note = '',
+  } = input;
+
+  const plan = QUOTATION_PLANS.find((p) => p.id === (planId || CUSTOMER_TIERS[tierKey]?.defaultPlan)) || QUOTATION_PLANS[1];
+  const custTier = CUSTOMER_TIERS[tierKey] || CUSTOMER_TIERS.standard;
+  const effDiscount = discount != null ? num(discount) : (custTier.defaultDiscount ?? plan.discountRate ?? 1);
+
+  const rooms = cabinets
+    .filter((c) => !c.excluded)
+    .map((c, i) => {
+      const base = mode === 'detail' ? detailLines(c, { rules }) : { ...quickLine(c, { tierName, floatPct, rules }), lines: null };
+      const id = c.id || `cab_${i + 1}`;
+      return { id, type: c.type, dims: base.dims || `${num(c.width)}m×${num(c.height)}m`, ...base };
+    });
+
+  const itemsSubtotal = r2(rooms.reduce((s, r) => s + (r.subtotal ?? r.total), 0));
+  const manualExtras = extras.map((e, i) => ({
+    id: e.id || `ext_${i + 1}`, name: e.name, qty: num(e.qty, 1), unit: e.unit || '项',
+    unitPrice: num(e.unitPrice), total: r2(num(e.qty, 1) * num(e.unitPrice)), kind: e.kind || 'add',
+  }));
+  const extrasTotal = r2(manualExtras.reduce((s, e) => s + (e.kind === 'remove' ? -e.total : e.total), 0));
+
+  const premiumRate = custTier.premiumRate || 1;
+  const afterPremium = r2((itemsSubtotal + extrasTotal) * premiumRate);
+  const afterDiscount = r2(afterPremium * effDiscount);
+
+  // 活动叠加(自动/手动)
+  const acts = (input.activities || (custTier.autoEnableActivity ? ACTIVITY_MODULES.map((m) => m.id) : []))
+    .map((id) => ACTIVITY_MODULES.find((m) => m.id === id)).filter(Boolean);
+  let activityTotal = 0;
+  const activityLines = [];
+  for (const m of acts) {
+    let amt = 0; let desc = m.desc || m.name;
+    if (m.type === 'tiered_cashback') {
+      const hit = [...(m.tiers || [])].sort((a, b) => b.min - a.min).find((t) => afterDiscount >= t.min);
+      if (!hit) continue;
+      amt = hit.cash; desc = `满¥${hit.min.toLocaleString()}返¥${hit.cash.toLocaleString()}`;
+    } else if (m.type === 'coupon' && afterDiscount < (m.threshold || 0)) continue;
+    else if (m.type === 'deposit' && afterDiscount < (m.minOrder || 30000)) continue;
+    else amt = m.amount || 0;
+    activityTotal += amt;
+    activityLines.push({ id: m.id, name: m.name, desc, amount: amt });
+  }
+  const finalPrice = Math.max(0, r2(afterDiscount - activityTotal));
+
+  // 谈价区间(浮动率)
+  const rate = num(floatPct, 15) / 100;
+  const range = { low: Math.round(finalPrice * (1 - rate)), high: Math.round(finalPrice * (1 + rate)) };
+
+  // 业主视角价值点
+  const selling = SELLING_POINTS[tierKey] || SELLING_POINTS.standard;
+
+  return {
+    meta: {
+      clientName, note: note || custTier.strategy, mode,
+      tierName: mode === 'quick' ? getTier(tierName).name : undefined,
+      floatPct, planId: plan.id, planName: plan.name,
+      customerTier: tierKey, customerLabel: custTier.label,
+      discount: effDiscount, premiumRate: premiumRate !== 1 ? premiumRate : undefined,
+      generatedAt: new Date().toISOString(),
+    },
+    rooms, manualExtras, activityLines,
+    totals: {
+      itemsSubtotal, extrasTotal, premiumRate, afterPremium, discount: effDiscount,
+      afterDiscount, activityTotal, finalPrice, range,
+      discountLabel: custTier.discounts?.find((d) => Math.abs(d.rate - effDiscount) < 0.001)?.label || `折扣${Math.round(effDiscount * 100)}%`,
+    },
+    selling,
+    config: { tierKey, tierName, floatPct, discount: effDiscount, planId: plan.id, mode },
+  };
+}
+
+// ---------- 语义化调整应用(销售边聊边改) ----------
+// ops: [{op:'add'|'remove'|'resize'|'rename'|'price', target, ...}]
+export function applyOps(cabinets, ops = []) {
+  const out = cabinets.map((c) => ({ ...c }));
+  for (const op of ops) {
+    if (op.op === 'add') {
+      out.push({ type: op.type || op.name || '柜体', width: num(op.width, 1.5), height: num(op.height, DEFAULTS.height), from: '语音新增' });
+    } else if (op.op === 'remove') {
+      const i = out.findIndex((c) => c.type === op.target);
+      if (i >= 0) out.splice(i, 1);
+    } else if (op.op === 'resize') {
+      const c = out.find((c) => c.type === op.target);
+      if (c) { if (op.width) c.width = num(op.width); if (op.height) c.height = num(op.height); c.from = '手动调整'; }
+    } else if (op.op === 'rename') {
+      const c = out.find((c) => c.type === op.target);
+      if (c) c.type = op.to || c.type;
+    } else if (op.op === 'exclude') {
+      const c = out.find((c) => c.type === op.target);
+      if (c) c.excluded = true;
+    } else if (op.op === 'include') {
+      const c = out.find((c) => c.type === op.target);
+      if (c) delete c.excluded;
+    }
+  }
+  return out;
+}
+
+// 授权范围内校验(浮动区间即授权边界)
+export function withinAuthority(quote, price) {
+  const { range } = quote.totals;
+  return price >= range.low && price <= range.high;
+}
+
+export default { buildQuote, applyOps, quickLine, detailLines, kitchenBillable, tierUnitPrice, getTier, withinAuthority };

+ 107 - 0
skill/src/redraw.js

@@ -0,0 +1,107 @@
+// ============================================================
+// 拉迷全屋定制 · 2D 户型图重绘(SVG,零依赖,浏览器/CLI 共用)
+// 策略(按用户要求:效果不理想可降级为原图展示):
+//   level A: 结构化重绘(房间矩形+柜体块+尺寸标注) — VLM 给出坐标时
+//   level B: 降级 — 直接嵌入原图(base64)+ 柜体标记叠加
+// SVG 优势:手机端矢量无损缩放、每个房间/柜体是 DOM 可点击交互
+// ============================================================
+import { clamp, r2 } from './geometry.js';
+
+// ---------- Level A:结构化重绘 ----------
+// plan: { rooms:[{name,x,y,w,h(米,原点左上)}], cabinets:[{type,x,y,w,h}], floorHeight, dims:[{text,x1,y1,x2,y2}] }
+export function renderSVG(plan = {}) {
+  const rooms = plan.rooms || [];
+  const cabs = plan.cabinets || [];
+  const PADDING = 0.6; // 米
+  const all = [
+    ...rooms.map((r) => ({ x: r.x, y: r.y, w: r.w, h: r.h })),
+    ...cabs.map((c) => ({ x: c.x, y: c.y, w: c.w, h: c.h })),
+  ].filter((r) => r.w > 0 && r.h > 0);
+  if (!all.length) return null;
+  const minX = Math.min(...all.map((r) => r.x)) - PADDING;
+  const minY = Math.min(...all.map((r) => r.y)) - PADDING;
+  const maxX = Math.max(...all.map((r) => r.x + r.w)) + PADDING;
+  const maxY = Math.max(...all.map((r) => r.y + r.h)) + PADDING;
+  const W = maxX - minX; const H = maxY - minY;
+  const S = 200; // 1米 = 200 svg 单位(输出按 viewBox 自适应)
+  const X = (x) => ((x - minX) * S).toFixed(1);
+  const Y = (y) => ((y - minY) * S).toFixed(1);
+  const L = (m) => (m * S).toFixed(1);
+
+  const parts = [];
+  parts.push(`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${X(maxX) - X(minX)} ${Y(maxY) - Y(minY)}" class="fpc-plan">`);
+  parts.push(`<g id="fpc-rooms">`);
+  for (const r of rooms) {
+    parts.push(`<g class="fpc-room" data-room="${r.name}">`);
+    parts.push(`<rect x="${X(r.x)}" y="${Y(r.y)}" width="${L(r.w)}" height="${L(r.h)}" fill="#F7FAFC" stroke="#2D3748" stroke-width="6"/>`);
+    // 门(简易开口示意:底边中点 0.9m 开口)
+    parts.push(`<line x1="${X(r.x + r.w / 2 - 0.45)}" y1="${Y(r.y + r.h)}" x2="${X(r.x + r.w / 2 + 0.0)}" y2="${Y(r.y + r.h)}" stroke="#fff" stroke-width="8"/>`);
+    parts.push(`<text x="${X(r.x + r.w / 2)}" y="${Y(r.y + r.h / 2)}" text-anchor="middle" dominant-baseline="middle" font-size="52" fill="#2D3748" font-weight="600">${esc(r.name)}${r.area ? `<tspan x="${X(r.x + r.w / 2)}" dy="60" font-size="40" fill="#718096">${r.area}m²</tspan>` : ''}</text>`);
+    parts.push(`</g>`);
+  }
+  parts.push(`</g>`);
+  parts.push(`<g id="fpc-cabinets">`);
+  for (const c of cabs) {
+    const isK = /厨房|橱柜/.test(c.type);
+    const fill = isK ? '#FEEBC8' : '#BEE3F8';
+    parts.push(`<g class="fpc-cab" data-cab="${esc(c.type)}">`);
+    parts.push(`<rect x="${X(c.x)}" y="${Y(c.y)}" width="${L(c.w)}" height="${L(c.h)}" rx="8" fill="${fill}" stroke="#1F497D" stroke-width="5" stroke-dasharray="18 8"/>`);
+    parts.push(`<text x="${X(c.x + c.w / 2)}" y="${Y(c.y + c.h / 2)}" text-anchor="middle" dominant-baseline="middle" font-size="44" fill="#1F497D" font-weight="700">${esc(c.type)} ${c.w}×${c.h}</text>`);
+    parts.push(`</g>`);
+  }
+  parts.push(`</g>`);
+  parts.push(`<g id="fpc-dims">`);
+  for (const d of (plan.dims || [])) {
+    if (!d || d.x1 == null) continue; // 尺寸线为可选
+    parts.push(`<g class="fpc-dim">`);
+    parts.push(`<line x1="${X(d.x1)}" y1="${Y(d.y1)}" x2="${X(d.x2)}" y2="${Y(d.y2)}" stroke="#C05621" stroke-width="4" marker-start="url(#arr)" marker-end="url(#arr)"/>`);
+    parts.push(`<text x="${X((d.x1 + d.x2) / 2)}" y="${Y((d.y1 + d.y2) / 2) - 14}" text-anchor="middle" font-size="40" fill="#C05621">${esc(d.text)}</text>`);
+    parts.push(`</g>`);
+  }
+  parts.push(`</g>`);
+  parts.push(`<defs><marker id="arr" viewBox="0 0 10 10" refX="5" refY="5" markerWidth="5" markerHeight="5" orient="auto"><path d="M0,0L10,5L0,10z" fill="#C05621"/></marker></defs>`);
+  parts.push(`</svg>`);
+  return parts.join('\n');
+}
+function esc(s) { return String(s ?? '').replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;'); }
+
+// 从 VLM 结构化数据构造重绘 plan(坐标由 VLM 直接给,或由网格布局近似)
+export function buildPlan({ analysis, cabinets, floorHeight }) {
+  // VLM 第二次调用:给出每个房间的相对坐标(0~1 归一化坐标)
+  // 若无坐标数据,用「分区网格」近似:主卧左、次卧左下、客厅右、厨房上等
+  const rooms = (analysis?.rooms || []).map((r) => {
+    const { x, y, w, h } = gridLayout(r.pos, r.approxArea);
+    return { name: r.name, x, y, w, h, area: r.approxArea || null };
+  });
+  const cabs = (cabinets || []).map((c) => {
+    const { x, y } = gridPos(c.pos || c.posLabel || '');
+    return { type: c.type, x, y, w: c.width, h: Math.min(c.height, 1.2), drawH: c.height }; // 重绘时柜体画为靠墙条带
+  });
+  return { rooms, cabinets: cabs, floorHeight: floorHeight || analysis?.floorHeight };
+}
+
+// 方位词 → 网格坐标(近似布局;纯前端展示用,标注"示意图")
+function gridPos(pos = '') {
+  const col = { 左: 0, 中: 1, 右: 2 };
+  const row = { 上: 0, 中: 1, 下: 2 };
+  let c = 1; let rr = 1;
+  for (const ch of String(pos)) {
+    if (col[ch] != null) c = col[ch];
+    if (row[ch] === 0) rr = 0; else if (row[ch] === 1) rr = 1; else if (row[ch] === 2) rr = 2;
+  }
+  return { x: c, y: rr };
+}
+function gridLayout(pos = '', area = null) {
+  const { x, y } = gridPos(pos);
+  const a = area || 10;
+  const w = clamp(Math.sqrt(a) * 1.25, 2, 6.5);
+  const h = clamp(Math.sqrt(a) * 0.9, 1.5, 5.5);
+  return { x: x * 4.2, y: y * 3.8, w: r2(w), h: r2(h) };
+}
+
+// ---------- Level B:原图降级(返回 dataUrl 直接嵌入 HTML) ----------
+export function fallbackImage(imageDataUrl) {
+  return { fallback: true, imageDataUrl };
+}
+
+export default { renderSVG, buildPlan, fallbackImage };

+ 270 - 0
skill/src/report.js

@@ -0,0 +1,270 @@
+// ============================================================
+// 拉迷全屋定制 · 报价 HTML 报告生成(手机端优先,单文件自包含)
+// 交互:材质档位/浮动率/折扣/增删柜体/活动开关 客户端即时重算
+// 技术内嵌:把 defaults.js + quote.js 打进 HTML(strip ESM 语法)
+// ============================================================
+import fs from 'node:fs';
+import path from 'node:path';
+import { fileURLToPath } from 'node:url';
+
+const __dirname = path.dirname(fileURLToPath(import.meta.url));
+const SRC = path.join(__dirname, '.');
+
+function loadEngineSource() {
+  const read = (f) => fs.readFileSync(path.join(SRC, f), 'utf8');
+  const strip = (src) => src
+    .replace(/^import[\s\S]*?from\s+['"][^'"]+['"];?\s*$/gm, '')
+    .replace(/^export\s+default\s+/gm, 'var __DEFAULT__ = ')
+    .replace(/^export\s+/gm, '');
+  return strip(read('defaults.js')) + '\n' + strip(read('quote.js'));
+}
+
+// ---------- 主入口 ----------
+export function renderReport({ quote, planSVG = null, imageDataUrl = null, analysis = null, clientName = '', salesNote = '' }) {
+  const engine = loadEngineSource();
+  const meta = quote.meta || {};
+  const t = quote.totals || {};
+  const money = (n) => '¥' + Number(n || 0).toLocaleString('zh-CN', { maximumFractionDigits: 0 });
+  const title = `${clientName || meta.clientName || '客户'} · 全屋定制报价方案`;
+
+  const planBlock = planSVG
+    ? `<div class="plan-wrap" id="planWrap">${planSVG}</div><p class="plan-note">📐 AI 结构化重绘示意图(非施工图,以实际量尺为准)</p>`
+    : imageDataUrl
+      ? `<div class="plan-wrap"><img src="${imageDataUrl}" alt="户型图" class="plan-img"></div><p class="plan-note">📐 原始户型图(以实际量尺为准)</p>`
+      : '';
+
+  const dimsBlock = analysis?.dimensions?.length
+    ? `<details class="dims"><summary>图面尺寸标注(${analysis.dimensions.length} 条,用作交叉验证)</summary><ul>${
+        analysis.dimensions.map((d) => `<li><b>${esc(d.text)}</b> — ${esc(d.what || '')}${d.nearRoom ? '(' + esc(d.nearRoom) + ')' : ''}</li>`).join('')}</ul></details>`
+    : '';
+
+  const roomsRows = (quote.rooms || []).map((r, i) => roomCard(r, i, money)).join('');
+
+  return `<!DOCTYPE html>
+<html lang="zh-CN">
+<head>
+<meta charset="UTF-8">
+<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
+<meta name="format-detection" content="telephone=no">
+<title>${esc(title)}</title>
+<style>
+:root{--p:#1F497D;--pa:#2B6CB0;--bg:#F5F7FA;--card:#fff;--bd:#E2E8F0;--tx:#2D3748;--mu:#718096;--ok:#38A169;--wa:#D69E2E;--dg:#E53E3E;--r:12px}
+*{box-sizing:border-box;margin:0;padding:0;-webkit-tap-highlight-color:transparent}
+body{font-family:-apple-system,BlinkMacSystemFont,"PingFang SC","Microsoft YaHei",sans-serif;background:var(--bg);color:var(--tx);line-height:1.55;padding-bottom:calc(88px + env(safe-area-inset-bottom))}
+header{background:linear-gradient(135deg,#1F497D,#2B6CB0);color:#fff;padding:18px 16px 14px}
+header h1{font-size:17px;font-weight:700}
+header .sub{font-size:12px;opacity:.85;margin-top:2px}
+.wrap{padding:12px;max-width:640px;margin:0 auto}
+.hero{background:var(--card);border-radius:var(--r);padding:16px;box-shadow:0 1px 3px rgba(0,0,0,.08);margin-top:-26px;position:relative;text-align:center}
+.hero .label{font-size:12px;color:var(--mu)}
+.hero .price{font-size:38px;font-weight:800;color:var(--p);letter-spacing:-1px;margin:2px 0}
+.hero .price small{font-size:16px;font-weight:600}
+.hero .range{font-size:13px;color:var(--mu)}
+.badge{display:inline-block;font-size:11px;padding:2px 10px;border-radius:20px;background:#EBF8FF;color:var(--pa);border:1px solid #BEE3F8;margin-top:6px}
+.sec{margin-top:14px}
+.sec h2{font-size:15px;font-weight:700;margin:0 2px 8px;display:flex;align-items:center;gap:6px}
+.card{background:var(--card);border-radius:var(--r);box-shadow:0 1px 3px rgba(0,0,0,.06);overflow:hidden}
+.room{border-bottom:1px solid var(--bd)}
+.room:last-child{border-bottom:none}
+.room-h{padding:12px 14px;display:flex;justify-content:space-between;align-items:center;gap:8px}
+.room-h .nm{font-weight:700;font-size:14px}
+.room-h .dm{font-size:11px;color:var(--mu)}
+.room-h .tt{font-weight:800;color:var(--p);font-size:15px;white-space:nowrap}
+.room-h .conf{font-size:10px;padding:1px 6px;border-radius:8px;background:#FFFAF0;color:#C05621;border:1px solid #FBD38D;flex-shrink:0}
+.items{padding:0 14px 10px;display:none}
+.room.open .items{display:block}
+.item{display:flex;justify-content:space-between;font-size:12.5px;padding:5px 0;color:#4A5568}
+.item .l span{color:var(--mu);font-size:11px;margin-left:6px}
+.item .v{white-space:nowrap}
+.trow{display:flex;justify-content:space-between;padding:8px 14px;font-size:13px}
+.trow .v{font-weight:700}
+.trow.total{background:#F0FFF4;color:#276749;font-weight:800;font-size:15px}
+.trow.dim{color:var(--mu);font-size:12px}
+.plan-wrap{background:var(--card);border-radius:var(--r);padding:10px;box-shadow:0 1px 3px rgba(0,0,0,.06)}
+.plan-wrap svg,.plan-img{width:100%;height:auto;display:block;border-radius:8px}
+.plan-note{font-size:11px;color:var(--mu);text-align:center;margin-top:6px}
+.selling{background:linear-gradient(135deg,#FFFAF0,#FFF5EB);border:1px solid #FBD38D;border-radius:var(--r);padding:12px 14px}
+.selling li{font-size:13px;margin:6px 0 6px 18px;color:#744210}
+.selling h3{font-size:13px;color:#C05621;margin-bottom:4px}
+.acts .item .v{color:var(--ok)}
+.ctrl{position:fixed;bottom:0;left:0;right:0;background:#fff;border-top:1px solid var(--bd);padding:10px 14px calc(10px + env(safe-area-inset-bottom));display:flex;gap:8px;z-index:99;max-width:640px;margin:0 auto}
+.btn{flex:1;padding:12px;border:none;border-radius:10px;font-size:14px;font-weight:700;cursor:pointer}
+.btn.pri{background:var(--p);color:#fff}
+.btn.sec{background:#EDF2F7;color:var(--tx)}
+details.dims{background:var(--card);border-radius:var(--r);padding:10px 14px;font-size:12px;color:var(--mu)}
+details.dims summary{cursor:pointer;font-weight:600;color:var(--tx)}
+details.dims li{margin:4px 0 4px 18px}
+.drawer{position:fixed;inset:0;background:rgba(0,0,0,.4);z-index:100;display:none}
+.drawer.open{display:block}
+.sheet{position:absolute;bottom:0;left:0;right:0;background:#fff;border-radius:16px 16px 0 0;max-height:82vh;overflow:auto;padding:16px 16px calc(20px + env(safe-area-inset-bottom));max-width:640px;margin:0 auto}
+.sheet h3{font-size:15px;margin-bottom:12px}
+.frow{margin-bottom:14px}
+.frow label{font-size:12px;color:var(--mu);display:block;margin-bottom:4px}
+.frow .opts{display:flex;gap:6px;flex-wrap:wrap}
+.chip{padding:8px 12px;border-radius:8px;border:1.5px solid var(--bd);background:#fff;font-size:13px;cursor:pointer}
+.chip.on{border-color:var(--pa);background:#EBF8FF;color:var(--pa);font-weight:700}
+input[type=range]{width:100%}
+.cabrow{display:flex;align-items:center;gap:8px;padding:8px 0;border-bottom:1px dashed var(--bd);font-size:13px}
+.cabrow input{width:64px;padding:6px;border:1px solid var(--bd);border-radius:6px;font-size:13px}
+.cabrow .del{color:var(--dg);font-size:18px;padding:0 6px;cursor:pointer}
+.cabrow .add{color:var(--ok)}
+@media(min-width:640px){.hero .price{font-size:44px}}
+</style>
+</head>
+<body>
+<header>
+  <h1>${esc(title)}</h1>
+  <div class="sub">${esc(meta.customerLabel || '')}方案 · 生成于 ${new Date(meta.generatedAt || Date.now()).toLocaleString('zh-CN', { month: 'numeric', day: 'numeric', hour: '2-digit', minute: '2-digit' })} · 拉迷全屋定制</div>
+</header>
+<div class="wrap">
+
+  <div class="hero">
+    <div class="label">全屋定制整装价</div>
+    <div class="price" id="finalPrice">${money(t.finalPrice)}</div>
+    <div class="range" id="finalRange">谈价参考区间 ${money(t.range?.low)} ~ ${money(t.range?.high)}</div>
+    <div id="badges">
+      <span class="badge" id="bTier">${esc(meta.tierName || '')}材质</span>
+      <span class="badge" id="bDisc">${esc(t.discountLabel || '')}</span>
+      ${meta.premiumRate ? `<span class="badge">品质配置 +15%</span>` : ''}
+    </div>
+  </div>
+
+  ${salesNote ? `<div class="sec"><div class="selling"><h3>💡 给销售的建议(${esc(meta.customerLabel || '')})</h3><ul><li>${esc(salesNote)}</li></ul></div></div>` : ''}
+
+  <div class="sec">
+    <h2>🏠 户型与定制布局</h2>
+    ${planBlock || '<div class="card" style="padding:14px;font-size:12px;color:var(--mu)">未提供户型图</div>'}
+    ${dimsBlock}
+  </div>
+
+  <div class="sec">
+    <h2>💰 报价明细 <span style="font-weight:400;font-size:11px;color:var(--mu)">(点击房间展开)</span></h2>
+    <div class="card" id="roomsCard">${roomsRows}</div>
+    <div class="card" style="margin-top:8px">
+      <div class="trow"><span>项目小计</span><span class="v" id="tSub">${money(t.itemsSubtotal)}</span></div>
+      <div class="trow dim"><span id="tDiscLabel">${esc(t.discountLabel || '')}</span><span class="v" id="tAfter">${money(t.afterDiscount)}</span></div>
+      <div class="trow" id="actRow" style="${t.activityTotal ? '' : 'display:none'}"><span>活动优惠</span><span class="v" style="color:var(--ok)" id="tAct">-${money(t.activityTotal)}</span></div>
+      <div class="trow total"><span>到手价</span><span class="v" id="tFinal">${money(t.finalPrice)}</span></div>
+    </div>
+  </div>
+
+  <div class="sec">
+    <div class="selling">
+      <h3>✨ 为什么选择拉迷(${esc(meta.customerLabel || '标准客户')})</h3>
+      <ul>${(quote.selling || []).map((s) => `<li>${esc(s)}</li>`).join('')}</ul>
+    </div>
+  </div>
+
+  <div class="sec" style="font-size:11px;color:var(--mu);line-height:1.7">
+    计价说明:柜体按投影面积(宽×高)×材质单价;厨房按计费延米(扣除转角重叠与冰箱位)。本报价为AI辅助估算,实际以现场量尺与正式合同为准。质保与赠送以当期活动政策为准。
+  </div>
+</div>
+
+<div class="ctrl">
+  <button class="btn sec" id="btnAdjust">⚖️ 调整报价</button>
+  <button class="btn pri" id="btnSave">📞 锁定到店礼</button>
+</div>
+
+<div class="drawer" id="drawer">
+  <div class="sheet">
+    <h3>⚖️ 销售操作台(客户不可见)</h3>
+    <div class="frow"><label>客户类型</label><div class="opts" id="optTier">${['price','standard','quality'].map((k) => `<button class="chip ${k === (quote.config?.tierKey || 'standard') ? 'on' : ''}" data-v="${k}">${ {price:'🏷️ 价格型',standard:'🏠 标准型',quality:'💎 品质型'}[k] }</button>`).join('')}</div></div>
+    <div class="frow"><label>材质档位</label><div class="opts" id="optMat">${['双饰面','PET','混油'].map((m) => `<button class="chip ${m === (quote.config?.tierName || 'PET') ? 'on' : ''}" data-v="${m}">${m}</button>`).join('')}</div></div>
+    <div class="frow"><label>报价浮动 <span id="floatV">${quote.config?.floatPct ?? 15}</span>%</label><input type="range" id="floatR" min="10" max="20" step="1" value="${quote.config?.floatPct ?? 15}"></div>
+    <div class="frow"><label>折扣</label><div class="opts" id="optDisc">
+      <button class="chip" data-v="1.00">原价</button>
+      <button class="chip" data-v="0.95">95折</button>
+      <button class="chip" data-v="0.90">9折</button>
+      <button class="chip" data-v="0.85">85折</button>
+      <button class="chip" data-v="0.80">8折</button>
+    </div></div>
+    <div class="frow"><label>柜体清单(宽=投影宽 m / 厨房为计费长)</label><div id="cabList"></div>
+      <div style="margin-top:8px"><button class="chip" id="btnAddCab">+ 增加柜体</button></div>
+    </div>
+    <div class="frow"><label>活动优惠</label><div class="opts" id="optAct"></div></div>
+    <button class="btn pri" style="width:100%" id="btnApply">应用并重算</button>
+  </div>
+</div>
+
+<script>
+${engine}
+window.__Q__ = ${JSON.stringify({ cabinets: (quote.rooms || []).map((r, i) => ({ id: r.id, type: r.type.split(/\d$/)[0] || r.type, width: (quote.rooms || [])[i]?.kb ? undefined : parseFloat(String(r.dims || '0×0').split('×')[0]), height: 2.6, rawName: r.type })), config: quote.config, salesNote: salesNote })};
+console.log('fpc engine ready');
+</script>
+<script>
+(function(){
+  var Q = window.__Q__; var S = document; var money = function(n){ return '¥' + Math.round(n||0).toLocaleString('zh-CN'); };
+  var cfg = Object.assign({ tierKey:'standard', tierName:'PET', floatPct:15, discount:1.0, mode:'quick' }, Q.config||{});
+  function calc(){ return buildQuote({ cabinets: Q.cabinets, tierKey: cfg.tierKey, tierName: cfg.tierName, floatPct: cfg.floatPct, discount: cfg.discount, mode: cfg.mode, clientName: ${JSON.stringify(clientName || '')} }); }
+  function refresh(){
+    var q = calc(); var t = q.totals;
+    S.getElementById('finalPrice').textContent = money(t.finalPrice);
+    S.getElementById('finalRange').textContent = '谈价参考区间 ' + money(t.range.low) + ' ~ ' + money(t.range.high);
+    S.getElementById('tSub').textContent = money(t.itemsSubtotal);
+    S.getElementById('tDiscLabel').textContent = t.discountLabel;
+    S.getElementById('tAfter').textContent = money(t.afterDiscount);
+    S.getElementById('tAct').textContent = '-' + money(t.activityTotal);
+    S.getElementById('tFinal').textContent = money(t.finalPrice);
+    S.getElementById('bTier').textContent = (q.meta.tierName||'') + '材质';
+    S.getElementById('bDisc').textContent = t.discountLabel;
+  }
+  // room expand
+  S.querySelectorAll('.room-h').forEach(function(el){ el.addEventListener('click', function(){ el.parentElement.classList.toggle('open'); }); });
+  // chips
+  function bindChips(id, fn){ var box = S.getElementById(id); if(!box) return; box.addEventListener('click', function(e){ var b = e.target.closest('.chip'); if(!b) return; box.querySelectorAll('.chip').forEach(function(x){x.classList.remove('on');}); b.classList.add('on'); fn(b.dataset.v); }); }
+  bindChips('optTier', function(v){ cfg.tierKey = v; refresh(); });
+  bindChips('optMat', function(v){ cfg.tierName = v; refresh(); });
+  bindChips('optDisc', function(v){ cfg.discount = parseFloat(v); refresh(); });
+  var fr = S.getElementById('floatR'); fr.addEventListener('input', function(){ S.getElementById('floatV').textContent = fr.value; cfg.floatPct = parseInt(fr.value); refresh(); });
+  // drawer
+  var d = S.getElementById('drawer');
+  S.getElementById('btnAdjust').addEventListener('click', function(){ d.classList.add('open'); renderCabs(); });
+  d.addEventListener('click', function(e){ if(e.target === d) d.classList.remove('open'); });
+  function renderCabs(){
+    var box = S.getElementById('cabList'); box.innerHTML = '';
+    Q.cabinets.forEach(function(c, i){
+      var row = S.createElement('div'); row.className = 'cabrow';
+      var dims = String(c.width||'') ? String(c.width) : '';
+      row.innerHTML = '<span style="flex:1">' + c.rawName + '</span>' +
+        '<input type="number" step="0.05" value="' + (c.width||1.5) + '" data-i="' + i + '" data-f="width" title="宽(米)">' +
+        '<span class="del" data-i="' + i + '">✕</span>';
+      box.appendChild(row);
+    });
+    box.onclick = function(e){
+      var del = e.target.closest('.del');
+      if (del) { Q.cabinets.splice(parseInt(del.dataset.i),1); renderCabs(); refresh(); return; }
+      var inp = e.target.closest('input');
+      if (inp) { inp.addEventListener('change', function(){ Q.cabinets[parseInt(inp.dataset.i)].width = parseFloat(inp.value)||1.5; refresh(); }); }
+    };
+  }
+  S.getElementById('btnAddCab').addEventListener('click', function(){ Q.cabinets.push({ type:'储物柜', rawName:'储物柜', width:1.5, height:2.6 }); renderCabs(); refresh(); });
+  S.getElementById('btnSave').addEventListener('click', function(){
+    alert('已记录!销售将带着这份报价与您确认到店时间,到店凭此单领取到店礼。');
+  });
+  refresh();
+})();
+</script>
+</body>
+</html>`;
+}
+
+function roomCard(r, i, money) {
+  const conf = r.cabinet?.confidence != null ? Number(r.cabinet.confidence) : null;
+  const confTag = conf != null && conf < 0.6 ? `<span class="conf" title="AI识别置信度">估</span>` : '';
+  const lines = r.lines || [];
+  const inner = lines.length
+    ? lines.map((l) => `<div class="item"><span class="l">${esc(l.name)}<span>${esc(l.note || '')}</span></span><span class="v">${money(l.total)}</span></div>`).join('')
+    : `<div class="item"><span class="l">柜体(${esc(r.tierName || '')}材质)<span>${esc(r.dims || '')} · 投影${r.qty}${r.unit}</span></span><span class="v">${money(r.total)}</span></div>`;
+  return `<div class="room" data-i="${i}">
+    <div class="room-h"><div><div class="nm">${esc(r.type)}</div><div class="dm">${esc(r.dims || '')}</div></div>${confTag}<span class="tt">${money(r.subtotal ?? r.total)}</span></div>
+    <div class="items">${inner}</div>
+  </div>`;
+}
+function esc(s) { return String(s ?? '').replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;'); }
+
+export function writeReport(html, outPath) {
+  fs.mkdirSync(path.dirname(outPath), { recursive: true });
+  fs.writeFileSync(outPath, html);
+  return outPath;
+}
+export default { renderReport, writeReport };

+ 275 - 0
skill/src/vision.js

@@ -0,0 +1,275 @@
+// ============================================================
+// 拉迷全屋定制 · 户型图视觉识别(GLM-5.3-flash 为主力)
+// 职责:
+//   1. 图片/PDF→结构化 JSON:房间、定制柜体、尺寸标注、厨房
+//   2. 像素比例推算与柜体尺寸交叉验证(图面尺寸标注为参考真值)
+//   3. 尺寸缺失时用房间面积先验+柜墙占比估算(标记置信度)
+// 环境变量:FMODE_API_KEY(或 HERMES 在 config.yaml 中已配置)
+// ============================================================
+import { DEFAULTS } from './defaults.js';
+import { isKitchenName } from './geometry.js';
+
+const API_BASE = process?.env?.FMODE_API_BASE || 'https://api.fmode.cn/v1';
+const VLM_MODEL = process?.env?.FPC_VLM_MODEL || 'glm-5.3-flash';
+
+function apiKey() {
+  if (process?.env?.FMODE_API_KEY) return process.env.FMODE_API_KEY;
+  return null; // 浏览器端由调用方传入
+}
+
+// ---------- 基础调用 ----------
+export async function callVLM({ imageDataUrl, prompt, model, maxTokens = 8192, temperature = 0.1, apiKey: key }) {
+  const k = key || apiKey();
+  if (!k) throw new Error('缺少 API key:请设置 FMODE_API_KEY 或传入 apiKey 参数');
+  const body = {
+    model: model || VLM_MODEL,
+    temperature,
+    max_tokens: maxTokens,
+    messages: [{ role: 'user', content: [
+      { type: 'image_url', image_url: { url: imageDataUrl } },
+      { type: 'text', text: prompt },
+    ] }],
+  };
+  const resp = await fetch(`${API_BASE}/chat/completions`, {
+    method: 'POST',
+    headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${k}` },
+    body: JSON.stringify(body),
+  });
+  if (!resp.ok) {
+    const t = await resp.text().catch(() => '');
+    throw new Error(`VLM 调用失败 ${resp.status}: ${t.slice(0, 300)}`);
+  }
+  const data = await resp.json();
+  const msg = data.choices?.[0]?.message || {};
+  // 思考型模型(glm-5.3-flash 等):正文可能为空,实际内容在 reasoning_content
+  const content = msg.content || msg.reasoning_content || '';
+  return {
+    content,
+    usage: data.usage || {},
+  };
+}
+
+// 截断 JSON 修复:补齐未闭合的字符串与括号
+function repairJSON(s) {
+  let r = String(s).trim();
+  const stack = [];
+  let inStr = false, esc = false;
+  for (let i = 0; i < r.length; i++) {
+    const c = r[i];
+    if (inStr) {
+      if (esc) esc = false;
+      else if (c === '\\') esc = true;
+      else if (c === '"') inStr = false;
+      continue;
+    }
+    if (c === '"') inStr = true;
+    else if (c === '{' || c === '[') stack.push(c);
+    else if (c === '}' || c === ']') stack.pop();
+  }
+  if (inStr) r += '"';
+  r = r.replace(/,\s*$/, '');
+  while (stack.length) r += stack.pop() === '{' ? '}' : ']';
+  return r;
+}
+
+function tryParse(c) {
+  try { return JSON.parse(c); } catch { /* next */ }
+  const clean = c.replace(/,/g, ',').replace(/。\s*$/g, '');
+  try { return JSON.parse(clean); } catch { /* next */ }
+  try { return JSON.parse(repairJSON(c)); } catch { /* next */ }
+  try { return JSON.parse(repairJSON(clean)); } catch { return null; }
+}
+
+function extractJSON(text) {
+  if (!text) return null;
+  const cands = [];
+  const greedy = text.match(/\{[\s\S]*\}/);
+  if (greedy) cands.push(greedy[0]);
+  const first = text.slice(text.indexOf('{'));
+  if (first && !cands.includes(first)) cands.push(first);
+  const flats = [...text.matchAll(/\{[^{}]*\}/g)].map((m) => m[0]);
+  if (flats.length) { cands.push(flats[flats.length - 1]); cands.push(flats[0]); }
+  for (const c of cands) {
+    const p = tryParse(c);
+    if (p && typeof p === 'object') return p;
+  }
+  return null;
+}
+
+// ---------- 第一阶段:结构化读图 ----------
+const PROMPT_ANALYZE = `你是全屋定制行业的户型图分析专家。分析这张户型图/房型图,严格只输出一个JSON对象(禁止markdown代码块、禁止多余文字):
+{
+ "rooms": [{"name":"房间名","pos":"方位(左上/中上/右上/左中/正中/右中/左下/中下/右下)","approxArea":数字或null,"confidence":0到1}],
+ "cabinets": [{"name":"定制柜体名,用标准名:主卧衣柜/次卧衣柜/儿童房衣柜/老人房衣柜/客厅电视柜/餐厅餐边柜/玄关鞋柜/阳台柜/厨房橱柜/书房书柜/衣帽间/储物柜/酒柜/浴室柜/榻榻米","pos":"方位","evidence":"判断依据,简短"}],
+ "dimensions": [{"text":"图上标注的尺寸或数字(如 3500、3.5m、110㎡)","what":"它标注的是什么(房间开间/进深/总面积/层高/门窗宽)","nearRoom":"附近的房间名或null"}],
+ "floorHeight": 数字或null,
+ "buildingArea": 数字或null,
+ "notes": "一句话概括:几室几厅几卫、建筑/套内面积",
+ "kitchen": {"exists":true/false,"layout":"U型/L型/一字型/二字型/未知"}
+}
+要求:
+- cabinets 只列「定制柜体」:衣柜、橱柜、鞋柜、电视柜、餐边柜、阳台柜、书柜、酒柜、浴室柜、榻榻米等,不要列沙发/床/餐桌等可移动家具
+- dimensions 逐条列出图面上所有能看到的数字标注(含总线尺寸、分间尺寸、门窗洞口尺寸),这是后续尺寸交叉验证的参考真值
+- 分不清的就标 confidence 低一点,不要编造`;
+
+export async function analyzeFloorplan({ imageDataUrl, apiKey, model, debug = false }) {
+  let { content, usage } = await callVLM({ imageDataUrl, prompt: PROMPT_ANALYZE, apiKey, model });
+  let parsed = extractJSON(content);
+  if (!parsed) {
+    // 重试:强调只输出 JSON 本体
+    const retry = await callVLM({ imageDataUrl, apiKey, model, temperature: 0.1,
+      prompt: PROMPT_ANALYZE + '\n\n【重要】直接输出JSON本体,第一个字符必须是 { ,最后一个字符必须是 } 。不要输出任何解释、思考过程或markdown。' });
+    usage = { ...usage, retry: retry.usage };
+    content = retry.content;
+    parsed = extractJSON(content);
+  }
+  if (!parsed) throw new Error('VLM 返回无法解析为 JSON:' + String(content).slice(0, 200));
+  if (debug) parsed._raw = content;
+  parsed._usage = usage;
+  return parsed;
+}
+
+// ---------- 第二阶段:柜体尺寸推理(与第一阶段分离,降低漏项) ----------
+const PROMPT_MEASURE = `基于这张户型图估算定制柜体的安装尺寸。已知信息:层高 ${'{height}'} 米。图上标注的尺寸数字(若有)为参考真值。严格只输出JSON:
+{
+ "cabinets": [{
+   "name":"柜体名(同标准名)",
+   "width": 数字(米), "height": 数字(米),
+   "basis": "尺寸依据:图上直读/按标注尺寸换算/按房间开间比例估算/先验估算",
+   "confidence": 0到1
+ }],
+ "kitchenWalls": [a,b,c,d] 或 null,
+ "ratioEvidence": "若图上有可推算全图比例的尺寸线,说明是哪条"
+}
+规则:
+- 衣柜/鞋柜/阳台柜/餐边柜高度通常 = 层高减100~200mm(顶部留空),即 2.4~2.7m;吊柜单独高度约0.7~0.9m
+- 橱柜按地柜沿墙长度估:厨房一面墙则 walls=[长,宽,长,宽];L型 [长,宽2,长,0];U型 [长,宽,长,宽](单位米)
+- 若图上有明确尺寸标注(如"3600"),优先用它换算,basis 写"图上直读"
+- 实在无依据时按房间开间的 50%~60% 估宽度,basis 写"先验估算",confidence ≤0.4`;
+
+export async function measureCabinets({ imageDataUrl, analysis, floorHeight, apiKey, model }) {
+  const h = floorHeight || analysis?.floorHeight || DEFAULTS.height;
+  const prompt = PROMPT_MEASURE.replace('{height}', String(h));
+  const { content, usage } = await callVLM({ imageDataUrl, prompt, apiKey, model });
+  const parsed = extractJSON(content) || {};
+  parsed._usage = usage;
+  return parsed;
+}
+
+// ---------- 第三阶段:交叉验证与尺寸合成 ----------
+// 已知房间面积(图上标注/销售口述)时,用 sqrt(面积)×先验长宽比 推边长做验证
+function areaBasedBounds(area) {
+  if (!area || area <= 0) return null;
+  const s = Math.sqrt(area);
+  return { min: s * 0.55, max: s * 1.5 }; // 常规房间长宽比 1:1.2~1:1.6
+}
+
+export function reconcile({ analysis, measures, floorHeight, knownRooms = {} }) {
+  const h = floorHeight || analysis?.floorHeight || DEFAULTS.height;
+  const dims = analysis?.dimensions || [];
+  const dimsByText = dims.map((d) => ({ ...d, nums: String(d.text || '').match(/\d+(?:\.\d+)?/g)?.map(Number) || [] }));
+
+  // 收集所有可用尺寸:图上标注 > VLM测量 > 先验估算
+  // 两阶段并集策略:measure 提供尺寸,analysis 中 measure 漏掉的柜体补入(默认估宽、低置信度),
+  // 防止 measure 阶段随机漏项导致整组柜体丢失
+  const out = [];
+  const seen = new Set();
+  const measures0 = measures?.cabinets?.length ? measures.cabinets : [];
+  for (const c of measures0) {
+    const name = c.name || '柜体';
+    const stdName = normalizeCabinetName(name);
+    seen.add(stdName);
+    let width = num2(c.width); let height = num2(c.height) || (h - 0.15);
+    let basis = c.basis || 'VLM估算';
+    let confidence = num2(c.confidence, 0.5);
+
+    // 尺寸线交叉验证:找名字/方位匹配的标注
+    const hint = dimsByText.find((d) => (d.nearRoom && (stdName.includes(d.nearRoom) || d.nearRoom.includes(stdName.slice(0, 2)))) 
+      || (d.what || '').includes(stdName.slice(0, 2)));
+    if (hint && hint.nums.length) {
+      const v = hint.nums[0] / 1000 >= 1.5 ? hint.nums[0] / 1000 : hint.nums[0]; // mm→m 或已是米
+      if (v >= 0.8 && v <= 8) { width = v; basis = `图上尺寸线直读(${hint.text})`; confidence = 0.95; }
+    }
+    // 房间面积交叉验证
+    const room = knownRooms[roomOf(stdName)] || null;
+    const ab = areaBasedBounds(room);
+    if (ab && width > ab.max * 1.3) { // 宽度明显超出该面积房间的可能边长 → 降级
+      width = r2(Math.min(width, ab.max)); confidence = Math.min(confidence, 0.5); basis += ',按房间面积修正';
+    }
+    if (!width) { width = r2(h > 0 ? 1.5 : 1.5); confidence = 0.3; basis = '默认估宽'; }
+    width = clamp(width, 0.3, 8); height = clamp(height || h - 0.15, 0.5, h);
+    out.push({ type: stdName, width: r2(width), height: r2(height), basis, confidence, walls: stdName.includes('厨房') ? (measures?.kitchenWalls || null) : undefined });
+  }
+  // analysis 阶段发现、measure 漏掉的柜体 → 补入(默认估宽,低置信度)
+  for (const c of (analysis?.cabinets || [])) {
+    const stdName = normalizeCabinetName(c.name || '');
+    if (!stdName || seen.has(stdName)) continue;
+    seen.add(stdName);
+    out.push({ type: stdName, width: 1.5, height: r2(h - 0.2), basis: 'analysis识别、尺寸先验估算', confidence: 0.3, walls: stdName.includes('厨房') ? (measures?.kitchenWalls || null) : undefined });
+  }
+  // 厨房保底(原项目规则:厨房默认参与计价)
+  if (!out.some((c) => isKitchenName(c.type))) {
+    out.push({ type: '厨房橱柜', width: 3, height: h, walls: [3, 2.4, 3, 2.4], basis: '默认厨房保底', confidence: 0.2 });
+  }
+  // 同名柜体合并(VLM 两阶段可能各报一次厨房/同一柜体):取置信度高者;厨房计费墙取 walls 最长者
+  const merged = [];
+  const byType = new Map();
+  for (const c of out) {
+    const k = c.type;
+    if (!byType.has(k)) { byType.set(k, c); merged.push(c); continue; }
+    const prev = byType.get(k);
+    if (isKitchenName(k)) {
+      const prevLen = (prev.walls || []).reduce((s, v) => s + (v || 0), 0);
+      const curLen = (c.walls || []).reduce((s, v) => s + (v || 0), 0);
+      if (curLen > prevLen) { Object.assign(prev, { width: c.width, walls: c.walls, basis: prev.basis + ' / 合并', confidence: Math.max(prev.confidence, c.confidence) }); }
+    } else if ((c.confidence || 0) > (prev.confidence || 0)) {
+      Object.assign(prev, { width: c.width, height: c.height, basis: c.basis, confidence: c.confidence });
+    }
+  }
+  return { cabinets: merged, floorHeight: h };
+}
+
+function num2(v) { const n = parseFloat(v); return Number.isFinite(n) ? n : 0; }
+function r2(v) { return Math.round(v * 100) / 100; }
+function clamp(v, a, b) { return Math.min(b, Math.max(a, v)); }
+
+export function normalizeCabinetName(name = '') {
+  const n = String(name).replace(/\s/g, '');
+  // 厨房类强制收敛为「厨房橱柜」(避免「厨房吊柜」等变体绕过按延米计价)
+  if (/厨房|橱柜|厨柜|灶台|操作台/.test(n)) return '厨房橱柜';
+  const table = [
+    [/^(主卧|主人房).*?(衣柜|柜)/, '主卧衣柜'], [/^(次卧|客房|儿童|老人|长辈).*?(衣柜|柜)/, (m) => m[0].startsWith('儿') ? '儿童房衣柜' : m[0].startsWith('老') ? '老人房衣柜' : '次卧衣柜'],
+    [/^.*(电视柜|背景墙柜)/, '客厅电视柜'], [/^.*(餐边|餐厅柜|备餐柜)/, '餐厅餐边柜'],
+    [/^.*(鞋柜|玄关柜|入户柜|进门柜)/, '玄关鞋柜'], [/^.*(橱柜|厨柜|厨房柜|开放式厨房)/, '厨房橱柜'],
+    [/^.*(阳台|洗衣|洗烘|生活阳台)/, '阳台柜'], [/^.*(书柜|书房柜|书桌柜)/, '书房书柜'],
+    [/^(衣帽间|步入.*?(衣|柜))/, '衣帽间'], [/^.*(酒柜|展示柜|陈列柜)/, '酒柜'],
+    [/^.*(浴室柜|卫生间柜|洗手台柜)/, '浴室柜'], [/^.*(榻榻米)/, '榻榻米'], [/^.*(储物|杂物|收纳柜)/, '储物柜'],
+  ];
+  for (const [re, to] of table) {
+    const m = n.match(re);
+    if (m) return typeof to === 'function' ? to(m) : to;
+  }
+  if (n.includes('衣柜')) return '次卧衣柜';
+  if (n.includes('柜')) return n.length <= 4 ? n : '储物柜';
+  return n || '柜体';
+}
+function roomOf(stdName) {
+  if (stdName.includes('主卧')) return '主卧';
+  if (stdName.includes('次卧') || stdName.includes('儿童') || stdName.includes('老人')) return '次卧';
+  if (stdName.includes('厨房')) return '厨房';
+  if (stdName.includes('玄关')) return '玄关';
+  if (stdName.includes('阳台')) return '阳台';
+  if (stdName.includes('书')) return '书房';
+  return '';
+}
+
+// ---------- 端到端:图 → 可计价柜体清单 ----------
+export async function recognizeFloorplan({ imageDataUrl, floorHeight, knownRooms = {}, apiKey, model, debug }) {
+  const analysis = await analyzeFloorplan({ imageDataUrl, apiKey, model, debug });
+  let measures = null;
+  try { measures = await measureCabinets({ imageDataUrl, analysis, floorHeight, apiKey, model }); }
+  catch (e) { if (debug) console.warn('[fpc] measure 阶段失败,使用分析阶段结果:', e.message); }
+  const reconciled = reconcile({ analysis, measures, floorHeight, knownRooms });
+  return { analysis, measures, ...reconciled, _usage: { analyze: analysis._usage, measure: measures?._usage } };
+}
+export default { callVLM, analyzeFloorplan, measureCabinets, reconcile, recognizeFloorplan, normalizeCabinetName };

+ 79 - 0
skill/test/run.js

@@ -0,0 +1,79 @@
+// fpc 端到端测试:计价引擎单元断言 + (可选)真实图纸识别
+import { buildQuote, kitchenBillable, tierUnitPrice, applyOps, withinAuthority } from '../src/quote.js';
+import { normalizeCabinetName } from '../src/vision.js';
+import { renderSVG, buildPlan } from '../src/redraw.js';
+import { renderReport } from '../src/report.js';
+import assert from 'node:assert';
+
+let pass = 0; let fail = 0;
+function t(name, fn) {
+  try { fn(); pass++; console.log('  ✓', name); }
+  catch (e) { fail++; console.error('  ✗', name, '→', e.message); }
+}
+
+console.log('== 计价引擎 ==');
+t('厨房U型计费米数: 长3.5宽2.4 → 扣2转角+冰箱 = 7.2m', () => {
+  const kb = kitchenBillable([3.5, 2.4, 3.5, 2.4]);
+  assert.equal(kb.kitchenType, 'U');
+  assert.equal(kb.billable, 7.2);
+});
+t('厨房L型计费米数: 长3.6宽1.8 → 扣1转角+冰箱 = 3.8m', () => {
+  const kb = kitchenBillable([3.6, 1.8, 0, 0]);
+  assert.equal(kb.kitchenType, 'L');
+  assert.equal(kb.billable, 3.8);
+});
+t('PET档15%浮动单价 = 区间上限5000', () => {
+  assert.equal(tierUnitPrice({ name: 'PET', min: 4500, max: 5000 }, 15), 5000);
+});
+t('PET档10%浮动单价 = 区间下限4500', () => {
+  assert.equal(tierUnitPrice({ name: 'PET', min: 4500, max: 5000 }, 10), 4500);
+});
+t('快速报价: 3.6×2.6衣柜@5000 = 46800', () => {
+  const q = buildQuote({ cabinets: [{ type: '主卧衣柜', width: 3.6, height: 2.6 }], tierName: 'PET', floatPct: 15 });
+  assert.equal(q.rooms[0].total, 46800);
+});
+t('价格型客户默认85折+自动活动', () => {
+  const q = buildQuote({ cabinets: [{ type: '主卧衣柜', width: 3.6, height: 2.6 }], tierKey: 'price', floatPct: 15 });
+  assert.equal(q.meta.discount, 0.85);
+  assert.ok(q.activityLines.length >= 1, '活动应自动启用');
+});
+t('品质型客户 +15% 品质系数', () => {
+  const q = buildQuote({ cabinets: [{ type: '主卧衣柜', width: 3.6, height: 2.6 }], tierKey: 'quality', floatPct: 15 });
+  assert.equal(q.meta.premiumRate, 1.15);
+});
+t('增删项 ops: add+remove 生效', () => {
+  const out = applyOps([{ type: '主卧衣柜', width: 3, height: 2.6 }, { type: '书房书柜', width: 2, height: 2.6 }],
+    [{ op: 'remove', target: '书房书柜' }, { op: 'add', type: '阳台柜', width: 1.8 }]);
+  assert.deepEqual(out.map((c) => c.type), ['主卧衣柜', '阳台柜']);
+});
+t('谈价区间 = 到手价 ± 浮动率', () => {
+  const q = buildQuote({ cabinets: [{ type: '玄关鞋柜', width: 1.2, height: 2.4 }], floatPct: 15 });
+  const f = q.totals.finalPrice;
+  assert.equal(q.totals.range.low, Math.round(f * 0.85));
+  assert.ok(withinAuthority(q, f));
+  assert.ok(!withinAuthority(q, f * 1.2), '超出区间的价格应在授权外');
+});
+
+console.log('== 名称归一 ==');
+t('厨房吊柜→厨房橱柜(强制延米计价)', () => {
+  assert.equal(normalizeCabinetName('厨房吊柜'), '厨房橱柜');
+  assert.equal(normalizeCabinetName('开放式厨房柜'), '厨房橱柜');
+});
+t('玄关柜/入户柜→玄关鞋柜', () => {
+  assert.equal(normalizeCabinetName('玄关柜'), '玄关鞋柜');
+  assert.equal(normalizeCabinetName('入户柜'), '玄关鞋柜');
+});
+
+console.log('== 报告渲染 ==');
+t('renderReport 产出含引擎与交互的HTML', () => {
+  const q = buildQuote({ cabinets: [{ type: '主卧衣柜', width: 3.2, height: 2.6 }], tierKey: 'standard' });
+  const plan = buildPlan({ analysis: { rooms: [{ name: '主卧', pos: '左中', approxArea: 14 }] }, cabinets: q.rooms.map((r) => ({ type: r.type, width: 3.2, height: 2.6, pos: '左中' })) });
+  const svg = renderSVG(plan);
+  const html = renderReport({ quote: q, planSVG: svg, clientName: '测试', salesNote: 'x' });
+  assert.ok(html.includes('id="finalPrice"'));
+  assert.ok(html.includes('function buildQuote'), '计价引擎应内嵌');
+  assert.ok(svg.startsWith('<svg'), 'SVG应可生成');
+});
+
+console.log(`\n结果: ${pass} 通过, ${fail} 失败`);
+process.exit(fail ? 1 : 0);