13 KiB
购买流程重构 — 分批执行指南
状态:方案已与用户确认(2026-08-31)。第 1 批已完成(2026-08-31,commit f27bbb4) —— 修 bug + 深链 builder 重构:
adminPurchaseLink({sku, notes?, username?})新签名、 SYSTEM_CONTEXT 两处示例改静态预编码 URL(无 ${…} 递归模板)、prompt.test.ts +6 用例 (153 全绿、typecheck/build 过、t.me/计数=1)。 第 2 批已完成(2026-08-31,commit fe9db79) —— 购买方式状态机 + inline 按钮:commands/purchase.ts(PURCHASE_INTENT_RE 三语 拦截、付款偏好提取、语言化深链 builder、三组按钮键盘)、bot.ts 购买意图拦截 +bot.on("callback_query:data")分派(buy:marketplace/admin/contact/back)、 tests/purchase.test.ts +18 用例(171 全绿、中性词纪律测试)。 第 3 批已完成(2026-08-31,commit 5f42f19) —— free 试看流程按钮化:startSku/confirmReuse 附 inline 按钮(trial:confirm / new-username / reuse),callback 分派接 pendingTrials 状态机,文字 fallback 保留。 第 4 批(收尾)已完成(2026-08-31,commit 见 git log) — 回归门禁 (typecheck + 175 tests + build 全绿)→ 文档同步(本文件 + ROADMAP + PROJECT_STATE 四·补·四)→ deploy_bot_dsm.py 部署(第1–3批全部上线)→ 生产验证全项绿:logs Database ready/webhook registered/HTTP listening、 公网 + DSM 本地 /health ok、容器内 node fetch getWebhookInfourl=…/webhook pending=0 last_err=none ok=true、migrations 1..7、 user_memory 表在、镜像内含 dist/channels/telegram/commands/purchase.js + data/catalog.js、callback 数据计数与本地构建一致。→ 用户手机端到端 实测通过(2026-08-31):bot 可正常使用,全流程闭环。 ⚠ 本次排查出的验证姿势修正:DSM 宿主 curl api.telegram.org token 端点对 有效 token 也回 404(出口怪癖,容器网络路径正常)——webhook 复查一律走 容器内docker exec digikedai-bot node -e 'fetch(...)'。 本文是执行蓝图,不是代码。执行时遵守仓库AGENTS.md规范: 开场呈菜单不猜、一次一条战线、动文件前先git status/git log核对、 凭据只在SECRETS.md。
背景与目标
现状 bot 的购买路径只有一条:顾客表达「要买 + 已知 SKU」→ 直接给一个
t.me/MrFullStackDev?text=… admin 深链。存在两个问题:
- Bug:深链 URL 双重嵌套。
adminPurchaseLink()已返回完整 URL,但 system prompt 里的示例模板又把它的输出再塞进?text=一次,导致线上 出现t.me/MrFullStackDev?text=t.me/MrFullStackDev?text=…。 - 流程单一:没有「多购买方式」的选择,顾客无法在「网店下单」与 「Telegram 找 admin」之间选,也没有任何购买前的确认/引导。
目标:引入一套确定性状态机(对齐现有 free-trial 流程的
pendingTrials 模式),用 inline keyboard 按钮(点击式)引导顾客
选择购买方式,并把已收集到的信息(SKU、付款偏好、username 等)拼进
admin 深链预填文案。admin 深链作为 fallback,不再是唯一路径。
最终口径(用户 2026-08-31 拍板)
- 中性词,不硬编码 Shopee/Lazada/Add-On/Touch'n Go 等平台名:
- 中文:「网店 / 网店平台」
- 英文:
online store / marketplace - 马来:
kedai dalam talian / marketplace - 理由:marketplace(Shopee/Lazada)尚未上线,Add-On 仅预定,暂不让 bot 提及。
- 购买方式菜单:两行按钮平铺,靠顺序表示优先级(网店在上)。
- 网店下单(首选,找不到可联系卖家或回来找 bot)
- 找 admin 购买(次选,直接给 admin 联系方式)
- 点「找 admin」→ 直接发链接,不先确认(少一步,fallback 图快)。
- admin 深链预填文案:跟随顾客当前语言,英文 fallback/default; 一段自然话,不强制字段,已收集到什么就放什么;顾客可随时补充或 再发新消息。
- free 试看流程同步按钮化:把文字回复式(同意/ok/setuju、新账号) 换成 inline 按钮。
分批计划(2–4 批,每批可独立提交 + 测试 + 部署)
第 1 批 — 修 Bug + 深链构建器重构(无行为变化,先止血)
目标:消除 URL 双重嵌套;把深链构建从「prompt 模板递归」改成 「确定性 builder」;不引入新 UI。
改动点:
bot/src/ai/prompts/system.ts- 修
adminPurchaseLink():签名改为接受一个自由文本notes参数, 拼在「我要买 …」段之后,仍encodeURIComponent一次。 例:adminPurchaseLink({ sku: "CZH01", notes: "(想用银行转账)" }) - 修 SYSTEM_CONTEXT 里那两处示例 URL —— 改成最终可用的静态预编码
字符串,绝不再用
${adminPurchaseLink(...)}这种会让 LLM 递归 塞值的模板占位符。示例里直接给一条已编码好的、可复制的真实 URL。 - 更新注释,说明「示例 URL 是 LLM 逐字复用 + 只替换字母数字 SKU/ username」的契约。
- 修
bot/src/ai/prompts/system.ts顶部的文档注释块同步更新。- 测试:更新
bot/tests/prompt.test.tsadminPurchaseLink用例改为新签名(含 notes / 不含 notes / 含 username 组合)。- 断言:示例输出都
startsWith("https://t.me/MrFullStackDev?text="), 且text参数不再包含t.me/或MrFullStackDev(防止再嵌套)。
验收:
npm run typecheck、npm test、npm run build全绿。- 手工:传参
adminPurchaseLink({sku:"CZH01",notes:"(想用银行转账)"})打印出的 URL 里t.me/只出现一次。 - 部署后线上「我要买 CZH01」→ 深链无双重嵌套。
第 2 批 — 购买方式选择状态机 + inline 按钮(核心)
目标:引入 pendingPurchase 状态机,顾客点了购买意图后弹按钮菜单,
点选「网店」给引导、点「找 admin」发深链。
改动点(bot/src/channels/telegram/):
- 新文件
commands/purchase.ts(或并入commands/index.ts):- 定义
PurchaseChoice = "marketplace" | "admin"。 - 导出购买引导文案
PURCHASE_MENU_TEXT(lang):上面第「最终口径」里的 菜单文案(网店在上)。 - 导出
marketplaceGuidanceText(lang):点网店后的中性引导 + 附 「联系 admin / 返回购买方式」按钮。 - 导出
adminContactText(link, lang):点找 admin 后,直接发深链 + 一句话说明「付款后开通到资源站账号」。 - 导出 inline keyboard 构造(
InlineKeyboard来自 grammY)。
- 定义
bot.ts:- 加
pendingPurchaseMap(类似pendingTrials),TTL 建议 30 分钟。 - 在
message:text处理里、free-trial 判定之后,加「购买意图拦截」: 用确定性规则(复用extractSku+ 新增PURCHASE_INTENT_RE,zh/en/ms: 买/购买/下单/付款/how to buy/order/pay/beli/bayar/order…)识别顾客想买; 命中 → 弹PURCHASE_MENU_TEXT+ 按钮,记pendingPurchase状态。 - 加
bot.callbackQuery处理器分派:buy:marketplace→ 回marketplaceGuidanceText+ 按钮buy:admin→ 构建深链 +adminContactText+ 「返回购买方式」按钮buy:back→ 重新弹PURCHASE_MENU_TEXT- 其余未知 callback → 只
answerCallbackQuery吞掉,不报错。
- 深链文案收集规则(见下),调用新签名
adminPurchaseLink。
- 加
- 深链预填内容收集(一段话,不强制字段):
- 有 SKU →
我要买 CZH01;无 SKU →I want to buy a product(英文 fallback)。 - 顾客已表态付款偏好/补充 → 追加(如「(想用银行转账)」)。
- 已知 username(复用账号场景)→ 追加
账号 username:xxx。 - 语言:跟随当前对话语言;无法确定则英文。
- 有 SKU →
验收测试(新增/更新 bot/tests/commands.test.ts 或新 purchase.test.ts):
extractPurchaseIntent/ 对应检测函数的中英马三语用例。- 深链构建器在「仅 SKU / SKU+偏好 / 无 SKU」三种输入下的输出。
- callback 分派逻辑(可用 mock
ctx)。
验收:
- typecheck/test/build 全绿。
- 手工(需部署后实测):说「怎么买」→ 弹菜单 → 点网店 → 引导文案 → 点找 admin → 深链;点返回 → 回菜单。
第 3 批 — free 试看流程按钮化(对齐体验)
目标:把试看领取的文字回复交互换成 inline 按钮。
改动点:
bot.ts里 free-trial 相关回复,逐处把「文字指令」升级为「文案 + 按钮」:startSkuText/confirmReuseText/ASK_USERNAME_TEXT/ASK_NEW_ACCOUNT_USERNAME_TEXT等文案函数增加相应按钮。- 按钮语义:
✅ 确认并开通(= 同意/ok/setuju,进入 provision)✏️ 提供新 username(= 新账号,进入等 username 状态)♻️ 加到现有账号(= 复用既有账号)
- callback 分派与
pendingTrials状态机解耦:点击按钮等价于原来用户 输入同意词/新账号词,直接推进runTrialFlow的对应分支。
- 保留文字回复作为 fallback(老顾客按文字输入照常能走通,按钮仅是
增强入口,不破坏现有
parseTrialConsent/parseAccountDecision)。
验收:
- typecheck/test 全绿。
- 手工:
/trial→ 按钮出现;点「确认并开通」→ 正常 provision; 点「提供新 username」→ 追问 username;老路径文字「同意 用户名:xx」 仍可用。
第 4 批(可选/收尾)— 全链路回归 + 文档 + 部署入档
目标:端到端冒烟、文档同步、提交入档。
改动点:
- 更新
bot/docs/ROADMAP.md:把「purchase handoff deep link」一段改为 新的多方式状态机描述;在 Phase 2 剩余项里勾掉相应条目。 - 更新
PROJECT_STATE.md工地看板:记录第 1–3 批的完成状态与新口径。 AGENTS.md若列了 bot 相关分账,同步一句话。- 端到端冒烟(关键,之前一直欠着的):
- 购买菜单全路径(网店 / admin / 返回)。
- admin 深链预填:仅 SKU / SKU+偏好 / 无 SKU / 中文 / 英文。
- free 试看按钮化:确认开通 / 提供新 username / 复用账号。
- 老文字路径回归。
- 部署 + 验证(
deploy_bot_dsm.py或按operations里既有姿势), 读回线上状态。
附:购买菜单文案草稿(三语,供执行时微调)
中文: 🛒 你可以通过以下方式购买: 1️⃣ 网店下单(找不到你要的商品,可联系卖家,或回来这里找我) 2️⃣ 在这里直接付款购买(走 Telegram,我给你 admin 的联系方式) 你想用哪种方式?
English: 🛒 You can purchase via: 1️⃣ our online store / marketplace (if you can't find the item, message the seller or come back here) 2️⃣ buy here directly (via Telegram — I'll share the admin's contact) Which would you like?
Bahasa Melayu: 🛒 Anda boleh membeli melalui: 1️⃣ kedai dalam talian / marketplace (kalau tak jumpa produk, hubungi penjual atau kembali ke sini) 2️⃣ beli terus di sini (melalui Telegram — saya kongsikan kontak admin) Yang mana satu?
按钮数组(示例,实际用 grammY InlineKeyboard):
[
[ { text: "🛍️ 网店下单", callback_data: "buy:marketplace" } ],
[ { text: "💬 找 admin 购买", callback_data: "buy:admin" } ],
]
附:admin 深链文案示例(跟随语言,英文 fallback)
- 仅 SKU(中文顾客)→
我要买 CZH01 - 仅 SKU(英文顾客)→
I want to buy CZH01 - SKU + 付款偏好 →
我要买 CZH01(想用银行转账) - 无 SKU →
I want to buy a product
注:
adminPurchaseLink内中文文案是否百分号编码由 builder 统一处理 (encodeURIComponent一次),执行时不要手工改编码。
风险与注意
- 不破坏现有 free-trial 状态机:购买状态机是独立
pendingPurchase, 与pendingTrials互不干扰;两者都要在message:text里按顺序判定, 先试看、后购买,避免误吞。 - callback 幂等:按钮点击可能重试,
answerCallbackQuery及时回执, 避免重复 provision。 - 不回退老顾客:第 3 批按钮化时保留文字 fallback。
- 中性词纪律:任何新文案不得出现 Shopee/Lazada/Add-On/TnG/bank-in 等 具体平台名(直到用户明确说「上线了」再补充)。