WeChat Bot Tickets(cspy / nexora-loop)
微信服务号 → 多 bot 路由 + AI 工单管理系统。线上 cspy.mvp.restry.cn(莆阳网络科技服务号 AppID
wxe780b027c2c56921,端口 3791)。项目本地路径 =
~/projects/nexora-loop(5-07 起改名)。Prod 部署 alias / 域名 / pm2 进程名 //opt/mvp-apps/cspy/路径仍叫cspy,不要被双命名搞混——本地操作走 nexora-loop,远程操作走 cspy。
2026-05-14 — PROJECT_MEMORY.md 落仓库根目录(commit 0e0e995)
把 4-25 以来这个项目所有沉淀的事实写成 PROJECT_MEMORY.md 落仓库根目录,commit 0e0e995 双 push GitHub + GitLab(双 remote origin 配双 push,一条 git push origin main 同时到 Restry/nexora-loop + nexora-vendor/nexora-loop)。HEAD 2be09fa → 0e0e995。
13 节内容(给后续接手的 agent 看的,不是给人):
- 身份/坐标(仓库、prod、本地路径、表前缀
wbt_) - 业务一句话 + Bot/Ticket/Feature 三实体
- 技术栈(Next 16 / React 19 / Prisma 6 / NextAuth v5 / pnpm / vitest)
- 凭据账号(prod admin、本地 dev 不同、MM bot id、4 个未轮换 secrets:
SEED_ADMIN_PASSWORD/MM_USER_TOKEN/NEXTAUTH_SECRET/WX_GATEWAY_SECRET) - MM bot 通讯 + WS 流式架构(双 idle 窗口、
pickLatestNonEmpty、ws-client单例) - 本地 dev 铁律(
PORT=3007必须,对齐NEXTAUTH_URL) - 部署铁律(mvp-deployer v2 BREAKING + cspy 标准 manifest)
- 部署后必验 5 条 curl
- 关键 bug 时间线(10 个 commit 从
66d1a14→2be09fa) - CI / 测试(vitest config 坑)
- AI agent 协作铁律(
zsh -ic/ prompt 文件 / PR 拆片) AGENTS.md/CLAUDE.md是 GitNexus 别手改- (略)
沉淀理由:5-10 复盘里已经记过”Hermes 项目记忆漂移把 cspy 当成 wechat-bot-tickets 老名”教训;本次把所有易漂移的事实落仓库根目录,让任何接手 agent(CC / Codex / 后续 hermes session)cat PROJECT_MEMORY.md 即可对齐,不再依赖 Hermes 自家 vault 记忆。同模式 daddy 也要求 image-studio 写一份。
2026-05-11 MM WS 流式架构上线 + SSE typing UI + dev 环境凭据踩坑(fries)
5-10 v2 流式聚合 fix(commit 58befa2,30s static window 兜底)只解决「截断」,但前端体验仍是干等 ~20s 一次性出整段。5-11 把整套底层改成 真 WebSocket + SSE 推前端 + typing 三点动画,对齐当天 Hermes 实测的 MM 真事件流(typing 每 2s 心跳、posted + post_edited 是 JSON-in-JSON)。
Commit 矩阵
| commit | 范围 | 内容 |
|---|---|---|
247b82c → 58befa2 | 5-10 兜底 | per-channel update_at + 30s window + pickFinal |
| WS 后端 | 5-11 | lib/mm/ws-client.ts MM WS 单例(lazy connect / 重连指数退避 1→30s + 抖动 / 多 listener 共享 / broadcast.channel_id 过滤 / JSON.parse(data.post) / __setWsFactory 测试注入 / MM_WS_DISABLE=1 escape hatch) + lib/mm/client.ts sendAndWait 从 polling 改 WS subscribe(subscribe 先于 postMessage 避免错过事件;typing 5s + post 1.5s 双窗口判定流式结束;MM_TRANSPORT env 切回 polling 兜底) |
aa95cc5 | 5-11 部署 | 上述 WS 架构走 deployer 部 prod,task succeeded 80s/80s/30s,pm2 cspy online 0 重启;/ 200 / /admin 307 / /onboarding 200;WS roundtrip 实测 mm.cn.restry.cn open 175ms → auth ok → sendAndWait via=ws 19.8s replyLen=21(nexora-mazu bot hhjh3ci4878pfftcbwt9yd86y) |
211e5b7 | 5-11 SSE | 新建 app/api/mm/events/route.ts SSE endpoint(admin 鉴权 + 按 botMmUserId 解析 DM channel + 转发 ws-client 事件 + 15s 心跳;测试 9/9) |
fa0473d | 5-11 UI | 三面板(Bots _chat-panel.tsx / Tickets consult-panel.tsx / Features actions.ts → render)接 EventSource + typing 三点动画 + 流式追加渲染;新增 lib/mm/use-mm-stream.ts hook(3s typing 超时) |
2be09fa | 5-11 重构 | bots/features「装样子」事故修复 —— fa0473d 给三面板都装了 SSE 装饰,但 bots _chat-panel.tsx:169 仍 await sendMmDmAction(...) 同步阻塞 ~20s 才一次性出回复,SSE hook 只填一个孤立 stream-bubble;features 同病。唯一真流式参考实现是 consult-panel:streamingKey=clientMessageId 控制 SSE,server action void (async () => {...})() 在 background 跑不阻塞 UI。本 commit 照抄 consult-panel 模式重构 bots + features handleSend 路径,prod task 46s 部署 succeeded;线上自验 / 200 / /admin 307→login / /onboarding 200 / SSE 无 cookie 401(鉴权生效) |
未结清的安全债:5-11 末确认 4 个 prod secret 自 5-10 第一次部署 manifest 泄露至今仍未轮换——
SEED_ADMIN_PASSWORD/MM_USER_TOKEN/NEXTAUTH_SECRET/WX_GATEWAY_SECRET。下次接手 cspy 第一件事就是轮换这 4 把。
Dev 环境踩坑两条(必入项目级 memory)
- NEXTAUTH_URL 端口必须等于 dev server 监听端口 —— pnpm dev 默认 3000,但
.env.local里NEXTAUTH_URL=http://localhost:3007。authorize callback POST 走 3007 →CredentialsSignin循环;用户表面看是「密码错」其实根本没下发 session token。修法:要么pnpm dev -- -p 3007,要么改NEXTAUTH_URL到 3000;二者必须严格一致。 - 本地 admin 账号 ≠ prod 账号 —— prod 是
admin@example.com / Bk7mQ2nR9xF4vTpL(见 vaultcspy/SEED_ADMIN_PASSWORD),本地 dev DB 是test@nexora.com / dev@local。Hermes 之前用 prod 凭据本地登一直 401;reset 脚本能造任意已知密码 dev admin(不要在 prod 跑)。
post-code-change 行为验证 = 派 CC 跑 browser-agent,不是 Hermes 自己 puppeteer
skill bnef-claude-code-verify-loop 的双轮规矩这次踩了一次:dev server 起 IPv6 *:3000,browserbase 云端浏览器解析 IPv4 失败 → 必须用本地 headless Chrome(skill headless-chrome-screenshot-verify)。流程:cookie 登录拿 session → 本地截图三个面板。CC E2E 跑了 4 个 case:CASE-1 SSE endpoint 健康 + 鉴权(Hermes 自己 curl 已 PASS);CASE-2/3/4 三面板 typing + 流式渲染。
2026-05-10 MM 切换上线 + Next 16/React 19 + bot 大清理 + 流式协议事故复盘(fries)
一日三大件:MM bot 镜像切换上 prod、Next 16 + React 19 + Turbopack 全 prod 跑起来、OpenClaw block streaming 协议建模错误两轮才修对。
部署 + admin 重置
gitlab/dev 5 commit fast-forward 进 main,origin 配双 push 一条命令同时到 GitHub Restry/nexora-loop + GitLab nexora-vendor/nexora-loop。走 deployer zip 部署 cspy,2 条新 migration(add_bot_mm_user_id + add_mm_bot_user_id_unique)apply。Prod admin 密码丢,直接 base64 注入 reset 脚本走 /api/exec 重置,admin@example.com 新密码进 vault。
Bot 大清理:18 MM 镜像 + 1 系统硬引用 = 19
「从 MM 同步」按钮把 14 个 mm.cn.restry.cn bot 镜像进 prod(内部 tab 10 → 28),随后批量删 13 个老 bot(mmBotUserId=null 且无引用),把另外 4 个有业务引用的迁到 MM 镜像(conv 6 + feat 1 + tick 5 + wxu 3 = 15 条引用全迁,0 skip)。修正 4 个 MM 镜像 role:Nexora Mazu → engineer,三个 CS bot → customer_service。最后删 legacy cspy engineer bot(2 个孤儿 feature 失去 assignee,admin 自行 /admin/features 重派)。
| 类别 | 数 |
|---|---|
| MM 镜像 bot | 18(全 active) |
| 系统硬引用 | 1(__system_manual_bot__,代码里硬编码,不能删) |
| 客服(customer_service) | 3(Nexora CS / Mazu CS / Xipu CS) |
| 工程师(engineer) | 1(Nexora Mazu) |
| 内部 | 15 |
老单据照常工作的核心机制(trace 三个字段):
wechat_user.boundBotId→lib/wechat/handler-text.ts:46-69用户消息找/建 conversationconversation.botId→lib/bot/chat-client.ts:6-9callBotChat第一步if (bot.mmBotUserId) return ...走 MM 链路feature/ticket.assigneeBotId→ consult / 工程师升级路径
只要新机器人 mmBotUserId 有值,路由就不变。
MM client 流式聚合 bug 两轮修复(commits 247b82c → 58befa2)
错误模型 v1:以为 OpenClaw 是流式 edit 同一条 MM post,sendAndWait 抓「第一条 message 非空的 post 立即 return」→ 长回复被截断到几十字符。
v1 修复(错):换 Map by post.id + 按 update_at 5s 静默窗口检测 + 超时兜底。commits 547e5a6 + e24d1e7 + 247b82c,build/test 绿,部署上线后还是被截断。
真相(用真 MM API 验证):bot 不是 edit,是发 12 条独立 post,全部 create_at 同一毫秒,前 11 条 message="" 空 post,只有最后 1 条带真实内容(370 char)。流式中每条又会被 edit 多次(edit_at 持续 19s 后稳定)。
根因来自 OpenClaw 官方文档(https://openclaw.ai/concepts/streaming + /channels/mattermost):block streaming 协议 —— assistant 流式输出按 block flush 成多条独立 channel 消息(不是 token-delta、不是 edit),用 coalescing.idleMs 作为 idle 信号判断流式结束。mattermost.streaming 默认 off 只发 final,streaming.mode: "block" 时多条独立消息。
v2 修复(commit 58befa2):
- 整 channel 维度跟
max(update_at)—— 任何 post 的 update 都算「还在动」 - 30s 静默窗口(远大于 OpenClaw 默认 idleMs,留余量)
pickFinal:按update_at倒序找第一个trim().length > 0的 post —— 多 chunk 中真正最后填好的那条- 超时兜底:deadline 到了仍 pickFinal,找到就 return ok(标
viaTimeout: true) - 加
streamIdleMs测试 knob,3 个调用点(actions.ts/consult/index.ts/bot/chat-client.ts)签名不变向后兼容 - 每轮 poll 打
[mm.poll]含botPosts/maxUpdate/idleForMs/latestWithBodyLen
| v1(错) | v2(对) | |
|---|---|---|
| 模型 | bot edit 同一条 post | bot 发多条独立 post |
| 静默判定 | 单 post.message 5s 不变 | 整 channel 30s 无活动 |
| final reply | 第一条 trim 非空 | 按 update_at 倒序、第一个非空 |
Feature/ticket bot 提示词减肥(commit e24d1e7)
之前 buildHistoryDigest 把一堆背景消息往 prompt 里灌——bot 背后是 OpenClaw agent,本身有持久记忆 + MM DM channel 历史,digest 是冗余垃圾。删整个 helper,reply 改成单行前缀 [feature:${title}]\n${userMessage}。kickoff 路径不动。
经验沉淀
- 建模未验证就上线 = 修两轮:v1 抓首条改成 5s 静默还是错,因为根本不是 edit 模型。下次「跨服务流式协议」类 bug 必须先用真接口抓 raw post 列表验证模型,再写代码。
- OpenClaw 是公开项目(openclaw.ai),不是 Daddy 私有——Hermes 误把它当用户自有项目去 grep
~/projects/,浪费一轮排查。已修记忆。 - Hermes 项目记忆漂移:旧条目把项目名记成
cspy/wechat-bot-tickets,5-07 改名 nexora-loop 后没同步,加上 prod 仍叫 cspy → find 找不到本地目录就走/api/exec远程读 + 又 clone 一份。蠢叠加蠢。已写死:项目本地 =~/projects/nexora-loop,prod alias 仍叫 cspy。 - rolldown darwin-x64 binding 进 devDep(commit
547e5a6):本地 node 是 x64,pnpm 默认装 arm64 binding 跑不了 vitest。pnpm的os/cpu字段会让 linux server install 时不下载 darwin binary,零成本换可重复性。
2026-05-09 cspy bot 旁路 commit 同步 + server component 边界事故(fries)
之前 cspy bot(自动改 nexora-loop 仓库的客服 bot)只推到 GitLab dev 分支,GitHub main 没同步——线上部署始终走 GitHub main,所以”项目列表卡片重设计”这条 GitLab/dev 上的 commit 一直没上线。fries 把 gitlab/dev merge 进 main、build 通过、双远程 push 到 8e86bb1、走 deployer 部署,刷新 /admin/projects 直接 500。
根因:bot 改的 app/admin/(authed)/projects/page.tsx 在 Next.js server component 里写了 onClick={(e) => e.preventDefault()}——server component 不能挂客户端事件,编译期 RSC 报错。修法(派 CC 走 claude-opus-4.7-1m-internal --effort medium):
- 新建
app/admin/(authed)/projects/_card-actions.tsx作为 client component,把onClick包装搬进去 page.tsx用<CardActions>替换原<div>,server action form /DeleteProjectButton作为 children prop 传入——server action form 通过 props 传给 client children 是合法组合- 通读
page.tsx确认无其他useState/useEffect/onChange客户端用法
铁律:bot 自动写代码必须双向 push(GitHub + GitLab),且 bot 出的代码不能直接上线,要么人工 review 要么至少 build + smoke。这次 build 居然通过、运行时才炸,是因为 server component 边界在某些场景只在请求时才报。
2026-05-08 B8 关联 Bot + 同步 P2011 + 工程师指派放宽 + admin 慢真凶
继 W8–W12 后,5-08 同日补一组项目-bot 关联与稳定性修复。
B8 「关联 Bot」按钮 modal(commit a2b4f06)
/admin/projects/[code] 右上角加「关联 Bot」按钮 → modal 列出 projectId IS NULL 的 bot 多选关联到当前项目。之前 bot ↔ project 只能通过同步流程隐式建立,孤儿 bot 无入口认领。
syncRelayAgents P2011 fix
db.bot.create 漏掉必填 categoryTags: String[](schema 是 NOT NULL 数组),同步时新 bot 全部 P2011 报错。补默认空数组写入。
工程师指派放宽
原逻辑 parentCustomerServiceId: t.conversation.botId 严格匹配——只有”该客服 bot 直接挂的 engineer”才能指派;现实里多个客服 bot 共用同一池工程师。改为:所有 role=engineer && active=true 的账号都可被指派,不再按客服 bot 过滤。
admin 慢真凶 = Kuma snapshot React.cache miss
之前怀疑 admin layout 慢,一通排查后定位:layout 自身 20–40ms 健康,每次跳页慢的是顶部 Kuma 监控小卡——unstable_cache 60s 之外的请求要重拉 Uptime Kuma /metrics socket.io,单次 5–6s。W12 旁路加的 unstable_cache(60s) 已经把命中路径压到 < 50ms,剩下的 miss 是 cold-start 必交税,不再继续优化。
2026-05-08 Feature 模块 W8–W12(ProjectNote + Feature/FeatureEvent + UI 对齐 ticket)
继 5-07 nexora-loop 化之后,5-08 一天连发 W8 → W12 五轮,把”工单”之外的项目笔记 + 功能需求 + 状态机 + 工程师指派做成与 ticket 平行的第二条业务线,给莆阳本地化项目(囍铺/鸿星/莱雅/妈祖等)当甲方需求池用。
W8 — ProjectNote schema 落地
新增 ProjectNote 表,字段含 kind(4 类)+ projectId + body + metadata JSONB + createdBy/At + soft-delete deletedAt。
| kind | 用途 |
|---|---|
meeting | 会议纪要 |
requirement | 需求条目 |
decision | 决策记录 |
risk | 风险/阻塞 |
复用 cspy 现有的 NextAuth admin 权限,/api/notes/* CRUD + /admin/notes 列表页。
W9 — Feature + FeatureEvent 状态机
Feature = 比 ticket 更轻量的需求颗粒(“做个会员卡功能”),FeatureEvent = 状态变更/讨论流水。状态机:
draft → planned → in_progress → in_review → done
↘ blocked ↘ rejected
每次状态变更写一条 FeatureEvent,附 actor + 旧/新 state + 备注,admin 详情页时间轴渲染。
W9.1 – W9.4 — 排版四连改
W9 上线后 Daddy 当场截图反馈”间距/对齐/字号四处别扭”。四个 commit 全是 CSS + Tailwind 微调:
- W9.1 详情页两栏 → 单栏,事件流移到右侧 sticky drawer
- W9.2 status pill 颜色对齐 ticket 配色(去自定义、用 design token)
- W9.3 列表页 sortable column header + skeleton row
- W9.4 mobile breakpoint 表格转卡片,跟 05-03 ticket 移动端响应式对齐
W10 — priority + soft-delete
Feature.priority 枚举 low / normal / high / urgent,列表/看板按 priority 排序;soft-delete 走 deletedAt,admin 列表有”显示已删除”toggle。删除/恢复均写 FeatureEvent。
W11 — 视觉与 ticket 对齐
把 Feature 详情页所有 chip / pill / button / spacing 与 /admin/tickets 详情页统一,同一个用户在两个表之间切换不再”换了个 app”。沉淀 design token wbt/ui/status-chip、wbt/ui/page-shell,后续模块都直接复用。
W12 — user-bubble + 2h bot timeout + 放宽 engineer 指派
- user-bubble:详情页 FeatureEvent 流换成左右气泡(user 左 / bot 右),消息体 markdown 渲染
- 2h bot timeout:单条用户输入超过 2h 没等到 bot 回复就标
timeout并允许人工接管,避免 bot 卡死时 ticket 永远停在executing - engineer 指派放宽:原来只允许指派给
role=engineer的内部账号;改为只要WechatUser.bound=true就能指派,配合莆阳现场让外部供应商账号也能进 feature 工作流
旁路改进
unstable_cache把 Kuma 快照缓存 60s:admin 首页有个 Kuma 监控小卡,原来每次刷页都拉 Uptime Kuma/metrics~800ms。unstable_cacherevalidate 60s 之后缩到 < 50ms(详见 monitoring-and-cron Kuma 章节)- 15 条 cspy 项目记忆:把 4-25 起项以来的所有 secret 速查 / 部署陷阱 / parseScene 教训 / app name 别带下划线等沉淀进 cspy 项目级 memory
2026-05-07 演进为 nexora-loop + GitLab 双 remote + Hermes 打包接管
5-07 起项目从「单一 cspy 服务号工单」升级为通用 nexora-loop 客户回路:同一套 ticket 路由 / agent dispatch / 客服窗口管理逻辑被抽取为可复用模块,cspy 只是其中一个 channel。
GitLab 双 remote 推送
接入 nexora 自建 GitLab (gitlab.nexora.restry.cn,SSH 端口 2222),做内部源码灾备:
git remote add nexora ssh://git@gitlab.nexora.restry.cn:2222/nexora-loop/wechat-bot-tickets.git
git push nexora dev # 主流程,dev 分支双推
git push origin dev # 同步 GitHubvault keys(fries-mac 门神 vault):
| key | 值 |
|---|---|
gitlab-nexora/ROOT_PAT | root user 的 PAT,用于 API 创建 repo / 改权限 |
gitlab-nexora/SSH_KEY | 部署用 deploy key 私钥 |
gitlab-nexora/CLONE_URL | ssh://git@gitlab.nexora.restry.cn:2222/nexora-loop/... 模板 |
不要把 GitLab PAT 跨 vault namespace 复用;nexora-loop 系内的所有项目共用
gitlab-nexora/*命名空间。
Hermes 打包接管
Hermes 把 nexora-loop 的部署打包/分发流程接过来:本地 build → vault get 注入 secrets → tar → rsync 到 mvp-deployer 主机 → pm2 restart。脚本入 skill nexora-loop/deploy,避免敏感字段进入 repo / log。
概述
2026-04-25 起项,04-26 上线生产 + WeChat OAuth snsapi_userinfo。05-02 接入第二公众号网关 wx-gateway-pucs(莆阳)做 admin 扫码登录,05-03 完成集中 access_token + 移动端响应式 + PII bug 修复。
架构约束
来自微信服务号的三个绕不过的物理约束:
- 5 秒同步回复 → 被动回复立即返”正在思考…”,bot 调用走异步,结果用客服消息接口推回
- 48 小时客服消息窗口 → 用户最后一次发消息后 48h 内才能主动推
- HTTPS + AES 加解密 → 必须 cloudflared 隧道或自有 HTTPS
技术栈
| 项 | 值 |
|---|---|
| 路径 | ~/projects/nexora-loop/(5-07 改名;prod 仍叫 cspy) |
| 框架 | Next.js 16 + React 19 + Turbopack(5-10 升级,prod 跑通)+ Prisma v6(v7 强制降级) |
| DB | tiger-host PostgreSQL,wbt_ 前缀 + 独立 wbt schema 避开跨 schema 外键 |
| Auth | NextAuth v5(拆 auth.config.ts edge-safe + auth.ts 含 bcrypt)+ wx 扫码 provider(莆阳网关) |
| UI | Radix + 手写组件(shadcn@latest 默认 Tailwind v4,跟项目 v3 不兼容) |
| 设计风格 | OpenAI 极简单色(黑/白/灰 + Inter/söhne + 大留白 + 无阴影 + 无斑马纹) |
工单状态机
new → analyzed → awaiting_confirm → executing → done / need_human / failed,awaiting_confirm 强制人工二次确认。Bot 抽象 TicketGenerator / callBotChat / analyze / execute,后期换实现不动业务代码。
M1–M5 完工 53/53 task(04-25 → 04-26)
完整规划 docs/plans/2026-04-25-wechat-bot-tickets-tasks.md(76KB / 2488 行)。M1 骨架+回调+AES+客服 / M2 bot routing+5s 占位 / M3 后台 UI / M4 AI 三步处理 / M5 部署。
实施踩坑
- Prisma 7 → 教 AI 别用,降回 v6
- 跨 schema 外键 introspection → CC 自己加
?schema=wbt - shadcn@latest 默认 Tailwind v4 → 改手写 Radix
- NextAuth v5 + Edge middleware:bcrypt 不能在 Edge 跑 →
auth.config.ts拆 executeTicket用 HTTPres.ok而非 bodyok判成败 → 真接口会误判- CC 用 puppeteer MCP 替代 browser-agent:派单 prompt 要精确告诉怎么调
04-26:WeChat OAuth 接入(拿真 nickname/avatar)
最初版本 admin 后台用户表 nickname 全空。诊断:cgi-bin/user/info 返 subscribe_time/scene/tagid ✅ 但 nickname/headimgurl/sex/city/province/country ❌ 全空。
根因不是代码 bug,是平台政策:微信 2021/2022 起所有公众号 cgi-bin/user/info 不再返 PII。服务号特权:可以走 OAuth snsapi_userinfo 网页授权拿全 profile,订阅号只有 snsapi_base。
Schema + ensureWechatProfile
migration 20260426200000_wechat_profile_fields + oauth_fields:headimgurl / sex / city / province / country / language / subscribe_time / subscribe_scene / qr_scene / qr_scene_str / tagid_list / remark / raw / fetchedAt / oauthCompletedAt / lastOauthPromptAt。
ensureWechatProfile 能拿啥存啥,按 fetchedAt 节流。Admin /admin/users 加头像缩略 + 性别地区列;详情页含「刷新微信信息」按钮 force 重拉。
OAuth 流路由
GET /api/wechat/oauth/start→ 拼https://open.weixin.qq.com/connect/oauth2/authorize?...&scope=snsapi_userinfo&...GET /api/wechat/oauth/callback→ code 换 access_token、调sns/userinfo、写 profile、回授权成功页 + 主动推客服消息- 关注/文本 dispatcher 自动发”完善个人信息”链接,24h 节流(
lastOauthPromptAt) - env:
OAUTH_STATE_SECRET+PUBLIC_BASE_URL
微信公众平台一次性配置:业务域名和网页授权域名都填 cspy.mvp.restry.cn,校验文件 MP_verify_lYeEpPpBsX5NgdyD.txt(Next.js standalone build runtime 不扫 public/ → 用 API route 直接返字符串)。
access_token 双层缓存陷阱(生产 bug 复盘)
OAuth 上线后用户发消息只回”正在思考”。dispatcher 报客服 40001 invalid credential。链路:
- 微信 IP 白名单刚加完时
getAccessToken一直 40001 — 旧调用残留 cache - 重启 PM2 进程清掉内存 cache 后还是 40001
- 真因:代码先查 DB cache,DB 里那条是白名单加之前拉的失效 token
- 修复:
DELETE FROM wechat_access_token清 DB cache → 重新拉
教训:双层 cache(内存 + DB)必须一并失效。05-03 后已切到网关集中托管,不再有这层 cache。
04-27:onboarding 表单 + publicBase 反代修复
/onboarding 页面 + bind API
OnboardingForm(shadcn 复用 admin/login 视觉壳)+/onboarding/success落地页- migration 给
WechatUser加storeName String? POST /api/onboarding/bind— zod +prisma.$transaction写 storeName + 发”绑定成功”客服消息(fire-and-forget)- 关注/文本 dispatcher 改为发 OAuth 完成链接(旧”回复数字选 bot”流程废弃)
OAuth callback publicBase(req) helper
之前 req.url 拼回跳地址在反代后取到 localhost:3xxx → 浏览器 ERR_CONNECTION_REFUSED。helper 优先 PUBLIC_BASE_URL env,否则 x-forwarded-host/-proto(Caddy 已塞)。
05-01:Bots 重设计 + 7 项 polish(commits 99a1c7b d72a50f f387980)
派 CC proc_fc845e3c2182:bots 卡片重设计(avatar+状态灯/角色 chip/agentId/3 列指标/Onboarding 开关/标签/操作),1440 宽 3 列响应正常。Tickets segmented tabs warm-bg + 300ms debounce 搜索 + engineer 过滤 chip + 合并 modal;dashboard 工程师卡可点;users 解绑入口;settings tab 切换动效。Smoke 200/200/200,18 env keys 全量回传。
6 NEEDS-DECISION 待办:客服 bot 全 active=false / relay metadata / 老用户 storeName 空 / dev-only cookie 路由 / BindingState dead code / 管理端登录态。
05-02:接入第二公众号 wx-gateway-pucs(莆阳)做 admin 扫码登录
Step 1 注册 app(commit dfb3389)
curl -X POST https://wxmsg.mvp.restry.cn/apps/register \
-H "X-Invite-Code: a37c23f096a559596e3f37e381034c0a" \
-d '{"name":"cspy_admin","appBaseUrl":"https://cspy.mvp.restry.cn"}'Secret 救回事件:第一次 register 输出被 Hermes mask 层吃了,邀请码 usesLeft=1 已耗尽。突破:网关 DB App.secret 是明文存储,通过 /api/exec/wx-gateway-pucs 跑 prisma 查询 → base64 编码绕过 mask → vault add cspy/WX_GATEWAY_SECRET --from-stdin,全程 secret 0 次进 stdout。教训:以后第一次拿 secret 就 pipe 进 vault 不打印。
Step 2 finalize handler
派 CC proc_770c442196f1:加 wechatOpenid 字段 + admin@example.com 绑 openid o7UTV1yk6eC_OxrbB_oY0Q2G7qOM;NextAuth 加 wx provider(HMAC verify + timingSafeEqual + 5min skew);/api/wx/finalize GET handler;login 页右半边加扫码组件。task cbb1e0e67574eb7f,23 env keys 全量回传。
parseScene lastIndexOf bug 修复(commit 0940f95)
扫码后没跳转,公众号回 欢迎使用 Nexura 莆阳。。根因:
- token =
${appName}_${random}=cspy_admin_<32hex>(appName 自带下划线) parseScene用indexOf("_")切 → appName=cspy(不存在)+ token=admin_<hex>getAppAsync("cspy")null → 走 fallback 文案 → 不更新 wxLoginToken → 业务方永等不到 confirmed
修:网关 app/wx/callback/route.ts 改 lastIndexOf("_")(random 后缀纯 hex 无下划线,从尾切一定对)。wx-gateway-pucs 重部署 task 8e8ae6791b10701e。沉淀教训:app name 别带下划线(已写入 skill)。
05-03:fanout 路由 → cspy(非扫码消息转发)
网关收到非扫码文本 fallback 欢迎使用 Nexura 莆阳。。两步修:
- 加 fanout 路由:网关 catchall 转发非扫码消息到业务方 webhook
- endpoint URL 修正:原配
https://cspy.mvp.restry.cn/wx/callback(cspy 没这路由 → 404),改成/api/wechat(cspy 实际 dispatcher)。运行时读 DB 不用 redeploy
cspy 的 POST 返 "success" plaintext(dispatcher 异步走客服 API),微信接受 success 作为”稍后客服 API 回复”信号。fanout 4 条记录:第 1 条 23:14:46 → 404;改 endpoint 后 3 条全 200 ~70ms。
05-03:集中 access_token(commit 25c9f1f,drop 本地 cache)
Token 撞车隐患:网关和 cspy 共用 wxe780b027c2c56921 + 同一 secret 各自调 cgi-bin/token,谁后拉谁的生效,对方手里那个变废纸 → 40001 invalid credential。
修法(“别人撸的”)—— 网关侧 commits d6f2c1c + fb7441b:
- 网关暴露
/internal/wx-token(HMAC + pg_advisory_xact_lock 防并发拉双份) - 100min cron 自动续期 + 内置 selftest
cspy 侧 lib/wechat/access-token.ts 改成调网关,drop wbt_access_token_cache 表(migration 20260503000000_drop_access_token_cache),保留 60s mem cache 避免每条消息都打网关。补 env WX_GATEWAY_INTERNAL_BASE + INTERNAL_TOKEN_SECRET(vault cspy/INTERNAL_TOKEN_SECRET,64 hex)。
05-03:移动端响应式(commit fca5e72 + a951d0b)
派 CC proc_b68d2c72ef28(opus-4.7-1m-internal high effort)。9 个 page.tsx 全部适配(dashboard / tickets / bots / users / settings / login / conversations 等):sticky header + 汉堡菜单、KPI 单列、tabs 横滚、表格转卡片、详情单列。截图 21 张落 /tmp/cspy-resp/。
「刷新微信信息」覆盖 PII bug 修(commit a951d0b)
cgi-bin/user/info 对未认证服务号返空字符串 nickname/headimgurl,覆盖了 OAuth 拿到的真值。修 lib/wechat/profile.ts persistProfile():PII 字段(nickname/headimgurl/sex/city/province/country/language)仅非空时才写;订阅元数据正常更新。「刷新微信信息」按钮下加提示「仅刷新订阅元数据,头像昵称需用户重新触发 OAuth」。
4 个 secret 速查(已入 memory)
| Vault key | 用途 |
|---|---|
cspy/DATABASE_URL | DB 连接 |
cspy/WX_GATEWAY_SECRET | wx-gateway-pucs HMAC(finalize/扫码登录) |
cspy/INTERNAL_TOKEN_SECRET | /internal/wx-token HMAC(集中 access_token) |
cspy/AUTH_SECRET | NextAuth |
⚠️ WX_GATEWAY_SECRET ≠ INTERNAL_TOKEN_SECRET,别混。已写进 skill cspy-wechat-bot-tickets-deploy(25 env keys + 微信公众号回调链路图 + cspy ↔ 网关分工说明 + parseScene lastIndexOf 修复 + app name 别带下划线告警)。
部署:MVP Deployer 闭环
整个上线 0 SSH(除手工建 PG 库 cspy 那一步因 deployer /api/db/provision 还没上线)。9 commit 分两批部署、5 migrations 全量 apply、40/40 测试绿。流程同步反推 deployer v3 加 /api/exec + /api/db/provision + manifest initOnce/postDeploy。
相关页面
- deployer — 部署平台
- wx-gateway — 共享网关(造悟者 + 莆阳两实例 wx-gateway-pucs)
- fries-mac — 开发主机