内容
# 麦麦预算餐盘 · mcd-budget-bite
> 给我一个预算,还你一份「吃得饱、花得值、热量心里有数」的麦当劳组合。
基于 [麦当劳中国 MCP](https://github.com/M-China/mcd-mcp-server) 开发的点餐决策 Skill。
在**预算上限**与**热量上限**双重约束下枚举全部可行组合,输出三套方案、热量贴士与性价比天梯榜。
**[👉 在线 Demo · 拖动滑块直接玩](https://tttmc.github.io/mcd-budget-bite/)** | [离线单文件版](./docs/index.html)(下载后双击打开,不联网也能用)
---
## 先玩一下
`docs/index.html` 是一个**零依赖单文件页面**——全部数据内联,不需要起服务、不需要 Token、不联网、不下单。
浏览器打开后拖动「预算」和「热量」两个滑块,三套方案和天梯榜实时重算。页面里的求解逻辑与命令行版**逐项对拍过**(见 `scripts/verify_demo.js`,6 个场景全一致),不是另一套近似实现。
数据来自麦当劳 MCP 的真实抓取快照(上海门店 `1450104`,2026-10-10)。
```bash
# 直接打开
python -m http.server 8000 # 然后访问 http://localhost:8000/docs/
# 或者直接双击 docs/index.html,效果完全一样
```
Regenerating:`python scripts/export_demo_data.py --offline && python scripts/build_demo.py`
---
## 为什么是它
麦当劳的点餐决策通常只有两个流派:
- **省钱派**:找券、凑满减、算折扣 —— 但不告诉你这顿多少大卡
- **热量派**:算热量、配宏量 —— 但不告诉你这顿要花多少钱
现实是,**预算和热量往往是同时存在的约束**。你说「今天午饭只有 30 块」,和「我在减脂,这顿别超 700 大卡」,通常是同一个人、同一顿饭。
麦麦预算餐盘把这两条约束放进同一个求解器,一次性给出答案。
## 它和其他点餐 Skill 有什么不同
| | 常见做法 | 麦麦预算餐盘 |
|---|---|---|
| 目标 | 单目标:最便宜 或 热量最低 | **双约束**:预算 × 热量同时成立 |
| 输出 | 一个「最优解」 | **三套方案**:最饱 / 最省 / 最均衡,决定权还给你 |
| 度量 | 无 | **性价比天梯榜**:每元热量、每元蛋白质 |
| 套餐 | 大多只算单品 | 拆解套餐子项累加营养 |
| 缺失数据 | 估算或用近似值 | **标注缺失并排除,绝不猜测** |
---
## 在 WorkBuddy 里直接用(推荐)
本项目同时是一个 **WorkBuddy Skill**。装好后不用敲命令,在对话里说人话就行:
```
30 块能吃啥麦当劳
预算 25,热量别超 700,帮我配一顿
我在减脂,今晚想吃麦当劳
换成北京王府井
```
Skill 会自动识别意图、补齐没说的参数(热量默认 800 kcal)、调用麦当劳 MCP、回传三套方案。
安装:把仓库放进 WorkBuddy 的 Skill 目录 `~/.workbuddy/skills/mcd-budget-bite/`,重启 WorkBuddy 即可。
开发过程与对话实测记录见 [workbuddy.md](./workbuddy.md)。
---
## 快速开始(命令行)
### 1. 申请 MCP Token
访问 [https://open.mcd.cn/mcp](https://open.mcd.cn/mcp) → 登录(手机号验证)→ 控制台 → 激活 → 复制 Token。
### 2. 配置 MCP
复制 `mcp-config.example.json`,把 `YOUR_MCP_TOKEN` 换成真实 Token。
在 WorkBuddy 中:左侧边栏【专家·技能·连接器】→【连接器】→【自定义连接器】→【配置MCP】。
### 3. 运行
```bash
git clone https://github.com/<你的账号>/mcd-budget-bite.git
cd mcd-budget-bite
export MCD_MCP_TOKEN=<你的 token>
# 预算 30 元、热量上限 800 大卡
python scripts/solve.py --store 1450104 --budget 30 --kcal 800
# 完全离线复算(用仓库自带的缓存数据,不联网、不需要 Token)
python scripts/solve.py --budget 30 --kcal 800 --offline
```
零第三方依赖,Python 3.9+ 即可。
> 完整使用说明(含 WorkBuddy 对话用法、换门店、场景配方、常见问题)
> 见 [使用教程.md](./使用教程.md)。
不知道自己所在城市的门店编码?
```bash
python scripts/find_store.py 北京市 王府井
```
### 参数
| 参数 | 默认 | 说明 |
|---|---|---|
| `--store` | `1450104` | 门店编码,用 `query-nearby-stores` 获取 |
| `--budget` | `30` | 预算上限(元) |
| `--kcal` | `800` | 热量上限(kcal) |
| `--max-items` | `3` | 组合最多件数 |
| `--no-combos` | 关 | 排除套餐,只组合单品 |
| `--offline` | 关 | 只用 `data/` 缓存,不联网 |
---
## 真实输出示例
以下为 `--budget 30 --kcal 800` 的实际运行结果(上海某门店,2026-10-10):
```
预算 ¥30 | 热量上限 800 kcal | 候选 55 项
【最饱型】 ¥30 | 742 kcal | 蛋白质 18g | 钠 623mg
· 麦乐鸡套餐 ¥25 / 649 kcal
· 圆筒冰淇淋 ¥5 / 93 kcal
每元热量 24.7 kcal | 每元蛋白质 0.60 g
【最省型】 ¥17 | 369 kcal | 蛋白质 15g | 钠 731mg
· 麦香鸡 ¥17 / 369 kcal
每元热量 21.7 kcal | 每元蛋白质 0.88 g
【最均衡型】 ¥29 | 576 kcal | 蛋白质 34g | 钠 1397mg
· 牛肉堡单品精选 ¥14 / 248 kcal
· 麦麦脆汁鸡1块 ¥15 / 328 kcal
每元热量 19.9 kcal | 每元蛋白质 1.17 g
💡 热量贴士
· 本组合热量与钠均在合理区间,可放心作为一餐
📊 性价比天梯榜(每元热量 kcal/¥,仅统计单品)
注:甜品常居榜首——每元热量高,但蛋白质和饱腹感都不划算
1. 派 ¥8.5 221 kcal 26.0 kcal/¥
2. 新地 ¥12 282 kcal 23.5 kcal/¥
3. 芝士安格斯厚牛堡 ¥31.5 696 kcal 22.1 kcal/¥
4. 麦麦脆汁鸡1块 ¥15 328 kcal 21.9 kcal/¥
5. 麦香鸡 ¥17 369 kcal 21.7 kcal/¥
6. 麦辣鸡腿汉堡 ¥22.5 485 kcal 21.6 kcal/¥
7. 培根安格斯厚牛堡 ¥34.5 707 kcal 20.5 kcal/¥
8. 双层深海鳕鱼堡 ¥24 485 kcal 20.2 kcal/¥
9. 巨无霸 ¥25.5 513 kcal 20.1 kcal/¥
10. 双层吉士汉堡 ¥23 429 kcal 18.7 kcal/¥
```
换个约束再看:
| 场景 | 最省型 | 最均衡型 |
|---|---|---|
| ¥25 / 700 kcal | 麦香鸡 ¥17 / 369 kcal | 双层深海鳕鱼堡 ¥24 / 485 kcal |
| ¥40 / 900 kcal | 麦香鸡 ¥17 / 369 kcal | 双层深海鳕鱼堡 + 麦麦脆汁鸡 ¥39 / 813 kcal |
¥40 那档的均衡解蛋白质高达 49g,但钠也到了 1822mg —— 求解器会直接给出
「约占日均参考值 91%,当天其余餐次建议清淡」的提醒,**不会只报好消息**。
---
## 目标用户
1. **预算敏感的人**:午饭就这么多钱,想知道钱能买到多少东西
2. **控制热量的人**:减脂期也想吃麦当劳,需要知道代价是多少
3. **纠结的人**:不想在点餐机前站五分钟
---
## 设计取舍
**为什么用全枚举而不是贪心/动态规划?**
单店在售餐品约 121 项,限制在 1~3 件组合,搜索空间只有十万量级,全枚举几秒内跑完且**保证全局最优**。用近似算法反而引入了不可解释性。
**为什么不做模糊匹配营养数据?**
实测过。`巨无霸三件套` 会被模糊匹配到 `巨无霸`,但三件套还含薯条和可乐,热量差一倍以上。
**「看起来匹配上了其实错了」比「匹配不上」危险得多。** 所以本项目只做严格匹配,
匹配不上一律标注缺失并排除。当前 121 项中约 55 项可精确计算,其余为限定新品或规格歧义项。
**为什么天梯榜只统计单品?**
套餐和多人份拼盘(如十翅拼盘)的份量口径不同,混排没有可比性。
**为什么限制一顿必须含主食?**
不加这条约束,「最省型」会给你「新地 + 圆筒冰淇淋」——确实便宜,但不是一顿饭。
---
## 目录结构
```
mcd-budget-bite/
├── SKILL.md # Skill 定义(触发条件与工作流)
├── README.md # 项目介绍
├── 使用教程.md # 完整使用说明
├── MCP_INTEGRATION.md # MCP 接入说明
├── CONTEST_DECLARATION.md # 参赛声明(官方模板,未改动)
├── mcp-config.example.json # 脱敏配置示例
├── workbuddy.md # WorkBuddy 开发上下文
├── docs/
│ ├── index.html # 在线 Demo 单文件页(数据内联,双击即用)
│ ├── template.html # Demo 模板(含占位符,供 build_demo 注入)
│ └── demo_data.json # Demo 用的候选集快照
├── scripts/
│ ├── mcp_client.py # MCP Streamable HTTP 客户端(零依赖)
│ ├── solve.py # 双约束求解器
│ ├── find_store.py # 门店编码查询
│ ├── export_demo_data.py # 导出候选集 → docs/demo_data.json
│ ├── build_demo.py # 把数据注入模板 → docs/index.html
│ ├── verify_demo.js # 前端与求解器的一致性对拍(Node 运行)
│ └── .env # Token(已在 .gitignore 中,不提交)
└── data/
├── raw_meals_<门店编码>.json # 菜单缓存(按门店隔离)
├── meal_details_<门店编码>.json # 套餐拆解缓存(按门店隔离)
└── raw_nutrition.json # 营养数据缓存(全国通用)
```
> **为什么缓存要按门店拆文件?**
> 菜单价格和套餐结构都是门店维度的(例如麦香鸡上海 ¥17 / 北京 ¥16.5)。
> 早期版本共用单个缓存文件,查过 A 店后再查 B 店会**静默读到 A 的数据**——
> 价格差只有几毛到 1 元,极难察觉。现在每个门店独立文件,且缓存里写入了
> 来源门店标记,离线读取时会校验,不一致直接报错而不是给出错误答案。
---
## 免责说明
本项目为麦当劳程序员节创意开发大赛参赛作品,由参赛者独立开发,**非麦当劳官方产品**。
项目输出仅供参考,**不构成医疗、营养或其他专业建议**。餐品信息、价格及供应状态
以麦当劳官方渠道的实时结果为准。营养数据来自麦当劳 MCP 提供的公开营养信息库,
不覆盖全部在售餐品。
---
© 2026. 麦当劳相关商标归其权利人所有,本项目不构成对麦当劳及其关联方商标的任何授权。