内容
# 麦麦全场景决策官
> 麦当劳(中国)全场景就餐决策助手 · WorkBuddy Skill
> 把「门店 + 活动 + 菜单 + 多人组合 + 优惠 + 积分」放进同一个候选空间,一次给出三套统一方案。
**本项目为「麦当劳程序员创意开发大赛」参赛作品。** 参赛材料与自检方式见下方[参赛材料](#参赛材料)一节。
本项目由参赛者独立开发,**非麦当劳官方产品**;输出仅供参考,不构成医疗、营养或其他专业建议;
餐品信息、价格及供应状态以麦当劳官方渠道的实时结果为准。
---
## 它和"六个独立问答"有什么区别
麦当劳相关的决策是**耦合**的。想省钱,得先知道哪家店有券;想订派对,得先知道这家店当晚有没有场次;想拼单,得同时满足每个人的忌口和预算。
六个独立功能各自都对,但串不成一条链——找店时不知道后面要按人均 60 算,选餐时不知道这家店有场次能订。
**本项目的做法:六个能力 = 同一个候选空间上的六组约束与目标函数。**
```
一句话输入
│
▼
┌──────────────────────────────────────────────────┐
│ 统一候选空间:(门店 × 餐期 × 餐品组合 × 活动场次) │
└──────────────────────────────────────────────────┘
│
├── 硬约束(必须满足):忌口/过敏、预算上限、成团人数上下限
├── 软偏好(尽量满足):口味、营养目标、体验
└── 目标函数(三选一):
费用优先 → 候选内总价严格最低
个人偏好优先 → 平均偏好得分不低于另两套
体验优先 → 共享餐品/品类丰富度/上新/成员差异度
│
▼
三套统一方案,每套都列出:
门店与活动状态 · 逐人餐品 · 个人应付 · 共享分摊 · 订单总价
营养 · 优惠 · 积分建议 · 未满足条件 · 推荐理由
```
**关键不变量:三个方案名各自对应真实的最优性。**
"费用优先"就是**本次已比较的候选里**总价最低的那套——不是加权求和算出来的"近似最低"。这一点用**字典序择优**保证,而不是加权和。加权和会让"偏好分高一点、价格贵 25 元"的方案胜出,然后却挂着"费用优先"的名字。
---
## 快速开始
```bash
# 0) 参赛条件自检(报名前跑一次,10 项自动检查)
node scripts/check_contest.mjs
# 1) 探活,确认 MCP 可达且工具清单完整(期望 35 个)
node scripts/probe_mcp.mjs
# 2) 跑一遍可复现演示(读本地脱敏快照,不发网络请求)
node demo/run_demo.mjs
# 3) 生成一份方案
node scripts/run_pipeline.mjs --in demo/cases/case6_replan.json --out ./out --demo
# 4) 提交前扫描敏感信息
node scripts/scan_secrets.mjs
```
`--live` 会调真实接口(需要凭据,见下)。
---
## 参赛材料
本项目按大赛规则准备了以下文件,全部位于仓库根目录:
| 文件 | 说明 |
| --- | --- |
| `README.md` | 项目介绍、安装方法、使用示例、目标用户(本文件) |
| `CONTEST_DECLARATION.md` | 参赛声明,**使用官方原文,逐字节一致** |
| `MCP_INTEGRATION.md` | 实际使用的 MCP Server、Tool、调用流程与业务价值 |
| `mcp-config.example.json` | 脱敏 MCP 配置示例,只含环境变量占位符 |
| `workbuddy.md` | WorkBuddy 开发对话上下文(申领专项奖励) |
配套文档:
- [`docs/PROJECT_INTRO.md`](docs/PROJECT_INTRO.md) —— 项目介绍
- [`docs/STATEMENT.md`](docs/STATEMENT.md) —— 原创性与合规性说明
- [`docs/CONTEST_GUIDE.md`](docs/CONTEST_GUIDE.md) —— 参赛操作指南
- [`docs/SUBMIT_ISSUE.md`](docs/SUBMIT_ISSUE.md) —— 报名 Issue 正文草稿
- [`DEVELOPMENT_LOG.md`](DEVELOPMENT_LOG.md) —— 完整开发过程记录
- [`docs/PHASES.md`](docs/PHASES.md) —— 分阶段交付与问题清单
**参赛条件自检:**
```bash
node scripts/check_contest.mjs
```
会核对必须文件、文件名、官方声明哈希、配置脱敏、全仓敏感信息、创建时间、Issue 字数等,
输出 `PASS / FAIL / SKIPPED` 以及**必须由本人确认**的 3 项(仓库是否 Public、内容合规、参赛资格)。
---
## 六类场景怎么用
| 你想要的 | 怎么说 | 关键机制 |
| --- | --- | --- |
| 多人聚会 | "周末 6 个人,想搞个生日会" | 活动场次可预约判定 + 共享餐品 + 体验目标 |
| 活动规划 | "这附近有什么派对能订" | 严格区分「活动存在」与「该场次能订」 |
| 健身餐 | "我在减脂,中午想吃点高蛋白的" | 营养偏好;官方营养缺失一律标"未知",不估算 |
| 多人拼单 | "4 个人,各人口味不一样" | 逐人 basket + 共享分摊 + 每人预算满足标记 |
| 省钱比较 | "人均 60,怎么点最省" | 门店券 + 官方算价 + 「最便宜」措辞守卫 |
| 积分建议 | "我积分快过期了" | 麦麦商城可兑换项;**默认不计入点餐价** |
四种就餐方式全覆盖:**到店自取 / 麦乐送 / 得来速 / 企业团餐**。
---
## 输出长什么样
每套方案都包含(真实字段,不是示意):
- **门店与活动状态** —— 门店编码、地址、当日各餐期;**并说明门店是怎么选出来的**(没给商圈时会明确告诉你「这只是接口返回的第一家,不代表离你近」)
- **活动全景表** —— 列出**所有**覆盖你城市的活动(人数区间 / 适合年龄 / 起价 / 能否订),而不只是被选中的那一个;活动列到**场次级**,标明余位与"可/不可预约"及判定依据
- **逐人餐品表** —— 每位成员的餐品、个人应付、共享分摊、合计、预算、预算是否满足、偏好得分
- **订单总价** —— 并标注价格来源(官方整篮快照 / 官方单价求和 / demo 推导)
- **官方算价对照** —— 若官方价与菜单推导价不一致,两个都显示并注明"以官方算价为准"
- **营养** —— 有官方数据的给明细;缺失的标"未知"并计入计数
- **优惠** —— 券结论注明来自 `query-store-coupons`(门店/渠道校验过),而不是卡包;**券在你用餐那天已过期的会打 ⚠️ 标出**(接口说"此刻可用"不等于那天能用)
- **积分建议** —— **只列真正需要积分的商品**(如 500 分换麦辣可乐组合),0 积分的现金商品不会被拿来充数;并明确"属独立交易、不计入点餐价"
- **未满足条件与未知项** —— 包括被排除的餐品及原因
- **推荐理由** —— 且与「最便宜」标记不矛盾
改条件后重跑会多出 **`## 9. 条件变更后的差异`**:列出变动的条件、受影响的分支、以及哪些部分沿用上次结果。
---
## 安全与边界(硬规则,不是免责套话)
| 规则 | 实现方式 |
| --- | --- |
| 不自动下单/领券/兑换/抽奖/预约/支付 | 代码层白名单拦截,不传 `confirm` 直接抛 `RequiresConfirmationError` |
| 个人账户数据不进输出 | 涉及账户的 11 个工具在制品模式下**结构性拒绝**,抛 `ForbiddenInArtifactsError` |
| 不保证食品安全、不给医疗建议 | 忌口只基于菜名/分类关键字,明确声明**不能替代配料表** |
| 营养缺失不估算 | 匹配不上就写"未知"并计数 |
| 价格必须可追溯 | 每个价格带 fidelity 标签,本机求和不冒充官方价 |
| "最便宜"不滥用 | 只在真正严格最低时出现,且限定"本次已比较的候选内" |
| 券 ID / 券码打码 | 按可核销凭据处理,只展示首尾字符 |
后两条不是文档约定——**测试里会真的去扫产物文件**,命中未打码凭据直接 FAIL。
---
## 数据模式
| 模式 | 触发条件 | 行为 |
| --- | --- | --- |
| `live` | 能读到凭据且探活成功 | 调真实 MCP 接口 |
| `demo` | 无凭据 / 探活失败 / 显式 `--demo` | 读 `demo/fixtures/`(2026-10-09 真实抓取,已脱敏),**不发网络请求** |
Demo 输出顶部有显著标注:
```
> ⚠️ DEMO 模式:以下为脱敏演示数据(快照于 2026-10-09),不是实时门店/价格/库存/场次。请勿据此下单。
```
---
## 实测结果
以下为**真实执行**输出,不是设计目标:
| 命令 | 结果 |
| --- | --- |
| `node scripts/probe_mcp.mjs` | `35 tools / 0 missing / 0 extra`,exit 0 |
| `node demo/run_demo.mjs` | `PASS 264 · FAIL 0 · ERROR 0 · SKIPPED 14` — **100.0%** |
| `node demo/run_demo.mjs --live` | `PASS 240 · FAIL 0 · ERROR 0 · SKIPPED 38` — **100.0%** |
| `node scripts/capture_prices.mjs --units-only` | 76/80 单价成功,4 个结构性不可核价 |
| `node scripts/scan_secrets.mjs` | 64 个文本文件,需处理 0 处(良性命中 67 处仅备案),exit 0 |
> 第 6 轮对比第 5 轮:demo `PASS 195 → 264`、live `PASS 177 → 240`,**FAIL 始终为 0**。
### 基准(golden)机制
demo 模式读固定快照,输出应逐字节可复现。因此每次 demo 运行都会把关键摘要与 `demo/expected/*.json` 比对:一致 PASS;不一致 FAIL 并打印逐字段差异摘要(`旧 → 新`),提示用 `--update-golden` 刷新。live 模式显式跳过(数据每次天然不同)。
这条机制上线当天就抓出了价格分级词表分叉(`official` → `official-snapshot`)——它确实在拦意外变更,不是装饰。
四态断言语义:
- `PASS` 执行了且成立
- `FAIL` 执行了但不成立
- `ERROR` 执行时抛异常(**未真正验证**)
- `SKIPPED` 依赖缺失或被显式跳过 —— **永不计入通过率,且必须给出原因**
SKIPPED 都不是漏跑,全部是"该断言在当前条件下不适用",且每条都写明原因。例:live 模式下的模式互斥断言、基准比对断言(live 数据每次天然不同)、"本次需求未涉及活动"、"本例忌口为 none 无需按人区分",以及"live 模式下真实门店的券未携带门槛/门店/时段/星期字段,无法验证复杂用券条件"。
---
## 目录结构
```
SKILL.md 技能主文件(给 Agent 读)
README.md 本文件(给人读)
scripts/
run_pipeline.mjs 端到端编排 + CLI
parse_requirements.mjs M0 需求解析
fetch_context.mjs M1/M2 门店 + 活动
validate.mjs M3 忌口/约束三态
generate_combos.mjs M4 多人组合生成
price.mjs M5 官方算价(含 fidelity 标注)
coupons.mjs / points.mjs M5 券与积分建议
points_expiry.mjs 积分到期管家(可选,默认关闭,需显式同意)
score_plans.mjs M8 字典序择优
diff_plans.mjs M9 条件变更差异
render.mjs M10 Markdown + HTML 渲染
capture_prices.mjs 官方价快照抓取
probe_mcp.mjs 探活 + 工具清单核对
scan_secrets.mjs 交付前凭据扫描
check_contest.mjs 参赛条件自检
lib/
mcp_shim.mjs MCP 客户端 + 三道结构性安全门
normalize.mjs 真实返回 → 内部模型的翻译层
redact.mjs 脱敏与密钥扫描
schema.mjs 零依赖校验器
util.mjs 金额/哈希/超时等工具
references/
mcp-tools.md 35 个工具的真实契约(含单位陷阱)
mcp-gotchas.md 20 个真实踩坑记录
decision-policy.md 择优策略与措辞守卫
output-template.md 输入输出字段与模板
docs/
PROJECT_INTRO.md 项目介绍
STATEMENT.md 原创性与合规性说明
PHASES.md 分阶段实施记录
CONTEST_GUIDE.md 参赛操作指南
SUBMIT_ISSUE.md 报名 Issue 正文草稿
demo/
run_demo.mjs 四态断言演示
cases/ 8 个可跑用例
fixtures/ 真实脱敏快照
expected/ 基准结果
# 参赛材料(仓库根目录)
README.md 项目介绍(本文件)
CONTEST_DECLARATION.md 参赛声明(官方原文,不可改动)
MCP_INTEGRATION.md MCP 接入指南
mcp-config.example.json 脱敏配置示例
workbuddy.md WorkBuddy 开发对话上下文
DEVELOPMENT_LOG.md 完整开发实录(真实命令 + 真实输出)
```
---
## 接入 MCP
```bash
export MCD_MCP_URL="https://mcp.mcd.cn"
export MCD_MCP_TOKEN="<你的 token>"
node scripts/probe_mcp.mjs
```
也可以写进 `~/.workbuddy/mcp.json`(配置示例见 `mcp-config.example.json`)。**不要把 token 提交进仓库。**
详见 `MCP_INTEGRATION.md`。
---
## 环境要求
- Node.js ≥ 18(用到原生 fetch、ESM、`node:test` 风格断言)
- 零第三方依赖 —— 全部使用 Node 内置模块
- 无凭据也能跑通 demo 模式
---
## 已知未解决问题
诚实记录,不掩饰:
1. **官方算价是 all-or-nothing** —— 篮里只要有一个餐品官方不给报价,整篮 `price=undefined`。实测 4 个餐品结构性不可核价(含 `9900011123 "辣"么快乐套餐`)。当前在 live 模式下现场发现并剔除,并在报告里如实披露。
2. **营养表覆盖率低** —— 12/80,且以**菜名**为索引,同名不同规格无法区分。
3. **单位混用陷阱** —— 场次价格是**分**,`query-meals.currentPrice` 是**元**,混用差 100 倍。已在翻译层统一,但新接触这个 API 极易踩。
4. **券打码后可核销性下降** —— 打码是为了不把凭据写进产物,代价是用户得去 App 核销。
5. **`campaign-calendar` 不是可预约场次** —— 它只返回营销内容;可预约必须走 `query-party-store-session`。
6. **覆盖城市随 SPU 变化** —— 同一活动不同 SPU 覆盖城市数差异极大(实测 4 vs 279),必须逐个核对而不是取并集。
7. **`query-meals` 按餐期返回** —— 同一家店同一天,白天 80 项、23:20 只有 71 项(甜品退场、巨无霸上线)。方案用实时菜单没问题,但快照刷新必须按域拆分(`--capture-activity`),否则会悄悄换掉菜单基准。
8. **活动详情有展开上限** —— 上海命中 17 个活动,只对前 14 个取人数/年龄详情,报告会显式说明"另有 3 个未展开"。
9. **积分清单是下限不是全集** —— 只有 7 项要积分,且都是"到店专用"体验券;本次没找到能查"积分可换餐食"的工具。
10. **营养目标在数据缺失时完全不生效** —— 80 个餐品只有 12 个能匹配营养表。“关注蛋白质”这类诉求本次**答不了**,工具会如实说明而不是给个估算值。
---
## License
MIT,见 `LICENSE`。
连接信息
猜你喜欢
暂无推荐服务器