Files
digikedai-bot/docs/PURCHASE_FLOW_REDESIGN.md

13 KiB
Raw Permalink Blame History

购买流程重构 — 分批执行指南

状态:方案已与用户确认(2026-08-31)。第 1 批已完成(2026-08-31commit f27bbb4 —— 修 bug + 深链 builder 重构:adminPurchaseLink({sku, notes?, username?}) 新签名、 SYSTEM_CONTEXT 两处示例改静态预编码 URL(无 ${…} 递归模板)、prompt.test.ts +6 用例 153 全绿、typecheck/build 过、t.me/ 计数=1)。 第 2 批已完成(2026-08-31commit fe9db79 —— 购买方式状态机 + inline 按钮:commands/purchase.tsPURCHASE_INTENT_RE 三语 拦截、付款偏好提取、语言化深链 builder、三组按钮键盘)、bot.ts 购买意图拦截 + bot.on("callback_query:data") 分派(buy:marketplace/admin/contact/back)、 tests/purchase.test.ts +18 用例(171 全绿、中性词纪律测试)。 第 3 批已完成(2026-08-31commit 5f42f19 —— free 试看流程按钮化:startSku/confirmReuse 附 inline 按钮(trial:confirm / new-username / reuse),callback 分派接 pendingTrials 状态机,文字 fallback 保留。 第 4 批(收尾)已完成(2026-08-31commit 见 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 getWebhookInfo url=…/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 深链。存在两个问题:

  1. Bug:深链 URL 双重嵌套。adminPurchaseLink() 已返回完整 URL,但 system prompt 里的示例模板又把它的输出再塞进 ?text= 一次,导致线上 出现 t.me/MrFullStackDev?text=t.me/MrFullStackDev?text=…
  2. 流程单一:没有「多购买方式」的选择,顾客无法在「网店下单」与 「Telegram 找 admin」之间选,也没有任何购买前的确认/引导。

目标:引入一套确定性状态机(对齐现有 free-trial 流程的 pendingTrials 模式),用 inline keyboard 按钮(点击式)引导顾客 选择购买方式,并把已收集到的信息(SKU、付款偏好、username 等)拼进 admin 深链预填文案。admin 深链作为 fallback,不再是唯一路径。

最终口径(用户 2026-08-31 拍板)

  1. 中性词,不硬编码 Shopee/Lazada/Add-On/Touch'n Go 等平台名:
    • 中文:「网店 / 网店平台」
    • 英文:online store / marketplace
    • 马来:kedai dalam talian / marketplace
    • 理由:marketplaceShopee/Lazada)尚未上线,Add-On 仅预定,暂不让 bot 提及。
  2. 购买方式菜单:两行按钮平铺,靠顺序表示优先级(网店在上)。
    • 网店下单(首选,找不到可联系卖家或回来找 bot)
    • 找 admin 购买(次选,直接给 admin 联系方式)
  3. 点「找 admin」→ 直接发链接,不先确认(少一步,fallback 图快)。
  4. admin 深链预填文案:跟随顾客当前语言,英文 fallback/default 一段自然话,不强制字段,已收集到什么就放什么;顾客可随时补充或 再发新消息。
  5. free 试看流程同步按钮化:把文字回复式(同意/ok/setuju、新账号) 换成 inline 按钮。

分批计划(2–4 批,每批可独立提交 + 测试 + 部署)

第 1 批 — 修 Bug + 深链构建器重构(无行为变化,先止血)

目标:消除 URL 双重嵌套;把深链构建从「prompt 模板递归」改成 「确定性 builder」;不引入新 UI。

改动点:

  1. bot/src/ai/prompts/system.ts
    • adminPurchaseLink():签名改为接受一个自由文本 notes 参数, 拼在「我要买 …」段之后,仍 encodeURIComponent 一次。 例:adminPurchaseLink({ sku: "CZH01", notes: "(想用银行转账)" })
    • 修 SYSTEM_CONTEXT 里那两处示例 URL —— 改成最终可用的静态预编码 字符串,绝不再用 ${adminPurchaseLink(...)} 这种会让 LLM 递归 塞值的模板占位符。示例里直接给一条已编码好的、可复制的真实 URL。
    • 更新注释,说明「示例 URL 是 LLM 逐字复用 + 只替换字母数字 SKU/ username」的契约。
  2. bot/src/ai/prompts/system.ts 顶部的文档注释块同步更新。
  3. 测试:更新 bot/tests/prompt.test.ts
    • adminPurchaseLink 用例改为新签名(含 notes / 不含 notes / 含 username 组合)。
    • 断言:示例输出都 startsWith("https://t.me/MrFullStackDev?text=")text 参数不再包含 t.me/MrFullStackDev(防止再嵌套)。

验收:

  • npm run typechecknpm testnpm run build 全绿。
  • 手工:传参 adminPurchaseLink({sku:"CZH01",notes:"(想用银行转账)"}) 打印出的 URL 里 t.me/ 只出现一次。
  • 部署后线上「我要买 CZH01」→ 深链无双重嵌套。

第 2 批 — 购买方式选择状态机 + inline 按钮(核心)

目标:引入 pendingPurchase 状态机,顾客点了购买意图后弹按钮菜单, 点选「网店」给引导、点「找 admin」发深链。

改动点(bot/src/channels/telegram/):

  1. 新文件 commands/purchase.ts(或并入 commands/index.ts):
    • 定义 PurchaseChoice = "marketplace" | "admin"
    • 导出购买引导文案 PURCHASE_MENU_TEXT(lang):上面第「最终口径」里的 菜单文案(网店在上)。
    • 导出 marketplaceGuidanceText(lang):点网店后的中性引导 + 附 「联系 admin / 返回购买方式」按钮。
    • 导出 adminContactText(link, lang):点找 admin 后,直接发深链 + 一句话说明「付款后开通到资源站账号」。
    • 导出 inline keyboard 构造(InlineKeyboard 来自 grammY)。
  2. bot.ts
    • pendingPurchase Map(类似 pendingTrials),TTL 建议 30 分钟。
    • message:text 处理里、free-trial 判定之后,加「购买意图拦截」: 用确定性规则(复用 extractSku + 新增 PURCHASE_INTENT_REzh/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
  3. 深链预填内容收集(一段话,不强制字段):
    • 有 SKU → 我要买 CZH01;无 SKU → I want to buy a product(英文 fallback)。
    • 顾客已表态付款偏好/补充 → 追加(如「(想用银行转账)」)。
    • 已知 username(复用账号场景)→ 追加 账号 username:xxx
    • 语言:跟随当前对话语言;无法确定则英文。

验收测试(新增/更新 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 相关回复,逐处把「文字指令」升级为「文案 + 按钮」:
    1. startSkuText / confirmReuseText / ASK_USERNAME_TEXT / ASK_NEW_ACCOUNT_USERNAME_TEXT 等文案函数增加相应按钮。
    2. 按钮语义:
      • ✅ 确认并开通= 同意/ok/setuju,进入 provision
      • ✏️ 提供新 username= 新账号,进入等 username 状态)
      • ♻️ 加到现有账号= 复用既有账号)
    3. callback 分派与 pendingTrials 状态机解耦:点击按钮等价于原来用户 输入同意词/新账号词,直接推进 runTrialFlow 的对应分支。
  • 保留文字回复作为 fallback(老顾客按文字输入照常能走通,按钮仅是 增强入口,不破坏现有 parseTrialConsent/parseAccountDecision)。

验收:

  • typecheck/test 全绿。
  • 手工:/trial → 按钮出现;点「确认并开通」→ 正常 provision; 点「提供新 username」→ 追问 username;老路径文字「同意 用户名:xx」 仍可用。

第 4 批(可选/收尾)— 全链路回归 + 文档 + 部署入档

目标:端到端冒烟、文档同步、提交入档。

改动点:

  1. 更新 bot/docs/ROADMAP.md:把「purchase handoff deep link」一段改为 新的多方式状态机描述;在 Phase 2 剩余项里勾掉相应条目。
  2. 更新 PROJECT_STATE.md 工地看板:记录第 1–3 批的完成状态与新口径。
  3. AGENTS.md 若列了 bot 相关分账,同步一句话。
  4. 端到端冒烟(关键,之前一直欠着的):
    • 购买菜单全路径(网店 / admin / 返回)。
    • admin 深链预填:仅 SKU / SKU+偏好 / 无 SKU / 中文 / 英文。
    • free 试看按钮化:确认开通 / 提供新 username / 复用账号。
    • 老文字路径回归。
  5. 部署 + 验证(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 状态机:购买状态机是独立 pendingPurchasependingTrials 互不干扰;两者都要在 message:text 里按顺序判定, 先试看、后购买,避免误吞。
  • callback 幂等:按钮点击可能重试,answerCallbackQuery 及时回执, 避免重复 provision。
  • 不回退老顾客:第 3 批按钮化时保留文字 fallback。
  • 中性词纪律:任何新文案不得出现 Shopee/Lazada/Add-On/TnG/bank-in 等 具体平台名(直到用户明确说「上线了」再补充)。