235 lines
13 KiB
Markdown
235 lines
13 KiB
Markdown
# 购买流程重构 — 分批执行指南
|
||
|
||
> 状态:方案已与用户确认(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 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`
|
||
- 理由:marketplace(Shopee/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 等
|
||
具体平台名(直到用户明确说「上线了」再补充)。
|