01 · Architecture

系统架构总览

这一页讲清楚 Softie 是怎么拼起来的:从用户点开页面到 LLM 流式返回字、再到推送召回沉默用户,所有组件都在同一张图里能找到位置。

分层架构

客户端
Web
www.softie.ai
Capacitor Android
Google Play / 官网 APK
Capacitor iOS
未上架
边缘 / CDN
Cloudflare
DNS + CDN(橙云)+ R2 自定义域
media.softie.ai
R2 公共桶 → 媒体 CDN
应用层
Next.js 15 App Router
SSR + API Routes (Fly.io · LAX)
BullMQ Worker
esbuild bundle · dist/worker.js
业务服务
Better Auth
Email + Google OAuth
Mastra Agents
AI 工作流 src/mastra/
Sparks Service
虚拟币
Payment Service
Airwallex 主 + Stripe 备
Media Service
付费媒体权限
SSE Connection
流式消息
Proactive Push
主动推送
Content Moderation
审核
数据层
PostgreSQL
Drizzle ORM · 28+ migrations
Redis
SSE pub/sub · BullMQ · 分布式锁
Cloudflare R2
S3 兼容 · 媒体存储
外部依赖
OpenRouter
主 LLM 网关
Google AI / Groq
备用 / 轻量任务
fal.ai
媒体生成
OneSignal
Push 通知
Resend
邮件
观测
Sentry · 错误
PostHog · 行为
Langfuse · LLM Trace
Clarity · 录屏热图
Lark Webhooks · 告警

核心目录结构

仓库根目录的 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                # 项目规范(必读)
⚠️ 命名陷阱:聊天有两套 store —— useChatV2Store(活跃)和 useChatStore(已废弃)。改聊天逻辑前先确认导入的是 V2,否则改的是死代码。

核心数据流:聊天消息

一条用户消息从浏览器到 LLM 再回到屏幕,会经过这些环节:

  1. 前端发送用户在 /new-chat/[id] 输入;useChatV2StorePOST /api/chat,附带 x-app-source: google-play(如果是 Android 客户端)。
  2. API Route 验权 + 计费Better Auth 校验 session;查 businessUser.id(UUID)做后续 DB 操作;按订阅状态判断是否本月用完免费 5 条;扣 Sparks(如适用)。
  3. 调 LLM(AI SDK v5)主模型:OpenRouter;轻量任务:grok-4-fast;记忆/工具:Mastra agent。所有调用包裹 Langfuse trace(含 flushAsync)。
  4. SSE 流式回写Worker 把 token 写到 Redis pub/sub channel;前端走 EventSource 订阅,边收边渲染。
  5. 持久化消息以 messagesV2 格式入库(注意:这表列名是 camelCase,其他表是 snake_case);服务端追踪 chat_message_receive 到 PostHog。
  6. 记忆抽取每 6 轮触发 extractMemoryAnchors,去重后落到 memory anchors 表,下一轮 pre-process 注入 system prompt。

核心数据流:支付

  1. 前端 paywallSubscriptionsDrawer 弹出,曝光时打 paywall_view;选套餐后调 POST /api/payment/create
  2. 三级降级① Airwallex SDK 嵌入式;② SDK 失败 → URL 直跳;③ Airwallex 整体不可用 → Stripe 兜底。三层都失败才返回错误。
  3. 用户支付跳第三方 hosted page;完成后回到 /payment/success,前端打 payment_success(兜底)。
  4. Webhook(权威源)Airwallex/Stripe 推送到 /api/webhooks/*;幂等校验 → 写订单 → 更新订阅 → posthog-node 服务端打 payment_success(防前端漏报)。
  5. Reconcile CronGHA payment-reconcile-cron.yml 周期跑 scripts/reconcile-payments.ts,把 webhook 漏的订单补回;崩了会推 Pay-Bot。
  6. Health Cronpayment-health-cron.yml 每 15 分钟从 PostHog 拉成功率,低于阈值推 Pay-Bot 告警。

核心数据流:主动推送(Push)

  1. 定时调度BullMQ delayed 任务由 worker 执行。频率分层 v2 上线后日均推送量 ↓60%。
  2. 角色选择 + 文案生成generate.message.ts 走 LLM 生成首条文字 / 视频问句;包 Langfuse trace(这是 95% 的 LLM 消耗)。
  3. 多路径分发在线用户走 SSE 直推到 /new-chat;离线用户走 OneSignal Push;跨设备走站内信。
  4. 过滤兜底新用户角色为空时使用 fallback 角色;Pro 用户漏发的 follow-up bug 已修;空角色 + 在线用户 + 角色数都有降级路径。

核心数据模型

用途关键字段注意
users业务用户id (UUID), email, role不是 Better Auth 的 session.user!
account / session / verificationBetter Auth 内部userId (text)session.user.id 是 text,不能直接当 UUID 用
charactersAI 角色(系统 + UGC)id, ownerId, isPrivate, statusUGC 默认强制 private(API 忽略前端 isPrivate)
messagesV2当前消息表chatId, userId (UUID), role, content (jsonb)列名是 camelCase,其他表 snake_case
orders支付订单userId (UUID), amount, statususerId 是 UUID,查询前必须翻译
subscriptions订阅状态userId, plan, expiresAtPro = 无限聊天
sparks_balance / sparks_ledger虚拟币账本userId, balance, txnType仅用于解锁付费媒体,用于聊天计费
memoryAnchors角色记忆chatId, anchor, strength每 6 轮抽取,去重后注入 system prompt

关键 API Route 一览

路由职责
/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/*管理后台(角色、内容审核、用户)

BullMQ Worker

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 jobsworker 进程主动推送、单用户调度
GitHub Actions cron.github/workflows/*-cron.yml支付对账、健康检查、每日报表、备份、邮件清理

选择规则:跟用户上下文相关 → BullMQ;纯系统级、不依赖应用进程 → GHA cron(重启 fly 不影响、跑失败有 GHA 历史可查)。

← 上一级
Softie 知识库首页