⏳ This skill is pending AI review.
Scores will appear once the review pipeline completes.
mcd-agent-gourmet
麦门点餐议事会。用户想按预算和口味比较完整的麦当劳餐单,或问“我的券快过期了吗”“这顿能用哪张券”时使用。先确认门店与到店/配送,保留有糖无糖和冰量偏好;分清券包、时段和门店适用性,再使用官方 MCP 核价。只查询核价,不领券不下单。
Choose how to use this skill
You do not need every option. Choose the path your AI client supports. The stable page stays the same; versioned files are immutable.
1. Native installer
This listing has no registered native installer command. Use the complete package or source fallback below, depending on what your client supports.
Do not guess an installer command or replace an existing version without reviewing the diff.
2. Complete package recommended
Download the ZIP when available. It includes SKILL.md plus the references, security notes and version metadata.
No complete ProSkills package is published for this listing yet.3. Prompt-only
Copy the prompt above when the agent can read the stable page or when you want to adopt the workflow without installing a skill.
Need only the instruction file?
Download SKILL.md only if your client requires a single file. The complete ZIP is safer for a full installation because it preserves the references and release context.
No path installs or executes anything by itself. Your agent still needs access to the project files. Before updating, compare the installed version and review the diff.
// RATINGS
Not yet listed on ClawHub or SkillsMP
// README
麦门点餐议事会 · mcd-agent-gourmet
同样一份餐,两条点法差 ¥12.00(24.7%)。 同店同时刻:单品直选 ¥48.50,三件套 ¥36.50。
一句话 → 一个答案 → 一张点单卡。 说「35 元,不辣,有糖可乐去冰,带走」,它把这一餐真正要付的钱算出来,给你一张能照着点的清单;没有方案就直说没有,不拿不完整的餐单凑数。
菜单写 ¥32.00,核价 ¥58.50 —— 它只认后者。 实测遇到过展示价要求随单购买麦金卡;这时它会同时给出条件与核价。金额一律取官方 calculate-price 返回的整数分。
不领券、不兑换、不下单、不支付,也不收集你的 Token。 网页打开就是答案:不用账号、不用 Token、不用安装;要实时核价时,Token 只留在你本机的环境变量里。
打开在线体验(免登录) · 安装 Skill · 真实联调记录 · English
English TL;DR — A read-only McDonald's China MCP client + agent skill. Say "35 yuan, not spicy, regular cola, no ice, takeaway" and it compares complete meals (main + side + drink) using official per-order pricing, tells you why each rejected meal lost (dominated by which candidate, on which axis), and when nothing fits it says so — plus how much budget would actually be needed. An explicit regular-cola choice is never silently swapped for zero sugar. It never orders, pays, or collects your token. English README · 30-second demo

先试 30 秒
不用账号、Token 或安装:
- 打开就是答案:默认直接给出这一餐(巨无霸三件套 ¥36.50)和一句可核对的理由——「比单品直选省 ¥12.00(24.7%)」。
- 点 「就这个」:出现可复制的点单卡(门店 / 取餐方式 / 每样官方名称 / 合计)。
- 把预算改成 30 元:它说没有方案,并告诉你最低需要多少(¥36.50)——不硬凑。
- 想看别的,点 「我想比较」:才展开全部候选与「为什么不是它」。默认不给表格。
- 想看得更深,展开 「更细的比较」:切换比较角度(价格与钠 / 口味与钠 / 四维营养),并看三种推荐角度各自选了什么。
点单卡就是最终产出物——门店、取餐方式、每样官方名称、合计,照着它就能在 App 或柜台点单:


网页使用历史快照,所有比较在你的浏览器内完成,不收集凭证、不调用下单、不加入追踪器;本项目为独立参赛作品,非麦当劳官方产品。也可看 30 秒录屏演示(当前界面,静音)。
首屏长这样(一个答案,不用你挑)
网页默认门店(fixtures/hongwu-single-vs-combo.json,2026-10-17 真实核价)上的实际输出:
这顿就它了 👇
巨无霸三件套 · 套餐(巨无霸 + 中薯条 + 可乐中杯)
¥36.50 南京某门店(名称与地址已脱敏) · 到店自取
比「单品直选」省 ¥12.00(24.7%)
点单卡(可复制)
门店:南京某门店(名称与地址已脱敏) · 到店自取
1. 巨无霸
2. 中薯条
3. 可乐中杯
合计:¥36.50
比「单品直选」省 ¥12.00(24.7%)
以上只是记录你的选择;没有下单、没有支付。
想在 CLI 复现同一段输出(默认快照是老的那组,省 ¥12.00 这组要显式指定):
python3 demo.py --input fixtures/hongwu-single-vs-combo.json \
--budget 40 --baseline-plan single-pick
- 默认只给一个答案,不让你在 3 份表格里做多目标权衡;理由必须是可核对的事实(省了多少钱 / 价格最低 / 钠最低),不是说服文案。
- 点单卡是最终产出物:门店、取餐方式、每样官方名称、合计——照着在 App 或柜台点即可。
- 想看别的:
--compare才展开全部候选与「为什么不是它」。
目标用户
- 一个人或两个人吃饭、预算有限、对口味有明确偏好的人;
- 以到店自取 / 外带为主,想在下单前把「这一顿多少钱、怎么点更划算」算清的人;
- 需要把钠 / 能量 / 蛋白质当筛选条件的人(条件由你自己设,不是健康建议);
- 用 WorkBuddy 等 AI 客户端、希望"说一句话就得到可核价餐单"的人;
- 开发者:想看一个只读 MCP 客户端 + 确定性比较 + 可复算省钱数字的实现样板。
三件我们做死的事
- 钱必须是真数:金额只认官方
calculate-price返回的整数分;菜单展示价附带的条件(例如「随单购麦金卡」)会一并说清。例:巨无霸三件套菜单价 ¥36.50、官方核价也是 ¥36.50;带「随单购卡」条件的商品两者会不同,此时它会同时显示条件与核价。 - 你的硬约束只说一次:不辣、去冰、有糖……记住你的条件,不反复问,也不因为"营养更优"替你改。
- 两个人也能算:份数完备性按人数(主食与饮料各 ≥ 人数),比较用人均价,而不是拿"两人总价"去比"一人总价"。
再加上一条:不领券、不兑换、不下单、不支付,也没有购物车。
它做什么
“35 元,不辣,有糖可乐去冰,带走。帮我看看附近哪家店合适。”这是我们希望用户直接表达的需求。Skill 会先补齐地点、让你确认门店,再比较完整餐单,不要求普通用户复制门店、商品或券的编码。这条路径由安装了 Skill 的 AI 客户端执行;网页是免登录的历史数据体验,CLI 是开发者入口。
比较程序按价格、已知钠含量、口味匹配三个明确标准给出结果。同一餐单可能符合多个标准;这不代表三个独立模型进行了讨论,也不承诺全菜单最优。
| 比较角度 | 本次选择 | 理由 |
|---|---|---|
| 价格较低 | 双层吉士汉堡 + 小杯玉米杯 + 无糖可口可乐中杯 | 当次整单核价 ¥32.00,在候选中价格较低 |
| 钠含量较少 | 麦香鱼 + 小杯玉米杯 + 无糖可口可乐中杯 | 当次 ¥32.50;官方标准份量单品合计钠 592 mg,在候选中较少 |
| 更贴近辣味偏好 | 麦辣鸡腿汉堡 + 小杯玉米杯 + 无糖可口可乐中杯 | 当次 ¥32.00,保留辣味偏好 |
上表是 2026-10-13 演示门店的真实核价快照,不是当前报价,也不是营养搭配建议。实际购买须重新核价。我们比较六份候选,不保证全菜单最低价。
和下单渠道是什么关系
App 是下单的地方;这里是打开 App 之前把「这顿怎么点」算清楚的地方。
- 下单仍在麦当劳 App 或门店完成;本工具不提供购物车、不代下单。
- 它只做「打开下单渠道之前」的那一步:把你的条件变成可核价的完整餐单,并说明取舍。
- 没有符合条件时会直说,并给出保持其他条件的预算阶梯(差多少钱、加多少后有几份可选)。 两者不冲突:用它决定点什么,用 App 完成购买。
在 WorkBuddy 里使用
先按官方文档配置麦当劳 MCP 连接器。mcp-config.example.json 只含环境变量占位符;若客户端不支持变量展开,请只在该客户端的私有配置中设置实际凭证。
可以把下面这段发给 WorkBuddy:
请安装这个技能:https://github.com/polaris-smart/mcd-agent-gourmet
保留 SKILL.md、gourmet/、demo.py、fixtures/ 的相对位置。
使用我已配置的麦当劳 MCP 连接器。
35 元,不辣,有糖可乐去冰,主食、小食、饮料各一份,带走。
我在南京江宁区。先显示门店名称和地址,让我选店。
再查当次菜单,给我最多三份符合要求的方案,说明价格和取舍。
没有满足条件的方案时说明原因,先别替我改饮料或加预算。
只查询和核价,不领券、不兑换、不下单、不支付。
偏好可按自己的习惯改成无糖或其他可选饮料。门店编码由客户端从查询结果中传递;不能在没有位置依据时声称自动选中了“离你最近”的店。
实现说明: Python 和网页使用确定性筛选、排序与非支配集合比较。SKILL.md 引导 AI 客户端理解需求、查询并解释结果,不制造三个独立模型的角色对话。新版在 WorkBuddy 客户端中的完整对话路径仍需验收,详见 验证记录。
两个零输入入口(不用你描述需求)
| 入口 | 行为 |
|---|---|
随便来一份(--any) | 不设预算时明说"未设预算",只按其他硬性条件给一个默认答案;不会偷偷替你假定一个预算 |
跟上次一样(--last,需 --live) | 读最近一次订单,只复用门店与商品编码,按当次价重新核价(不复用历史金额);上次的商品若已售罄会逐条告诉你并去掉,不替换;上次那家店不在营业时间就明说,退回默认方案 |
| 企业团餐 / 主题派对 | 不是个人点餐的渠道,属于独立场景(query-meal-assistance / party-*),本工具不把它们混进来 |
先说清楚它查不到什么(免得你白等)
官方 MCP 覆盖的是麦当劳自有渠道的只读数据。下面三类现在拿不到,遇到时它会直说,而不是猜:
- 券/卡的完整条款原文:「不与其他优惠同享」的具体边界没有对应工具,只有券名、官方状态与时间字段。
- 麦金卡 / 早餐卡的实时权益(剩余次数、适用门店):35 个工具里没有对应入口。
- 份量 / 克重 / 尺寸:没有数据源,所以它不会回答「是不是缩水了」,只会给同价位的替代方案。
另外,MCP 不含美团、饿了么、京东、抖音、淘宝闪购等第三方渠道——第三方价格与券以各渠道自己的页面为准。以上三类之外的实时价格、门店供应、营业状态、券的可用性判定,都走官方接口。
更多取舍细节
前沿解释(其他候选被谁、差在哪个维度比下去)与预算阶梯(差 ¥2 能多几份)的完整说明见 功能细节。
网页收录的独立历史样例由 fixtures/demo-snapshots.json 管理,当前包括三组南京门店样例;其中一组的门店名与地址已脱敏,因为该门店位于作者住所附近,公开它等于公开个人位置。其余两组均包含实测“薯条+有糖可乐”和“薯条+无糖可乐”的去冰组合。各组独立标明门店、公开地址与核价时间,不跨店混比;脱敏只作用于门店标识,核价与营养数据未做修改。网页不实时核价、不接受 Token。
以下命令供开发者调试;日常使用可直接采用上面的 WorkBuddy 对话入口。
python3 demo.py --sugar zero --energy-limit 700 --protein-min 20
python3 demo.py --search-stores --city 南京 --keyword 新街口
# 用返回的真实门店和套餐编码替换下列占位文本
python3 demo.py --list-meals --store-code 门店编码
python3 demo.py --live --store-code 门店编码 --meal-code 套餐编码 --expand --sugar zero --ice none
没有核验冰量的历史样例不能被改成去冰报价。CLI 当前只自动构造三轮完整套餐;全部杯型、儿童玩具、辣度分级及过敏原尚未完整支持。
积分、优惠券与到店/配送
券包的时间窗口判定、积分兑换、会员积分、配送地址、以及到店/得来速/麦乐送的编码对应关系,见 功能细节 · 券与会员。
WorkBuddy Skill 市场
已按官方格式补齐元数据与资源目录,可构建 ZIP 分发包:
python3 scripts/build_skill_package.py --output /tmp/mcd-agent-gourmet-1.0.0-rc.zip
包可用于开放平台投稿,目前未提交审核、未上架。安装与上架文案见 市场发布说明。Skill 优先使用客户端已配置的麦当劳 MCP;脚本只需 Python 标准库。
本地运行
Python 3.9+,仅标准库,零第三方依赖。
git clone https://github.com/polaris-smart/mcd-agent-gourmet.git
cd mcd-agent-gourmet
python3 demo.py
python3 demo.py --budget 35 --taste no-spicy --sodium-limit 800
python3 demo.py --budget 35 --taste spicy --side fries --json
默认读取 fixtures/snapshot.json。即使本机有 Token,也不会自动联网。
实时核价
- 在 麦当劳开放平台 MCP 登录页 用手机号登录后申请自己的 MCP Token;工具清单、限额与字段说明见 官方文档。
- 在本机私有环境中设置
MCD_MCP_TOKEN。不要提交到 Git,也不要粘贴到公共对话或网页。 - 先搜索并选择门店。实时模式必须明确传入
--store-code或已确认的--plan,不会自动沿用南京样例店。再用--list-meals查看该店当次菜单,用--meal-code选择套餐。只传门店编码不会载入演示套餐;覆盖计划的旧门店时,必须重新选择餐品及适用的渠道业务编码。 - 显式运行:
python3 demo.py --list-tools
python3 demo.py --live --plan fixtures/plan.json --budget 35 --taste spicy
程序读取当次菜单、官方营养数据,对候选逐份 calculate-price。真实查询失败会停止,不拿历史快照伪装成实时结果。自动构造支持即时三轮套餐;到店已实测,配送/得来速参数已接入但未完成真实核价联调。默认不使用优惠券。
券查询与多维比较
按名称查可用券、--save-snapshot 保存脱敏快照、以及“价格与钠 / 口味与钠 / 四维营养”的非支配比较规则,见 功能细节 · 券查询与多维比较。
取舍原则
- 先完整,再便宜。 只有满足主食、小食、饮料和份数要求的餐单才进入候选。
- 硬性条件与偏好分开。 “不吃辣”严格排除;“想吃辣”作为偏好,无法满足时解释替代。
- 比较标准透明。 分别按价格、已知钠含量、口味匹配筛选;多个标准可能选中同一餐单,不制造虚假的“争论”。
- 未知就是未知。 营养表按餐品完整名称精确匹配。无法匹配则保留
null;不能把缺失变成 0,也不输出“营养均衡”标签。 - 选择不是下单。
--select和网页按钮只记录餐单决定,不创建订单或支付。
适合预算有限、想看清取舍的个人用户,以及学习真实业务
// HOW IT'S BUILT
// FILES
72 files shipped with this skill. Inline text is the catalog copy. The upstream link is the source repository.
SKILL.md16 KB605611046912upstream ↗
--- name: mcd-agent-gourmet description: 麦门点餐议事会。用户想按预算和口味比较完整的麦当劳餐单,或问“我的券快过期了吗”“这顿能用哪张券”时使用。先确认门店与到店/配送,保留有糖无糖和冰量偏好;分清券包、时段和门店适用性,再使用官方 MCP 核价。只查询核价,不领券不下单。 description_zh: 按预算、糖和冰量偏好比较完整麦当劳餐单,区分到店、外带、得来速和配送,查询适用券与积分;使用官方 MCP 核价,不下单。 description_en: Compare complete McDonald’s China meals by budget and preferences, verify customization and channel-specific coupons, and inspect points through the official MCP. Read-only, no ordering. version: 0.3.2 author: Polaris-Smart LAB display_name: 麦门点餐议事会 agent_created: true --- # 点餐议事会 先帮用户选一顿完整的饭,再讨论省钱、口味和营养数据。不要要求用户说出“多 Agent”这样的实现术语。 ## 执行边界 - 只调用查询与核价工具;本 Skill 不执行 `create-order`、领券、积分兑换、支付。 - Token 仅在客户端私有配置或本机环境;不让用户粘贴到公开网页、Issue 或日志。 - 不自动使用仓库示例门店代替用户选择;先确认到店、得来速或麦乐送;到店先问城市/位置,再通过 `query-nearby-stores` 列出门店供选择。 - 用户明确预算、主食/小食/饮料份数、排除项。“不吃辣”是硬约束,“想吃辣”可作为软偏好。 - 营养指标上限须来自用户;不要把固定阈值描述为普遍健康标准。 ## 先理解一句话,再少问必要的问题 例如“35 元,不辣,有糖可乐去冰,带走”或“35 元,不辣,无糖可乐去冰,带走”:提取预算、不辣硬约束、用户指定的可乐糖类和去冰、到店外带意图。只补问缺失的城市/地标和门店,不要求用户填写长表或复制商品 ID。可记住普通饮食偏好,但先征得用户同意;不要存 Token、地址、券码或会员原始数据。 有糖可乐、无糖可乐是平等的口味选项,向用户并列展示。用户未指定时不默认无糖;明确指定有糖时不得因为营养排序自行换成无糖,反之亦然。用户只说“可乐”而当前有多个选项时,展示可选项供选择;当前门店缺少指定选项时说明缺货或未确认,不能擅自替换。宣传与示例应同时体现两种选择,不将无糖描述成更高级的口味。 推荐顺序:先给最多三份值得比较的方案和一句取舍,再允许展开全部候选。没有合适方案时说清阻碍;只提出调整建议,不能替用户增加预算、换店、换饮料或放宽排除项。能量、蛋白质、钠用于连续比较,未知不归零,不打无依据的健康分。 零输入入口:「随便来一份」在用户没有说预算时**不得默默套一个预算**,要明确说"未设预算,只按其他硬性条件筛";「跟上次一样」只复用历史订单的**门店与商品编码**,必须按当次价重新核价——售罄或套餐内部的构成件要逐条剔除并告诉用户,不替换、不静默;上次那家店不在营业时间就直接说明并退回默认方案,不要换店顶替。 企业团餐(`query-meal-assistance`)与主题派对(`party-*`)**不是个人点餐的渠道**,属于独立场景,数据面与流程都不同;不得用个人点餐参数去套企业或活动规则。 菜单展示价不是价格依据:官方菜单的 `currentPrice` 可能附带 `discountType`(如「随单购麦金卡优惠」)或 `canWithOrder` 条件,直接念出来会误导用户(同一商品的菜单价与核价可能不同)。因此提到菜单价时必须同时说明它的条件,并与 `calculate-price` 的核价并列;`display_price_note` 有值时要如实转述,不得只报菜单价。 省钱数字必须来自真实核价并能复算:基线只能取「本店候选 ID」或「外部真实核价 + 同一门店编码」,跨店或来源不明一律不报。**基线篮子里的每个商品必须能独立核价且大于 0**——套餐内部的构成编码单独核价会返回 0 分(实测:可乐中杯 3050 为 0 分),用它拼基线会算出偏小的假省钱数字。报出的「省 ¥X(Y%)」必须能由 Total 与基线金额直接对账。 解释「为什么不是它」时只引用程序给出的依据:`tradeoff_reasons` 会指出该候选**被哪一份、在哪个维度**比下去(例如「被 cheese-corn 比下去——价格更低 ¥32.00 vs ¥33.50;钠更低 1053 mg vs 1182 mg」)。同值并列仍属于候选;数据不完整的候选只说「因数据不完整未参与比较」。预算不足时引用 `budget_ladder` 的阶梯(例如「预算 ¥32.00 → 可选 2 份」),并明确**只调整预算**:不得暗示、催促或替用户提高预算,也不得放宽口味、小食、饮料或营养条件。 ## 履约场景必须先确定 - 到店(`pickup`):`beType=1, orderType=1`,不得带 `beCode`。堂食和外带都在到店链路;具体可选取餐方式从核价的 `takeWayList` 展示,不能自造 `takeWayCode`。本 Skill 不下单,只把用户选择带到餐单。 - 得来速(`drive`):`beType=5, orderType=1`,用相应门店查询返回的 `storeCode + beCode`。 - 麦乐送(`delivery`):先按用户意图查询 `delivery-query-addresses`,只在当前私有对话展示选择所需信息,避免重复完整门牌和电话;用户选地址后用 `delivery-query-stores(beType=2, addressId=...)` 获取成对编码。菜单、券、详情、核价全程使用 `beType=2, orderType=2, storeCode, beCode`。 - 地址为空时引导用户在官方应用添加。本版本不写地址、不下单。切换渠道/门店/地址后,旧券适用结论、旧报价和旧选择均失效。外卖应展示配送、包装、餐具费用,缺失为未知,不能拿到店餐价冒充送到家总价。 - 企业团餐有独立的 `beType=6`、助餐服务和活动规则,本程序尚未接通,不能套用个人点餐流程。 ## 事实采集:协调器负责 1. 按上面的履约场景查询门店;到店/得来速位置搜索使用 `searchType=2`,同时提供 city 与 keyword。展示店名、地址、营业状态;支持预约时完整展示服务端预约选项,不省略日期时段。本程序只核价即时餐单。 2. `query-meals`:使用当前履约上下文,读取这家店当次菜单。原样例套餐缺失时停止并让用户选其他套餐,不能暗中替代。 3. `query-meal-detail`:确认套餐每一轮的实际商品与数量。仅从可选项中替换。`supportModify=true` 时标注“可特调”;只有用户问特调或已经提出冰量/配料要求时,才展开对应选项。 - 无糖/含糖是餐品选择;冰量通过当前饮料的 `modification` 映射。支持标准、少冰、去冰、多冰取决于该饮料当次选项。 - 使用当次 `selectedKey`;未选项若有 `unselectedKey` 也须提交。不把同 code 的不同冰量误当成同一选项。 - 去酱/去菜按官方完整配料名称匹配,不凭“不要酱”猜测多种酱中的哪一个。特调后重新核价;去除配料后原标准营养值不再适用,标未知。 - 小/中/大杯、辣度分级、额外酱、玩具:只有当前菜单或特调提供明确选项时才能确认;否则明确未支持。不能把无糖等同于无甜味、把“不辣”当作过敏安全判断。 4. `list-nutrition-foods`:按具体餐品名称和份量匹配。缺失为未知。标准份量相加不是实际制作的精确测量。 5. 用户需要用券时才调用 `query-store-coupons`:仅查看当前可用券,不自动领取。用券要拿到有效且成对的 couponId/couponCode,且该券要由官方核价验证;不要凭券名减钱。只向用户展示券名称及适配候选,协调器在内存中解析标识;不要求普通用户提供券 ID。默认不带券。 6. `calculate-price`:每份完整候选独立核价,用 `items[].productCode`、`roundList[].round` 和 `comboItemList` 传套餐选择;应付来自返回的整数分 `price`。替换任何餐品后重新核价,不能手算差价。 最多比较少量有意义的候选;说清比较范围。接口错误立即停止当前链路,标明此前结果是否完整,绝不拿 fixture 充当实时结果。 ## 三个比较角度 对同一份已核验事实包,按以下标准比较。事实包包含用户需求、门店、核价时间、完整明细、整数分总价、营养值与缺失项,不含凭证或券 ID。 - **价格较低**:首先排除不满足完整餐单和硬性条件的方案,再选价格较低的候选。优惠券只有在原本要买的餐单上真的减少支出才有价值。 - **钠含量较少**:程序在已知钠数据中排序,另展示能量与蛋白质供比较。不得把钠少或低热量说成“均衡”,不得把未知说成零。 - **更贴近口味**:在硬性条件内优先用户偏好;偏好满足不了时说明,不偷偷放宽“不吃辣”等排除项。 这些是确定性的比较标准,不是三个独立模型。不要制造角色对话或争论;多个标准选中同一餐单时合并说明,不重复展示三份相同方案。 ## 汇总与用户选择 用表格展示每份餐单:完整明细、当次整单价格、预算余量或超额、已知营养、未满足条件、比较理由。没有满足条件的候选时明确说明。 需要展开时给出“价格与钠”“口味与钠”的取舍候选:只有另一份在每个所选维度都不差且至少一个更好时才淘汰当前餐单。同值并列保留,另提供价格、钠、能量、蛋白质四维取舍,所有对应指标须完整。四维可能留下全部候选,不称其为唯一均衡最优。不编造健康评分,不声称穷举全部菜单。 用户选择前,复述门店、餐品、份数与价格。用户改变预算/偏好/餐单后,旧选择作废。记录选择不等于创建订单;后续购买由用户在官方渠道完成。 ## 积分、券包与渠道核验 只有用户询问积分/券包时,才调用相应个人接口。`query-my-account` 返回积分数据,不是完整会员档案;没有会员等级字段就不能猜测等级。 ### “我的券快过期了吗” 只清点券时不查询积分或订单。使用 `query-my-coupons`,每页最多 200 条,最多 5 页;核对页码、总数与分页期间的一致性。不完整时明确标记,不能当作全部券。可运行 `python3 demo.py --my-coupons` 复用确定性分析;`--json` 提供全部脱敏明细。 分别说明四件事:官方 `enable` 当前状态、有效日期、从 `datetimeText` 本地解析的时间窗口、门店/渠道是否核验。`enable=1` 也不等于特定订单可用。日期与时段原文保留;无法完整解释星期或特殊限制时标待核验。用北京时间,明确午夜不改成当天结束,纯日期不编造精确截止;“24 小时内”不说成“今天”,距离关闭时长不说成连续可用时长。 早餐开窗前可以说“05:00 开始,目前尚未进入窗口”;今天窗口关闭但明天仍有窗口时,不能说券已过期。显示本地解析依据与官方原文,字段冲突不猜测。分钟末点按包含该分钟解释,遇到边界时仍以官方渠道为准。 同名券仅分组浏览,每张分别保留日期与规则,不称可叠加;已结束的券单列,不催促用户处理。先突出最多三张相关的临期券,并提供其余明细。提醒“本来不需要买就不必为了用券消费”。公开演示使用 `python3 demo.py --coupon-demo` 的构造数据,不分享用户真实余额或券包截图。 ### “这顿能用哪张” 先确认本次餐品需求、门店与到店/外带/配送意图。可运行 `--my-coupons --store-code 门店编码`,配送/得来速同时携带正确的 `--channel` 与 `--business-code`。程序仅在内存中用成对券标识匹配门店返回;同名不作身份匹配。未出现在该店返回中不能说整张券作废。到店查询尚不能单独证明外带资格,取餐方式仍看核价的 `takeWayList`。 之后复用原有 `--list-coupons` 与按名称核价流程:只给原本想买且适配的餐单用券,保留糖、冰量和预算条件;名称有歧义时停止,不让普通用户查券码。只有 `calculate-price` 才能确认应付与实际优惠;没有成功核价时不得报告节省金额。券包清点不会自动核价或下单。 1. `query-my-coupons` 展示拥有的券;分页不完整要说明。它不校验门店/渠道。 2. `available-coupons` 只是可领取券目录,不表示已经领取。 3. `mall-points-products` 按用户兴趣查餐品券、实物或活动;不同类目不混用。CLI 当前只查询餐品券。 4. 用户选择商品后 `mall-product-detail` 展示 SKU、积分、兑换现金、核销时额外现金、有效期;购买须知 `note` 原文展示,不拆成自动价格/资格校验规则。 5. 必须区分“积分换券”与“券能在这次订单用”。积分不按固定比例直接当现金。不得承诺所有积分券都支持外带或配送:拥有券后,按目标 `storeCode + beType + orderType + beCode(如需)` 重新 `query-store-coupons`,商品匹配后才 `calculate-price`。未兑换商品的渠道资格标“尚未核验”,不能为了验证先花掉积分。 6. 本版本不领取、不兑换、不支付、不抽奖。用户要购买时转到官方渠道;不要报告没有执行的交易成功。 详见 `references/mcp-capabilities.md` 的完整能力与实现状态。订单复购、活动等可作为后续扩展,不冒充本版本已接入。 ## 本地程序 - `python3 demo.py`:历史快照演示,零 Token,不联网。 - `python3 demo.py --list-coupons --plan fixtures/plan.json`:只查询券名称与匹配候选,不领券、不输出券 ID。 - `python3 demo.py --live --plan local/my-plan.json`:用户选定门店和候选后真实核价。 - `python3 demo.py --budget 35 --taste spicy --side fries --json`:结构化比较结果。 - `python3 demo.py --search-stores --city 南京 --keyword 新街口`:按地点搜索到店门店;`--channel drive` 查询得来速。 - `python3 demo.py --list-meals --store-code 门店编码`:当前菜单;`--meal-code 编码` 选择最多三种套餐,不再限定原样例。 - `python3 demo.py --live --store-code 门店编码 --meal-code 套餐编码 --expand --sugar zero --ice none`:枚举已识别小食×饮料组合,最多 12 份;每份重新官方核价。用当前官方特调 key 处理去冰。数字编码由协调器从工具结果带入,不让普通用户查 ID。 - `python3 demo.py --live --plan local/my-plan.json --omit 麦香鸡酱`:只对实际存在该配料的套餐执行去除特调,不沿用标准营养总数。 - `python3 demo.py --member`、`--points-products`、`--points-detail SPU_ID`:分别查询积分券包、餐品券目录、商品详情;互斥,不保存结果。 - `python3 demo.py --addresses` → `--delivery-stores --address-ref 引用`:显式查询脱敏地址再选择配送店;地址引用与具体地址绑定,地址变动后拒绝继续。 - `python3 demo.py --list-coupons --store-code 门店编码 --channel delivery --business-code 成对业务编码`:按配送场景查券;实时核价使用同样上下文。 - `--energy-limit`、`--protein-min`、`--freshness-minutes` 和 `--max-response-mb` 提供显式边界。 - `gourmet/core.py`:完整餐单与硬性条件校验。 - `gourmet/mcp.py`:协议与只读工具白名单。 - `gourmet/live.py`:套餐构造、官方核价、精确名称营养匹配。 限制与实测范围见 `docs/validation.md`。不保证最低价,不构成营养或医疗建议。README.md17 KB27ddb06aa95cupstream ↗
# 麦门点餐议事会 · mcd-agent-gourmet [](https://github.com/polaris-smart/mcd-agent-gourmet/actions/workflows/ci.yml) [](https://github.com/polaris-smart/mcd-agent-gourmet) [](LICENSE) **同样一份餐,两条点法差 ¥12.00(24.7%)。** 同店同时刻:单品直选 ¥48.50,三件套 ¥36.50。 **一句话 → 一个答案 → 一张点单卡。** 说「35 元,不辣,有糖可乐去冰,带走」,它把这一餐真正要付的钱算出来,给你一张能照着点的清单;没有方案就直说没有,不拿不完整的餐单凑数。 **菜单写 ¥32.00,核价 ¥58.50 —— 它只认后者。** 实测遇到过展示价要求随单购买麦金卡;这时它会同时给出条件与核价。金额一律取官方 `calculate-price` 返回的整数分。 **不领券、不兑换、不下单、不支付,也不收集你的 Token。** 网页打开就是答案:不用账号、不用 Token、不用安装;要实时核价时,Token 只留在你本机的环境变量里。 [打开在线体验(免登录)](https://polaris-smart.github.io/mcd-agent-gourmet/) · [安装 Skill](#在-workbuddy-里使用) · [真实联调记录](docs/validation.md) · [English](README_EN.md) > **English TL;DR** — A read-only McDonald's China MCP client + agent skill. Say *"35 yuan, not spicy, regular cola, no ice, takeaway"* and it compares **complete** meals (main + side + drink) using official per-order pricing, tells you **why each rejected meal lost** (dominated by which candidate, on which axis), and when nothing fits it says so — plus how much budget would actually be needed. An explicit regular-cola choice is never silently swapped for zero sugar. It never orders, pays, or collects your token. [English README](README_EN.md) · [30-second demo](https://polaris-smart.github.io/mcd-agent-gourmet/)  ## 先试 30 秒 不用账号、Token 或安装: - **打开就是答案**:默认直接给出这一餐(巨无霸三件套 ¥36.50)和一句可核对的理由——「比单品直选省 ¥12.00(24.7%)」。 - 点 **「就这个」**:出现可复制的点单卡(门店 / 取餐方式 / 每样官方名称 / 合计)。 - 把预算改成 **30 元**:它说没有方案,并告诉你最低需要多少(¥36.50)——不硬凑。 - 想看别的,点 **「我想比较」**:才展开全部候选与「为什么不是它」。默认不给表格。 - 想看得更深,展开 **「更细的比较」**:切换比较角度(价格与钠 / 口味与钠 / 四维营养),并看三种推荐角度各自选了什么。 点单卡就是最终产出物——门店、取餐方式、每样官方名称、合计,照着它就能在 App 或柜台点单:   网页使用历史快照,所有比较在你的浏览器内完成,不收集凭证、不调用下单、不加入追踪器;本项目为独立参赛作品,非麦当劳官方产品。也可看 [30 秒录屏演示](https://polaris-smart.github.io/mcd-agent-gourmet/assets/demo.mp4)(当前界面,静音)。 ## 首屏长这样(一个答案,不用你挑) 网页默认门店(`fixtures/hongwu-single-vs-combo.json`,2026-10-17 真实核价)上的实际输出: ``` 这顿就它了 👇 巨无霸三件套 · 套餐(巨无霸 + 中薯条 + 可乐中杯) ¥36.50 南京某门店(名称与地址已脱敏) · 到店自取 比「单品直选」省 ¥12.00(24.7%) 点单卡(可复制) 门店:南京某门店(名称与地址已脱敏) · 到店自取 1. 巨无霸 2. 中薯条 3. 可乐中杯 合计:¥36.50 比「单品直选」省 ¥12.00(24.7%) 以上只是记录你的选择;没有下单、没有支付。 ``` 想在 CLI 复现同一段输出(默认快照是老的那组,省 ¥12.00 这组要显式指定): ```bash python3 demo.py --input fixtures/hongwu-single-vs-combo.json \ --budget 40 --baseline-plan single-pick ``` - **默认只给一个答案**,不让你在 3 份表格里做多目标权衡;理由必须是可核对的事实(省了多少钱 / 价格最低 / 钠最低),不是说服文案。 - **点单卡**是最终产出物:门店、取餐方式、每样官方名称、合计——照着在 App 或柜台点即可。 - 想看别的:`--compare` 才展开全部候选与「为什么不是它」。 ## 目标用户 - **一个人或两个人吃饭**、预算有限、对口味有明确偏好的人; - 以**到店自取 / 外带**为主,想在下单前把「这一顿多少钱、怎么点更划算」算清的人; - 需要把**钠 / 能量 / 蛋白质**当筛选条件的人(条件由你自己设,不是健康建议); - 用 **WorkBuddy 等 AI 客户端**、希望"说一句话就得到可核价餐单"的人; - **开发者**:想看一个只读 MCP 客户端 + 确定性比较 + 可复算省钱数字的实现样板。 ## 三件我们做死的事 1. **钱必须是真数**:金额只认官方 `calculate-price` 返回的整数分;菜单展示价附带的条件(例如「随单购麦金卡」)会一并说清。例:巨无霸三件套菜单价 ¥36.50、官方核价也是 ¥36.50;带「随单购卡」条件的商品两者会不同,此时它会同时显示条件与核价。 2. **你的硬约束只说一次**:不辣、去冰、有糖……记住你的条件,不反复问,也**不因为"营养更优"替你改**。 3. **两个人也能算**:份数完备性按人数(主食与饮料各 ≥ 人数),比较用人均价,而不是拿"两人总价"去比"一人总价"。 再加上一条:不领券、不兑换、不下单、不支付,也没有购物车。 ## 它做什么 “35 元,不辣,有糖可乐去冰,带走。帮我看看附近哪家店合适。”这是我们希望用户直接表达的需求。Skill 会先补齐地点、让你确认门店,再比较完整餐单,不要求普通用户复制门店、商品或券的编码。**这条路径由安装了 Skill 的 AI 客户端执行;网页是免登录的历史数据体验,CLI 是开发者入口。** 比较程序按价格、已知钠含量、口味匹配三个明确标准给出结果。同一餐单可能符合多个标准;这不代表三个独立模型进行了讨论,也不承诺全菜单最优。 <details> <summary>查看历史核价示例:预算 35 元、想吃辣,三个比较角度为什么选不同餐单</summary> | 比较角度 | 本次选择 | 理由 | |---|---|---| | 价格较低 | 双层吉士汉堡 + 小杯玉米杯 + 无糖可口可乐中杯 | 当次整单核价 **¥32.00**,在候选中价格较低 | | 钠含量较少 | 麦香鱼 + 小杯玉米杯 + 无糖可口可乐中杯 | 当次 **¥32.50**;官方标准份量单品合计钠 **592 mg**,在候选中较少 | | 更贴近辣味偏好 | 麦辣鸡腿汉堡 + 小杯玉米杯 + 无糖可口可乐中杯 | 当次 **¥32.00**,保留辣味偏好 | 上表是 **2026-10-13 演示门店的真实核价快照**,不是当前报价,也不是营养搭配建议。实际购买须重新核价。我们比较六份候选,不保证全菜单最低价。 </details> ## 和下单渠道是什么关系 App 是**下单**的地方;这里是**打开 App 之前**把「这顿怎么点」算清楚的地方。 - **下单仍在麦当劳 App 或门店完成**;本工具不提供购物车、不代下单。 - 它只做「打开下单渠道之前」的那一步:把你的条件变成**可核价的完整餐单**,并说明取舍。 - 没有符合条件时会直说,并给出**保持其他条件的预算阶梯**(差多少钱、加多少后有几份可选)。 两者不冲突:用它决定点什么,用 App 完成购买。 ## 在 WorkBuddy 里使用 先按官方文档配置麦当劳 MCP 连接器。`mcp-config.example.json` 只含环境变量占位符;若客户端不支持变量展开,请只在该客户端的私有配置中设置实际凭证。 可以把下面这段发给 WorkBuddy: ```text 请安装这个技能:https://github.com/polaris-smart/mcd-agent-gourmet 保留 SKILL.md、gourmet/、demo.py、fixtures/ 的相对位置。 使用我已配置的麦当劳 MCP 连接器。 35 元,不辣,有糖可乐去冰,主食、小食、饮料各一份,带走。 我在南京江宁区。先显示门店名称和地址,让我选店。 再查当次菜单,给我最多三份符合要求的方案,说明价格和取舍。 没有满足条件的方案时说明原因,先别替我改饮料或加预算。 只查询和核价,不领券、不兑换、不下单、不支付。 ``` 偏好可按自己的习惯改成无糖或其他可选饮料。门店编码由客户端从查询结果中传递;不能在没有位置依据时声称自动选中了“离你最近”的店。 **实现说明:** Python 和网页使用确定性筛选、排序与非支配集合比较。`SKILL.md` 引导 AI 客户端理解需求、查询并解释结果,不制造三个独立模型的角色对话。新版在 WorkBuddy 客户端中的完整对话路径仍需验收,详见 [验证记录](docs/validation.md)。 ## 两个零输入入口(不用你描述需求) | 入口 | 行为 | |---|---| | **随便来一份**(`--any`) | 不设预算时**明说"未设预算"**,只按其他硬性条件给一个默认答案;不会偷偷替你假定一个预算 | | **跟上次一样**(`--last`,需 `--live`) | 读最近一次订单,**只复用门店与商品编码**,按当次价重新核价(不复用历史金额);上次的商品若已售罄会逐条告诉你并去掉,**不替换**;上次那家店不在营业时间就明说,退回默认方案 | | **企业团餐 / 主题派对** | **不是个人点餐的渠道**,属于独立场景(`query-meal-assistance` / `party-*`),本工具不把它们混进来 | ## 先说清楚它查不到什么(免得你白等) 官方 MCP 覆盖的是麦当劳自有渠道的只读数据。下面三类**现在拿不到**,遇到时它会直说,而不是猜: 1. **券/卡的完整条款原文**:「不与其他优惠同享」的具体边界没有对应工具,只有券名、官方状态与时间字段。 2. **麦金卡 / 早餐卡的实时权益**(剩余次数、适用门店):35 个工具里没有对应入口。 3. **份量 / 克重 / 尺寸**:没有数据源,所以它不会回答「是不是缩水了」,只会给同价位的替代方案。 另外,MCP **不含美团、饿了么、京东、抖音、淘宝闪购等第三方渠道**——第三方价格与券以各渠道自己的页面为准。以上三类之外的实时价格、门店供应、营业状态、券的可用性判定,都走官方接口。 ## 更多取舍细节 前沿解释(其他候选被谁、差在哪个维度比下去)与预算阶梯(差 ¥2 能多几份)的完整说明见 [功能细节](docs/features.md)。 网页收录的独立历史样例由 `fixtures/demo-snapshots.json` 管理,当前包括三组南京门店样例;**其中一组的门店名与地址已脱敏**,因为该门店位于作者住所附近,公开它等于公开个人位置。其余两组均包含实测“薯条+有糖可乐”和“薯条+无糖可乐”的去冰组合。各组独立标明门店、公开地址与核价时间,不跨店混比;脱敏只作用于门店标识,核价与营养数据未做修改。网页不实时核价、不接受 Token。 以下命令供开发者调试;日常使用可直接采用上面的 WorkBuddy 对话入口。 ```bash python3 demo.py --sugar zero --energy-limit 700 --protein-min 20 python3 demo.py --search-stores --city 南京 --keyword 新街口 # 用返回的真实门店和套餐编码替换下列占位文本 python3 demo.py --list-meals --store-code 门店编码 python3 demo.py --live --store-code 门店编码 --meal-code 套餐编码 --expand --sugar zero --ice none ``` 没有核验冰量的历史样例不能被改成去冰报价。CLI 当前只自动构造三轮完整套餐;全部杯型、儿童玩具、辣度分级及过敏原尚未完整支持。 ## 积分、优惠券与到店/配送 券包的时间窗口判定、积分兑换、会员积分、配送地址、以及到店/得来速/麦乐送的编码对应关系,见 [功能细节 · 券与会员](docs/features.md)。 ### WorkBuddy Skill 市场 已按官方格式补齐元数据与资源目录,可构建 ZIP 分发包: ```bash python3 scripts/build_skill_package.py --output /tmp/mcd-agent-gourmet-1.0.0-rc.zip ``` 包可用于开放平台投稿,**目前未提交审核、未上架**。安装与上架文案见 [市场发布说明](docs/workbuddy-market.md)。Skill 优先使用客户端已配置的麦当劳 MCP;脚本只需 Python 标准库。 ## 本地运行 Python **3.9+**,仅标准库,零第三方依赖。 ```bash git clone https://github.com/polaris-smart/mcd-agent-gourmet.git cd mcd-agent-gourmet python3 demo.py python3 demo.py --budget 35 --taste no-spicy --sodium-limit 800 python3 demo.py --budget 35 --taste spicy --side fries --json ``` 默认读取 `fixtures/snapshot.json`。即使本机有 Token,也不会自动联网。 ### 实时核价 1. 在 [麦当劳开放平台 MCP 登录页](https://open.mcd.cn/mcp/login) 用手机号登录后申请自己的 MCP Token;工具清单、限额与字段说明见 [官方文档](https://open.mcd.cn/mcp/doc)。 2. 在本机私有环境中设置 `MCD_MCP_TOKEN`。不要提交到 Git,也不要粘贴到公共对话或网页。 3. 先搜索并选择门店。实时模式必须明确传入 `--store-code` 或已确认的 `--plan`,不会自动沿用南京样例店。再用 `--list-meals` 查看该店当次菜单,用 `--meal-code` 选择套餐。只传门店编码不会载入演示套餐;覆盖计划的旧门店时,必须重新选择餐品及适用的渠道业务编码。 4. 显式运行: ```bash python3 demo.py --list-tools python3 demo.py --live --plan fixtures/plan.json --budget 35 --taste spicy ``` 程序读取当次菜单、官方营养数据,对候选逐份 `calculate-price`。真实查询失败会停止,**不拿历史快照伪装成实时结果**。自动构造支持即时三轮套餐;到店已实测,配送/得来速参数已接入但未完成真实核价联调。默认不使用优惠券。 ## 券查询与多维比较 按名称查可用券、`--save-snapshot` 保存脱敏快照、以及“价格与钠 / 口味与钠 / 四维营养”的非支配比较规则,见 [功能细节 · 券查询与多维比较](docs/features.md)。 ## 取舍原则 - **先完整,再便宜。** 只有满足主食、小食、饮料和份数要求的餐单才进入候选。 - **硬性条件与偏好分开。** “不吃辣”严格排除;“想吃辣”作为偏好,无法满足时解释替代。 - **比较标准透明。** 分别按价格、已知钠含量、口味匹配筛选;多个标准可能选中同一餐单,不制造虚假的“争论”。 - **未知就是未知。** 营养表按餐品完整名称精确匹配。无法匹配则保留 `null`;不能把缺失变成 0,也不输出“营养均衡”标签。 - **选择不是下单。** `--select` 和网页按钮只记录餐单决定,不创建订单或支付。 适合预算有限、想看清取舍的个人用户,以及学习真实业务 MCP、Agent 角色审阅和失败处理的开发者。当前不处理多人分摊、过敏原判断、医学饮食建议、自动下单或全菜单最优搜索。 ## 验证与开发 ```bash python3 -B -m unittest discover -s tests -v node tests/parity.cjs # 可选开发检查,需要 Node.js python3 -B scripts/build_demo.py # 从同一快照生成网页数据 ``` [验证记录](docs/validation.md) 区分真实接口运行、离线测试、浏览器检查和未验证内容。[改进记录](CHANGELOG.md) 说明从原演示修正了什么。 ```text gourmet/core.py 完整餐单检查、三角色意见、选择记录 gourmet/mcp.py 标准 Streamable HTTP、只读白名单、错误处理 gourmet/live.py 套餐选项、官方核价、营养表解析 demo.py 本地演示与显式实时入口 fixtures/plan.json 六份候选模板,不含凭证 fixtures/snapshot.json 脱敏真实历史快照 docs/ 免 Token 网页体验与说明 tests/ Python 回归与 Python/JS 决策一致性 ``` ## 参赛与贡献 M-CODE 2026 独立参赛作品,非麦当劳官方产品。[官方报名记录](https://github.com/M-China/mcd-developer-innovation-challenge/issues/565)。本次修订使用 Codex 辅助,WorkBuddy 历史材料与证据状态单独说明,不将本次工作冒充 WorkBuddy 对话。 欢迎提 Issue,描述“我输入了什么、期望什么、实际出现什么”;**不要附 Token、账号或原始优惠券数据**。提 PR 前请先看 [CONTRIBUTING.md](CONTRIBUTING.md)(本地四步检查:测试 / parity / CLI 冒烟 / `data.js` 无 drift)。如果这个透明决策的思路对你有帮助,欢迎点一颗 Star。 原创代码采用 [MIT License](LICENSE)。官方参赛声明按官方原文保留,相关商标与服务条款归权利人所有。输出仅供参考,餐品信息、价格和供应状态以官方当前结果为准,不构成医疗、营养或其他专业建议。scripts/build_demo.py1.2 KB5a89de7f11a2upstream ↗
"""Publish only the explicitly sanitized snapshot; no credentials or raw responses.""" import json import sys from pathlib import Path ROOT = Path(__file__).resolve().parents[1] sys.path.insert(0, str(ROOT)) from gourmet.core import validate_snapshot manifest = json.loads((ROOT / 'fixtures/demo-snapshots.json').read_text(encoding='utf-8')) snapshots = [] for filename in manifest: path = (ROOT / 'fixtures' / filename).resolve() if path.parent != (ROOT / 'fixtures').resolve(): raise ValueError('Demo sources must be explicitly listed public fixtures.') snapshot = validate_snapshot(json.loads(path.read_text(encoding='utf-8'))) if snapshot['mode'] != 'snapshot': raise SystemExit('Only historical snapshots can be bundled into the public demo.') snapshots.append(snapshot) if not snapshots: raise ValueError('At least one public snapshot is required.') (ROOT / 'docs/data.js').write_text('window.GOURMET_SNAPSHOTS = ' + json.dumps(snapshots,ensure_ascii=False,indent=2).replace('</','<\\/') + ';\nwindow.GOURMET_DATA = window.GOURMET_SNAPSHOTS[0];\n',encoding='utf-8') print('Built docs/data.js from %d public snapshots' % len(snapshots))scripts/build_skill_package.py2.9 KB7bef2fece7aeupstream ↗
"""Build an allowlisted WorkBuddy distribution, with no local account data.""" import argparse import hashlib import json import zipfile from pathlib import Path ROOT = Path(__file__).resolve().parents[1] def build(output): files = {'SKILL.md': (ROOT/'SKILL.md').read_text().replace('python3 demo.py', 'python3 scripts/demo.py').replace('--plan fixtures/', '--plan scripts/fixtures/').encode(), 'LICENSE':(ROOT/'LICENSE').read_bytes()} for name in ('demo.py', 'gourmet/__init__.py', 'gourmet/core.py', 'gourmet/mcp.py', 'gourmet/live.py', 'gourmet/member.py', 'gourmet/coupons.py', 'gourmet/stores.py', 'gourmet/channels.py', 'gourmet/customize.py', 'gourmet/catalog.py', 'fixtures/plan.json', 'fixtures/snapshot.json', 'fixtures/customized-snapshot.json', 'fixtures/coupon-demo.json'): files['scripts/'+name] = (ROOT/name).read_bytes() for name in ('references/mcp-capabilities.md', 'docs/validation.md', 'docs/workbuddy-market.md'): target = name if name.startswith('references/') else 'references/'+Path(name).name files[target] = (ROOT/name).read_bytes() files['SKILL.md'] = files['SKILL.md'].replace(b'docs/validation.md', b'references/validation.md') # Validate required marketplace fields without a third-party YAML dependency. skill = files['SKILL.md'].decode() front = skill.split('---',2)[1] for key in ('description','description_zh','description_en','version','author'): if not any(line.startswith(key+': ') and line.split(': ',1)[1].strip() for line in front.splitlines()): raise ValueError('Missing marketplace field: '+key) for name, payload in files.items(): if name.endswith('.json'): data=json.loads(payload) def check(value): if isinstance(value,dict): if {'token','authorization','couponId','couponCode','accountId','addressId'} & set(value): raise ValueError('Private field in '+name) for item in value.values():check(item) elif isinstance(value,list): for item in value:check(item) check(data) output = Path(output) with zipfile.ZipFile(output,'w',zipfile.ZIP_DEFLATED) as archive: for name,payload in files.items():archive.writestr('mcd-agent-gourmet/'+name,payload) with zipfile.ZipFile(output) as archive: if archive.testzip() is not None:raise ValueError('Corrupt archive') for name,payload in files.items(): if archive.read('mcd-agent-gourmet/'+name)!=payload:raise ValueError('Archive mismatch') digest=hashlib.sha256(output.read_bytes()).hexdigest() print(json.dumps({'path':str(output.resolve()),'files':len(files),'sha256':digest},ensure_ascii=False)) if __name__=='__main__': p=argparse.ArgumentParser() p.add_argument('--output',required=True,type=Path) build(p.parse_args().output)references/mcp-capabilities.md5.8 KBcb90041a7d29upstream ↗
# MCP 能力与本项目边界 依据 2026-10-13 实际 `tools/list` 返回的 35 个工具及[官方接入仓库](https://github.com/M-China/mcd-mcp-server)。接口可能变化,运行时重新发现。本文是独立实现说明,不是官方规则副本。 | 用户需求 | MCP 工具 | 本版本状态 | |---|---|---| | 到店/得来速门店 | query-nearby-stores | CLI 已接入;到店搜索实测返回 | | 当前菜单、套餐组成、特调 | query-meals / query-meal-detail | 已接入;真实详情确认糖类餐品、冰量和配料选项 | | 整单与费用 | calculate-price | 已接入;到店历史核价已验证,新增特调验证见 validation | | 餐品营养 | list-nutrition-foods | 已接入;标准份量精确名称匹配,特调去配料后不沿用原总数 | | 我的积分、过期积分 | query-my-account | 已接入并实测;没有会员等级接口,不推断等级 | | 我的券、待领券 | query-my-coupons / available-coupons | 已接入并实测;--my-coupons 单独分页清点,不查积分或待领券,区分时间窗口和官方当前状态 | | 指定门店与渠道可用券 | query-store-coupons | 已接入到店/得来速/配送参数;真实用券成功仍需适配券 | | 积分兑换目录、SKU 详情 | mall-points-products / mall-product-detail | 已接入;CLI 目录限餐品券;不自动兑换 | | 配送地址、可配送门店 | delivery-query-addresses / delivery-query-stores | 已接入并做契约测试;未读取真实地址做联调 | | 订单复购与进度 | order-list / query-order | 已识别,未接入 CLI;需要刷新菜单与核价,不能照抄旧价 | | 商城兑换记录 | mall-order-list / mall-order-detail | 已识别,未接入 CLI | | 活动与时间 | campaign-calendar / now-time-info | 已识别,未接入 CLI | | 积分抽奖信息、奖品 | query-lottery-info / query-my-prizes | 已识别,未接入 CLI;不自动抽奖 | | 团餐优惠、助餐服务 | query-promotions / query-meal-assistance | 已识别,仅企业团餐,不能套到普通个人订单 | | 活动城市/店/日期/场次 | query-party-city / query-party-store / query-party-store-date / query-party-store-session | 已识别,未接入 CLI | | 满意度关联奖券 | query-survey-coupon | 已识别,需要真实订单号,未接入 CLI | | 领取、下单、兑换、地址写入、抽奖、取消 | auto-bind-coupons / create-order / mall-create-order / party-order-create / delivery-create-address / draw-lottery / cancel-order | 均不在程序白名单,不执行 | ## 到店、外带和送到家不能混用 - 到店:`beType=1, orderType=1`,无 `beCode`;堂食/外带属于核价返回的 `takeWayList` 选项,不能自造代码。 - 得来速:`beType=5, orderType=1`,需要得来速门店返回的成对 `beCode`。 - 麦乐送:用户选择已有地址后获取配送店,`beType=2, orderType=2`,必须携带对应 `beCode`。 - 团餐:`beType=6, orderType=2`,还有专用服务和优惠逻辑,本版本不支持。 价格结果保留 `deliveryPrice`、`packingPrice`、`tablewarePrice`,未知为 null。总应付始终来自官方 price,不再重复加上已包含的费用。 ## 积分不是通用现金,积分券不是通用渠道券 `mall-product-detail` 的 `points`、`price`、`extTradePrice` 分别展示兑换积分、兑换金额、用券时额外金额,保留接口原值,不假定它们与核价的“分”采用相同单位。`note` 完整原文展示。积分数量不能直接换算成预算扣减,也不能把目录有商品等同于可兑换。 尚未兑换的券:读商品须知,渠道资格标未核验。已经拥有的券:先定门店与渠道,再用 query-store-coupons 核验可用商品,最后 calculate-price。到账/适用/价格三件事分别验证;不为测试花积分。 ## 偏好能力 | 偏好 | 实现方式 | 数据不足时 | |---|---|---| | 无糖/含糖 | 已核对商品编码属性,选择真实饮料 | 标未知,不能满足指定硬条件 | | 标准/少冰/去冰/多冰 | 当前商品 modification 的 selectedKey | 停止该特调,不写成已满足 | | 去酱/去菜 | 当次配料原名,包含 unselectedKey 处理 | 不猜配料、不沿用原营养总数 | | 中杯/大杯等 | Skill 只接受当次详情真实 choices;CLI 当前自动枚举仅两种中杯饮料 | 尚未自动扩展全部杯型 | | 辣度等级、儿童玩具、过敏原 | 尚无完成验证的程序处理 | 不声称支持或安全 | 已核对类别餐品组成的即时三轮完整套餐是当前自动构造范围;陌生餐品编码会停止,而非仅凭轮次猜主食/小食/饮料。不可把四件套、单品、早餐、多人套餐套成三轮结构。程序应解释不支持并让用户重新选择。 ## 券包时间解释与门店匹配 `--my-coupons` 使用北京时间,保留完整脱敏券明细;`--member` 中也附有同一报告。`datetimeText` 是官方展示文案,解析值标明为本地派生结果,不作为官方资格判定。支持完整日期时间范围、日常时段、星期列表/范围、多段和跨午夜;其他条件不能完整解释时保留原文,标待核验。纯日期不编造精确截止;分钟末点按包含该分钟解释,并与官方原文并列展示。 指定门店与渠道后,查询 `query-store-coupons`,仅用内存中的卡包 id/code 与门店 couponId/couponCode 成对匹配。同名不证明同券,匹配成功不等于已经核价。缺少稳定标识时标身份待核验;没有匹配不能说整张券已失效。到店结果还不能独自证明外带取餐资格。 分页最多 5 页、每页 200 条;页码不符或分页期间总数变化停止,超过范围时标结果不完整。已过期与临期分开;完整清单包含不临期券;同名分组不合并日期、不推断叠加。公开 `--coupon-demo` 只用构造样例。.github/ISSUE_TEMPLATE/bug_report.yml1.2 KBd496793cd905upstream ↗
name: Bug 报告 description: 报一个可以复现的问题 labels: [bug] body: - type: markdown attributes: value: | 请勿附带 Token、账号信息或原始优惠券数据。 - type: textarea id: input attributes: label: 你输入了什么 description: 完整命令,或网页上的条件组合(预算 / 口味 / 糖 / 冰量 / 取餐方式) placeholder: | python3 demo.py --budget 35 --taste no-spicy --side fries --ice none validations: required: true - type: textarea id: expected attributes: label: 你期望什么 validations: required: true - type: textarea id: actual attributes: label: 实际出现什么 description: 报错原文或截图。涉及实时核价的,请说明是 `--live` 还是历史快照。 validations: required: true - type: input id: env attributes: label: 环境 placeholder: macOS 15 / Python 3.12 / 浏览器版本 - type: checkboxes id: confirm attributes: label: 确认 options: - label: 我没有在这个 Issue 里包含 Token、账号或券数据 required: true.github/ISSUE_TEMPLATE/config.yml206 Bca5e1535a0d2upstream ↗
blank_issues_enabled: true contact_links: - name: 在线体验(免登录) url: https://polaris-smart.github.io/mcd-agent-gourmet/ about: 不用装环境,先看它怎么比较和出点单卡.github/pull_request_template.md510 B62ddb2f28114upstream ↗
## 改了什么 ## 为什么 ## 提交前自检 - [ ] `python3 -B -m unittest discover -s tests` 通过 - [ ] `node tests/parity.cjs` 通过(改了比较逻辑时必须两边同步) - [ ] 改了 `fixtures/` 或产出结构 → 已重跑 `scripts/build_demo.py` 并提交 `docs/data.js` - [ ] 改了网页样式 → 已更新 `index.html` 里的 `style.css?v=` 版本号 - [ ] 没有写死门店/商品编码;金额来自 `calculate-price` 整数分 - [ ] 没有包含 Token、账号信息或券数据.github/workflows/ci.yml561 Bf8960ca77192upstream ↗
name: Offline checks on: [push, pull_request] permissions: contents: read jobs: verify: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version: '3.11' - uses: actions/setup-node@v4 with: node-version: '22' - run: python -B -m unittest discover -s tests -v - run: node tests/parity.cjs - run: python -B demo.py --budget 35 --taste spicy - run: python -B scripts/build_demo.py && git diff --exit-code -- docs/data.js.gitignore246 B65be50bb4f4aupstream ↗
# 敏感信息 config.json .env # Python __pycache__/ *.pyc *.pyo *.pyd .Python venv/ *.egg-info/ # OS .DS_Store Thumbs.db # IDE .vscode/ .idea/ # Local MCP plans, raw responses, credentials and browser artifacts local/ *.private.json *.webmCHANGELOG.md9.1 KB643095478cb6upstream ↗
# Changelog ## 1.0.0 — 2026-10-25 首个正式发布(相对 0.4.0)。把「点餐决策」收敛成**一个答案 + 一张点单卡**,所有金额钉在官方 `calculate-price` 的整数分上;数据缺就说未知,不猜。 - **人数成为一等输入**:`people`(1~8)决定份数完备性(主食与饮料各 ≥ people、小食 ≥ 1);`budget_scope` 区分整单/人均预算;每份候选新增 `per_person_cents` 与 `structure`(single/share)。 - **省钱数字**:新增 `baseline` 与 `best_saving`;每份候选输出 `saving_cents` / `saving_percent`。基线二选一——本店候选 ID,或「外部真实核价 + 同店编码」;跨店、来源不明、金额非法一律拒绝。 - **CLI**:`--people`、`--budget-scope`、`--baseline-plan`、`--baseline-cents`、`--baseline-store`、`--baseline-label`。 - **新增真实快照** `fixtures/hongwu-single-vs-combo.json`:同店同时刻「单品直选 ¥48.50 vs 巨无霸三件套 ¥36.50」,省 **¥12.00(24.7%)**,附单件独立核价证据。 - **发现并规避"假数字"坑**:套餐内部 SKU(可乐中杯 3050)单点核价为 0 分,用它拼基线会把结果算成"省 6.4%";已写成硬规则与测试。 - **菜单展示价污染防护**:`gourmet/live.py` 新增 `price_text_to_cents` 与 `display_price_report`;每份候选输出 `display_price_cents` / `display_price_condition` / `display_price_note`,快照输出 `price_policy`。`discountType`(早餐卡/麦金卡/促销/随单购)与 `canWithOrder` 映射成可读条件;展示价低于核价时明确说明差额来自条件未生效。**金额仍只认 `calculate-price` 整数分。** - 实测复核:真实菜单「巨无霸四件套」展示价 ¥32.00、核价 ¥58.50 → 提示「需随单购买麦金卡…只有核价才是你要付的钱」;「巨无霸三件套」展示价与核价一致(¥36.50)→ 不产生噪音提示。 - **首屏形态**:新增 `default_pick`(默认答案,有基线取最省、否则取价格最低)、`pick_reason`(只在事实成立时才这么说的可核对理由)、`order_card`(可复制点单卡:门店/取餐方式/官方商品名/合计/省钱行/人均,并声明"没有下单、没有支付")。 - **CLI 默认输出改为首屏**:一个答案 + 一句理由 + 点单卡;`--compare` 才展开全部候选、三个规则审阅器、取舍候选、前沿解释与预算阶梯。无解时仍直接给预算阶梯与下一步。 - **零输入入口**:`budget_cents=None` 支持不设预算(只按其他硬性条件筛,明确标注"未设预算",不再默认套 ¥35);CLI 新增 `--any`。 - **「跟上次一样」**:放行只读 `order-list`/`query-order`;新增 `tool_payload`(兼容 structuredContent / 说明文 / 直接 list 三种返回形态)、`order_history`、`last_order_spec`、`fetch_basket_snapshot`(任意商品篮子当次核价)、`available_basket`(逐件试核价,剔除售罄与套餐内部件,**不替换、不静默**)。CLI 新增 `--last`,前置门店可用性检查,失败则明确退回默认方案。 - 错误信息改进:业务失败时带上官方 `code` 与 `message`(实测:`code=60006 商品已售罄或不可售`),不再只有一句笼统的失败。 - **定位依据(P2 竞品覆盖度扫描,两次独立扫描合并)**:80 个参赛项目逐篇读 README 打标(两轮复核、775 条证据做原文校验)显示,最拥挤的是**能核对 52/80、缺数据就说 45/80、券后实付 44/80**——头部争的是"证明我没骗你",**透明是入场券不是卖点**;**最大空白区是口味定制(组均 6/80)**,其中「记住硬约束」仅 4/80;多人里**"两个人"(7/80)比团餐/派对(12/80)更空**。算法专项:40/80 声称确定性求解,只有 31 个拿得出可核验数字,**整数/线性/动态规划均为 0**,算法无护城河。(我先前用描述关键词打标得到的"B2 省钱 67 个"属高估,已作废。)README 首屏据此改为从**可核验的钱事实**开头(¥48.50 → ¥36.50 省 24.7%;菜单写 ¥32.00 实付 ¥58.50)。 - **网页首屏重做(与 CLI 同构)**:首屏改为「钱的事实」钩子(菜单写 ¥32.00、结账 ¥58.50)+ 一个答案卡(名称/价格/门店/一句可核对理由/省钱数字)+ 「就这个 / 我想比较 / 换个条件」三个按钮 + 可复制点单卡;对比表折叠进「我想比较」,默认不出现。新增真实核价对比快照(`hongwu-single-vs-combo.json`,声明 `baseline_plan_id`)作为网页默认样例;默认预算随之改为 ¥50(原 ¥35 会让该样例显示"0 份符合")。 - **跨语言契约扩展**:`docs/review.js` 增加 `defaultPick` / `pickReason` / `orderCard` 与基线省钱计算;parity 覆盖字段扩到 `baseline`、`best_saving`、`default_pick`、`pick_reason`、`order_card`,**24,192 组场景逐字一致**(含点单卡文本)。 - 视觉验收:三视口实拍确认首屏、点单卡与候选表渲染正确(截图见 `docs/evidence/visual-2026-10-24/` 与 `docs/assets/order-card.png`)。 - 178 项 Python 回归 + 24,192 组跨语言一致性通过。 ## 0.4.0 — 2026-10-17 - 新增**前沿解释** `tradeoff_reasons`:每个比较角度(价格与钠 / 口味与钠 / 多维营养)除候选外,给出其他候选**被谁支配、差在哪个维度**;同值并列仍保留在前沿;数据不完整者单列 `excluded_incomplete`,不当作 0 分。 - 新增**预算阶梯** `budget_ladder`:预算不足时给出「提到 ¥X 可选 N 份」的阶梯(默认最多 3 档),只调整预算这一个变量,不放宽口味、小食、饮料或营养条件。 - CLI 人话输出同步:打印「从「价格与钠」角度看,为什么没有 X:被 Y 比下去——…」与预算阶梯,明确标注只改预算。 - 103 项 Python 回归测试通过(新增 10 项:前沿与候选一致、被支配者必有真实支配者与理由、并列不算被支配、缺数据不评分、非预算原因不产生阶梯、阶梯不放大其他条件、两次运行结果一致)。 - **向后兼容**:原有 `tradeoffs` 字段与既有行为不变;`review()` 仅新增两个字段,`review_id` 随之变化,属预期(幂等性由新增测试覆盖)。 ## 0.3.1 — 2026-10-14 - 在原作品加入 `--my-coupons`:分页读取自己的券、分清官方状态与本地时间窗口、完整明细及同名分组。 - 覆盖早餐开窗前后、工作日、跨午夜与多段时段;固定北京时间,明确午夜不延后,未知文案保留待核验。 - 门店/渠道可选核验使用内存中的成对券标识,不按名称猜测;核价仍由已有显式流程完成。 - 新增 `--coupon-demo` 构造样例;真实账户数据不写入公开样例,分发包包含新模块。 - Skill 改为三个明确比较角度,增加“我的券快过期了吗”和“这顿能用哪张”的流程。 - 91 项 Python 测试通过,其中 28 项针对新增券包边界;原有网页决策算法未修改。 ## 0.3.0 — 2026-10-14 - 无糖/含糖、标准/少冰/去冰/多冰及按名去配料;特调用官方 key 核价,去配料后营养未知。 - 门店及当次菜单搜索、显式选店、小食×饮料最多 12 份枚举;不沿用缺货候选。 - 能量/蛋白质条件、四维取舍、预算不足提示、本地保存偏好和动态 WorkBuddy 提示词。 - 会员积分券包、积分商品两阶段费用;到店/得来速/配送上下文和脱敏地址稳定引用。 - 真实联调四份去冰组合及一份少冰去酱,新增独立历史网页样例。 - 商品编码判断已识别薯条;复核时间与响应上限可配置,超限停止而非截断使用。 - 59 个 Python 回归、12,096 组跨语言规则一致性检查通过;新版浏览器验收未完成。 - 35 工具能力地图、WorkBuddy 市场元数据与 ZIP 打包脚本;市场尚未提交。 ## 0.2.1 — 2026-10-13 - 强化 CLI / JSON / 网页的来源提示;合成测试数据保存后仍明确标为合成。 - 增加可用券名称查询、候选匹配与显式按名称用券核价,默认不带券。 - 增加价格与钠、口味与钠两组非支配候选,保持硬性条件,不生成无依据的健康分数。 - 补充偏好矩阵、门店体验及数据边界的待办;37 个 Python 测试和 168 组决策一致性检查通过。 ## 0.2.0 — 2026-10-13 - 将“便宜单品排序”改为完整餐单检查;预算、口味排除、小食要求和自定义钠上限独立验证。 - 修复缺失营养被当成 0、可乐/薯条被标为“营养均衡”的错误。 - 真实联调官方菜单、套餐详情、营养与六次整单核价,替换来源无法核验的旧 fixture。 - 增加免 Token 的响应式网页、三角色意见、选择撤销和带快照说明的餐单导出。 - 明确区分规则审阅器、AI 客户端角色协作、历史快照与实时核价。 - 移除未实现的安全下单、全菜单最优、自动过期提醒等宣传和无依据的竞品比较。 - 恢复官方参赛声明原文,补占位符配置、回归测试和 CI。 - 本次修订由 Codex 辅助完成;保留历史 WorkBuddy 记录的出处边界。CONTEST_DECLARATION.md1.5 KB136e76f31603upstream ↗
# 参赛声明 ## 原创性声明 参赛项目系原创开发,或已取得合法授权使用相关代码、组件、素材或其他内容,不侵犯任何第三方的知识产权、隐私权、商业秘密或其他合法权益。 ## 合规声明 参赛项目及相关内容不存在违法犯罪、侵权、低俗、暴力、歧视、赌博等情形,不包含恶意代码、病毒、后门程序、钓鱼链接或其他可能危害网络安全的内容,亦不含有指向违规站点的外部链接。 ## 信息安全声明 参赛项目及提交材料中不包含真实 Token、密钥、密码、账号凭证,亦不包含他人个人信息,配置文件仅使用环境变量占位符。 ## 使用提示 参赛项目为麦当劳程序员节创意开发大赛参赛作品,由参赛者独立开发,非麦当劳官方产品。项目输出仅供参考,不构成医疗、营养或其他专业建议;餐品信息、价格及供应状态以麦当劳官方渠道的实时结果为准。 ## 规则遵守声明 本人已仔细阅读并同意遵守《麦当劳程序员节创意开发大赛活动规则》,理解并接受其中关于参赛资格、项目要求、评审机制、奖品发放、个人信息处理及违规处理的相关规定。 如本人违反活动规则,或上述声明内容不真实,本人同意麦当劳有权在不事先通知的情况下取消本人的参赛资格、获奖资格及相关奖励;如因此给麦当劳造成损失的,本人同意依法承担相应责任。CONTRIBUTING.md1.7 KB8524147e6cb1upstream ↗
# Contributing 欢迎提 Issue 和 PR。这个项目有两条硬规矩:**金额必须能核**、**数据缺就说未知**。 ## 提 Issue 麻烦写清三件事,否则很难复现: 1. **你输入了什么**(完整命令,或网页上的条件组合) 2. **你期望什么** 3. **实际出现什么**(报错原文 / 截图) > 不要附 Token、账号信息或原始优惠券数据。Token 只放在本机环境变量 `MCD_MCP_TOKEN` 里,永远不会进入仓库或对话。 ## 提 PR 提交前本地跑一遍(和 CI 相同的四步): ```bash python3 -B -m unittest discover -s tests -v # 178 项 Python 回归 node tests/parity.cjs # Python/JS 决策一致性(24,192 组) python3 -B demo.py --budget 35 --taste spicy # CLI 冒烟 python3 -B scripts/build_demo.py && git diff --exit-code -- docs/data.js ``` 几个容易踩的点: - **改了 `fixtures/` 或产出结构** → 必须重跑 `scripts/build_demo.py` 并提交 `docs/data.js`,否则 CI 的 `git diff --exit-code` 会失败。 - **改了比较/筛选逻辑** → Python 和 `docs/review.js` 要同步改,`parity.cjs` 会逐字比对两边输出。 - **改了网页样式** → `docs/style.css` 的版本号(`index.html` 里的 `?v=`)要一起改,否则回访用户会看到旧样式。 - **不要写死门店或商品编码**:核价必须来自当次 `calculate-price` 返回的整数分。 ## 发布流程 版本号在 `gourmet/__init__.py`;更新 `CHANGELOG.md` 后打 tag 并构建分发包: ```bash python3 scripts/build_skill_package.py --output /tmp/mcd-agent-gourmet.zip ``` `main` 分支受保护:合并前必须通过 `verify` 检查,禁止强推与删除。demo.py25 KB0cc95e0f6884upstream ↗
#!/usr/bin/env python3 """Offline snapshot by default; --live requests official MCP pricing explicitly.""" import argparse import json import sys from pathlib import Path from gourmet.core import default_pick, money_to_cents, order_card, pick_reason, review, select_plan from gourmet.live import available_basket, fetch_basket_snapshot, fetch_snapshot, last_order_spec, store_coupons, coupon_choices from gourmet.mcp import Client, MCPError from gourmet.member import member_overview, points_products, points_detail, my_coupons from gourmet.coupons import coupon_report, render_coupon_report from gourmet.stores import search_stores, address_choices, delivery_stores from gourmet.channels import order_context ROOT = Path(__file__).resolve().parent def main(argv=None): parser = argparse.ArgumentParser(description='麦门点餐议事会:完整餐单,透明取舍。') parser.add_argument('--budget', default='35', help='预算(元),默认 35') parser.add_argument('--taste', choices=['any', 'spicy', 'no-spicy'], default='any') parser.add_argument('--side', choices=['any', 'fries'], default='any') parser.add_argument('--sodium-limit', type=int, help='自定义钠上限 mg;不是医学建议') parser.add_argument('--live', action='store_true', help='使用本机 MCD_MCP_TOKEN 实时核价') parser.add_argument('--plan', type=Path, default=None) parser.add_argument('--input', type=Path, help='读取标准化报价 JSON') parser.add_argument('--json', action='store_true', help='输出结构化审阅结果') parser.add_argument('--compare', action='store_true', help='展开全部候选与「为什么不是它」;默认只给一个答案 + 点单卡') parser.add_argument('--any', action='store_true', help='不设预算:只按其他硬性条件给候选,并明确标注"未设预算"') parser.add_argument('--last', action='store_true', help='跟上次一样:读取最近一次订单的商品,按当次价重新核价(需 --live;找不到历史则退回默认)') parser.add_argument('--save-snapshot', type=Path, help='保存脱敏报价,不存 Token 或券 ID') parser.add_argument('--select', help='明确选择候选 ID,只记录选择,不下单') parser.add_argument('--list-tools', action='store_true', help='读取允许的 MCP 工具参数') parser.add_argument('--list-coupons', action='store_true', help='联网查询计划门店可用券,只显示名称与匹配候选,不输出券 ID') parser.add_argument('--coupon-title', help='使用查询返回的完整券名称;须与 --live、--coupon-candidate 配合') parser.add_argument('--coupon-candidate', help='只对这个候选 ID 提交用券核价,不改变其他候选') parser.add_argument('--member', action='store_true', help='显式查询自己的积分、券包和待领取券;不保存个人结果') parser.add_argument('--my-coupons', action='store_true', help='只读我的券:日期、时段、官方状态;可选门店与渠道核验') parser.add_argument('--coupon-demo', action='store_true', help='不联网,体验明确标记的构造券包样例') parser.add_argument('--points-products', action='store_true', help='查询积分餐品券目录,不兑换') parser.add_argument('--points-detail', type=int, metavar='SPU_ID', help='查询所选积分商品的 SKU 和两阶段费用,不兑换') parser.add_argument('--sugar', choices=['any','zero','regular'], default='any', help='饮料:不限 / 无糖 / 含糖;未知不满足指定条件') parser.add_argument('--energy-limit', type=int, help='个人能量上限 kcal,不是健康评分') parser.add_argument('--protein-min', type=int, help='个人蛋白质下限 g') parser.add_argument('--freshness-minutes', type=int, default=15, help='本工具实时核价复核窗口 1~60 分钟') parser.add_argument('--max-response-mb', type=int, default=8, choices=range(1,65), metavar='1..64', help='MCP 响应大小上限 MB,超限停止而非截断使用') parser.add_argument('--search-stores', action='store_true', help='按城市和地点关键词查门店') parser.add_argument('--city', help='门店搜索城市') parser.add_argument('--keyword', help='地点关键词,例如地铁站或商场') parser.add_argument('--store-code', help='从搜索结果中选定的门店编码;不自动定位') parser.add_argument('--expand', action='store_true', help='实时枚举选定套餐的已识别小食×饮料组合,逐份核价,最多12份') parser.add_argument('--ice', choices=['any','standard','less','none','extra'], default='any', help='饮料冰量:不限/标准/少冰/去冰/多冰;实时按官方特调核价') parser.add_argument('--people', type=int, default=1, help='就餐人数 1~8:主食与饮料各 ≥ 人数、小食 ≥ 1') parser.add_argument('--budget-scope', choices=['total','per_person'], default='total', help='预算口径:整单(默认)或人均') parser.add_argument('--baseline-plan', help='把本店某份候选当作"你本来会点的",用于计算省了多少钱') parser.add_argument('--baseline-cents', type=int, metavar='分', help='外部真实核价基线(整数分),必须与 --baseline-store 同店') parser.add_argument('--baseline-store', help='基线报价所属门店编码,必须与本轮门店一致') parser.add_argument('--baseline-label', help='基线的可读说明,例如「单品直选:巨无霸+薯条+可乐」') parser.add_argument('--omit', action='append', default=[], metavar='配料原名', help='去除主食配料,可重复;必须匹配官方特调名称,营养数据改为未知') parser.add_argument('--channel', choices=['pickup','drive','delivery'], default='pickup', help='到店 / 得来速 / 麦乐送;菜单、券和核价使用同一场景') parser.add_argument('--business-code', help='得来速或配送门店返回的 beCode,必须与 storeCode 配对') parser.add_argument('--addresses', action='store_true', help='显式查询脱敏地址引用,不输出姓名电话门牌') parser.add_argument('--delivery-stores', action='store_true', help='查询所选地址可配送的门店') parser.add_argument('--address-ref', help='从 --addresses 选择的稳定地址引用') parser.add_argument('--list-meals', action='store_true', help='查看所选门店当次菜单名称与编码') parser.add_argument('--meal-code', action='append', default=[], help='从当次菜单选择的套餐编码,可重复;替代样例套餐,不沿用原模板属性') args = parser.parse_args(argv) try: def client(): return Client(max_response_bytes=args.max_response_mb * 1_000_000) if (args.city or args.keyword) and not args.search_stores: raise ValueError('--city 和 --keyword 仅用于 --search-stores。') if args.store_code and not (args.live or args.list_coupons or args.list_meals or args.my_coupons): raise ValueError('切换门店必须实时核价或查询门店券;历史快照不能改门店。') if args.expand and not args.live: raise ValueError('--expand 必须与 --live 一起使用,每份组合重新官方核价。') if args.omit and not args.live: raise ValueError('去配料必须实时读取特调并核价,不修改历史营养值。') actions = [args.addresses, args.delivery_stores, args.search_stores, args.member, args.my_coupons, args.coupon_demo, args.points_products, args.points_detail is not None, args.list_coupons, args.list_tools, args.list_meals] if sum(actions) > 1 or (any(actions) and (args.live or args.input or args.select or args.save_snapshot or args.coupon_title or args.coupon_candidate)): raise ValueError('独立查询入口不能相互混用,也不能与核价、快照保存或选择混用。') if args.address_ref is not None and not args.delivery_stores: raise ValueError('--address-ref 仅与 --delivery-stores 配合。') if args.business_code and not (args.live or args.list_coupons or args.list_meals or args.my_coupons): raise ValueError('--business-code 仅用于所选场景的实时核价或券查询。') if args.channel != 'pickup' and not (args.live or args.list_coupons or args.list_meals or args.my_coupons or args.search_stores or args.points_detail or args.addresses or args.delivery_stores): raise ValueError('不能把历史到店快照改成其他渠道;请使用相应实时查询入口。') if args.my_coupons or args.coupon_demo: if args.plan or args.meal_code or args.ice != 'any': raise ValueError('券包清点不修改餐单;门店核验请传 --store-code 和对应渠道,核价另行执行。') if args.coupon_demo: example = json.loads((ROOT / 'fixtures/coupon-demo.json').read_text(encoding='utf-8')) value = coupon_report(example['coupons'], len(example['coupons']), True, now=example['reference_time'], mode='synthetic') else: if not args.store_code and (args.business_code or args.channel != 'pickup'): raise ValueError('按渠道核验券须提供所选门店;得来速/配送还须提供同次查询的业务编码。') context = {'store':{'code':args.store_code}, 'fulfillment':{'channel':args.channel, 'be_code':args.business_code}} if args.store_code else None value = my_coupons(client(), context) print(json.dumps(value, ensure_ascii=False, indent=2) if args.json else render_coupon_report(value)) return 0 if args.addresses or args.delivery_stores: value = address_choices(client()) if args.addresses else delivery_stores(client(), args.address_ref) print(json.dumps(value, ensure_ascii=False, indent=2)) return 0 if args.search_stores: value = search_stores(client(), args.city, args.keyword, args.channel) print(json.dumps(value, ensure_ascii=False, indent=2)) return 0 if args.member or args.points_products or args.points_detail is not None: connection = client() value = member_overview(connection) if args.member else points_products(connection) if args.points_products else points_detail(connection, args.points_detail) if args.points_detail is not None: value['requested_channel'] = args.channel print(json.dumps(value, ensure_ascii=False, indent=2)) return 0 if not 1 <= args.freshness_minutes <= 60 or any(v is not None and v <= 0 for v in (args.sodium_limit,args.energy_limit,args.protein_min)): raise ValueError('营养条件须为正整数,复核窗口须为 1~60 分钟。') budget = None if args.any else money_to_cents(args.budget) plan = None if args.live or args.list_coupons or args.list_meals: if not args.store_code and args.plan is None: raise ValueError('请先选门店:--search-stores --city 城市 --keyword 地点,然后用 --store-code 编码;或显式指定已确认的 --plan 文件。') plan = json.loads(args.plan.read_text(encoding='utf-8')) if args.plan else {'candidates': []} if args.store_code: if not args.store_code.isdigit(): raise ValueError('门店编码必须是数字字符串。') if args.plan and plan.get('store', {}).get('code') != args.store_code and plan.get('candidates') and not args.meal_code: raise ValueError('切换计划门店时请重新用 --meal-code 选择当前菜单餐品;不继承旧门店的餐品或优惠券。') if args.plan and plan.get('store', {}).get('code') != args.store_code and plan.get('fulfillment', {}).get('channel', 'pickup') != 'pickup' and (args.channel == 'pickup' or not args.business_code): raise ValueError('切换配送或得来速门店时必须重新明确 --channel 和成对 --business-code,不沿用旧业务编码。') plan['store'] = {'code':args.store_code, 'name':'所选门店 '+args.store_code} if args.channel != 'pickup' or args.business_code: plan['fulfillment'] = {'channel':args.channel, 'be_code':args.business_code} if args.last: connection = client() spec = last_order_spec(connection.call('order-list', {}), plan) if spec is None: print('没有可复用的历史订单,按默认来。', file=sys.stderr) else: # 先确认"上次那家店"现在可用(菜单拉得到=在营业);不可用就明说并退回默认。 try: connection.call('query-meals', dict(order_context(spec))) except MCPError as exc: print('上次那家店(%s)现在不可用:%s' % (spec['store'].get('name', ''), exc), file=sys.stderr) print('已退回按当前条件给默认方案。', file=sys.stderr) spec = None if spec is not None: kept, dropped = available_basket(connection, spec, spec['candidates']) for entry in dropped: print('上次的「%s」现在不可用:%s(已去掉,不替换)' % (entry['item'].get('product_name') or entry['item']['product_code'], entry['reason']), file=sys.stderr) if not kept: print('上次的商品现在都不可用,已退回按当前条件给默认方案。', file=sys.stderr) spec = None else: spec['candidates'] = kept if spec is not None: basket = fetch_basket_snapshot(connection, spec, spec['candidates'], label='跟上次一样') print('跟上次一样(%s 于 %s)' % (spec['source_order']['created_at'], spec['source_order']['store_name'])) for index, item in enumerate(basket['items'], 1): print(' %d. %s ×%d' % (index, item.get('product_name') or item['product_code'], item.get('quantity', 1))) ways = basket['quote']['take_way_options'] or [] way = ways[0]['title'] if ways and isinstance(ways[0], dict) else '到店自取' print(' 当次核价:¥%.2f(%s · %s)' % (basket['quote']['price_cents'] / 100, basket['store'].get('name', ''), way)) if basket['nutrition_missing']: print(' 营养数据缺失:%s(按未知处理,不当作 0)' % '、'.join(basket['nutrition_missing'])) print(' %s' % spec['source_order']['note']) print(' 这是记录,不是下单;没有支付。') return 0 if args.meal_code: if len(set(args.meal_code)) != len(args.meal_code) or len(args.meal_code) > 3 or not all(c.isdigit() for c in args.meal_code): raise ValueError('请选择最多 3 个不同的有效套餐编码。') plan['candidates'] = [{'id':code,'product_code':code,'label':'当次默认搭配'} for code in args.meal_code] if args.live and not plan.get('candidates'): raise ValueError('请先用 --list-meals --store-code 编码 查看当次菜单,再用 --meal-code 选择套餐;不自动套用演示餐品。') print('本轮所选门店:%s(%s)' % (plan['store']['name'], plan['store']['code']), file=sys.stderr) if args.meal_code and not (args.live or args.list_coupons): raise ValueError('--meal-code 只用于实时核价或匹配门店券。') if args.list_meals: menu = client().call('query-meals', order_context(plan)) if not isinstance(menu, dict) or not isinstance(menu.get('meals'), dict): raise MCPError('无法识别菜单。') print(json.dumps({'meals':[{'code':code,'name':item.get('name')} for code,item in menu['meals'].items()], 'notice':'从当次菜单选择套餐编码 --meal-code;目前程序只构造三轮完整套餐。单品和其他结构不会当作完整餐单。'}, ensure_ascii=False, indent=2)) return 0 if args.live and args.input: raise ValueError('--live 与 --input 不能同时使用。') if (args.coupon_title or args.coupon_candidate) and not args.live: raise ValueError('用券仅支持显式 --live,不修改历史快照。') if args.list_coupons and (args.live or args.input or args.select or args.save_snapshot or args.list_tools): raise ValueError('--list-coupons 是独立查询,请不要与核价、选择或保存操作混用。') if args.list_coupons: choices = coupon_choices(store_coupons(client(), plan), plan) print(json.dumps({'store': plan['store'], 'coupons': choices, 'notice': '仅查询,不领券。candidate_ids 为空表示当前候选不支持该券;不会自动用券。'}, ensure_ascii=False, indent=2)) return 0 if args.list_tools: print(json.dumps(client().tools(), ensure_ascii=False, indent=2)) return 0 if args.live: print('模式:本轮官方实时核价。', file=sys.stderr) data = fetch_snapshot(client(), plan, coupon_title=args.coupon_title, coupon_candidate=args.coupon_candidate, expand=args.expand, ice=args.ice, omit=args.omit) else: data = json.loads((args.input or ROOT / 'fixtures/snapshot.json').read_text(encoding='utf-8')) result = review(data, budget, args.taste, args.sodium_limit, args.side, sugar=args.sugar, energy_limit=args.energy_limit, protein_min=args.protein_min, freshness_minutes=args.freshness_minutes, ice=args.ice, people=args.people, budget_scope=args.budget_scope, baseline_plan_id=args.baseline_plan, baseline_cents=args.baseline_cents, baseline_label=args.baseline_label, baseline_store_code=args.baseline_store) print(result['data_notice'], file=sys.stderr) print('门店:%s;数据时间:%s' % (data['store']['name'], data['captured_at']), file=sys.stderr) if args.save_snapshot: args.save_snapshot.write_text(json.dumps(dict(data, mode='synthetic' if data['mode'] == 'synthetic' else 'snapshot'), ensure_ascii=False, indent=2) + '\n', encoding='utf-8') if args.select: result['selected'] = select_plan(result, args.select, result['review_id']) if args.json: print(json.dumps(result, ensure_ascii=False, indent=2)) else: label = result['data_notice'] print('麦门点餐议事会 | ' + label) print(data['store']['name'], '|', data['captured_at']) req = result['requirements'] if req['budget_unspecified']: scope = ('%d 人,主食与饮料各 ≥ %d 份、小食 ≥ 1 份' % (req['people'], req['people'])) if req['people'] > 1 else '主食+小食+饮料各至少一份' print('需求:%s;未设预算(只按其他硬性条件筛)' % scope) elif req['people'] > 1: if req['budget_scope'] == 'per_person': print('需求:%d 人;主食与饮料各 ≥ %d 份、小食 ≥ 1 份;人均预算 ¥%.2f(整单上限 ¥%.2f)' % (req['people'], req['people'], budget / 100, req['effective_budget_cents'] / 100)) else: print('需求:%d 人;主食与饮料各 ≥ %d 份、小食 ≥ 1 份;整单预算 ¥%.2f' % (req['people'], req['people'], budget / 100)) else: print('需求:主食+小食+饮料各至少一份;预算 ¥%.2f' % (budget / 100)) pick = default_pick(result) if pick: ways = pick.get('take_way_options') or [] way = ways[0].get('title') if ways and isinstance(ways[0], dict) else '到店自取' print('\n这顿就它了 👇\n') print(' %s · %s' % (pick['name'], pick['variant'])) print(' ¥%.2f %s · %s' % (pick['price_cents'] / 100, result['store']['name'], way)) print(' %s' % pick_reason(result, pick['id'])) if pick.get('display_price_note'): print(' ⚠️ %s' % pick['display_price_note']) print('\n点单卡(可复制)') for line in order_card(result, pick['id'])['lines']: print(' ' + line) print('\n想看别的:加 --compare 展开全部候选与「为什么不是它」。') if args.compare: for p in result['plans']: print('\n[%s] %s · %s:¥%.2f' % (p['id'], p['name'], p['variant'], p['price_cents'] / 100)) print(' ' + ' + '.join(i['name'] + ('('+ '、'.join(i['customizations'])+')' if i.get('customizations') else '') for i in p['items'])) print(' 钠:%s;能量:%s' % (str(p['sodium_mg']) + ' mg' if p['sodium_mg'] is not None else '未知', str(p['energy_kcal']) + ' kcal' if p['energy_kcal'] is not None else '未知')) if result['requirements']['people'] > 1: print(' 人均 ¥%.2f(%d 人)|结构:%s' % (p['per_person_cents'] / 100, result['requirements']['people'], p['structure'])) if result.get('baseline') and 'saving_cents' in p: if p['saving_cents'] > 0: print(' 比「%s」省 ¥%.2f(%.1f%%)' % (result['baseline']['label'], p['saving_cents'] / 100, p['saving_percent'])) elif p['saving_cents'] < 0: print(' 比「%s」贵 ¥%.2f' % (result['baseline']['label'], -p['saving_cents'] / 100)) else: print(' 就是基线本身') print(' ' + ('符合当前硬性条件' if p['eligible'] else ';'.join(p['reasons']))) print('\n三个规则审阅器:', result['reviewers']) print('价格与钠的取舍候选:', ', '.join(result['tradeoffs']['price_sodium']) or '无') print('口味与钠的取舍候选:', ', '.join(result['tradeoffs']['taste_sodium']) or '无') print('多维营养取舍候选:', ', '.join(result['tradeoffs']['nutrition_multi']) or '无') reasons = result.get('tradeoff_reasons', {}) axis_titles = {'price_sodium': '价格与钠', 'taste_sodium': '口味与钠', 'nutrition_multi': '多维营养'} shown = 0 for name in ('price_sodium', 'taste_sodium', 'nutrition_multi'): for plan_id, info in (reasons.get(name, {}).get('dominated') or {}).items(): if shown >= 4: break print(' 从「%s」角度看,为什么没有 %s:被 %s 比下去——%s' % (axis_titles[name], plan_id, info['by'], ';'.join(info['why']))) shown += 1 if result.get('best_saving'): print('最省的是 %s:比「%s」省 ¥%.2f(%.1f%%)' % (result['best_saving']['plan_id'], result['baseline']['label'], result['best_saving']['saving_cents'] / 100, result['best_saving']['saving_percent'])) if not pick: ladder = result.get('budget_ladder') or {} if ladder.get('steps'): print('\n预算阶梯(只调整预算,不放宽口味、小食、饮料或营养条件):') for step in ladder['steps']: print(' 预算 ¥%.2f → 可选 %d 份(比现在多 ¥%.2f)' % (step['budget_cents'] / 100, step['candidates'], step['increase_cents'] / 100)) print(result['next_step']) elif args.compare: print(result['next_step']) if not pick or args.compare: print('观点存在差异,由你选择。' if result['disagreement'] else '当前条件下意见一致,或没有可选方案。') if result['selected']: print('已记录餐单选择;未创建订单,未支付。') print('仅比较上述候选,不保证全菜单最低价;不构成营养或医疗建议。') return 0 except (MCPError, ValueError, KeyError, OSError) as exc: print('本轮停止:' + str(exc), file=sys.stderr) return 2 if __name__ == '__main__': raise SystemExit(main())DESIGN.md1.3 KB6461855dc95cupstream ↗
# 点餐议事会视觉约定 场景:午餐前,用户在明亮桌面或手机上比较餐单,需要马上看清条件与差异。 - 白底;深墨色文字;玫红主动作。比较标签靠文字而非颜色识别。 - 系统中文无衬线字体;首屏标题桌面 58px、手机 34px,正文 14–17px;数字使用 tabular-nums。 - 桌面用对比表,手机将每行重排成完整餐单。不存在横向隐藏的关键操作。 - 标准输入/select/checkbox;清晰 focus;触摸按钮至少 44px。 - 只有交互状态的短过渡,尊重 prefers-reduced-motion;没有假加载/假思考动画。 - 显著区分真实历史快照与当下交易价格。复制/下载结果继续携带来源说明。 ## 2026-10-14 消费流程整理 首屏说明用途与历史报价边界,按门店、条件、餐单展示。默认口味不限,有糖与无糖平等呈现;营养条件、营养比较与比较规则解释渐进展开。手机条件单列;关键按钮至少 44px。样例名称、份数、冰量说明从公共数据生成。2026-10-14 已在部署后的公开 Pages 上用隔离的 Chrome 完成 1440px、390px、320px 视觉与关键点击验收;窄屏“重置条件”换行已修复。截图和交互结果见 [验证记录](docs/validation.md#网页视觉与交互验收2026-10-14)。这不等于实体手机或辅助技术验收。docs/app.js14 KB6b7d1637239bupstream ↗
'use strict'; const $=id=>document.getElementById(id);let data=window.GOURMET_DATA; const names={budget:'价格较低',nutrition:'钠含量较少',taste:'更贴近口味'}; const escape=s=>String(s).replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c])); const money=c=>(c/100).toFixed(2);let result,selected=null,lastCard=null; function itemName(i){return i.name+(i.customizations?.length?'('+i.customizations.join('、')+')':'');} function requirements(){return {budget_cents:Math.round(Number($('budget').value)*100),taste:$('taste').value,sodium_limit:$('sodium').value?Number($('sodium').value):null,side:$('fries').checked?'fries':'any',sugar:$('controls').elements.namedItem('sugar').value,ice:$('ice').value,energy_limit:$('energy').value?Number($('energy').value):null,protein_min:$('protein').value?Number($('protein').value):null};} function render(){ const hadSelection=selected!==null;selected=null;$('receipt').hidden=true; $('selection-message').textContent=hadSelection?'条件已改变,之前的选择已清除。请重新确认餐单。':''; updatePrompt(); const valid=$('controls').checkValidity();$('budget-error').textContent=valid?'':'请检查预算(1~10000 元、最多两位小数)和营养条件(正整数或留空)。'; if(!valid){$('answer').hidden=true;$('plans').replaceChildren();$('voices').replaceChildren();$('tradeoffs').replaceChildren();$('result-summary').textContent='先填一个有效预算';$('consensus').textContent='';$('empty-message').hidden=true;result=null;return;} result=GourmetReview.evaluate(data,requirements()); renderAnswer(); const picks=result.tradeoffs[$('strategy').value].map(id=>result.plans.find(p=>p.id===id)); $('tradeoffs').innerHTML=picks.length?picks.map(p=>`<p><strong>${escape(p.name)} · ${escape(p.variant)}</strong><br>¥${money(p.price_cents)} · 钠 ${p.sodium_mg} mg · ${p.energy_kcal??'未知'} kcal · 蛋白质 ${p.protein_g??'未知'} g${result.requirements.taste==='spicy'?' · '+(p.taste_match?'保留辣味偏好':'无辣味,供取舍'):''}</p>`).join(''):'<p>当前没有满足条件且钠数据完整的取舍候选。</p>'; document.querySelectorAll('[data-budget]').forEach(b=>b.setAttribute('aria-pressed',Number(b.dataset.budget)*100===result.requirements.budget_cents?'true':'false')); $('result-summary').textContent=`${data.plans.length} 份完整餐单 · ${result.feasible_count} 份符合当前硬性条件`; $('empty-message').hidden=!!result.feasible_count; $('empty-message').textContent=result.next_step; $('consensus').textContent=!result.feasible_count?'没有符合当前条件的餐单,可调整条件后再比较。':result.disagreement?'不同标准选出了不同餐单,看看哪种取舍更适合你。':'多个标准选中了同一餐单,你仍然可以比较后再选。'; $('voices').innerHTML=Object.entries(names).map(([role,name])=>{const p=result.plans.find(x=>x.id===result.reviewers[role]);const desc=!p?'没有符合条件且数据足够的餐单':role==='budget'?`当次核价 ¥${money(p.price_cents)},在可选餐单中价格较低。`:role==='nutrition'?`钠 ${p.sodium_mg} mg,在可选且数据完整的餐单中较少。`:result.requirements.taste==='spicy'&&!p.spicy?'没有满足其他条件的辣味餐单;这个是备选。':result.requirements.taste==='spicy'?'这份保留了你想吃的辣味。':'这份符合口味条件,同等情况下先看价格。';return `<div class="voice"><span class="avatar" aria-hidden="true">${role==='budget'?'省':role==='nutrition'?'钠':'味'}</span><div><h3>${name}</h3><p>${p?escape(p.name):'先不推荐'}</p><small>${p?escape(p.variant)+' · ':''}${escape(desc)}</small></div></div>`}).join(''); $('plans').innerHTML=result.plans.filter(p=>p.eligible||$('show-excluded').checked||!result.feasible_count).map(p=>{let tags=Object.entries(result.reviewers).filter(([,id])=>id===p.id).map(([role])=>`<span class="role-tag">${names[role]}</span>`).join('');let note=p.reasons.join(';')||(result.requirements.taste==='spicy'&&!p.spicy?'符合硬性条件,但没有辣味。':'符合硬性条件。'+(p.spicy?'保留辣味。':'非辣味模板。'));if(p.sodium_mg==null)note+=' 营养数据不完整。';return `<tr class="${p.eligible?'':'blocked'}" data-plan="${p.id}"><td><span class="meal-title">${escape(p.name)}</span><span class="meal-items">${p.items.map(i=>escape(itemName(i))).join(' + ')}</span>${tags}</td><td><span class="price">¥${money(p.price_cents)}</span><span class="remaining">${p.remaining_cents>=0?'余量 ¥'+money(p.remaining_cents):'超出 ¥'+money(-p.remaining_cents)}</span></td><td class="nutrients">${p.sodium_mg==null?'钠未知':p.sodium_mg+' mg 钠'}<small>${p.energy_kcal==null?'能量未知':p.energy_kcal+' kcal'} · ${p.protein_g==null?'蛋白质未知':p.protein_g+' g 蛋白质'}</small></td><td class="tradeoff">${escape(note)}</td><td><button type="button" class="choose" data-select="${p.id}" aria-pressed="false" aria-label="选择 ${escape(p.name+' '+p.variant)}" ${p.eligible?'':'disabled'}>${p.eligible?'选这份':'不符合条件'}</button></td></tr>`}).join(''); } function renderAnswer(){ const pick=GourmetReview.defaultPick(result); const section=$('answer'); if(!pick){section.hidden=true;lastCard=null;return;} section.hidden=false; $('answer-meal').textContent=pick.name+' · '+pick.variant; $('answer-price').textContent='¥'+money(pick.price_cents); const ways=Array.isArray(pick.take_way_options)?pick.take_way_options:[]; const way=ways.length&&ways[0]&&ways[0].title?ways[0].title:'到店自取'; $('answer-store').textContent=' '+data.store.name+' · '+way; $('answer-reason').textContent=GourmetReview.pickReason(result,pick.id); lastCard=GourmetReview.orderCard(result,pick.id); $('card-lines').innerHTML=lastCard.lines.map(line=>`<p>${escape(line)}</p>`).join(''); $('card').hidden=true;$('card-message').textContent=''; } $('go-card').addEventListener('click',()=>{$('card').hidden=false;$('card').scrollIntoView({behavior:matchMedia('(prefers-reduced-motion: reduce)').matches?'instant':'smooth',block:'nearest'});$('card').focus({preventScroll:true});}); $('copy-card').addEventListener('click',async()=>{if(!lastCard)return;try{await navigator.clipboard.writeText(lastCard.text);$('card-message').textContent='已复制,照着在 App 或柜台点即可。';}catch{$('card-message').textContent='请手动选择并复制上方文字。';}}); $('open-compare').addEventListener('click',()=>{const box=$('compare-wrap');if(!box)return;box.open=true;box.scrollIntoView({behavior:matchMedia('(prefers-reduced-motion: reduce)').matches?'instant':'smooth',block:'start'});}); $('change-conditions').addEventListener('click',()=>$('budget').focus()); $('strategy').addEventListener('change',render); $('show-excluded').addEventListener('change',render); $('handoff-location').addEventListener('input',updatePrompt); $('handoff-channel').addEventListener('change',updatePrompt); $('remember').addEventListener('click',()=>{if(!$('controls').checkValidity())return;try{localStorage.setItem('gourmet.preferences.v1',JSON.stringify(requirements()));$('preference-message').textContent='已保存在这个浏览器。只记条件,不保存账号、位置或餐单选择。';}catch{$('preference-message').textContent='浏览器未允许保存;本次仍可正常使用。';}}); $('forget').addEventListener('click',()=>{try{localStorage.removeItem('gourmet.preferences.v1');$('preference-message').textContent='已清除保存的条件。';}catch{$('preference-message').textContent='浏览器未允许访问保存区。';}}); function updatePrompt(){const q=requirements();if(!$('controls').checkValidity()){ $('workbuddy-prompt').textContent='请先填写有效的预算和选填营养条件。';return '';} const text=`请用麦门点餐议事会帮我选一顿完整的麦当劳:预算 ${money(q.budget_cents)} 元,主食、小食、饮料各一份。口味:${$('taste').selectedOptions[0].text};饮料:${{regular:'有糖可乐(必须满足)',zero:'无糖可乐(必须满足)',any:'有糖或无糖可乐都可以'}[q.sugar]};冰量:${$('ice').selectedOptions[0].text};${q.side==='fries'?'必须有薯条。':''}${q.sodium_limit?'钠上限 '+q.sodium_limit+' mg。':''}${q.energy_limit?'能量上限 '+q.energy_limit+' kcal。':''}${q.protein_min?'蛋白质至少 '+q.protein_min+' g。':''} 查询位置:${$('handoff-location').value.trim() || '尚未指定,请补问城市和附近地标或配送地址'}。\n使用方式:${$('handoff-channel').selectedOptions[0].text}。先确认位置或配送地址,让我选择门店;按当前菜单和官方特调重新核价,最多比较 12 份。若我想用券或积分,请区分兑换与本次渠道适用性。未知条件请说清,不要自动放宽。只查询,不领券、不兑换、不下单。`; $('workbuddy-prompt').textContent=text;return text;} $('copy-prompt').addEventListener('click',async()=>{const text=updatePrompt();if(!text)return;try{await navigator.clipboard.writeText(text);$('prompt-message').textContent='已复制,粘贴到安装本 Skill 的 WorkBuddy 对话即可。';}catch{$('prompt-message').textContent='请手动选择并复制上方文字。';}}); try{const saved=JSON.parse(localStorage.getItem('gourmet.preferences.v1')||'null');if(saved&&typeof saved==='object'){ if(Number.isInteger(saved.budget_cents)&&saved.budget_cents>=100&&saved.budget_cents<=1000000)$('budget').value=saved.budget_cents/100; if(['any','spicy','no-spicy'].includes(saved.taste))$('taste').value=saved.taste; if(['any','standard','less','none','extra'].includes(saved.ice))$('ice').value=saved.ice; if(['any','regular','zero'].includes(saved.sugar))$('controls').elements.namedItem('sugar').value=saved.sugar; if(Number.isInteger(saved.sodium_limit)&&saved.sodium_limit>0)$('sodium').value=saved.sodium_limit; for(const [id,key] of [['energy','energy_limit'],['protein','protein_min']])if(Number.isInteger(saved[key])&&saved[key]>0)$(id).value=saved[key]; $('fries').checked=saved.side==='fries';$('nutrition-options').open=!!($('sodium').value||$('energy').value||$('protein').value);$('preference-message').textContent='已载入你在这个浏览器保存的条件,可随时清除。'; }}catch{/* Storage is optional. */} $('controls').addEventListener('submit',e=>e.preventDefault());$('controls').addEventListener('input',render); $('controls').addEventListener('click',e=>{const b=e.target.closest('[data-budget]');if(b){$('budget').value=b.dataset.budget;render();}}); $('reset').addEventListener('click',()=>{$('controls').reset();render();}); $('plans').addEventListener('click',e=>{const b=e.target.closest('[data-select]');if(!b||b.disabled||!result)return;selected=result.plans.find(p=>p.id===b.dataset.select&&p.eligible);if(!selected)return;document.querySelectorAll('[data-select]').forEach(x=>{const active=x.dataset.select===selected.id;x.setAttribute('aria-pressed',String(active));x.textContent=x.disabled?'不符合条件':active?'已选择 ✓':'选这份';x.closest('tr').classList.toggle('is-selected',active);});$('receipt').hidden=false;$('chosen').textContent=selected.items.map(itemName).join(' + ')+` · 当次整单核价 ¥${money(selected.price_cents)}`;$('selection-message').textContent='已记录你的选择,未创建订单或支付。';$('receipt').focus({preventScroll:true});$('receipt').scrollIntoView({behavior:matchMedia('(prefers-reduced-motion: reduce)').matches?'instant':'smooth',block:'nearest'});}); $('copy').addEventListener('click',async()=>{if(!selected)return;const text=`麦门点餐议事会|历史快照演示,非当前报价\n${data.store.name}\n地址:${data.store.address ? (data.store.city ? data.store.city + " · " : "") + data.store.address : "未记录,请在官方渠道核对"}\n核价时间:${data.captured_at}\n${selected.items.map(itemName).join(' + ')}\n当次整单核价:¥${money(selected.price_cents)}\n钠:${selected.sodium_mg??'未知'} mg;能量:${selected.energy_kcal??'未知'} kcal\n已选餐单,未下单。当前购买须在官方渠道重新核价。\nhttps://polaris-smart.github.io/mcd-agent-gourmet/`;try{await navigator.clipboard.writeText(text);$('selection-message').textContent='餐单已复制,包含历史快照与未下单说明。';}catch{const a=document.createElement('a');a.href=URL.createObjectURL(new Blob([text],{type:'text/plain;charset=utf-8'}));a.download='麦门餐单-历史快照.txt';a.click();setTimeout(()=>URL.revokeObjectURL(a.href),1000);$('selection-message').textContent='浏览器未允许复制,已下载餐单文本。';}}); const iceLabels={standard:'标准冰',less:'少冰',none:'去冰',extra:'多冰'}; function renderSource(){ const drinks=data.plans.flatMap(p=>p.items.filter(i=>i.kind==='drink')); const iceValues=[...new Set(drinks.map(i=>iceLabels[i.ice]).filter(Boolean))]; $('ice-note').textContent=(iceValues.length?'这组历史样例已核验:'+iceValues.join('、')+'。':'这组历史样例未核验冰量。')+'其他冰量需要查询当前门店后确认。'; $('store-name').textContent=data.store.name; $('store-address').textContent='地址:'+(data.store.address?(data.store.city?data.store.city+' · ':'')+data.store.address:'这份历史样例未记录地址,请在官方渠道核对。'); $('store-address-source').textContent=data.store.address_checked_at?'地址来源:麦当劳 MCP 门店查询 · '+new Date(data.store.address_checked_at).toLocaleString('zh-CN',{timeZone:'Asia/Shanghai',hour12:false})+'(北京时间)核验;餐单报价时间另见下方。':''; $('data-notice').textContent='当前为历史快照演示,非实时价格。'+data.store.name+' · '+new Date(data.captured_at).toLocaleString('zh-CN',{timeZone:'Asia/Shanghai',hour12:false})+'(北京时间)。实时核价请使用 WorkBuddy 或本机 CLI。'; $('source').textContent=`${data.store.name} · ${new Date(data.captured_at).toLocaleString('zh-CN',{timeZone:'Asia/Shanghai',hour12:false})}(北京时间)核价 · 不含用券优惠`; } $('sample').addEventListener('change',()=>{data=window.GOURMET_SNAPSHOTS[Number($('sample').value)];renderSource();render();}); $('sample').replaceChildren(...window.GOURMET_SNAPSHOTS.map((snapshot,index)=>{ const option=document.createElement('option');option.value=String(index);option.textContent=snapshot.store.name+' · '+snapshot.plans.length+' 份餐单';return option; })); renderSource(); render();docs/assets/demo.mp41.8 MB7ddddf7c6d83upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
docs/assets/hero.png120 KB400078ed45fcupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
docs/assets/mark.svg326 B10e600e29054upstream ↗
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 40"><rect width="40" height="40" rx="12" fill="oklch(0.49 0.19 343)"/><g fill="white"><circle cx="13" cy="14" r="4"/><circle cx="27" cy="14" r="4"/><circle cx="20" cy="26" r="4"/></g><path d="M9 24a12 12 0 0 0 22 0" fill="none" stroke="white" stroke-width="1.5"/></svg>docs/assets/mobile.png79 KB86bd970f1ee2upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
docs/assets/order-card.png31 KB1852e3a73becupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
docs/assets/results.png136 KB315d209e4748upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
docs/assets/social.png81 KBadcf84a8b9a3upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
docs/backlog.md2.6 KB7ed33338c4f6upstream ↗
# 体验改进与边界 ## 0.3.1 已落地 - 原项目内新增“我的券”:独立券包查询、完整明细、临期/已结束分开、同名但不同日期分别保留。 - 北京时间解释已识别日期/星期/多段/跨午夜窗口,官方 enable 与本地时间解释分开,未知格式不猜测。 - 可选门店与渠道查询,成对券标识仅内存匹配;复用已有查询和核价流程,不扩大写操作权限。 - 全新构造样例可零 Token 运行,真实个人券包不纳入公开夹具。 ## 0.3.0 已落地 - P0:CLI、网页、导出标明历史与实时、门店和时间;合成输入不冒充官方数据。 - P1:默认无券,查询名称和匹配候选,内存解析券 ID;渠道参数一致;无适配券不强套。 - P2:连续营养值、个人能量/蛋白质条件、多维非支配候选;不编造健康评分。 - P2:当次菜单小食×饮料枚举,最多 12 份、逐份核价;可选择当次套餐,缺货停止。 - P3:门店/菜单搜索、无糖/含糖、四种冰量、按名去配料、偏好本地保存。 - P3:已核对商品编码替代薯条名称子串;复核窗口和响应大小可配置,超限报错。 - 新增:会员积分券包、积分商品费用分开展示、履约渠道上下文、脱敏地址稳定引用和市场包构建。 ## 实测边界 - 到店扩展、去冰、少冰+去酱已核价;详情确认四种冰量,但未逐一实测标准冰/多冰核价。 - 到店搜索、会员积分券包、积分目录已实测;配送/得来速未实测,不声称已验证。 - 无适配券不能验证折扣。积分未兑换,不能声称所有积分券支持外带或配送。 - 网页规则一致性及新版公开页面的桌面、手机视口视觉/点击验收已完成;实体手机触控和屏幕阅读器仍未验证,见 `docs/validation.md`。 ## 后续范围 - 全部杯型和份量自动枚举、儿童玩具、辣度分级、过敏原;早餐、多人餐、团餐。 - 多门店同条件比较,以及用户选定范围内随机推荐;不自动改店。 - 订单复购、活动和主题派对。MCP 存在不等于本项目已接通。 - 可靠健康评分需要更完整的数据和适用方法,目前不生成医学判断或个体营养目标。 - WorkBuddy 市场投稿暂缓;当前交付范围为本机导入和离线体验。未来若决定上架,再完成平台认证、提交和审核。 - 更广泛的优惠券文案格式、节假日例外以及券驱动的单品/早餐组合仍需明确支持和真实核价验证;不是看到早餐券就能自动构造早餐订单。docs/data.js26 KB984c5ee09d85upstream ↗
window.GOURMET_SNAPSHOTS = [ { "mode": "snapshot", "captured_at": "2026-10-17T02:14:12+08:00", "store": { "code": "已脱敏", "name": "南京某门店(名称与地址已脱敏)", "address": "(门店名与地址已脱敏)", "city": "南京" }, "source": { "tools": [ "query-nearby-stores", "query-meals", "query-meal-detail", "calculate-price", "now-time-info" ], "note": "2026-10-17 02:13(北京时间)通过官方 MCP 实时核价后存为历史快照。所有金额为 calculate-price 返回的整数分。" }, "scope": { "note": "同店同时刻对比「同样的东西:巨无霸 + 中薯条 + 中杯可乐」两条点法。套餐内饮料使用套餐内部 SKU(3050),单品直选用可单点 SKU(9900008751)——两者等价,但 3050 单独核价为 0 分,属套餐内部构成项。" }, "evidence": { "per_item_cents": { "1100": 2550, "4810": 1350, "9900008751": 950, "3050": 0 }, "single_pick_cents": 4850, "combo_cents": 3650, "note": "3050 单点核价为 0,说明它是套餐内部 SKU;基线篮子不能用它,否则会算出偏小的省钱数字。" }, "plans": [ { "id": "single-pick", "name": "单品直选", "variant": "巨无霸 + 中薯条 + 可乐(单点)", "store_code": "已脱敏", "items": [ { "code": "1100", "name": "巨无霸", "quantity": 1, "kind": "main", "spicy": false }, { "code": "4810", "name": "中薯条", "quantity": 1, "kind": "side", "is_fries": true }, { "code": "9900008751", "name": "可乐", "quantity": 1, "kind": "drink", "ice": "standard", "sugar": "regular" } ], "price_cents": 4850, "spicy": false, "nutrition_missing": true, "energy_kcal": null, "sodium_mg": null, "protein_g": null, "quote_tool": "calculate-price" }, { "id": "combo-3pc", "name": "巨无霸三件套", "variant": "套餐(巨无霸 + 中薯条 + 可乐中杯)", "store_code": "已脱敏", "items": [ { "code": "1100", "name": "巨无霸", "quantity": 1, "kind": "main", "spicy": false }, { "code": "4810", "name": "中薯条", "quantity": 1, "kind": "side", "is_fries": true }, { "code": "3050", "name": "可乐中杯", "quantity": 1, "kind": "drink", "ice": "standard", "sugar": "regular" } ], "price_cents": 3650, "spicy": false, "nutrition_missing": true, "energy_kcal": null, "sodium_mg": null, "protein_g": null, "quote_tool": "calculate-price" } ], "baseline_plan_id": "single-pick" }, { "mode": "snapshot", "fulfillment": { "channel": "pickup" }, "store": { "code": "已脱敏", "name": "南京某门店(名称与地址已脱敏)", "address": "(门店名与地址已脱敏)", "city": "南京", "address_checked_at": "2026-10-13T16:49:03.901391+00:00", "address_source": "(来源隐藏)" }, "captured_at": "2026-10-13T16:49:08+00:00", "source": "麦当劳官方 MCP:calculate-price + list-nutrition-foods", "scope": "仅比较本轮候选;不保证全菜单最低价;营养为标准份量的单品加总。", "plans": [ { "id": "9900005456-4810-3050", "name": "麦辣鸡腿汉堡三件套", "variant": "中薯条 · 可乐中杯", "store_code": "已脱敏", "items": [ { "code": "1440", "name": "麦辣鸡腿汉堡", "quantity": 1, "kind": "main", "customizations": [], "ice": null, "nutrition_customized": false, "is_fries": false, "sugar": null }, { "code": "4810", "name": "中薯条", "quantity": 1, "kind": "side", "customizations": [], "ice": null, "nutrition_customized": false, "is_fries": true, "sugar": null }, { "code": "3050", "name": "可乐中杯", "quantity": 1, "kind": "drink", "customizations": [ "去冰" ], "ice": "none", "nutrition_customized": false, "is_fries": false, "sugar": "regular" } ], "price_cents": 3350, "spicy": true, "nutrition_missing": [], "coupon_requested": false, "take_way_options": [ { "code": "eat-in", "title": "堂食", "subtitle": "店内用餐" }, { "code": "take-in-store", "title": "外带", "subtitle": "店内自提" } ], "fees_cents": { "deliveryPrice": null, "packingPrice": null, "tablewarePrice": null }, "coupon_applied": false, "quote_tool": "calculate-price", "energy_kcal": 921.0, "sodium_mg": 1373.0, "protein_g": 28.0 }, { "id": "9900005456-4810-3071", "name": "麦辣鸡腿汉堡三件套", "variant": "中薯条 · 无糖可口可乐中杯", "store_code": "已脱敏", "items": [ { "code": "1440", "name": "麦辣鸡腿汉堡", "quantity": 1, "kind": "main", "customizations": [], "ice": null, "nutrition_customized": false, "is_fries": false, "sugar": null }, { "code": "4810", "name": "中薯条", "quantity": 1, "kind": "side", "customizations": [], "ice": null, "nutrition_customized": false, "is_fries": true, "sugar": null }, { "code": "3071", "name": "无糖可口可乐中杯", "quantity": 1, "kind": "drink", "customizations": [ "去冰" ], "ice": "none", "nutrition_customized": false, "is_fries": false, "sugar": "zero" } ], "price_cents": 3350, "spicy": true, "nutrition_missing": [], "coupon_requested": false, "take_way_options": [ { "code": "eat-in", "title": "堂食", "subtitle": "店内用餐" }, { "code": "take-in-store", "title": "外带", "subtitle": "店内自提" } ], "fees_cents": { "deliveryPrice": null, "packingPrice": null, "tablewarePrice": null }, "coupon_applied": false, "quote_tool": "calculate-price", "energy_kcal": 774.0, "sodium_mg": 1408.0, "protein_g": 28.0 }, { "id": "9900005464-4810-3050", "name": "双层吉士汉堡中套餐", "variant": "中薯条 · 可乐中杯", "store_code": "已脱敏", "items": [ { "code": "1120", "name": "双层吉士汉堡", "quantity": 1, "kind": "main", "customizations": [], "ice": null, "nutrition_customized": false, "is_fries": false, "sugar": null }, { "code": "4810", "name": "中薯条", "quantity": 1, "kind": "side", "customizations": [], "ice": null, "nutrition_customized": false, "is_fries": true, "sugar": null }, { "code": "3050", "name": "可乐中杯", "quantity": 1, "kind": "drink", "customizations": [ "去冰" ], "ice": "none", "nutrition_customized": false, "is_fries": false, "sugar": "regular" } ], "price_cents": 3400, "spicy": false, "nutrition_missing": [], "coupon_requested": false, "take_way_options": [ { "code": "eat-in", "title": "堂食", "subtitle": "店内用餐" }, { "code": "take-in-store", "title": "外带", "subtitle": "店内自提" } ], "fees_cents": { "deliveryPrice": null, "packingPrice": null, "tablewarePrice": null }, "coupon_applied": false, "quote_tool": "calculate-price", "energy_kcal": 865.0, "sodium_mg": 1182.0, "protein_g": 31.0 }, { "id": "9900005464-4810-3071", "name": "双层吉士汉堡中套餐", "variant": "中薯条 · 无糖可口可乐中杯", "store_code": "已脱敏", "items": [ { "code": "1120", "name": "双层吉士汉堡", "quantity": 1, "kind": "main", "customizations": [], "ice": null, "nutrition_customized": false, "is_fries": false, "sugar": null }, { "code": "4810", "name": "中薯条", "quantity": 1, "kind": "side", "customizations": [], "ice": null, "nutrition_customized": false, "is_fries": true, "sugar": null }, { "code": "3071", "name": "无糖可口可乐中杯", "quantity": 1, "kind": "drink", "customizations": [ "去冰" ], "ice": "none", "nutrition_customized": false, "is_fries": false, "sugar": "zero" } ], "price_cents": 3400, "spicy": false, "nutrition_missing": [], "coupon_requested": false, "take_way_options": [ { "code": "eat-in", "title": "堂食", "subtitle": "店内用餐" }, { "code": "take-in-store", "title": "外带", "subtitle": "店内自提" } ], "fees_cents": { "deliveryPrice": null, "packingPrice": null, "tablewarePrice": null }, "coupon_applied": false, "quote_tool": "calculate-price", "energy_kcal": 718.0, "sodium_mg": 1217.0, "protein_g": 31.0 } ] }, { "mode": "snapshot", "fulfillment": { "channel": "pickup" }, "store": { "code": "已脱敏", "name": "南京某门店(名称与地址已脱敏)", "address": "(门店名与地址已脱敏)", "city": "南京", "address_checked_at": "2026-10-13T16:43:12.911097+00:00", "address_source": "(来源隐藏)" }, "captured_at": "2026-10-13T16:05:17+00:00", "source": "麦当劳官方 MCP:calculate-price + list-nutrition-foods", "scope": "仅比较本轮候选;不保证全菜单最低价;营养为标准份量的单品加总。", "plans": [ { "id": "9900005456-4810-3050", "name": "麦辣鸡腿汉堡三件套", "variant": "中薯条 · 可乐中杯", "store_code": "已脱敏", "items": [ { "code": "1440", "name": "麦辣鸡腿汉堡", "quantity": 1, "kind": "main", "customizations": [], "ice": null, "nutrition_customized": false, "is_fries": false, "sugar": null }, { "code": "4810", "name": "中薯条", "quantity": 1, "kind": "side", "customizations": [], "ice": null, "nutrition_customized": false, "is_fries": true, "sugar": null }, { "code": "3050", "name": "可乐中杯", "quantity": 1, "kind": "drink", "customizations": [ "去冰" ], "ice": "none", "nutrition_customized": false, "is_fries": false, "sugar": "regular" } ], "price_cents": 3350, "spicy": true, "nutrition_missing": [], "coupon_requested": false, "take_way_options": [ { "code": "eat-in", "title": "堂食", "subtitle": "店内用餐" }, { "code": "take-in-store", "title": "外带", "subtitle": "店内自提" } ], "fees_cents": { "deliveryPrice": null, "packingPrice": null, "tablewarePrice": null }, "coupon_applied": false, "quote_tool": "calculate-price", "energy_kcal": 921.0, "sodium_mg": 1373.0, "protein_g": 28.0 }, { "id": "9900005456-4810-3071", "name": "麦辣鸡腿汉堡三件套", "variant": "中薯条 · 无糖可口可乐中杯", "store_code": "已脱敏", "items": [ { "code": "1440", "name": "麦辣鸡腿汉堡", "quantity": 1, "kind": "main", "customizations": [], "ice": null, "nutrition_customized": false, "is_fries": false, "sugar": null }, { "code": "4810", "name": "中薯条", "quantity": 1, "kind": "side", "customizations": [], "ice": null, "nutrition_customized": false, "is_fries": true, "sugar": null }, { "code": "3071", "name": "无糖可口可乐中杯", "quantity": 1, "kind": "drink", "customizations": [ "去冰" ], "ice": "none", "nutrition_customized": false, "is_fries": false, "sugar": "zero" } ], "price_cents": 3350, "spicy": true, "nutrition_missing": [], "coupon_requested": false, "take_way_options": [ { "code": "eat-in", "title": "堂食", "subtitle": "店内用餐" }, { "code": "take-in-store", "title": "外带", "subtitle": "店内自提" } ], "fees_cents": { "deliveryPrice": null, "packingPrice": null, "tablewarePrice": null }, "coupon_applied": false, "quote_tool": "calculate-price", "energy_kcal": 774.0, "sodium_mg": 1408.0, "protein_g": 28.0 }, { "id": "9900005464-4810-3050", "name": "双层吉士汉堡中套餐", "variant": "中薯条 · 可乐中杯", "store_code": "已脱敏", "items": [ { "code": "1120", "name": "双层吉士汉堡", "quantity": 1, "kind": "main", "customizations": [], "ice": null, "nutrition_customized": false, "is_fries": false, "sugar": null }, { "code": "4810", "name": "中薯条", "quantity": 1, "kind": "side", "customizations": [], "ice": null, "nutrition_customized": false, "is_fries": true, "sugar": null }, { "code": "3050", "name": "可乐中杯", "quantity": 1, "kind": "drink", "customizations": [ "去冰" ], "ice": "none", "nutrition_customized": false, "is_fries": false, "sugar": "regular" } ], "price_cents": 3400, "spicy": false, "nutrition_missing": [], "coupon_requested": false, "take_way_options": [ { "code": "eat-in", "title": "堂食", "subtitle": "店内用餐" }, { "code": "take-in-store", "title": "外带", "subtitle": "店内自提" } ], "fees_cents": { "deliveryPrice": null, "packingPrice": null, "tablewarePrice": null }, "coupon_applied": false, "quote_tool": "calculate-price", "energy_kcal": 865.0, "sodium_mg": 1182.0, "protein_g": 31.0 }, { "id": "9900005464-4810-3071", "name": "双层吉士汉堡中套餐", "variant": "中薯条 · 无糖可口可乐中杯", "store_code": "已脱敏", "items": [ { "code": "1120", "name": "双层吉士汉堡", "quantity": 1, "kind": "main", "customizations": [], "ice": null, "nutrition_customized": false, "is_fries": false, "sugar": null }, { "code": "4810", "name": "中薯条", "quantity": 1, "kind": "side", "customizations": [], "ice": null, "nutrition_customized": false, "is_fries": true, "sugar": null }, { "code": "3071", "name": "无糖可口可乐中杯", "quantity": 1, "kind": "drink", "customizations": [ "去冰" ], "ice": "none", "nutrition_customized": false, "is_fries": false, "sugar": "zero" } ], "price_cents": 3400, "spicy": false, "nutrition_missing": [], "coupon_requested": false, "take_way_options": [ { "code": "eat-in", "title": "堂食", "subtitle": "店内用餐" }, { "code": "take-in-store", "title": "外带", "subtitle": "店内自提" } ], "fees_cents": { "deliveryPrice": null, "packingPrice": null, "tablewarePrice": null }, "coupon_applied": false, "quote_tool": "calculate-price", "energy_kcal": 718.0, "sodium_mg": 1217.0, "protein_g": 31.0 } ] }, { "mode": "snapshot", "store": { "code": "已脱敏", "name": "南京某门店(名称与地址已脱敏)", "address": "(门店名与地址已脱敏)", "city": "南京", "address_checked_at": "2026-10-13T16:43:12.911097+00:00", "address_source": "(来源隐藏)" }, "captured_at": "2026-10-13T14:14:10+00:00", "source": "麦当劳官方 MCP:calculate-price + list-nutrition-foods", "scope": "仅比较本轮候选;不保证全菜单最低价;营养为标准份量的单品加总。", "plans": [ { "id": "chicken-classic", "name": "麦辣鸡腿汉堡三件套", "variant": "中薯条 · 可乐", "store_code": "已脱敏", "items": [ { "code": "1440", "name": "麦辣鸡腿汉堡", "quantity": 1, "kind": "main", "is_fries": false }, { "code": "4810", "name": "中薯条", "quantity": 1, "kind": "side", "is_fries": true }, { "code": "3050", "name": "可乐中杯", "quantity": 1, "kind": "drink", "is_fries": false, "sugar": "regular" } ], "price_cents": 3350, "spicy": true, "nutrition_missing": [], "coupon_applied": false, "quote_tool": "calculate-price", "energy_kcal": 921.0, "sodium_mg": 1373.0, "protein_g": 28.0 }, { "id": "chicken-corn", "name": "麦辣鸡腿汉堡三件套", "variant": "玉米杯 · 无糖可乐", "store_code": "已脱敏", "items": [ { "code": "1440", "name": "麦辣鸡腿汉堡", "quantity": 1, "kind": "main", "is_fries": false }, { "code": "4437", "name": "小杯玉米杯", "quantity": 1, "kind": "side", "is_fries": false }, { "code": "3071", "name": "无糖可口可乐中杯", "quantity": 1, "kind": "drink", "is_fries": false, "sugar": "zero" } ], "price_cents": 3200, "spicy": true, "nutrition_missing": [], "coupon_applied": false, "quote_tool": "calculate-price", "energy_kcal": 538.0, "sodium_mg": 1244.0, "protein_g": 26.0 }, { "id": "cheese-classic", "name": "双层吉士汉堡中套餐", "variant": "中薯条 · 可乐", "store_code": "已脱敏", "items": [ { "code": "1120", "name": "双层吉士汉堡", "quantity": 1, "kind": "main", "is_fries": false }, { "code": "4810", "name": "中薯条", "quantity": 1, "kind": "side", "is_fries": true }, { "code": "3050", "name": "可乐中杯", "quantity": 1, "kind": "drink", "is_fries": false, "sugar": "regular" } ], "price_cents": 3350, "spicy": false, "nutrition_missing": [], "coupon_applied": false, "quote_tool": "calculate-price", "energy_kcal": 865.0, "sodium_mg": 1182.0, "protein_g": 31.0 }, { "id": "cheese-corn", "name": "双层吉士汉堡中套餐", "variant": "玉米杯 · 无糖可乐", "store_code": "已脱敏", "items": [ { "code": "1120", "name": "双层吉士汉堡", "quantity": 1, "kind": "main", "is_fries": false }, { "code": "4437", "name": "小杯玉米杯", "quantity": 1, "kind": "side", "is_fries": false }, { "code": "3071", "name": "无糖可口可乐中杯", "quantity": 1, "kind": "drink", "is_fries": false, "sugar": "zero" } ], "price_cents": 3200, "spicy": false, "nutrition_missing": [], "coupon_applied": false, "quote_tool": "calculate-price", "energy_kcal": 482.0, "sodium_mg": 1053.0, "protein_g": 29.0 }, { "id": "fish-classic", "name": "麦香鱼中套餐", "variant": "中薯条 · 可乐", "store_code": "已脱敏", "items": [ { "code": "1600", "name": "麦香鱼", "quantity": 1, "kind": "main", "is_fries": false }, { "code": "4810", "name": "中薯条", "quantity": 1, "kind": "side", "is_fries": true }, { "code": "3050", "name": "可乐中杯", "quantity": 1, "kind": "drink", "is_fries": false, "sugar": "regular" } ], "price_cents": 3400, "spicy": false, "nutrition_missing": [], "coupon_applied": false, "quote_tool": "calculate-price", "energy_kcal": 761.0, "sodium_mg": 721.0, "protein_g": 20.0 }, { "id": "fish-corn", "name": "麦香鱼中套餐", "variant": "玉米杯 · 无糖可乐", "store_code": "已脱敏", "items": [ { "code": "1600", "name": "麦香鱼", "quantity": 1, "kind": "main", "is_fries": false }, { "code": "4437", "name": "小杯玉米杯", "quantity": 1, "kind": "side", "is_fries": false }, { "code": "3071", "name": "无糖可口可乐中杯", "quantity": 1, "kind": "drink", "is_fries": false, "sugar": "zero" } ], "price_cents": 3250, "spicy": false, "nutrition_missing": [], "coupon_applied": false, "quote_tool": "calculate-price", "energy_kcal": 378.0, "sodium_mg": 592.0, "protein_g": 18.0 } ] } ]; window.GOURMET_DATA = window.GOURMET_SNAPSHOTS[0];docs/evidence/legacy/preview-old-ui.png133 KBa5dd0529e311upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
docs/evidence/visual-2026-10-14/desktop.png480 KB62ba1edafc75upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
docs/evidence/visual-2026-10-14/mobile.png427 KB0e3bbd0d6b9dupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
docs/evidence/visual-2026-10-14/narrow.png426 KBed09d25f8bd2upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
docs/evidence/visual-2026-10-14/results.json4.4 KBed70a2b1ddabupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
docs/evidence/visual-2026-10-24/desktop.png427 KB6b416e6b690cupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
docs/evidence/visual-2026-10-24/mobile.png363 KB3f12283d326dupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
docs/evidence/visual-2026-10-24/narrow.png361 KB7eb27d00ca6cupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
docs/features.md7.8 KB91f920e36d01upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
docs/hardcoding-audit.md4.2 KB3bce2f74f843upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
docs/implementation-plan.md1.2 KBa1e6ec060a15upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
docs/index.html14 KB21e966b424c2upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
docs/promotion-kit.md6.6 KBcde56057261dupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
docs/review.js7.9 KB4a84fa2b4e55upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
docs/style.css20 KB3df558027064upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
docs/upgrade-design.md1.1 KB66a540cd70a3upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
docs/validation.md16 KB145d6e2592b1upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
docs/workbuddy-market.md2.6 KB7358fc15ae3cupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
gourmet/__init__.py83 Bcd9b6ade486bupstream ↗
"""麦门点餐议事会。只使用 Python 标准库。""" __version__ = "1.0.0"gourmet/catalog.py284 Bbcebdf4f34a8upstream ↗
"""Reviewed product codes from the official menu; unknown codes stay unknown.""" FRIES_CODES = frozenset({'4810'}) DRINK_SUGAR = {'3050': 'regular', '3071': 'zero'} SIDE_CODES = ('4810', '4437') DRINK_CODES = ('3050', '3071') MAIN_SPICY = {'1440': True, '1120': False, '1600': False}gourmet/channels.py1.8 KBedea3283b01bupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
gourmet/core.py22 KBf50aa6f3ccc8upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
gourmet/coupons.py11 KB1b8f04d10989upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
gourmet/customize.py2.9 KB637936469074upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
gourmet/live.py20 KB4f5ff364eb76upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
gourmet/mcp.py7.3 KB612626e03bcdupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
gourmet/member.py5.9 KBa44ea163bfa1upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
gourmet/stores.py3.6 KB1f7d8cbd3d1aupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
LICENSE1.0 KB50c39cbe2b45upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
mcp-config.example.json178 Ba98880d0c689upstream ↗
{ "mcpServers": { "mcd-mcp": { "type": "streamablehttp", "url": "https://mcp.mcd.cn", "headers": {"Authorization": "Bearer ${MCD_MCP_TOKEN}"} } } }MCP_INTEGRATION.md4.9 KB45ee6b203c4dupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
NOTICE611 B15a6e9b7ec03upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
PRODUCT.md1.0 KB0d975c1f080cupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
README_EN.md7.5 KB4c579f1d0909upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
tests/parity.cjs2.5 KB9af641afa605upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
tests/test_coupons.py11 KBf9bbd212d4efupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
tests/test_gourmet.py5.8 KBebc41253264cupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
tests/test_improvements.py5.7 KBaa325cf9d990upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
tests/test_user_flows.py13 KBf9727b3671d5upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
tests/test_v04_explain.py4.9 KB7fa3f38f88d8upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
tests/test_v1_card.py5.7 KB3dfb227cfedfupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
tests/test_v1_entry.py11 KB79271a674836upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
tests/test_v1_people.py4.9 KB2f4e02eb4b0dupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
tests/test_v1_price_policy.py5.7 KB9215631ec152upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
tests/test_v1_savings.py6.9 KBf5ba9950a1a3upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
tests/ui-dom.cjs3.5 KBf4947d6ae85cupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
workbuddy.md24 KB6abc9b04ecaaupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
Plain-text copy, no login: /skill-bundles/polaris-smart-mcd-agent-gourmet.md