--- title: "我让 AI 助手管我的 Shopee 优惠券 —— 它建了一张我没批准的券" description: "优惠券没有草稿态:点 Confirm 就是上线并开始扣 escrow。本文记录我用「只读审计 → 提议 → 自动」三层设计把 AI 挡在那个按钮之外,以及那次因为列表延迟十分钟而多建一张券的教训。" pubDate: 2025-07-08 updatedDate: 2026-09-29 category: ai tags: [ai-agent, shopee, automation, human-in-the-loop, ecommerce, cron] ogImage: /og/zh-i-let-an-agent-manage-my-shopee-vouchers.jpg banner: /banners/i-let-an-agent-manage-my-shopee-vouchers.jpg draft: false dynamicCta: true --- 我在 Shopee 上开了一家小店,卖数字商品。店还年轻,挡在第一笔订单前面的不是库存,而是一个**必须先在 30 天内完成一笔订单才能申请**的商家计划。所以:先有订单。 优惠券是我手上最便宜的杠杆:数字商品的边际成本几乎为零,所以一张 RM6 的券,只有在真的有人买的时候才花我 RM6,平时什么也不花。而且我的 AI 助手本来就在用 Chrome DevTools Protocol 驱动 Shopee 卖家后台发商品,把优惠券自动化看起来只是顺手的事。 结果并不顺手。不是自动化难,而是平台有一个特性: **Shopee 的优惠券没有草稿态。** 商品可以点 *Save and Delist* 存成草稿,以后再决定;优惠券不行 —— `Confirm` 既是发布按钮也是花钱按钮,点下去买家就能立刻领用下单,每一笔核销都会直接从你的订单结算款(escrow)里划走真金白银。中间没有「已保存、未上线」这个可以躲的位置。 这一句话决定了整套设计,也是我如果重做一次会最先做对的地方。 ## 平台实际是怎么运作的(全都是试出来的) 四条规则,每一条我都是踩了才知道: 1. **没有草稿态。** 如上。修改已有的券 = 打开它的编辑页再点一次 `Confirm`(是重新保存,不会建出第二张),但依然没有「先摆好,再发布」这一步。 2. **每个店只能有 1 张 New Buyer(“欢迎券”)。** 想建第二张会弹提示:*“Please create a new shop welcome voucher after the existing one is expired.”* 点击是有效的、URL 不变、而**任何一个字段旁边都没有红字** —— 这一点很重要,见后文。 3. **Shop 券不受这个限制。** 我同时跑了 3 张 Shop 券,都正常。所以「每种券只能一张」这个概括是错的,欢迎券才是特例。 4. **券一旦进入 `Ongoing`,经济条件就锁死。** 编辑页里折扣金额、最低消费、开始时间都是 `disabled`,只剩下名称、代码后缀、结束日期、数量可以改。而且**后台根本没有删除入口** —— 列表页没有,编辑页也没有。已上线的券只能改,或者等它过期。 第 4 条改变的是策略:你没法「修好」一个已经在跑的折扣,只能让它死掉再重建。也就是说,**建券这个动作比看起来要不可逆得多**。 ## 三层设计 我不想给 AI 一个花钱按钮,也不想手动盯着券。最后落成的是三层结构,区别在于**谁做决定**: | 层 | 做什么 | 谁决定 | |---|---|---| | **A — 审计** | 只读:列出所有券,标记即将到期/已用完/长期无人领,输出一行摘要 | 不需要人,全自动 | | **B — 半自动** | 报告问题;人回一句话;助手执行修改 | 人 | | **C — 提议** | 算出它*打算*建的券(面额、最低消费、数量、周期、敞口),然后停下 | 人,针对价格 | A 层是一个 cron 任务:每天早上 09:00,脚本驱动浏览器读券列表,打印一行 digest。浏览器/CDP 连不上或登录态失效时退出码是 `2`,页面解析失败是 `3` —— 所以「没有输出」永远不等于「一切正常」。 C 层是有意思的部分。策略写在一个 JSON 文件里而不是代码里,每种券是一个 **slot**: ```json { "enabled": true, "mode": "propose", "replenish": { "templates": [ { "slot": "new_buyer", "type": "new_buyer", "name": "New Buyer RM9.60 off min RM18", "amount": 9.6, "min_spend": 18, "qty": 20, "window_days": 14, "code_prefix": "NB", "max_per_7_days": 1 } ] }, "caps": { "monthly_exposure_rm": 300 } } ``` 助手只回答一个问题:*有哪个 slot 是空的?* 有的话,它算出参数 —— 在 `propose` 模式下 —— 打印出来然后停下: ``` PROPOSAL (nothing created yet): slot 'new_buyer' is empty → 9.6 off / Min RM18 / 20 qty / 14 days, code NBX4K, exposure RM192 why: no live voucher in slot 'new_buyer' WAITING FOR PRICE CONFIRMATION before creating (policy mode=propose) ``` 只有在策略里写着 `"mode": "auto"` **并且**这次运行不是 dry run 时,创建路径才会执行: ```python mode = str(pol.get("mode") or "propose").lower() if a.dry or mode != "auto": print_proposal(actions) # 完全不碰平台 else: apply_auto(actions) # 唯一一条能花钱的代码路径 ``` 整个技巧就是这样,一点也不高级:**你和按钮之间隔一个布尔值,默认值是「先问」**。上限、slot、digest 都是配角。 ## 那个错误:一份迟到了十分钟的列表 接下来这段我不太想写,因为它花了钱,而原因纯粹是我不耐烦。 我填好优惠券表单,点了 `Confirm`,然后按读任何网页表单的方式读结果:URL 变了吗?没有。成功提示?没有。红色错误?没有。于是我去问脚本要券列表 —— 列表里没有这张券。 我由此得出的结论是:创建被静默拒绝了,大概又是某种「每类只能一张」的限制,只是这次平台懒得给错误信息。于是我**又点了一次** `Confirm`。再点了一次。 券其实**已经建好了**,只是列表延迟了十分钟。它后来出现了,状态 `Ongoing`,参数是我填的 RM14 off / 最低消费 RM29 —— 一张我从没打算建的券,最坏情况下敞口 RM140(十张全部被领用)。 三条教训,按代价排序: 1. **「看不到变化」不等于「失败」,它等于「未知」。** 通过 UI 发出的写操作有三种状态:成功了、被拒了、还没反映出来 —— 而后两种第一眼长得一模一样。 2. **幂等保护要建在写路径之前,而不是之后。** 先问列表有没有同款券,建一次,等,再读。从那以后,这个项目里每一次创建都先做一次「是否已存在」的读取。 3. **永远不要重复点击一个你还没验证过的提交按钮。** 我的脚本报告 `Confirm: clicked @926,619` —— 一句关于一次鼠标事件的真话,和一句关于世界的废话。**点击日志不是结果。** 现在我把写入之后的读取当作唯一权威,其他一切当作噪声,并且在允许自己下结论之前强制等待。 ## 我会怎么做不同 - **一开始就用 propose 模式。** 我最初上线的是「自动建 + 敞口上限」。上限限制的是错误的*大小*,propose 模式阻止的是错误本身。基于上限的设计建起来更安心,实际用起来明显更糟。 - **按「slot 是否为空」判断,而不是「店里有没有券」。** 因为第 2 条规则,「有没有券」是错的问题;「*这一类*券在不在」才是对的,也正是它让自动补位这件事成立。 - **让每一次写入自己断言自己的结果。** 现在创建路径靠另一次审计来验证;它应该自己验证,并在回读不一致时大声失败。 - **动手前先把意图写进日志。** 「我准备创建 RM9.60/18 × 20,敞口 RM192」和动作出现在同一份输出里,我那次重复点击当场就会暴露,而不是等下一次审计才发现。 ## 结果 现在店里有四张券在跑,合起来正好构成一个按客单价分档的阶梯 —— 这才是原本的商业目标(注:上文的 JSON 片段为早期试跑模板,下方表格记录的则是最终线上稳定运行的四档规则): | 篮子金额 | 折扣 | 买家实付 | |---|---|---| | RM9(单件) | RM6 off | **RM3** | | RM18(两件) | RM9.20 off | **RM8.80** | | RM29 | RM14 off | **RM15** | | 新买家 | RM4.50 off | **RM4.50** | 如果每一张的每一份都被领完,最坏敞口是 RM474。真实支出只是其中一小部分,因为只有订单完成核销时才会实际扣款。而那张误建出来的 RM140 敞口券(RM29 减 RM14 × 10 张),我最终决定保留,因为它刚好补上了我原本就想建的 RM29 满减档位。 自打 propose 闸门装上之后,助手自己创建的券数量是 **0**。它现在做的事,是每天打印一段参数,然后等我点头。 ## 想给你的生意做一套类似的? 真正值得抄的不是 Shopee 这部分,而是那个模式:**一个会监控、会计算、并且会在唯一那个花钱动作前自己停下来的助手**。它适用于广告预算、退款审批、供应商下单,任何带「花钱」调用的流程。我在你已经在用的工具上构建这类自动化 —— Shopee 或 WooCommerce、你自己机器上的 Docker 和 Traefik、n8n,以及用来汇报的 Telegram。 在 [WhatsApp](https://wa.me/60127972969) 或 [me@hoelee.com](mailto:me@hoelee.com?subject=Agent%20automation%20for%20my%20business) 找我 —— 告诉我你想让助手在哪一步停下来问你,我会诚实地说这是周末就能搞定的事,还是个坏主意。更多见 [hoelee.com](https://hoelee.com)。