mcd-dual-budget-coach · v0.3.0 · 2026-10-25 · sha256 460d5eb63c8b5a4f

mcd-dual-budget-coach v0.3.0A

Immutable. This exact content never changes and is served at /api/v1/blob/460d5eb63c8b5a4f.

---
name: mcd-dual-budget-coach
description: 麦门双预算军师——一个人吃,告诉它多少钱多少卡,它配出三档最划算的一顿;一群人吃,告诉它几个人什么场景,它算出整桌怎么拼单、比各点各的省多少,并生成点单接龙。输入示例:「30 块 600 卡」「4 个人 150 块聚餐怎么点」。
version: 0.3.0
author: erikzeng2008-del
---

# 麦门双预算军师 (McDualBudget Coach)

> 钱是一个预算,热量是另一个预算。两个都不超,一顿配到位。

## 实测情报(2026-10-09 校准,必读)

以下全部来自对 `https://mcp.mcd.cn` 的真机实测,写死在本 Skill 里,运行时不必重新探索:

### 枚举值(写死,不要猜)

| 参数 | 取值 |
|---|---|
| `beType` | 1=到店自提,2=麦乐送到家,5=得来速DT,6=企业团餐 |
| `orderType` | 1=到店(含自取+DT),2=外送(含麦乐送+团餐) |
| `searchType`(query-nearby-stores) | 2=按位置搜索,1=搜索收藏餐厅 |

### 关键坑(实测踩过)

1. **价格单位是「分」**:`calculate-price` 返回 `price: 2650` = ¥26.5。展示前必须 ÷100。
2. **`query-nearby-stores` 的 city 和 keyword 必须同时非空**:只传 city 会报 `600058 城市名或者关键词不能为空`。city 传城市名,keyword 传区域/地标关键词(如「仓山」「万达」)。
3. **`query-meals` 的返回已升级(2026-10-25 复测,旧记录作废)**:现在直接返回 `data.meals[code]` 完整实体——`name` / `image` / `currentPrice`(现价)/ `originalPrice`(划线价)/ `discountType` / `withOrder`(随单购)。**不必再逐个 `calculate-price` 补名称和价格**;`originalPrice` 还能直接算「省多少」。旧版只给 `code + tags` 的写法仍兼容,但属于浪费调用。
   - 热量仍需 `list-nutrition-foods` 按名称匹配;**新品(龙焰系列、韩式炸鸡、薄皮 V 翅等)营养表查不到**,必须标「未知」,不得估算
   - 价格单位:`query-meals` 的 `currentPrice` 是**元**(字符串,如 "26.9");`calculate-price` 返回的 `price` 仍是**分**,两者别混
   - `query-meal-detail` 的参数名是 **`code`**,不是 `mealCode`(传错会报 `400 code不得为空白`);返回 `rounds[].choices[]`,`isDefault=1` 是默认档,`diffPrice` 是换档差价
   - **分类有坑**:同一 SKU 会同时出现在「人气热卖」(营销位)和真实品类下。建索引时真实品类必须优先,否则「精选单人餐三件套」「龙焰鸡腿堡三件套」会被误判出候选池
   - **文本换行有坑**:`list-nutrition-foods` 等接口把换行转义成字面 `\n` 两字符,解析前先统一替换成真换行,否则整段被当成一行
4. **饮品单独核价返回 ¥0**:`calculate-price` 单瓶可乐返回 0(饮料必须挂在套餐/1+1 内售卖)。组合算法里饮品不要单独参与单品核价,改用套餐/1+1 的 `diffPrice` 差价逻辑。
5. **到店自提(beType=1)无 beCode**:门店返回里 beCode 为空,后续所有调用不传 beCode;得来速(beType=5)必传。
6. **响应格式**:MCP 返回的是「markdown 字段说明 + 原始 JSON」混合文本,JSON 以 `{"success":...` 开头(可能与说明文字同行,用 raw_decode 解析)。
7. **限流**:每 Token 600 次/分钟。本 Skill 单次任务 ≤15 次调用,安全。

### 两类宝藏 SKU(组合算法的优先候选)

- **随心配 1+1**(¥13.9~14.9):「蓝区主食/小食 + 粉区饮品/甜品」任意拼,是低预算组合的神器。code 示例:`9900015568`(¥13.9)、`9900013304`(¥14.9),内容随门店/时段变化,用 `query-meal-detail` 查 rounds 实时构成。
- **官方「500大卡套餐」分类**:菜单里有专门的 500 大卡档套餐(如「堡」你欢乐套餐 ¥28),与热量预算天然对齐,优先进入候选池。

## 何时使用本 Skill

当用户提出以下任意需求时触发:

- 「帮我配一顿 30 块以内的」
- 「600 大卡能吃麦当劳的什么」
- 「中午 40 块、想吃点扎实的,帮我配一个」
- 「健身完只敢吃 500 卡,麦门还有救吗」
- 「帮我用最少的钱吃到巨无霸」

**判定特征**:用户输入中包含(明示或暗示)**金额预算**和/或**热量预算**,且意图是「在麦当劳吃一顿」。只给一个预算时,另一个按「不设限」处理,但要向用户说明。

## 输入解析(第一步)

从用户的话里抽取五个字段:

| 字段 | 必填 | 示例 | 缺省处理 |
|---|---|---|---|
| `money_budget` | 否 | 30 / 三十块 / ¥35 | 不设限(但组合时优先性价比) |
| `calorie_budget` | 否 | 600 / 五百大卡 | 不设限(但仍输出热量供参考) |
| `protein_min` | 否 | 蛋白要 30g / 练完腿补蛋白 | 不设下限(蛋白始终输出供参考) |
| `meal_time` | 否 | 早餐 / 宵夜 / 下午茶 | 由 `now-time-info` 当前时间推断 |
| `preferences` | 否 | 想吃扎实的 / 不要辣 / 有可乐吗 | 无 |

⚠️ 用户没给任何预算时(只说「帮我配一顿」),先追问一句「多少钱、多少卡的预算?」——不猜。这是唯一允许的追问,一次问完,不许分两轮。

**`protein_min` 识别信号**:「练完」「健身」「增肌」「补蛋白」「蛋白 ≥Xg」→ 抽取为第三道闸门。抽取后必须向用户复述一句「已按蛋白 ≥Xg 过滤」,不许静默启用。

## 调用流程(第二步)

严格按以下顺序调用麦当劳 MCP 工具(Server:`mcd-mcp`):

```
1. now-time-info          → 取当前时间,推断时段(早餐菜单和正餐菜单不同!)
2. query-nearby-stores    → beType=1 + searchType=2 + city 和 keyword 都传(缺一报 600058)
                            从返回的 data[] 里挑门店,拿 storeCode(自提无 beCode 不传)
3. query-meals            → beType=1, orderType=1, storeCode
                            拿分类+code 池。重点关注:随心配1+1、500大卡套餐、单人餐分类
4. list-nutrition-foods   → 一次性返回约 160 个餐品的热量(energyKcal),按 productName 匹配
5. query-meal-detail      → 对候选套餐/1+1 查 rounds 构成(每段的候选 code + diffPrice 差价)
6. query-my-coupons       → 拉用户已有券(返回 markdown,含券名/用券价/有效期)
7. calculate-price        → 对每个候选组合核价(items[].productCode + quantity + roundList)
                            ⚠️ 价格单位是分;饮品单核价返回 0 属正常,饮品走套餐内 diffPrice
```

**关键纪律**:
- **只报价不下单**。本 Skill 不调用 `create-order`,输出组合后由用户自己决定是否下单。用户明确说「直接帮我下单」时,再次确认门店、餐品、就餐方式后才能走 `create-order`。
- **先候选后核价**:从菜单池筛出 8~12 个候选 code(主食/小食/套餐/1+1),逐个核价,不要对 128 个 SKU 全量核价。
- `query-meals` 返回的是「当前时段可售」——不要拿营养表里的全量餐品硬凑,先取交集(名称匹配)。

## 双预算组合算法(第三步)

拿到菜单+营养+券后,按以下策略组合:

### 约束检查
```
组合实付价(券后,分→元) ≤ money_budget
组合总热量 (kcal,营养表 energyKcal 求和) ≤ calorie_budget
组合总蛋白 (g,营养表求和) ≥ protein_min        ← 仅在用户要了蛋白下限时启用
```

**三道闸门的取舍是显式算出来的,不藏着**:蛋白下限会推高金额或热量,当三者冲突无解时,诚实报告「这三个条件同时满足不了,最接近的方案是……还差 X」。

### 组合优先级(从高到低)
1. **官方热量档套餐 / 1+1 随心配**:与双预算天然对齐,优先
2. **主食+饮品+小食 三件套**:结构完整优先(像一顿正经饭)
3. **主食+饮品/小食 两件套 或 套餐降配**(用 roundList 的 diffPrice 换低价件)
4. **单品**:双预算都极紧时的最后答案

### 组合搜索方法
- 把候选按「饱腹感权重(主食 3 : 小食 2 : 饮品 1)」和单价打分
- 在预算内枚举 2~6 个组合,每个组合必须同时过两道预算闸门
- 有券的餐品用券价参与计算(券价来自 query-my-coupons 的「用券价格」)
- 输出**恰好 3 个方案**:🎉 最划算(价格最低,锚点:证明低价也能吃好)、💪 最扎实(热量最高,管饱优先)、⚖️ 最均衡(**钱花到预算的 85% 附近**、热量贴近目标——让预算真正被用上)

### 热量超限的降级话术
用户热量预算给太低配不出来时(如 300 卡想吃套餐),不硬凑,改输出:
- 「300 卡在麦门只能吃这些单品:……」+ 诚实列出
- 补一句「加 X 元升到 Y 卡就能吃上正经套餐」——给台阶,不泼冷水

## 输出格式(第四步)

固定用「本单战报」格式输出(这是本 Skill 的记忆点,不许改;标题与收束块必须保留):

````
🍔 **麦门双预算战报** · ¥30 / 600 kcal
📍 麦当劳福州金洲南路餐厅 · 正餐时段

| | 方案 | 组合 | 实付 | 热量 | 蛋白 | 钠 |
|---|---|---|---|---|---|---|
| 🎉 | 最划算 | 精选超值随心配(那么大鸡排 + 可乐中杯) | ¥13.9 | 532 kcal | 24 g | 996 mg |
| 💪 | 最扎实 | 麦辣鸡腿汉堡 + 圆筒冰淇淋 | ¥27.5 | 578 kcal | 26 g | 1310 mg |
| ⚖️ | 最均衡 | 巨无霸 | ¥26.5 | 513 kcal | 27 g | 961 mg |

💰 **预算执行** ¥13.9 / ¥30 | 剩 ¥16.1
🔥 **热量执行** 532 / 600 kcal | 剩 68
🧂 **钠摄入** 996 / 2000 mg(50% 每日上限)
🏃 **运动等价物** 慢跑 53 分钟 = 游泳 67 分钟
🧧 **省钱心情** 你手上有「中薯条买一送一」¥13.5 —— 这一单结算时用上,直接抵掉一部分

🎟️ **这一单能用的券**
- 王牌小食拼 ¥15.9 → 用在方案二「最扎实」(¥27.5 → ¥11.6,省 ¥15.9)
- 用不上:中薯条买一送一(这一单没有薯条)、早餐两件套(正餐时段)

> 回复「1 / 2 / 3」看明细;「换一批」再出三个
````

**排版的三个纪律**:
1. **表格承载事实**(组合/实付/热量/蛋白/钠五列),**收束块承载情绪**(预算执行→省钱心情)——事实与情绪分区,不混排
2. 用户要了蛋白下限时,表格「蛋白」列变为必需,并在开头复述一句「已按蛋白 ≥Xg 过滤」
3. 表格渲染不了的环境(纯文本渠道)降级为逐行列表,**分区结构不变**

**钠行为什么必须有**:热量和钠是两个独立维度——实测存在「热量很漂亮、钠吃掉一整天上限」的组合(不限钠时最优解钠可达 1996mg)。只报热量的「健康配餐」是半张答卷。钠合计取各单品 `钠(mg)` 求和;含未知营养单品时写「本单含未知单品,钠合计不完整」,不假装完整。

**运动等价物换算表**(按 70kg 成人估算,输出时四舍五入):

| 运动 | 换算系数 |
|---|---|
| 慢跑 | ≈ 10 kcal/分钟 |
| 游泳 | ≈ 8 kcal/分钟 |
| 爬楼梯 | ≈ 6 kcal/分钟(约 0.5 层/分钟) |
| 快走 | ≈ 4.5 kcal/分钟 |

「省钱心情」一句的规则:省 0 元不输出此行;省钱等价物优先从小食/甜品里找(比「省了 3 元 = 白赚一个圆筒」),找不到再报纯金额;用户有即将到期的券时优先提醒。

### 券 × 方案匹配(第 3.5 步,省钱军师的本分)

组合出来后,把 `query-my-coupons` 的券逐张对到方案上——**匹配看成分,不看名字**:

1. **读成分再比**:券适用品类(薯条/小食/派/饮品…)要拿到**套餐内部构成**去比对(`query-meal-detail` 的默认档),不能只看商品名——「酥脆无双套餐」名字里没有「薯条」,但默认档含迷你薯条,薯条券就能用。
2. **算划算**:券价 > 方案原价 → 判定「不划算」,直说「这张券比单点还贵」,不硬推。
3. **说清原因**:用不上的券也要列出并说明(「这一单里没有薯条,这张券用不上」)——不藏着。
4. **诚实说没有**:全部用不上时直说「没券可用,按原价算」,不许硬凑一张显得省钱。

**为什么这条重要**:券到底能不能用,用户自己看不出(要翻套餐构成对照券规则)。替用户把这步算清楚,是「省钱军师」和「菜单搬运工」的分界线。

### 换一批(用户追问时)

用户说「再来一批 / 换一批 / 还有别的吗」时:

1. **不重置已推荐的组合**——从过闸集合里排除已经给过的组合
2. 按三档重新排序,取前 3 个,输出格式完全不变
3. 若该预算下已无未推荐过的过闸组合,诚实回答:「这个预算下能配的都在这儿了(共 N 个),要么加钱、要么放宽某一侧」

实测参考(2026-10-10 金洲南路店):30 元/600 卡有 40 个过闸组合,35 元/700 卡 118 个,50 元不限卡 1280 个——足够换十几批。**三档不是上限,是入口。**

## 多人拼单模式(2026-10-25 新增 · 与单人双预算并列的第二条产品线)

一个人吃用上面的双预算;**一群人吃切到这一节**。两条线共用同一套「闸门 + 三档 + 诚实报告」的思路,但算法完全不同——单人算的是「组合」,多人算的是「分摊」。

### 何时触发

「4 个人吃麦当劳怎么点」「办公室 6 个人凑一单」「周末带娃一家三口」「150 块请同事吃麦门」「下午茶 5 个人拼一下」。
**判定特征**:出现人数(≥2)或「拼单 / 聚餐 / 一起点 / 凑单 / 团建 / 接龙」。单人需求不要误切到这里。

### 输入解析

| 字段 | 必填 | 示例 | 缺省 |
|---|---|---|---|
| `people` | **是** | 4 人 / 我们六个 | 没有就追问一句「几个人?」——这是本模式唯一允许的追问 |
| `budget` | 否 | 150 / 人均 40 | 不限;给了人均就 ×人数 换算成总预算 |
| `scene` | 否 | 朋友聚餐 / 办公室加班 / 家庭带娃 / 下午茶 | 朋友聚餐 |
| `prefs` | 否 | 不要辣 / 有小孩 / 来杯咖啡 / 想轻一点 / 加甜品 | 无 |

### 调用流程

```
1. now-time-info        → 时段(早餐与正餐菜单不同)
2. query-nearby-stores  → city + keyword 同传,拿 storeCode
3. query-meals          → 一次拿全量 名称/图/现价/划线价(不要再逐个核价)
4. query-meal-detail    → 对多人餐与候选套餐查 rounds 构成(参数名是 code)
5. list-nutrition-foods → 名称匹配热量;套餐热量 = 默认构成单品求和
6. campaign-calendar    → 本周活动与新品(聚餐常被新品驱动,别漏)
7. available-coupons    → 有可领券时提示,用户说「领」才调 auto-bind-coupons
```

**多人餐是这条线的核心资产,必须先查构成再算**:BFF四宫格小食盘双人餐、堡堡随心拼(7件任选)、薄皮V翅双拼十翅、开心亲子双人餐……这些 SKU 名字看不出里面有什么,`rounds` 才知道。不查构成就报「人均」,是在猜。

### 拼单算法(灵魂公式)

> **随心配解决「主食 + 饮品」,共享拼盘解决「小食」。**
> 不要给每个人都点一份三件套——那是「各点各的」,不是拼单。

- **各点各的基准**:`N × 精选单人餐三件套`(麦当劳最常规的单人点法)。所有「省多少」都对它算,不许对自己编造的更贵基准算。
- **最省钱**:`N × 随心配 + 共享小食(约每 4 人 1 份)`。这一档**绝不顶格加菜**——加满了就不是最省钱。
- **最丰盛**:`双人餐 × ceil(N/2) + 分享装加菜(十翅优先)`,份数优先。
- **最均衡**:`双人餐 × floor(N/2) + 奇数人补一份单人餐`;有预算时把钱用到预算 85% 附近,加的是**共享小食**而不是再给每人加一份。
- **场景修正**:家庭带娃 → 开心亲子双人餐/开心乐园餐(含玩具);办公室加班 → 补麦咖啡;下午茶 → **不硬塞主食**,按「共享小食 + 饮品/甜品」凑;不要辣 → 双人餐改「堡堡随心拼」(主食可自选不辣),**共享小食里的辣款也要一起剔**,只换主食是自欺欺人。
- **想轻一点**:在 500 大卡套餐里选**最便宜的**(选热量最低的会把最省钱档顶成全场最贵)。

实测(2026-10-25 福州金洲南路,4 人):各点各的 ¥138 → 拼单最省钱 ¥75.5,**省 ¥62.5(45%)**,人均 ¥18.9;最均衡 ¥125.8 / 16 份;最丰盛 ¥175.7 / 21 份。

### 多人模式的三条诚实纪律(比省钱更重要)

1. **主食覆盖必须报数**:双人餐算 2 人、单人餐/随心配/单品汉堡算 1 人,输出「主食覆盖 X / N 人」。预算实在不够时,**不许偷偷少给两份主食**,要明示「这个预算下主食只够 X 人,还差 Y 份,要么加预算要么减人」。
2. **超预算不静默**:先砍加菜与共享拼盘、再砍主食;砍完仍超就在卡片上挂警告,不能装作没超。
3. **营养未知不凑数**:多人餐里新品占比高(薄皮 V 翅、韩式鸡块、龙焰系列营养表都查不到),未知项单独计数并写出「含 N 项营养未知,合计不完整」;玩具不是食物,不计热量也不计未知。

### 输出格式

先给一句对比,再三档卡片,最后接龙文案:

```
🆚 各点各的:4 × 精选单人餐三件套(¥34.5/人)= ¥138.0
🧩 拼单共享:¥75.5,人均 ¥18.9 —— 这一桌省 ¥62.5,相当于白得 12 个圆筒冰淇淋

🎉 最省钱|🍗 最丰盛|⚖️ 最均衡
每档:清单(含每份套餐的内部构成)/ 整桌合计 / 人均 / 份数 / 主食覆盖 / 整桌热量 / 对比划线价

🍔 麦门拼单接龙 · 4人 · 🎉最省钱
📍 麦当劳福州金洲南路餐厅(到店自提)
<逐行清单 + 每份套餐构成>
合计 ¥75.5 | 人均 ¥18.9 | 共 10 份
接龙:
1.
2.
3.
4.
—— 数据:麦当劳官方 MCP 快照 <时间>,以实际结算为准
```

接龙文案必须**可直接复制发微信群**:纯文本、不依赖表格渲染、留好空接龙行。

### 拼单自测

`python scripts/run_party_selftest.py` —— 33 项断言跑在 `party.html` 发货代码上,含「拼单真的比各点各的便宜」「主食覆盖与人数匹配」「预算不足明示缺口」「不要辣时小食也不带辣」「三档价格有序」等。改拼单算法后先跑再提交。

## 边界与红线

- 营养/价格数据全部以麦当劳 MCP 实时返回为准,**禁止凭记忆编造价格或热量**
- **营养数据缺失的当季新品**(MCP 营养表查不到的新品,实测约 35 个):用户设了热量预算 → 该单品不参与组合(不拿未知数字凑闸门);用户只设金额 → 可参选,但必须在明细里标注「MCP 营养表暂无此品数据」,热量栏写「未知」不做运动等价物换算
- 不做医疗、营养学专业建议(与参赛声明一致),热量数据仅作参考
- 用户预算明显低于门店最低消费组合时,诚实说「配不出来」并给最接近的方案
- **预算花不完时要解释**:三档是按「最划算/最扎实/最均衡」选的,不按「花光预算」选——若方案最高只花到 ¥X 而预算还有一大截,明确告诉用户「再加预算不会换方案」,别让用户反复调预算以为工具没反应
- 不贬低、不对比其他品牌(参赛合规要求),玩梗只玩「麦门」正向梗
- Token 限流 600 次/分钟,本 Skill 单次任务 ≤15 次调用,不会触发

## 领券(可选流程,用户点头才动)

当用户问「有什么券能领」「还有更省的吗」,或预算紧且愿意多省一步时:

1. `available-coupons` 列出可领券
2. 按上面的**券 × 方案匹配**规则筛出「能和这一单凑上」的,告诉用户:「有 N 张券可以领,领了这单能再省 ¥X」
3. ⚠️ **只有用户明确说「领」才调 `auto-bind-coupons`**——领券不花钱,但会改变账户状态,和下单一样要用户点头
4. 领完复述一句「领到了什么」,别静默执行

## 自测(改算法后必跑)

本仓库 `scripts/run_selftest.py` 内置 43 项断言,直接跑在 demo.html 的**发货代码**上(数据完整性 / 双闸门 / 券匹配 / 对话解析 / 边界诚实性 / 单文件自包含):

```bash
python scripts/run_selftest.py    # 期望输出:共 43 项 | 通过 43 | 失败 0
```

修改组合算法、券匹配规则或数据快照后,**先跑自测再提交**。

## 工具速查

详见 [references/tools.md](references/tools.md)。