投标报价对比分析驾驶舱(Bid Quote Cockpit)
把「一堆报价表」变成「一份可决策、可发布、可复用的看板」。本技能不含任何具体门店或公司名称,全部由使用者提供的配置驱动;换门店、换供应商、换货币、换分项结构、换评委都不需要改代码。
定位与边界
| 场景 | 用本技能 | 改用 |
|---|---|---|
| 多家供应商报价 → 横向比价 + 看板 | ✅ | |
| 多轮回标(首轮 / 二轮 / 澄清后)对比 | ✅ | |
| 评委打分 → 有效平均 → 综合评分 → 定标建议 | ✅ 见「评分与定标链路」 | |
| 看板要发给别人看 / 需固定在线链接 | ✅ 见 references/publish-and-verify.md |
|
| 某家澄清后修订价回灌既有对比表 | ➖ 核心回灌流程 | bid-quote-clarification-sync |
| 填制「商务标分析表」/ 串标特征识别 | ➖ | bid-price-analysis |
| 两表逐项机械对比 | ➖ | excel-quote-compare |
| 同批供应商 × 多个子项目并置(不做跨项目合计) | ➖ | multi-project-quote-board |
本技能与
bid-quote-clarification-sync互补:澄清回灌负责修数据,本技能负责出看板。两者共用同一套「只用原文数字」红线。
快速开始(3 步)
SK=~/.workbuddy/skills/bid-quote-cockpit
# 1. 复制配置模板,填入项目信息(门店/供应商/轮次/分项都在这里改)
cp $SK/scripts/config.example.json ./config.json
# 2. 生成看板(读 config.json + quotes.json [+ history.json])
# out 为相对路径时,按 config.json 所在目录解析(不是 cwd),产物不会乱落
python $SK/scripts/build_dashboard.py --config ./config.json --report
# 3. 交付前必跑校验①:语法门禁 + 桩化运行时冒烟 + NaN 扫描
node $SK/scripts/verify_dashboard.js ./报价分析看板.html
# 第 3 道:静态查「Python 常量名泄漏进 JS」(运行时会静默中断后续全部渲染)
python scripts/lint_py_const_leak.py ./build_dashboard.py ./报价分析看板.html
# 4. 交付前必跑校验②:版式溢出体检(内容是否超出页面/卡片范围)
python $SK/scripts/check_overflow.py ./报价分析看板.html 1500,1280,1100
校验输出 ✅ JS 执行无异常 / 图表已 setOption: [...] / 内容为空的容器: 无 (OK) / 含 undefined|NaN 的容器: 无 (OK) 且 exit 0,并第二道校验输出 ✅ 无横向溢出 且 exit 0,才可交付。两道校验互不覆盖:前者查运行时,后者查版式。
交付前 5 条文案自检(脚本查不了,必须自己过一遍,否则容易交付"上一版残留"):
H = open(html, encoding="utf-8").read()
assert "名义·不完整" not in H # 本次若有整包价有效报价,不应再出现
assert ("剔除后" in H) == (facade_cat_key is not None) # 未设剔除项时不得出现
assert "cNoFac" in H == (facade_cat_key is not None)
# 有 history 时:'cHistSq' in H 且 '过往项目单方均值' in H
# 有 addenda 时:'附录 A' in H
第一步:收集输入(数据契约)
本技能吃 3 个文件,其中历史数据是可选的。
| 文件 | 必需 | 内容 |
|---|---|---|
config.json |
✅ | 项目元信息、供应商清单与配色、轮次口径、模块开关、文件路径 |
quotes.json |
✅ | 各家各轮的总价、分项小计、逐条明细(单一真相源) |
history.json |
⬜ 可选 | 历史门店数据。不提供 → 看板不出现任何历史对比 |
字段级规格见 references/data-contract.md(含最小可运行示例与字段校验规则)。
从 Excel 抽取数据
使用者通常给的是 Excel/PDF 报价表。抽取时必须保留原文数字,禁止任何推算:
# 用技能自带的归一化/统计工具做清洗与自检
python $SK/scripts/normalize_quote.py --in raw_quotes.json --out quotes.json --report
该工具负责:描述级匹配(多轮对照)、零单价逐条重算、分项求和 vs 原文总价勾稽、小计行剔除。详见 references/pitfalls.md。
第二步:模块开关(含历史数据这一开放选项)
看板共 9 节,条件模块未满足时整节不渲染(不留空白占位、不写"暂无数据"凑数)。
| 节 | 模块 | 出现条件 |
|---|---|---|
| 一 | 报价总览表 | 必选(各家 × 各轮 × 方案价/优惠价) |
| 二 | 供应商报价概况 | 必选(指标速览带 + 报价标尺 + 卡片网格 三种形式) |
| 三 | 总价 / 单方 / 剔除后对比 | 必选;「剔除后」需 facade_cat_key 命中 |
| 四 | 分部分项横向对比 | 必选(各轮 × 金额/单方,N 柱 + 均值线) |
| 五 | 分项明细逐条对比 | 需 items 明细非空 |
| 六 | 异常报价专项核查 | 需存在零单价/疑问/未报价条目;全部正常则整节跳过 |
| 七 | 历史对比 | ⬜ 需 history.json 非空;7.1 分项对比需历史门店有分项拆分,否则 7.1 不渲染、只保留 7.2 单方对比(含过往均值线) |
| 八 | 勾稽校验 | 必选 |
| 九 | 行动清单 | 必选 |
| 附录 A | 清单外增项核查 | ⬜ 需 quotes.json 里有 addenda(供应商以邮件/另表补充、不在 BOQ 内的报价项) |
整包价单位(noBreakdown)
某家只给整包总价 + 书面 Included/Excluded 范围说明、不给逐条拆分时(常见于店中店/快闪店短工期项目):
- 它是有效报价,不是"报价不完整" —— 只要给出明确总价与范围声明,就
"complete": true+"status": "",正常计入排名。 - 在该供应商对象里加
"noBreakdown": true。效果:① 总价/单方(第二、三节)照常参与;② 第四、七节分项图不含它(避免 0 柱被读成"该项报价 0");③ 它的「分项求和 vs 采用价」勾稽转成**口径说明(已注明)**而非 ✗ 差异;④ 不拉低本项目分项均值。 - 它的
cats仍须填满ncat个 0(长度校验),但不要把 0 当作"已含"来解读。 - 它的范围差(Excluded 项)用其他家原文数字求和量化,写进
notes,属透明算术。
清单外增项(addenda)
供应商邮件另附、不在 BOQ 内的报价(如西班牙法定的电气合法化+OCA、安全协调员、安全计划、分包台账等),在 quotes.json 里加:
"addenda": [{
"vendor": "供应商名", "title": "…", "source": "邮件附件文件名",
"items": [{"no":1,"desc":"原文条目","desc_cn":"中文","sub":"子行","amount":4800,
"mandatory":"法定强制","basis":"法规依据","verdict":"必要性判定","flex":"高"}],
"subtotal_printed": 10320, "vat_rate": 0.21, "vat": 2167.2, "total": 12487.2,
"sum_unit_col": 10500, // 逐项相加(与原文小计不一致时必填,用于暴露其内部勾稽问题)
"reconcile_note": "…", "conclusion": "…", "impact": "…"
}]
看板渲染为附录 A(不动 9 节编号),含 A.1 指标带 / A.2 逐项核查表 / A.3 结论与处理建议;同时自动进第九节行动清单(P0 澄清 + P0 对齐四家口径)。
⚠️ 口径纪律:法定合规义务的责任主体是施工承包单位本身,不是业主漏项。结论必须写"要求四家在同一口径下各自含入或书面声明不含",而不是把某一家的加价当作既定增项照收。原文小计与逐项相加不一致时,照原文列示并标注差额,不自行更正。
历史数据:必须显式询问使用者
开工前先问一句(这是开放选项,不要替使用者决定):
是否需要把本次报价与历史门店做对比?如需要,请提供历史数据清单,每条包含: **门店/项目名称、建筑面积(㎡)、总报价、**以及(可选)按相同分项拆分的小计金额。 只有单方造价(€/㎡)而无分项拆分也可以,此时仅参与单方对比,不参与分项对比。 若不提供,看板中将不体现任何历史对比内容。
- 提供 → 填
history.json,第七节出现(分项金额对比 + 单方对比 + 历史均值线),且第四节出现「历史均值线」、第二节指标带出现「历史单方均值」格。 - 不提供 →
config.json里"history": null(或干脆没有history.json)。此时:- 第七节整节不渲染;
- 第四节不画历史均值线,图例与说明同步消失;
- 第二节指标带不出现历史单方均值格;
- 全文不出现「历史」「过往」字样。
模板见 examples/history.json(含"有分项"与"仅单方"两类条目,可直接删改)。
可直接跑的示例(脱敏,无真实门店/供应商名)
examples/ 下有成套示例,4 家供应商(A/B/C/D)× 8 个分项 × 首轮+二轮,其中 C 家含零单价疑问项、D 家无二轮报价,用于演示完整性标注与沿用首轮补位:
cd ~/.workbuddy/skills/bid-quote-cockpit/examples
python ../scripts/build_dashboard.py --config config.json # → demo.html (提供历史:9 节 / 15 图)
python ../scripts/build_dashboard.py --config config_without_history.json # → demo_without_history.html(不给历史:8 节 / 10 图)
对比打开两个 demo_*.html 即可直观确认:不给历史时,第七节整节消失,第四节历史均值线与第二节历史单方均值格同步消失,全文无「历史 / 过往」字样(仅保留一句"本次未提供历史门店数据"的说明)。
第三步:生成与交付
- 跑
build_dashboard.py生成 HTML(echarts 内联,产出即离线自包含单文件,无外部依赖)。 - 跑
verify_dashboard.js校验,必须 exit 0(查的是"JS 能不能跑、图能不能画、有无 NaN")。 - 跑
check_overflow.py做版式溢出体检,必须 exit 0(查的是"内容有没有超出页面/卡片范围",与上一步互不覆盖,两个都要跑):
判读:python $SK/scripts/check_overflow.py ./报价分析看板.html 1500,1280,1100PAGE_SCROLL=0+BEYOND_WRAP=0+CLIPPED=0。CLIPPED= 内容被「人为高度上限」裁掉(如.tblwrap{max-height:420px}遇到超高表格), 溢出部分只有滚动才看得到,截图与打印一律丢失,典型受害行是「合计 / 综合总分」。 这是纵向裁切,旧的三个横向指标一个都命不中 —— 详见references/pitfalls.md§15。SCROLLABLE>0只是信息项(某容器内部横滚,仍可看到),--strict可把它也当失败。这个检查专抓"表格里有整段文字(法规依据/条款原文/长描述)被td{white-space:nowrap}撑破卡片"这类版式事故——曾因漏检此步被使用者退回(原话"显示出现重大失误,已经超出页面范围")。成因与修复见references/pitfalls.md§10。
- 跑
dump_rendered.py做渲染级「单位↔金额」配对断言,必须 exit 0 (查的是"客户端 JS 现拼的文案里,单位名有没有配错金额"):
⚠️ 前三道都拦不住这一类:python $SK/scripts/dump_rendered.py ./报价分析看板.html --rules render_rules.json --log gate.lognode --check/verify_dashboard.js只看能不能跑、有无 NaN;check_overflow.py只看版式;lint_py_const_leak.py只看常量泄漏。而grep也查不到 —— 千分位是客户端fmt()现拼的,源码里只有裸数字。规则写法与实盘案例见references/pitfalls.md§14。 - 用 Chrome headless 截图抽查长图渲染(可选但推荐):
chrome --headless --disable-gpu --screenshot=./shot.png --window-size=1500,6000 ./报价分析看板.html
交付约定
- 默认只交付 HTML,不主动生成/同步 PDF(除非使用者明确要求)。要 PDF 时用 Chrome headless 渲染,且先写临时文件名再
cp覆盖主文件(预览锁定会 0x5 拒绝)。 - 若使用者要求托管到资料库云端:先过图片托管自检(无第三方
<img>外链),再import_html.py --node-block-id <节点>覆盖同步。完整发布与线上核验手册见references/publish-and-verify.md(含"分享链接只是壳页""版本号会递增""双档案一致性"三个必修点)。
评分与定标链路(可选):评委打分 → 有效平均 → 综合评分 → 定标结论
当评审规则里含人工打分时启用。若项目是纯价格比价、无评委环节,整套跳过,看板不出现任何评分节。
契约与算法见 references/score-panel-contract.md(唯一口径,改分流程也在那里)。
核心原则:打分必须固化,不能让它在浏览器里活着
老做法把打分存 localStorage。三个坑:① file:// 与 https:// 是不同 origin,本地有分、云端全空;
② 云文档在 iframe 沙箱里,setItem 可能直接抛异常;③ 探不到分就静默回退到静态基线 ——
不报错、不空白,只是显示一个看起来合理却是错的分数(最危险)。
定稿做法:分数烘焙进 HTML 字节(window.__SCORE_SEED__),页面只读,任何设备一致。
SK=~/.workbuddy/skills/bid-quote-cockpit
# 1. 由业务侧确认好评委 × 维度 × 单位的分数,写进 score_config.json
cp $SK/examples/score_config.example.json ./score_config.json
# 2. 生成打分区块 + 元数据(--report 打印综合分、名次、缺项告警)
python $SK/scripts/gen_score_panel.py --config ./score_config.json \
--out ./score_panel.html --report
# → score_panel.html 评分区块片段(CSS + 只读表 + 内联 seed)
# → score_panel.meta.json 各格有效平均 / 综合分 / 名次 / 告警(供主看板取数)
# 3. 生成主看板时用 --score-panel 一步装配(⚠️ 路径按 config.json 所在目录解析)
python $SK/scripts/build_dashboard.py --config ./config.json \
--score-panel ./score_panel.html --out ./看板.html
# 4. 交付前必跑:固化一致性真机校验(两个全新浏览器配置 = 两台电脑)
python $SK/scripts/verify_score_frozen.py ./看板.html --config ./score_config.json \
--log ./verify_score.log
⚠️ 装配只能靠 --score-panel,不要事后再往 HTML 里注入
产物里内联了压缩版 ECharts,而 ECharts 的 saveAsImage 干函数中有一个字符串字面量
包含完整的 </body>、<style> 片段。因此 re.search("<style") / html.find("</body>")
这类"找锚点"的写法会命中假锚点,把面板塞进 JS 字符串:
- 文件变大 ✅、页面不报错 ✅、能搜到
scorePanel✅(在 JS 源码里) - 但渲染出来没有评分节 ❌ —— 校验器 10 项全 FAIL,且极难定位
正确路径就是生成器的 --score-panel(内部按结构顺序嵌入,不靠字符串匹配)。
评分节是可选模块(跳过 ≠ 通过)
纯价格比价版看板没有评分节时,verify_score_frozen.py 会打印 -- 级说明并跳过
综合分/名次断言 —— 这不是失败。但若这个看板本应含评分节,说明 render_rules.json
的 _selectors 没指到正确容器 id(或用错了示例文件)。跳过不能当通过读。
{ "_selectors": {
"panel": "#scorePanel, #teamPanel",
"scoreTable": "#tScore, #scoreTable",
"hero": "#heroReco, #heroCards",
"rulesBox": "#scoreRulesBox, #rulesBox" } }
三条不可协商的规则
| 规则 | 内容 |
|---|---|
| 页面只读 | 打分面板内 0 个 input/select/button/textarea;不写入任何评分类 localStorage;不提供「保存/导出打分码/清空」入口。校验器按「控件数 == 0」与「无评分类 key」硬断言 |
| 脏数据置空,不许补齐 | 打分缺失 / 越界 / 非整数 → 该格置空并逐条告警,绝不自动填 max、填 0 或四舍五入。这是"只用原文数字"红线在打分场景的体现 |
| 缺项要留痕 | 某维度全无有效打分时按 0 计入,但必须记入 missing_dims 并在页面提示。静默压分会让人误以为该单位真的差 |
改分流程(交付后,唯一路径)
改 score_config.json 里的那格 → 重跑 gen_score_panel.py → 带 --score-panel 重新生成看板 → 重新发布 → verify_score_frozen --online 复核
不要为了"方便"在页面上留改分后门 —— 一旦开口子,「任何设备结果一致」这个保证就失效了。
上线发布与线上核验
交付链接时只给一个最终链接,并附上核验结论。关键要点(完整版见 references/publish-and-verify.md):
- 发布前硬门:无第三方
<img>外链、无 CDN 外链(ECharts/前端 SDK 一律内联 —— 国内网络与微信内置浏览器取不到 CDN)、磁盘字节数 == 生成时打印的字节数。 - 分享链接 ≠ 产物:发布返回的 URL 通常只是几 KB 的引导壳(含
__PUBLISH_BOOTSTRAP__),真实 HTML 在另一个静态域名下、版本号递增。核验必须下载真字节再断言,否则你校验的是壳页。 - 线上必须重跑固化一致性:
verify_score_frozen.py ./online_static.html --config ./score_config.json --online。客户端 JS 现算的数字(分数/名次/千分位)grep 不到,只能渲染断言。 - 双档案是唯一真答案:两个全新
--user-data-dir结果一致,才证明"换台电脑不会变"。同一 profile 跑两次证明不了任何事。
红线(所有环节,不可协商)
- 只用报价文件原文数字。禁止推算、补全、"同口径估算"。确需推算(如总价÷面积=单方)属透明算术,允许但必须在页面上标注公式。
- 口径全程统一:含税/不含税二选一并贯彻始终;跨项目对比一律不含税。原文未标税率 → 显示"待确认",不自作主张。
- 三态规则(易错):有报价+有税率=数字 / 有报价+无税率=待确认 / 无报价=暂无报价。严禁混用。
- 名义最低价必须显式标注:某家数字最低但报价不完整(大量零单价/缺项/空表)时,排序一律加「名义·不完整」,并同时给出有效可比排名(只含完整报价单位)。绝不为给老板一个"最低价"而隐去完整性问题。
- 反向也成立(2026-09-14 教训):某家给了整包总价 + 书面 Included/Excluded 范围说明、只是没做逐条拆分时,不算不完整 —— 必须作为有效报价计入排名。不要把"未拆分"误当作"报价无效/名义价"。范围缺口单独作为
notes说明,不据此剥夺其排名资格。
- 反向也成立(2026-09-14 教训):某家给了整包总价 + 书面 Included/Excluded 范围说明、只是没做逐条拆分时,不算不完整 —— 必须作为有效报价计入排名。不要把"未拆分"误当作"报价无效/名义价"。范围缺口单独作为
- 中间产物 JSON 的统计字段不可直接采信(如
zero_count可能算错)。凡零单价、条目数、完整性占比一类统计,一律回到items逐条重算。 - 多轮对比时保留首轮列不删除,二轮在旁边新增列;无二轮报价的单位沿用首轮价参与排名并标注,不静默剔除。
定标建议模块(可选):满足工期前提下,价格最低优先
当使用者要求给出定标建议、或项目有明确的开业/安装目标日时启用本节。判定逻辑必须唯一、可复算,不要把工期、价格、口径可信度混在一张表里讲(2026-09-12 用「方案 A/B/C 矩阵」的写法被明确否掉,理由是"很乱")。
三条判定规则(缺一不可)
| 规则 | 内容 |
|---|---|
| ① 起算日 | 统一按一个明确日期起算(如「自 2026-09-15 起」),不沿用各家自报的不同起算点 |
| ② 工期门槛 | 各家所报工期自起算日推算,能在截止日(如 2026-10-31)前完成 → 满足工期;不能 → 列入**「需请对方澄清」**(澄清能否压缩),不是直接淘汰 |
| ③ 定标原则 | 在满足工期的候选中,报价最低者优先 —— 工期是门槛,价格是排序键 |
数据层建议留三个常量 + 一个函数,便于复算与改口径:LEAD_START(统一起算日)、LEAD_DEADLINE(完成截止日)、LEAD_SPAN[{sup: {pid: 天数}}](各家自起算至完成的总天数,严格取自其报价原文)、lead_result(sup, pid) → {days, est, ok}。
三个易错点
- 绝对日期排期者不顺延:某家若给的是绝对日期排期(如「工程开发 9/10 起 → 安装 10/26–10/30」),其自身起点可能早于统一起算日,此时完成日按其原排期固定,不可用「统一起算日 + 天数」再算一遍(会得出错误结论)。
- 只有相对工期者才平移:给「N 周 / N 天」的家,才用
统一起算日 + N 天推算完成日。 - 最低价 ≠ 可定标:新口径下经常出现「原最低价因工期超期被排除」——这正是本模块的价值所在,务必在结论里显式点出来,避免使用者误以为排名没变。
两个必备呈现
- 概览/回标状态表必须同时含「报价」与「所报工期 → 完成日」,不能只有价格。列结构建议:
供应商 / 状态 / 回标日期 /〔项目A 报价 + 项目A 工期→完成日〕/〔项目B 报价 + 项目B 工期→完成日〕/ 合计;工期格用 ✓满足 / ✗超期 双色,并加脚注说明起算日、截止日与「绝对日期不顺延」的例外。 - 定标建议节固定六段式(顺序不要变):
- 判定规则三条(起算日 / 门槛 / 原则)
- 工期满足性判定表(每家 × 每项目:天数、工期依据原文、完成日、✓/✗)
- 满足工期的候选 —— 按报价排序(①②③…,首推行高亮)
- 定标推荐结论(每项目一张推荐卡 + 合计金额 + 备选方案及合计)
- 需请对方澄清(工期不满足者:完成日、超出天数、拟澄清事项)
- 定标前置条件(口径澄清、排期自洽、起算日统一定义、交付地等)
设计惯例(速查,详见 references)
- 配色:莫兰迪低饱和多彩,商务活泼。钢蓝
#3E6E9E/ 赭石#B5791F/ 雾青#3E8E80/ 紫灰#8B7FA8/ 琥珀#C99245。禁用纯粉、纯红、高饱和霓虹。供应商配色由浅(首轮)/深(二轮)两档表达轮次。 - 成本涨降配色:上涨=琥珀橙,下降=雾青绿(成本语境,与股市红涨绿跌相反,不冲突)。
- 核心模块须 ≥3 种互补呈现形式(指标带 + 横向标尺 + 卡片网格),避免满屏卡片的呆板感。
- 基准线分两类:总额级(不同城市/面积的历史金额均值)不可比,不画;分项级(同一分项工程量级)可比,可画。
完整规范见 references/design-conventions.md,数据陷阱见 references/pitfalls.md,各节字段级规格见 references/module-spec.md。
技能内文件地图
| 文件 | 作用 |
|---|---|
scripts/build_dashboard.py |
主生成器(配置驱动,出 9 节看板 HTML;--score-panel 装配评分区块) |
scripts/gen_score_panel.py |
评分区块生成器(评委打分固化,出只读表 + seed + meta) |
scripts/verify_score_frozen.py |
固化一致性真机校验(双全新档案 = 两台电脑,本地/线上通用) |
scripts/verify_dashboard.js |
语法门禁 + 桩化运行时冒烟 + NaN 扫描 |
scripts/check_overflow.py |
版式溢出体检(PAGE_SCROLL / BEYOND_WRAP / CLIPPED) |
scripts/dump_rendered.py |
渲染级「单位 ↔ 金额」配对断言 |
scripts/lint_py_const_leak.py |
查 Python 常量泄漏进 JS(会静默中断后续渲染) |
scripts/normalize_quote.py |
报价数据归一化 / 统计自检 |
references/data-contract.md |
config.json / quotes.json / history.json 字段级契约 |
references/score-panel-contract.md |
score_config.json 契约 + 有效平均算法 + 改分流程 |
references/publish-and-verify.md |
上线发布与线上核验手册(含缓存/壳页/版本陷阱) |
references/module-spec.md |
各节出现条件、数据源、呈现形式、易错点 |
references/pitfalls.md |
全部实盘踩坑记录(含判据、根因、修复) |
references/design-conventions.md |
配色 / 版式 / 图表规范 |
examples/ |
全套脱敏示例(可直接跑) |
See Also
bid-quote-clarification-sync(澄清/修订价回灌)bid-price-analysis(商务标分析表 / 串标识别)excel-quote-compare(两表逐项对比)multi-project-quote-board(同批供应商 × 多子项目并置,不做跨项目合计)
