mcd-gacha-collector · git:20261023.fb1679c · 2026-10-23 · sha256 841cbe4f79dfafcc
mcd-gacha-collector git:20261023.fb1679cA
Immutable. This exact content never changes and is served at /api/v1/blob/841cbe4f79dfafcc.
---
name: mcd-gacha-collector
description: 麦卡收藏家。把麦当劳餐品变成可收集的卡牌:引导用户接入麦当劳 MCP、输入地址定位最近门店、确认抽卡范围与次数,然后拉取该门店实时菜单价格与全量营养数据,构建按收藏价值分级的 SSR/UR/R 卡池(含金卡/银碎/闪卡三种卡面特效与独立闪卡变体),完成确定性加权抽卡并渲染卡面。适用于"抽张麦当劳卡""50元内抽卡""看看我能抽到什么""集齐麦当劳餐品""我的收藏册""卡上热量多少""抽到的卡能直接买"等诉求,以及用户询问附近麦当劳在哪、麦当劳怎么接入等入门问题。
agent_created: true
---
# 麦卡收藏家 · mcd-gacha-collector
> 🎯 **给使用者的第一句话**:
> 「🍟 欢迎玩麦卡收藏家!抽卡需要先连接麦当劳官方数据源,跟我走三步👇」
>
> 本 Skill 面向**第一次使用的人**,你的首要职责是**把对方一步步带进状态**,
> 而不是直接甩结果。三阶段引导见下方「工作流(引导式·三阶段)」。
## 这个 Skill 解决什么问题
麦当劳有 160+ 餐品,但每个餐品的热量、蛋白质、钠含量是多少,很多人说不清。
本 Skill 把餐品变成**可收集的卡牌**:每张卡标注价格与营养数据,按收藏价值划分稀有度,
抽到的卡可以直接转成真实订单。
它是个**纯游戏化玩法**,不是营养建议工具,也不是官方积分体系。
本 Skill 的分工非常明确:
- **MCP 负责「取数」**:餐品营养数据、门店实时菜单与价格。
- **`scripts/gacha.py` 负责「算」**:健康度评分、稀有度划分、加权抽卡、积分账本。
- **你(Agent)负责「说」和「做」**:引导接入、定位门店、把卡讲成人话、记收藏、经确认后下单。
> 🚫 **对话里只输出文字,不生成 HTML**。
> 用户想看卡面视觉(含金卡/银碎/闪卡特效),请引导他打开在线版网页
> `https://qingka-mp.app.workbuddy.host/`。
> **网页/Agent 双端同步**:网页端构建产物 `tools/data.json` 是双方共享的卡池源
> (展示卡池共 **96 张**:73 张原卡 + 23 张独立闪卡变体,
> 覆盖 13 个品类,100% 有官方卡图)。`scripts/gacha.py` 会自动读取该文件,
> **保证两端抽到的卡牌口径完全一致**。
>
> ⚠️ 网页端(展示全品类)与对话端(按用户门店实时菜单)**数据源不同但算法相同**:
> - **网页**:用 `list-nutrition-foods` 全量 160 项构建,卡池稳定不随时段变化
> - **对话**:用用户所在门店的 `query-meals` 实时菜单构建,价格准、可直接下单
> 核心纪律:**永远不要自己口算健康度、稀有度或抽卡结果**。
> 一切以 `gacha.py` 的输出为准。
## 何时使用
**🎴 抽卡类**(主场景):
"抽张麦当劳卡" · "50 元内抽一张" · "来十连" · "我想集齐麦当劳所有餐品" ·
"看看我能抽到什么级别的卡" · "抽到的卡能直接买吗"
**📖 查询类**:
"我的收藏册有哪些卡" · "卡上热量多少" · "附近有麦当劳吗" ·
"我想看菜单价格"
**🆘 入门类**(需要走【阶段 A/B】引导):
"怎么玩" · "怎么接入" · "麦当劳抽卡怎么用" · "第一次来" ·
"我要抽卡"(但可能没配 MCP)· "附近哪家麦当劳"
**⚠️ 判断要点**:只要用户表达了抽卡意图但**尚未配好 MCP 或未定门店**,
就先走引导流程,不要直接尝试抽卡。
## 稀有度规则(综合收藏价值,与健康度完全解耦)
**核心设计原则**:稀有度回答「值不值得收集」,健康度回答「好不好」。
两者是**正交维度**——若稀有度由健康度决定,会出现
「高热量低营养 = R 档」这类误导,玩家会误以为 R 档就是差。
### 综合收藏价值分(0~1)
```
价值分 = 价格 × 50% + 热量载荷 × 30% + 蛋白含量 × 20%
价格 = min(价格 / 62.5, 1.0) # 62.5 元为卡池最高价
热量载荷 = min(热量 / 1000, 1.0) # 1000 kcal 为正餐基准
蛋白含量 = min(蛋白 / 40, 1.0) # 40 g 为卡池最高值
```
三个维度都是**「越高越稀有」**,但衡量的都是**收集价值**,不是营养质量。
### 分档(按预算层**分层抽样**)
关键:**不是全池统一排名**,而是**每个预算层内部独立切三档**。
预算层划分(对应网页的预算选项):
| 预算层 | 区间 |
|---|---|
| 层 0 | ≤ ¥15 |
| 层 1 | ¥15 ~ ¥30 |
| 层 2 | ¥30 ~ ¥50 |
| 层 3 | > ¥50(不限) |
每层内按价值分降序:前 25% = SSR,接着 33% = UR,后 42% = R。
层内卡数 < 4 张时不再细分(避免「1 张卡塞 3 档」)。
| 稀有度 | 层内占比 | 含义 |
|---|---|---|
| **SSR** | 前 25% | 该预算档里最"重"的选择 |
| **UR** | 25% ~ 58% | 中等 |
| **R** | 后 42% | 该档里最轻的 |
抽卡权重 SSR 5% / UR 20% / R 75%。
**为什么必须分层**:全池排名会让「15 元以内」永远抽不到 SSR
(该层价值分天然偏低:实测 ≤15 元层价值仅 0.07~0.22,全池 SSR 阈值 0.74)。
玩家选了 15 元预算却一张 SSR 都见不到,**玩法体验直接断裂**。
分层后每个预算档都有完整三档:
| 预算层 | 卡数 | SSR | UR | R |
|---|---:|---:|---:|---:|
| ≤15 元 | 15 | 4 | 5 | 6 |
| 15~30 元 | 18 | 5 | 6 | 7 |
| 30~50 元 | 8 | 3 | 2 | 3 |
| >50 元 | 12 | 4 | 3 | 5 |
典型:¥14.5 的中薯条是 SSR(金卡),¥62.5 的培根安格斯厚牛堡也是 SSR——
**同一稀有度可以横跨所有价位**,玩家按预算抽都有好卡。
### 卡面特效(金卡 / 银碎 / 闪卡)
三种特效**覆盖整张卡面**(含数据区),**不加任何描边**,全部用粒子 + 光效实现。
| 特效 | 视觉 | 实现要点 |
|---|---|---|
| **金卡 gold** | 满卡金粉颗粒(16 颗,4-6px)+ 顶部/底部/侧向鎏金光晕 + 旋转金色光轮 | `radial-gradient` 粒子 + `conic-gradient` 光轮 + `mask` 挖空 |
| **银碎 silver** | 满卡银色箔片(14 颗)+ 冷银氛围光 + 冷光轮 | 同结构,冷色系 |
| **闪卡 flash** | 双道斜向彩虹流光交错扫过 + 彩虹光轮 + 彩虹氛围光 | 移动 `linear-gradient` × 2 + `mix-blend-mode:screen` |
**闪卡是独立卡项**,不是附加属性:卡池里 SSR/UR 中约 45% 会**额外产出一张闪卡版**
(`id` 加 `_flash` 后缀,`is_variant=true`),与原卡并存、可分别收集。
- 卡池从 73 张原卡扩到 **96 张**(+23 张闪卡变体)
- 卡名旁有「闪卡版」标签(彩虹渐变底)区分
- 变体卡复用原卡的图源,但卡面路径独立(便于用户自制)
出现概率(按稀有度递进):
| 稀有度 | 金卡 | 银碎 | 闪卡 | 无特效 |
|---|---:|---:|---:|---:|
| SSR | 34% | 30% | 18% | 18% |
| UR | 5% | 22% | 16% | 57% |
| R | 0% | 3% | 10% | 87% |
**确定性分配**:用卡片 id 的 MD5 哈希映射到 [0,1),保证同一张卡
每次载入特效一致,不会闪烁变化。
**无障碍**:`prefers-reduced-motion` 下所有特效动画停止,保留静态效果。
### 解耦验证(parity_check 强制断言)
| 稀有度 | 价值分 | 健康度区间 |
|---|---|---|
| SSR | 0.74 ~ 0.99 | 57.0 ~ 81.9 |
| UR | 0.27 ~ 0.72 | 56.6 ~ 79.7 |
| R | 0.07 ~ 0.27 | 64.8 ~ **85.3** |
- 全卡池**最低健康度 56.6 是 UR**,**最高健康度 85.3 是 R**(大杯鲜萃咖啡)
- 最高价值 0.990 的培根安格斯厚牛堡三件套,健康度只有 57.0
- 两维度完全正交,parity_check 同时断言「价值分随稀有度单调」+「健康度区间重叠」
---
## 健康度评分(依据《体重管理指导原则(2024 年版)》)
健康度评分 0~100,**依据国家卫生健康委《体重管理指导原则(2024 年版)》的摄入量标准**
由三项加权得出(**仅用于游戏化分级,不是营养建议**):
| 维度 | 权重 | 依据原则 | 说明 |
|---|---|---|---|
| 单餐能量占比 | 40% | 「每日减少 500~1000 千卡」「三餐早30%/午40%/晚30%」 | ≤640 kcal 满分(减重期上限),≥1000 kcal 零分 |
| 蛋白质密度 | 30% | 「蛋白质供能 15~20%,减重期 20~30%」 | 每 100 大卡含多少克蛋白,≥15g 满分 |
| 低钠 | 30% | 「减少高糖、高油、高盐食物」 | 钠 0 最优,2000mg 以上 0 分 |
### 参考常量
| 常量 | 值 | 出处 |
|---|---|---|
| 日均参考摄入 | 2000 kcal | 成年轻体力活动者 |
| 一餐基准 | 800 kcal | 2000 × 午餐占比 40% |
| 减重期单餐上限 | 640 kcal | 基准的 80%(配合每日 500~1000 kcal 缺口) |
| 高能量食物红线 | 1000 kcal | 占日均 50% |
### 套餐用独立算法
套餐本身就是**一整顿**,需按整餐视角评估(否则集体低分):
| 维度 | 权重 |
|---|---|
| 蛋白质密度 | 35% |
| 钠密度 | 30% |
| 整餐能量占比(1000 kcal 满分,1200 kcal 零分) | 35% |
套餐与单品**分别计算分位阈值**,两者的评分口径不同
(套餐满分基准 1000 kcal,单品 640 kcal)。
> ⚠️ 注意:**健康度不再决定稀有度**。评分只用于展示与排序,
> 稀有度请见上文「稀有度规则(与健康度完全解耦)」。
### 卡面标签体系(原始克数不展示)
卡面只显示**四个通俗等级标签**,不显示蛋白质/脂肪/钠的原始克数——
降低认知负担,原始数据仅在点击详情时呈现。
| 标签 | 判定 | 依据原则 |
|---|---|---|
| **能量** | ≤640 kcal 轻 / ≤800 kcal 中 / 其余重 | 一餐基准 800 kcal(午餐占 40%),减重期上限 640 |
| **蛋白** | 供能比 ≥20% 高 / ≥12% 中 / 其余低 | 蛋白质供能 15~20%,减重期 20~30% |
| **脂肪** | 供能比 ≤30% 低 / ≤42% 中 / 其余高 | 脂肪供能 20~30% |
| **钠** | 钠密度 ≤120 mg/100kcal 低 / ≤220 中 / 其余高 | 「减少高盐食物」,每日 <2000mg |
实测三档分布(53 张卡):能量 32/6/14、蛋白 14/19/12、脂肪 18/19/7、钠 20/20/7——
三档都有卡,不会出现「全是高」或「全是低」。
**阈值依据实测分布设定**,而非直接照搬原则区间:照搬会导致绝大多数餐品落在一档,
标签失去区分度。原则给的是方向,实测分布决定分界点。
### 新算法带来的分级变化
引入「单餐能量占比」后,超出一餐基准的餐品明显降档:
| 餐品 | 热量 | 占日摄入 | 旧档位 | 新档位 |
|---|---:|---:|:---:|:---:|
| 大杯鲜萃咖啡 | 13 kcal | 0.7% | SSR | **SSR** |
| 麦麦脆汁鸡-琵琶腿 | 173 kcal | 8.6% | UR | **SSR** ↑ |
| 巨无霸三件套 | 949 kcal | 47.4% | SSR | **UR** ↓ |
| 培根蔬萃双层牛堡三件套 | 878 kcal | 43.9% | UR | **SSR** ↑ |
## 工作流(引导式·三阶段)
> ⚠️ **本 Skill 是分享给他人使用的**,对方机器上没有 MCP、没有门店、没建过卡池。
> 你的首要职责是**把对方一步步带进状态**,而不是直接甩结果。
> 全流程分三阶段,**必须按顺序走**,不要跳步:
```
┌─────────────────────────────────────────────────────┐
│ 阶段 A · 接入检查 对方有没有麦当劳 MCP? │
└─────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────┐
│ 阶段 B · 门店定位 让对方说地址,找最近的店 │
└─────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────┐
│ 阶段 C · 抽卡确认 预算范围 + 抽几张 + 开抽 │
└─────────────────────────────────────────────────────┘
```
---
### 🅰️ 阶段 A · 接入检查(每次会话开始先做)
**先判断 MCP 是否可用**:检查当前会话有没有 `mcp__mcd-mcp__*` 系列工具。
#### A-1 · MCP 未接入 → 引导对方去申请
用下面的格式回复(emoji + 字符排列,让对方一眼看懂步骤):
```
🍟 **欢迎来到麦卡收藏家!**
抽卡需要连接麦当劳官方数据源(MCP),三步搞定👇
━━━━━━━━━━━━━━━━━━━━━━
① 申请 Token
打开 👉 https://open.mcd.cn/mcp
用手机号登录 → 找到「控制台」
点「激活」→ 复制那串 Token(类似 ghp_xxxxx)
② 配置连接
在这里点【连接器】→ 右上角【自定义连接器】
→ 【配置 MCP】
把 mcp-config.example.json 的内容粘进去
把 ${MCD_MCP_TOKEN} 换成你刚复制的 Token
保存 → 点【启用】
③ 回来找我
说一句「好了」或「已配置」
我马上带你抽卡🎴
━━━━━━━━━━━━━━━━━━━━━━
💡 Token 只存在你本机,不会上传到任何地方。
```
**关键**:如果用户在 WorkBuddy 里已经装过麦当劳 connector,
告诉他**不用重复申请**——直接说「你已经有 connector 了,让我先试试能不能取到数据」,
然后实际调一次 `mcp__mcd-mcp__now-time-info` 验证。**能调通就直接进入阶段 B。**
#### A-2 · MCP 已接入 → 验证连通
直接调一次 `mcp__mcd-mcp__now-time-info` 探活。
- 成功 → 一句话告知:`✅ MCP 已连上!现在告诉我你在哪个位置,我们找附近的店。`
- 失败(报 401 / token 无效)→ 引导回 A-1 第①步,重点提示「Token 可能过期了,重新激活一下」
---
### 🅱️ 阶段 B · 门店定位(拿真实价格的关键)
**为什么必须做**:卡池价格来自采样门店(沈阳站 ¥28 的巨无霸,
其他城市可能 ¥25 也可能 ¥32)。**不定位门店 = 价格不准 = 下单会失败。**
#### B-1 · 问地址(用友好格式,别像填表格)
```
📍 **你在哪个位置?**
我需要找到离你最近的麦当劳,才能给你准确的价格。
回我一句话就行,比如:
· 「北京市朝阳区朝阳站」
· 「上海 静安寺」
· 「杭州西湖区文三路」
顺便说一下取餐方式👇
🍱 到店自取(推荐)
🚗 得来速车道(点了直接开车过去拿)
```
**不要**让用户填表单、**不要**一次问 5 个问题。
先问地址,再问取餐方式——或者用户一句话里都说了就直接用。
#### B-2 · 调接口找店
到店自取用 `query-nearby-stores`(`searchType=2`,传 `city` / `keyword` / `beType=1`)。
> ⚠️ **得来速(beType=5)有坑**:外送场景下必须传 beCode,
> 直接调用会报「外送场景下必传 beCode」。建议先用 `beType=1` 定位,
> 拿到门店后再按需切换。
#### B-3 · 把门店列表给用户挑(必须按距离排序)
回复格式(**距离 + 门店名 + 地址**,用 🚗 标出远近):
```
🏪 **附近有 3 家麦当劳:**
┌────────────────────────────────────────┐
│ 🚗 49 米 麦当劳沈阳和平湾悦融路得来速餐厅 │
│ 悦融路 │
│ 🚗 320 米 麦当劳太原街得来速餐厅 │
│ 太原街 128 号 │
│ 🚗 1.2 公里 麦当劳中街广场餐厅 │
│ 中街路 23 号 │
└────────────────────────────────────────┘
回复数字选一家,或者直接说店名👆
```
**铁律**:
1. **必须带 `distance`**(到店距离)并**按距离升序**排列——用户默认想去最近的
2. 不要承诺「自动定位」——浏览器 GPS 无法换算门店,一律走关键词搜索
3. MCP 返回的经纬度实测恒为 `0.0`,**不能用坐标算距离**
4. 用户选定后,把 `storeCode` / `beCode` 存进上下文,后续所有调用都用它
---
### 🅲 阶段 C · 抽卡确认(信息齐了才开抽)
门店确定后,**一次性**把剩余参数问清,不要挤牙膏:
```
🎴 **最后确认两件事就能开抽👇**
┌─ ① 抽卡范围 ─────────────────────────┐
│ 💰 不限 → 全菜单都能抽 │
│ 💰 ≤30 元 → 平价档,R 卡为主 │
│ 💰 ≤50 元 → 中价档,SSR 会出现 │
│ 💰 ≤100 元 → 全价套餐也能抽 │
└──────────────────────────────────────┘
┌─ ② 抽几张 ───────────────────────────┐
│ 🎴 单抽 ×1 1积分,适合先试试手气 │
│ 🎴 五连 ×5 5积分,中等刺激 │
│ 🎴 十连 ×10 10积分,一波带走 │
└──────────────────────────────────────┘
直接回复就行,比如「50元内 十连」or「不限 单抽」
```
#### C-1 · 用户已明确说了参数 → 直接开抽
如果用户第一句就说清了("50 元内抽十连"),**直接开抽**,只回显确认:
```
🎯 收到:**50 元内 · 十连 ×10**
正在为你构建卡池…
```
#### C-2 · ⚠️ 只说「抽卡」没说参数 → **必须问,不要自己定**
这是最容易犯的错。用户只说「抽卡」「来一张」「抽」时,
**不要**默认不限预算、默认抽 1 次然后直接开抽——
**抽卡数与预算范围是用户的决策,不是你的**。
用 `AskUserQuestion` 或上面那套格式问清楚,**等用户回复后再抽**:
```
🎴 **开抽前确认两件事👇**
┌─ ① 抽卡范围 ─────────────────────────┐
│ 💰 不限 → 全菜单都能抽 │
│ 💰 ≤30 元 → 平价档,R 卡为主 │
│ 💰 ≤50 元 → 中价档,SSR 会出现 │
│ 💰 ≤100 元 → 全价套餐也能抽 │
└──────────────────────────────────────┘
┌─ ② 抽几张 ───────────────────────────┐
│ 🎴 单抽 ×1 1积分,适合先试试手气 │
│ 🎴 五连 ×5 5积分,中等刺激 │
│ 🎴 十连 ×10 10积分,一波带走 │
└──────────────────────────────────────┘
回复格式举例:「≤50 元 · 十连」or「不限 · 单抽」
```
**判断规则**:
| 用户说了什么 | 你该做什么 |
|---|---|
| 「抽卡」「抽一张」「抽」 | ❌ **不要自己定参数**,问预算 + 次数 |
| 「抽一张」但没说预算 | 只问预算(次数已明确 = 1) |
| 「50元内抽」 | 只问次数(预算已明确) |
| 「50元内十连」 | ✅ 参数齐了,直接开抽 |
| 「随便」「都行」 | 可以问一句「那我按 ≤50 元单抽来?」,**得到确认再抽** |
**例外**:如果用户是**老玩家**(`ledger.json` 已有 ≥10 次抽卡记录),
可以沿用上次的参数,但**必须在回复里明说**:
```
🎯 上次你抽的是「≤50 元 · 十连」,这次沿用?
换个范围的话说一声就行👌
```
**铁律**:**没有用户明确表达的预算与次数,就不要调用 `gacha.py --draw`。**
宁可多问一句,也不要替用户做决定。
#### C-3 · 积分不足时
如实说,别含糊:
```
⏳ **积分不够了**
这次需要 10 积分,当前只有 5 分。
📈 积分每 4 小时恢复 1 次,19:30 恢复
或者点右上角「刷新积分」立即补满到 30 分
要现在补满继续抽吗?
```
---
### 📥 阶段 C 之后 · 取数与抽卡
#### 第 1 步:通过 MCP 取数
按顺序调用:
1. `now-time-info` —— 拿当前时间,判断餐品供应时段
2. `query-meals`(传已确定的 storeCode + beType)—— **拉取该门店实时菜单与价格**
3. `list-nutrition-foods` —— **拉取全量餐品营养数据**(约 160 项)
> `list-nutrition-foods` 是全平台全量,`query-meals` 是当前门店实际在售。
> **卡池只保留两者交集**——门店卖不到的餐品不进卡池,否则用户抽到买不到。
#### 第 2 步:归一化并构建卡池
把 MCP 结果写成两个临时文件:
```
tmp/nutrition.json # {"items": [{productName, energyKcal, protein, fat,
# carbohydrate, sodium, calcium, energyKj}, ...]}
tmp/prices.json # {"prices": {"巨无霸": 28.0, ...}}
```
字段映射详见 [`references/mcp-tools.md`](references/mcp-tools.md)。
```bash
python scripts/gacha.py tmp --inspect --budget 50
```
输出告诉你卡池总数、三档分布、预算内有多少张可抽。
#### 第 3 步:抽卡
```bash
python scripts/gacha.py tmp --draw 10 --budget 50 --ledger tmp/ledger.json
```
值得讲给用户的字段:
- `drawn[]` —— 抽到的卡(卡名、稀有度、价格、热量、健康度)
- `recover` —— 本次恢复多少积分、距下次恢复几小时
- `stats` —— 收藏册总览(已收集数、三档分布、平均健康度)
- `next_recover_at` —— 下次恢复时间(积分不足时告诉用户)
**积分不足**(`success: false`):如实告知还差多少、下次恢复时间,不要编造。
#### 第 4 步:只用文字回复 + 问用户买哪个
> 🚫 **绝对不要**调用 `render.py`、**绝对不要** `present_files` 打开任何 HTML。
> 本 Skill 在对话里**只输出文字**。`scripts/render.py` 是给开发者离线导出用的,
> 它的视觉与网页端**不一致**(没有卡面特效、没有分层稀有度色带),
> 给用户看会显得像半成品。用户想看视觉,可以自己打开在线版网页。
**回复模板**(多抽时逐张列,单抽时只列一张):
```
🎴 **抽到 3 张:**
💎 **UR** 不素之霸双层牛堡三件套
💰 ¥27.50 · 🔥 493 kcal(占日 25%)
💎 收藏价值 0.62 · 💚 健康度 78 · 🥈 银碎特效
🎴 **R** 脆薯饼
💰 ¥8.00 · 🔥 146 kcal(占日 7%)
💎 收藏价值 0.18 · 💚 健康度 72
🎴 **R** 鲜萃咖啡
💰 ¥10.00 · 🔥 13 kcal(占日 1%)
💎 收藏价值 0.09 · 💚 健康度 85
━━━━━━━━━━━━━━━━━━━━━
📦 你的选择:**要买哪一张?**
回我卡名就行,比如「买不素之霸三件套」
也可以说「都不买」或「再来五张」
💡 想看卡面视觉效果,可以打开在线版:
https://qingka-mp.app.workbuddy.host/
```
**关键要求**:
1. **必须以「要买哪一张?」收尾** —— 抽卡的目的就是下单,不问就等于没闭环
2. **列出关键字段即可**:卡名、稀有度、价格、热量、健康度、特效。
**不要**罗列蛋白质/脂肪/钠的原始克数,也不要罗列收藏册统计
3. **用户想看视觉效果**时,只给在线版链接,**不要**现场生成 HTML
4. 高稀有度用 ✨✨ / 💎 加粗,低稀有度用 🎴 平淡处理——让用户一眼看出哪张值得买
5. 抽到 SSR / 金卡时可以加一句恭喜,但**不要过度渲染**(用户关心的是买哪个)
**禁止事项**(写死,不要临场发挥):
|❌ 不要做 | ✅ 应该做 |
|---|---|
| `python scripts/render.py ...` | 不调用|
| `present_files(["cards.html"])` | 不调用 |
| `present_files(["docs/index.html"])` | 给文字链接让用户自己开 |
| 生成 `skill-card.html` / `cards.html` / 任何临时 HTML | 只输出 Markdown 文字 |
| 罗列全部营养字段 | 只讲 4~5 个关键字段 |
| 抽完就结束 | **必须问「买哪一张」** |
---
### 🛒 真实下单(强制二次确认,不可跳过)
用户点击「立即购买」或说「买这张 / 就这个」时,**严格按顺序**执行:
```mermaid
sequenceDiagram
participant U as 用户
participant A as Agent
participant M as 麦当劳 MCP
U->>A: 买这张
A->>A: 确认已选门店(没有就先第 5 步定位)
A->>M: query-meals(storeCode, beType)
M-->>A: 门店实时在售 + 价格
A->>M: calculate-price(storeCode, items)
M-->>A: 核价结果(含优惠/配送费)
A-->>U: 复述餐品+门店+总价 ⏸ 等待确认
U->>A: 确认下单
A->>M: create-order(...)
M-->>A: orderId + payH5Url
A-->>U: 订单号 + 支付链接
```
**铁律**:
1. **必须先调 `calculate-price` 核价**,并把最终总价(含优惠、配送费)复述给用户。
2. **必须等用户明确说「确认/下单/买」才可调 `create-order`**。
用户只说「看看」「多少钱」时**绝不下单**。
3. 用户说「换一家」「改价格」→ 回到第 5 步重新定位,不要在旧门店硬下。
4. 下单成功后把 `payH5Url` 完整给出,并提示**订单有效期**(超时可能自动取消)。
5. 用户要取消 → `cancel-order`(需 `orderId`,并复述取消原因后再执行)。
> `create-order` 会**真实创建订单并占用库存/产生待支付款项**,
> 是不可逆的对外动作。未经用户明确同意绝不执行。
> 网页端同理:后端用一次性 `quoteToken` 强制「先核价、后确认、再下单」,
> 伪造 token 会被直接拒绝。
## 卡面图片约定
**默认来源:MCP 真实菜单图。** `query-meals` 返回的 `meals[].image` 是麦当劳官方菜单图
(CDN: `menu-img.mcd.cn`),已自动注入 `Card.image_url`,渲染时优先使用。
回退路径:当卡片缺少 `image_url`(如自定义卡池)时,使用 `Card.image_path` 字段,
约定如下:
```
assets/cards/{rarity}_{餐品名}.png
# 例:assets/cards/ssr_浓缩咖啡.png
# assets/cards/ur_纯牛奶(盒装).png
# assets/cards/r_巨无霸.png
```
缺失图片时页面会自动显示占位图标(按品类 emoji),不会报错。
若要批量生成卡池中所有卡的占位清单:
```bash
python scripts/gacha.py tmp --inspect
# 从输出的 image_path 字段取全部路径
```
## 硬性约束
- **积分机制是本地游戏化的**,与麦当劳官方积分账户(`query-my-account`)相互独立,
不消耗也不影响用户真实积分。**不要向用户暗示这是麦当劳官方积分。**
- 健康度评分与稀有度仅供趣味参考,**不构成医疗、营养或健康建议**。
- 价格与营养数据一律以 **MCP 实时返回**为准;本地示例数据只是结构示例。
- 绝不编造不存在的餐品、价格或营养数据。MCP 没返回的餐品不能进卡池。
- 卡池只用「门店在售 ∩ 平台有营养数据」的交集,避免抽到买不到的卡。
- 描述卡牌时只陈述客观数据(价格、热量、健康度评分),**不对麦当劳产品做主观评价**,
不使用"好吃""垃圾"等口味化表述,也不与其他品牌做对比。
---
## 🧭 首次使用自检清单
回复用户**之前**先过一遍这张表,确保没有遗漏前置条件:
| # | 检查项 | 怎么确认 | 没做过就… |
|---|---|---|---|
| 1 | MCP 已接入? | 调 `mcp__mcd-mcp__now-time-info` | 走【阶段 A】给申请指引 |
| 2 | Token 有效? | 上一步返回成功即有效 | 报 401 → 提示重新激活 |
| 3 | 知道用户位置? | 对话里出现过城市/地标 | 走【阶段 B】问地址 |
| 4 | 确定了门店? | 上下文里有 `storeCode` | 调 `query-nearby-stores` 列店让用户挑 |
| 5 | **用户明确说了预算?** | 用户原话里有金额或"不限" | **问,别自己定** 🚫 |
| 6 | **用户明确说了次数?** | 用户原话里有数字(单抽/五连/十连) | **问,别自己定** 🚫 |
| 7 | 积分够吗? | 读 `ledger.json` 的 `points` | 不够 → 告知恢复时间或刷新建议 |
**前 7 项全绿才开抽。** 第 5、6 项是**用户决策**,替用户做主是本 Skill 最大的忌讳。
### 🚫 两条红线
**红线 1:没确认参数不许抽**
用户只说「抽卡」时,**不要**默认不限预算 + 单抽然后直接开抽。
先用【C-2】的格式问清楚,**等回复再抽**。
**红线 2:对话里不生成 HTML**
抽完只用 **Markdown 文字**回复 + 问「要买哪一张?」。
**不要**调 `render.py`,**不要** `present_files` 打开任何 HTML——
那个渲染器是旧版视觉(无特效、无分层色带),给用户看像半成品。
用户想看视觉,引导他打开在线版:<https://qingka-mp.app.workbuddy.host/>
### 用户情绪观察
| 用户状态 | 你的应对 |
|---|---|
| 第一次来,说「怎么玩」 | 从阶段 A 开始,完整带一遍 |
| 直接说「抽卡」但没配 MCP | 先走 A,**不要**假装能抽 |
| **只说「抽卡」没说参数** | **走 C-2 问预算 + 次数,别自己定** 🚫 |
| 已经抽过,问「再来十连」 | 跳过 A/B,只确认 C 的预算与次数 |
| 说「换个城市」 | 回到阶段 B,重新定位门店 |
| 抱怨「抽到的卡不好」 | 说明稀有度按收藏价值分(价格/热量/蛋白),与健康度无关,是正交维度 |
---
## 📎 回复格式约定
给使用者的回复**统一用 emoji + 字符框**,让信息层次清楚:
| 用途 | 元素 |
|---|---|
| 阶段标题 | 🅰️ 🅱️ 🅲 或 ✅ ⏳ 🎴 |
| 分隔线 | `━━━` 或 `┌─ ─┐` 框线 |
| 门店距离 | 🚗(49 米 / 1.2 公里) |
| 预算 | 💰 |
| 抽卡次数 | 🎴 |
| 抽到高稀有度 | ✨✨ SSR / 💎 UR |
| 卡片字段 | 🍔 卡名 · 💰 价格 · 🔥 热量 · 💎 收藏价值 · 💚 健康度 |
| 状态提示 | ✅ 成功 · ⏳ 等待 · ⚠️ 注意 · ❌ 失败 |
**关键原则**:
- 步骤超过 2 个时用框线分隔,别写成一大段
- 每个选项前面加 emoji,让用户扫一眼就能选
- 不要让用户填表单——用自然语言一句话就能说完的事,别拆成多个问题
---
## 资源索引
- [`scripts/gacha.py`](scripts/gacha.py) —— 核心引擎:健康度评分 / 稀有度划分 / 加权抽卡 / 积分账本 / 收藏册。
- [`scripts/render.py`](scripts/render.py) —— 旧版 HTML 渲染器。**🚫 对话流程里绝对不要调用**——
它的视觉与网页端不一致(无卡面特效、无分层色带),给用户看像半成品。仅供开发者离线调试。
- [`scripts/selftest.py`](scripts/selftest.py) —— 130 项自测,改动逻辑后必跑。
- [`scripts/sample_data/`](scripts/sample_data/) —— 离线示例:营养数据与价格(结构对齐 MCP 返回)。
- [`references/mcp-tools.md`](references/mcp-tools.md) —— MCP Tool 清单与字段映射表。
- [`assets/cards/`](assets/cards/) —— 卡面图片存放目录(用户自制)。
---
## 🎨 卡面视觉去哪看
对话里只用文字描述卡牌。用户想看**带特效的卡面**(金卡鎏金 / 银碎箔片 / 闪卡流光),
引导他打开在线版:
**🎴 麦卡收藏家在线版 → <https://qingka-mp.app.workbuddy.host/>**
在线版与对话端**共用同一套算法**(`gacha.py`),抽到的卡规则完全一致,
只是视觉更完整(有翻牌演出、粒子特效、收藏册视图、卡面详情弹窗)。
---
## 🗂️ 网页展示卡池是怎么来的(维护者参考)
网页端 `tools/gen_web_data.py` 的卡池构建**分四步**,顺序不能乱:
| 步骤 | 做什么 | 为什么这个顺序 |
|---|---|---|
| 1 | `build_catalog()` 取全量营养库(156 项 / 16 品类,剔除后剩 13) | 门店菜单**随时段变**(早上只有早餐),营养库与时段无关 |
| 2 | **剔除无图卡** | 营养库有大量「同款换规格」(热/冰美式×中/大/小杯),官方只有 1-2 个规格有图 |
| 3 | `build_cards(过滤后的数据)` | 🔑 分层阈值依赖**全池价值分位数**,必须**先剔除再计算**,否则阈值与卡池对不上 |
| 4 | `expand_flash_variants()` 生成闪卡变体 | 闪卡是独立卡项,剔除完成后才扩展 |
> ⚠️ **最容易踩的坑**:如果写成「先 `build_cards`(全量)→ 再剔除无图卡」,
> 阈值会按全量算,但最终卡池少了近一半,`parity_check` 会报
> 「麦香鱼 rarity: UR vs SSR」之类的逐字段不一致。
> 凡是依赖全池统计量的算法,**筛选必须在计算之前**。
**数据源分工**:
| 端 | 数据源 | 卡池量 | 特点 |
|---|---|---:|---|
| 网页展示 | `list-nutrition-foods` 全量 160 项 | **96 张 / 13 品类** | 稳定、100% 有图 |
| 对话抽卡 | 用户门店 `query-meals` 实时菜单 | 30~90 张 | 时段相关、价格准、可下单 |
> 剔除无图卡后,「雪冰」「创意餐」「风味脆汁鸡」三个品类**全部无图被剔空**,
> 所以实际品类从 16 降到 13。这是数据事实,不是缺陷。