这一页讲清楚 Vivi 是怎么拼起来的:从用户在 Telegram 里点开 Bot、到 LLM 流式吐字、再到生成一张照片,所有组件都能在同一张图里找到位置。先看分层图,再看三条核心数据流(聊天 / 生图 / 支付)。
Vivi 不是孤岛。生图和角色资产在三个仓库之间分工,新人理解这个边界能少走很多弯路:
Telegram Mini App 全部业务代码:聊天、商业化、亲密度、礼物、虚拟角色、实时生图直调(fal.ai / Venice.ai)。对话中触发的生图改这里。
角色图库预生成走 n8n + ComfyUI + RunPod Serverless。Vivi 的 scripts/submit_new_prompts.py POST 到 n8n webhook。批量生图节流、模型部署改这里。
角色 prompt / 图片 / 视频的源数据库,Vivi 通过 scripts/ 拉取并转换成 characters.json。媒体走共享 CDN media.softie.ai。
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 # 项目规范(必读)
webapp/js/bundle.js,views/*.js 是源文件,改完必须手动同步到 bundle.js,否则改动不生效。每次发布前还要 bump index.html 里的 ?v=N 破坏 CDN 缓存。
一条用户消息从 Telegram 到角色回复,经过这些环节:
chat.js 调 POST /api/chat/send,header 附 Telegram initData(每次请求懒读,不能在模块顶层捕获)。auth.py 用 HMAC-SHA256 校验 initData;扣 1⚡(订阅用户跳过);亲密度 +2 XP;触发每日能量重置检查。database.py,注入在 routes.py。mem0_search() 从 Qdrant 召回相关长期记忆,拼入 system prompt(语义检索 ≠ keyword)。sao10k/l3.3-euryale-70b;temperature 0.85;启用流式 → 后端转 SSE 推前端。LLM 调用失败必须退 1⚡。EventSource 接收,逐 token 渲染(打字机效果 + 光标闪烁)。注意:reader.read() 循环 break 前必须 flush buffer,否则最后一条丢失。conversations 表;mem0_add() 异步抽取记忆写 Qdrant;_track_llm_metrics() 把 model / tokens / latency 推 PostHog。用户在聊天里点 📷 后看到照片的全过程:
POST /chat/generate-image,扣 15💎,强制 is_nsfw=True(与聊天 NSFW 行为一致)。_run_image_task() 写 image_tasks 表;前端调 GET /chat/image-task/{id}/stream SSE 监听。safety_checker=False)→ 返回 URL → 下载 + JPEG 压缩 → 推 base_ready SSE → 前端立即显示模糊预览(~6-11s)。done SSE → 前端替换为最终清晰图(~26s 总耗时)。422(无人脸)必须优雅降级返回 base 图,不能 raise_for_status。_build_consistency_hint() 从角色姓名推断种族(Rivera→Latina, Khan→South Asian, Rossi→Italian 等 20+ 映射),prompt 末尾追加 "consistent character appearance"。#/plan 或 #/store;前端打 plan_subscribe / gems_purchase_click 埋点。POST /api/store/create-invoice → 后端调 Telegram Bot API createInvoiceLink → 返回 invoice URL。tg.openInvoice(url, callback);用户在 Telegram 内完成 Stars 支付。必须追踪 callback:事件名 invoice_callback,状态 paid / cancelled / failed。successful_payment update 到 /webhook → handlers.py 校验签名 → 加💎或激活订阅 + 首充翻倍逻辑 → Bot 发确认消息。| 表 | 用途 | 关键字段 | 注意 |
|---|---|---|---|
users | 用户主表 | tg_user_id, gems, energy, energy_reset_date, subscription_expires | tg_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, used | used=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_url | SSE 推流靠这张表持久化进度 |
| characters.json | 角色定义 | id, name, voice, premium, stories[3] | 不在 DB,LRU 缓存加载 |
| items.json | 礼物定义 | id, name, cost, prompt_effect | cost 单位是 💎 |
| 路由 | 职责 |
|---|---|
POST /webhook | Telegram Bot Webhook(命令、付费回调、群成员事件) |
POST /api/chat/send | 聊天主入口;扣⚡;构建三层 prompt;调 LLM;SSE 流式返回 |
POST /chat/generate-image | 生图触发;扣 15💎;后台 task |
GET /chat/image-task/{id}/stream | SSE 推送生图进度(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 拦截) |
所有 /api/* 请求都靠 Telegram WebApp 的 initData 鉴权:
Telegram.WebApp.initData,放进 X-Telegram-Init-Data header(不能在模块顶层捕获)auth.py 用 HMAC-SHA256 + bot token 计算签名校验signature 字段也纳入 HMAC,不能 pop,否则老版客户端校验失败MODE=webhook 下激活