01 · Architecture

系统架构总览

这一页讲清楚 Vivi 是怎么拼起来的:从用户在 Telegram 里点开 Bot、到 LLM 流式吐字、再到生成一张照片,所有组件都能在同一张图里找到位置。先看分层图,再看三条核心数据流(聊天 / 生图 / 支付)。

分层架构

客户端
Telegram WebApp
@ViviDreamsBot · iOS / Android / Desktop
Vanilla JS SPA
无构建步骤 · hash 路由
边缘 / CDN
Cloudflare Pages
vivi-app-e7m.pages.dev · 静态资源
media.softie.ai
R2 · 角色图片 / 视频(与 Softie 共用)
应用层
FastAPI
vividreams.fly.dev · ams
Telegram Bot
webhook 模式 · /webhook
业务服务
Auth
initData HMAC-SHA256
Chat
三层 Sandwich Prompt + SSE
Image
两阶段 SSE 推流
Memory
Mem0 + Qdrant 向量检索
Payment
TG Stars invoice handler
Affinity
6 级亲密度系统
Items / Gifts
礼物效果注入
Admin API
/api/admin/* 后台
数据层
Turso (LibSQL)
EU West · users / conv / txn / image_tasks
Qdrant
/app/data/mem0_qdrant · Fly volume
JSON 文件
characters.json / items.json · LRU cache
外部依赖
OpenRouter
Euryale 70B (聊天) · Qwen 2.5 7B (提取)
fal.ai
FLUX schnell + FLUX Pro · face-swap
Venice.ai
lustify-v8 NSFW · fallback 路径
Telegram Stars
付费通道
edge-tts
语音合成
观测
PostHog · 行为 + LLM trace
Sentry · 异常
Better Stack · /health 拨测
Clarity · 录屏 + 热图
Lark Bot 矩阵 · 告警

三个仓库的关系

Vivi 不是孤岛。生图和角色资产在三个仓库之间分工,新人理解这个边界能少走很多弯路:

Vivi · 主战场

ViviDreams/Vivi

Telegram Mini App 全部业务代码:聊天、商业化、亲密度、礼物、虚拟角色、实时生图直调(fal.ai / Venice.ai)。对话中触发的生图改这里。

Workflow · 批量生图

Workflow(n8n)

角色图库预生成走 n8n + ComfyUI + RunPod Serverless。Vivi 的 scripts/submit_new_prompts.py POST 到 n8n webhook。批量生图节流、模型部署改这里。

Softie · 资产源

Softieweb (Neon DB)

角色 prompt / 图片 / 视频的源数据库,Vivi 通过 scripts/ 拉取并转换成 characters.json。媒体走共享 CDN media.softie.ai

判断改动归属:实时生图问题(用户对话中按 📷 那个)→ 改 Vivi app/api/routes.py;批量生图问题(角色图库预生成)→ 改 Workflow 仓库的 n8n 工作流;新增角色 → 拉 Softieweb Neon DB 后改 Vivi 的 JSON。

核心目录结构

仓库根 CLAUDE.md 是规范来源;新人入职第一件事就是读完它。源码骨架:

Vivi/
├─ app/
│  ├─ main.py              # FastAPI 入口、lifespan、/webhook
│  ├─ config.py            # Pydantic Settings 环境变量
│  ├─ database.py          # Turso 适配 + 全部 CRUD(routes 不直接写 SQL)
│  ├─ memory.py            # Mem0 封装:mem0_add / mem0_search
│  ├─ auth.py              # Telegram initData HMAC 校验
│  ├─ api/
│  │  ├─ routes.py         # 所有 /api/* 端点(chat / store / image / admin)
│  │  └─ posthog_proxy.py  # PostHog 反向代理(绕 ISP 拦截)
│  ├─ bot/handlers.py      # /start /switch /reset 等 Bot 命令
│  ├─ llm/openrouter.py    # OpenRouter 异步客户端 + LLMMetrics
│  └─ characters/
│     ├─ data/characters.json   # 45 角色定义
│     ├─ data/items.json        # 礼物物品定义
│     └─ loader.py              # LRU cache + Your Character 注入
├─ webapp/                 # 前端(无构建,CF Pages 直接部署)
│  ├─ index.html           # Mini App 入口(注意 ?v=N 缓存破坏)
│  └─ js/
│     ├─ app.js            # SPA hash 路由
│     ├─ api.js            # HTTP 客户端 + initData header
│     ├─ bundle.js         # 唯一被加载的 JS(views/* 改完必须同步)
│     └─ views/            # 各页面模块(IIFE 封装)
├─ scripts/
│  ├─ dev.sh               # 本地启动(polling 模式)
│  ├─ daily_monitor.py     # DailyLoop 主脚本
│  └─ submit_new_prompts.py # 批量生图:POST n8n webhook
├─ tests/                  # pytest(asyncio_mode = auto)
├─ .github/workflows/
│  ├─ deploy.yml           # main → Fly.io + Cloudflare Pages 并行部署
│  ├─ daily_monitor.yml    # 每日 10:00 跑分析管线
│  └─ clarity_report.yml   # 每日 09:00 拉 Clarity 写 Bitable
├─ fly.toml                # region ams · volume vividreams_data → /app/data
└─ CLAUDE.md               # 项目规范(必读)
⚠️ 前端 bundle 陷阱:页面只加载 webapp/js/bundle.jsviews/*.js 是源文件,改完必须手动同步到 bundle.js,否则改动不生效。每次发布前还要 bump index.html 里的 ?v=N 破坏 CDN 缓存。

核心数据流:聊天消息

一条用户消息从 Telegram 到角色回复,经过这些环节:

  1. 前端发送用户在 Mini App 聊天页输入;chat.jsPOST /api/chat/send,header 附 Telegram initData(每次请求懒读,不能在模块顶层捕获)。
  2. 认证 + 计费auth.py 用 HMAC-SHA256 校验 initData;扣 1⚡(订阅用户跳过);亲密度 +2 XP;触发每日能量重置检查。
  3. 构建三层 Sandwich PromptROLEPLAY_META → 角色人设 + BEHAVIOR_GUIDE + AFFINITY_BEHAVIOR → 历史对话 → ROLEPLAY_REMINDER → 用户消息。常量在 database.py,注入在 routes.py
  4. Mem0 记忆检索mem0_search() 从 Qdrant 召回相关长期记忆,拼入 system prompt(语义检索 ≠ keyword)。
  5. 调 OpenRouter 流式SFW/NSFW 都走 sao10k/l3.3-euryale-70b;temperature 0.85;启用流式 → 后端转 SSE 推前端。LLM 调用失败必须退 1⚡。
  6. SSE 流式回写前端 EventSource 接收,逐 token 渲染(打字机效果 + 光标闪烁)。注意:reader.read() 循环 break 前必须 flush buffer,否则最后一条丢失。
  7. 持久化 + 异步写消息入 conversations 表;mem0_add() 异步抽取记忆写 Qdrant;_track_llm_metrics() 把 model / tokens / latency 推 PostHog。

核心数据流:生图(两阶段 SSE)

用户在聊天里点 📷 后看到照片的全过程:

  1. 触发前端 POST /chat/generate-image,扣 15💎,强制 is_nsfw=True(与聊天 NSFW 行为一致)。
  2. 后台 asyncio task + DB 持久化_run_image_task()image_tasks 表;前端调 GET /chat/image-task/{id}/stream SSE 监听。
  3. Phase 1: base 生图fal.ai FLUX Pro v1.1(28 步、safety_checker=False)→ 返回 URL → 下载 + JPEG 压缩 → 推 base_ready SSE → 前端立即显示模糊预览(~6-11s)。
  4. Phase 2: face-swapfal.ai face-swap(base URL + 角色 avatar URL)→ 推 done SSE → 前端替换为最终清晰图(~26s 总耗时)。422(无人脸)必须优雅降级返回 base 图,不能 raise_for_status。
  5. 角色一致性兜底_build_consistency_hint() 从角色姓名推断种族(Rivera→Latina, Khan→South Asian, Rossi→Italian 等 20+ 映射),prompt 末尾追加 "consistent character appearance"。
⚠️ NSFW 路由决策:Venice.ai 返回 data URI 会让 fal.ai face-swap 超时 111s 后 422,所以 NSFW 现在统一走 fal.ai FLUX Pro。Venice.ai 仅作 fallback 路径,但 禁止对 Venice 返回的 JPEG data URI 二次压缩(会双重压缩明显模糊)。

核心数据流:支付(Telegram Stars)

  1. 前端付费墙能量耗尽或主动充值 → 跳 #/plan#/store;前端打 plan_subscribe / gems_purchase_click 埋点。
  2. 创建 invoicePOST /api/store/create-invoice → 后端调 Telegram Bot API createInvoiceLink → 返回 invoice URL。
  3. 用户支付前端调 tg.openInvoice(url, callback);用户在 Telegram 内完成 Stars 支付。必须追踪 callback:事件名 invoice_callback,状态 paid / cancelled / failed。
  4. Bot Webhook(权威)Telegram 推 successful_payment update 到 /webhookhandlers.py 校验签名 → 加💎或激活订阅 + 首充翻倍逻辑 → Bot 发确认消息。
  5. 幂等 + 去重同一 invoice 多次回调要去重(已修过 #103);callback 失败不影响实际入账,因为 Bot webhook 才是真相。

核心数据模型

用途关键字段注意
users用户主表tg_user_id, gems, energy, energy_reset_date, subscription_expirestg_user_id 是主键,不要用 internal id
conversations聊天记录user_id, character_id, story_id, role, content三层 prompt 不入库,只入用户/AI 消息
transactions交易流水user_id, type, amount, telegram_payment_charge_id支付幂等性靠 charge_id 唯一约束
user_items已购礼物user_id, character_id, story_id, item_id, usedused=0 时下条 AI 回复触发 prompt 注入
affinity亲密度user_id, character_id, xp每条消息 +2 XP,6 级阈值 0/10/30/60/100/150
image_tasks生图任务id, status, base_url, final_urlSSE 推流靠这张表持久化进度
characters.json角色定义id, name, voice, premium, stories[3]不在 DB,LRU 缓存加载
items.json礼物定义id, name, cost, prompt_effectcost 单位是 💎

关键 API 一览

路由职责
POST /webhookTelegram Bot Webhook(命令、付费回调、群成员事件)
POST /api/chat/send聊天主入口;扣⚡;构建三层 prompt;调 LLM;SSE 流式返回
POST /chat/generate-image生图触发;扣 15💎;后台 task
GET /chat/image-task/{id}/streamSSE 推送生图进度(base_ready → done)
POST /api/store/create-invoice创建 Telegram Stars invoice
GET /api/affinity/{charId} · /api/affinities亲密度查询
POST /api/checkin每日签到 +5⚡
POST /api/settings/verify-age · /nsfw年龄认证 + NSFW 开关
POST /api/events前端埋点服务端转发(绕广告拦截)
GET /health深探 Turso / OpenRouter / fal.ai;2s 单探超时;503 + errors[]
POST /api/webhooks/betterstack · /sentry第三方监控告警 → Lark P0 卡片
/api/admin/*Admin 后台(Bearer ADMIN_SECRET),见 Operations 篇
/phog/*PostHog 反向代理(绕 ISP/DNS 拦截)

认证机制(Telegram initData)

所有 /api/* 请求都靠 Telegram WebApp 的 initData 鉴权:

← 上一级
Vivi 知识库首页