Files
digikedai-bot/docs/PURCHASE_FLOW_REDESIGN.md

235 lines
13 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 购买流程重构 — 分批执行指南
> 状态:方案已与用户确认(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.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-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 部署(第13批全部上线)→
> 生产验证全项绿: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 typecheck`、`npm test`、`npm 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_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`。
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 状态机**:购买状态机是独立 `pendingPurchase`
与 `pendingTrials` 互不干扰;两者都要在 `message:text` 里按顺序判定,
先试看、后购买,避免误吞。
- **callback 幂等**:按钮点击可能重试,`answerCallbackQuery` 及时回执,
避免重复 provision。
- **不回退老顾客**:第 3 批按钮化时保留文字 fallback。
- **中性词纪律**:任何新文案不得出现 Shopee/Lazada/Add-On/TnG/bank-in 等
具体平台名(直到用户明确说「上线了」再补充)。