这一页讲清楚 Softie 是怎么拼起来的:从用户点开页面到 LLM 流式返回字、再到推送召回沉默用户,所有组件都在同一张图里能找到位置。
仓库根目录的 CLAUDE.md 是规范来源;新人入职第一件事就是把它读完。源码骨架:
softieweb/
├─ src/
│ ├─ app/ # Next.js App Router(页面 + API routes)
│ │ ├─ api/ # 后端:chat / payment / sparks / media / webhooks ...
│ │ ├─ new-chat/[id]/ # 当前活跃聊天页(旧 /chat/[id] 已重定向)
│ │ ├─ characters/ # 角色 CRUD 页
│ │ └─ admin/ # 管理后台
│ ├─ components/ # 复用 UI(Radix + shadcn/ui + cva)
│ ├─ stores/ # Zustand state(useChatV2Store 是当前的)
│ ├─ hooks/ # camelCase 自定义 hook
│ ├─ lib/ # 核心服务与工具
│ │ ├─ db/ # Drizzle schema · queries · 客户端
│ │ ├─ auth/ # Better Auth 配置 + auth-utils
│ │ ├─ payment/ # Airwallex + Stripe + 统一服务
│ │ ├─ sparks/ # 虚拟币
│ │ ├─ sse-connection/ # 流式消息
│ │ ├─ proactive/ # 主动推送
│ │ ├─ analytics/ # PostHog/GA/Clarity 封装
│ │ ├─ langfuse.ts # LLM 观测
│ │ ├─ r2-storage.ts # 对象存储
│ │ └─ redis-client.ts # ioredis 单例
│ └─ mastra/ # Mastra agents / tools / workflows
├─ scripts/ # tsx 脚本(worker 启动 / 监控 / 一次性维护)
├─ drizzle/migrations/ # 0001 ~ 0028 migration(按编号顺序)
├─ tests/
│ ├─ unit/ # vitest
│ └─ integration/ # vitest,连真 DB
├─ android/ # Capacitor Android(JDK 21 必需)
├─ .github/workflows/ # CI/CD:fly-deploy / pr-checks / cron 报表 ...
├─ fly.toml # 生产配置(已绑 NEXT_PUBLIC_* build args)
└─ CLAUDE.md # 项目规范(必读)
useChatV2Store(活跃)和 useChatStore(已废弃)。改聊天逻辑前先确认导入的是 V2,否则改的是死代码。
一条用户消息从浏览器到 LLM 再回到屏幕,会经过这些环节:
/new-chat/[id] 输入;useChatV2Store 调 POST /api/chat,附带 x-app-source: google-play(如果是 Android 客户端)。businessUser.id(UUID)做后续 DB 操作;按订阅状态判断是否本月用完免费 5 条;扣 Sparks(如适用)。flushAsync)。EventSource 订阅,边收边渲染。messagesV2 格式入库(注意:这表列名是 camelCase,其他表是 snake_case);服务端追踪 chat_message_receive 到 PostHog。extractMemoryAnchors,去重后落到 memory anchors 表,下一轮 pre-process 注入 system prompt。SubscriptionsDrawer 弹出,曝光时打 paywall_view;选套餐后调 POST /api/payment/create。/payment/success,前端打 payment_success(兜底)。/api/webhooks/*;幂等校验 → 写订单 → 更新订阅 → posthog-node 服务端打 payment_success(防前端漏报)。payment-reconcile-cron.yml 周期跑 scripts/reconcile-payments.ts,把 webhook 漏的订单补回;崩了会推 Pay-Bot。payment-health-cron.yml 每 15 分钟从 PostHog 拉成功率,低于阈值推 Pay-Bot 告警。delayed 任务由 worker 执行。频率分层 v2 上线后日均推送量 ↓60%。generate.message.ts 走 LLM 生成首条文字 / 视频问句;包 Langfuse trace(这是 95% 的 LLM 消耗)。/new-chat;离线用户走 OneSignal Push;跨设备走站内信。| 表 | 用途 | 关键字段 | 注意 |
|---|---|---|---|
users | 业务用户 | id (UUID), email, role | 不是 Better Auth 的 session.user! |
account / session / verification | Better Auth 内部 | userId (text) | session.user.id 是 text,不能直接当 UUID 用 |
characters | AI 角色(系统 + UGC) | id, ownerId, isPrivate, status | UGC 默认强制 private(API 忽略前端 isPrivate) |
messagesV2 | 当前消息表 | chatId, userId (UUID), role, content (jsonb) | 列名是 camelCase,其他表 snake_case |
orders | 支付订单 | userId (UUID), amount, status | userId 是 UUID,查询前必须翻译 |
subscriptions | 订阅状态 | userId, plan, expiresAt | Pro = 无限聊天 |
sparks_balance / sparks_ledger | 虚拟币账本 | userId, balance, txnType | 仅用于解锁付费媒体,不用于聊天计费 |
memoryAnchors | 角色记忆 | chatId, anchor, strength | 每 6 轮抽取,去重后注入 system prompt |
| 路由 | 职责 |
|---|---|
/api/chat | 聊天主入口;扣免费额度 / Sparks;调 LLM;写 messagesV2 |
/api/auth/* | Better Auth 自动挂载(不要手动模拟 cookie) |
/api/payment/create | 创建订单 → 返回支付 URL(含三级降级) |
/api/webhooks/airwallex/api/webhooks/stripe | 支付权威源;幂等;写订单 + posthog-node 打 payment_success |
/api/sparks/* | 余额查询、消费、退款 |
/api/media/*/api/upload/* | R2 直传 + 付费内容权限校验 |
/api/characters/* | 角色 CRUD(注意:UGC 强制 private) |
/api/proactive-messaging/* | 主动推送触发与状态 |
/api/admin/* | 管理后台(角色、内容审核、用户) |
Worker 是独立进程(fly.toml 中 [processes].worker),用 esbuild 把 scripts/start-worker.ts 打成 dist/worker.js 单文件。它负责:
scripts/check-queue-depth.ts)。每天 08:00 BJT 检查 waiting>200/1000、failed>50/200,超阈值推 Lark 卡片。
跑定时任务有两个地方,新人容易混:
| 类型 | 位置 | 用例 |
|---|---|---|
| BullMQ delayed jobs | worker 进程 | 主动推送、单用户调度 |
| GitHub Actions cron | .github/workflows/*-cron.yml | 支付对账、健康检查、每日报表、备份、邮件清理 |
选择规则:跟用户上下文相关 → BullMQ;纯系统级、不依赖应用进程 → GHA cron(重启 fly 不影响、跑失败有 GHA 历史可查)。