docs(server): add ai-context
This commit is contained in:
@@ -0,0 +1,38 @@
|
||||
# AIRI Server AI Context
|
||||
|
||||
这组文档面向后续 AI / 开发者协作,目标是让人快速回答四个问题:
|
||||
|
||||
1. 服务端是怎么启动和组装的
|
||||
2. 每条 API / WS 请求最终落到哪个服务
|
||||
3. 哪些状态以 Postgres 为真相源,哪些只是缓存或派生数据
|
||||
4. 计费、充值、事件分发这些高风险链路有哪些约束
|
||||
|
||||
## 文档索引
|
||||
|
||||
- `architecture-overview.md`
|
||||
- 入口、依赖注入、应用装配、核心边界
|
||||
- `transport-and-routes.md`
|
||||
- HTTP / WebSocket 接口面、路由到服务映射、鉴权与中间件
|
||||
- `data-model-and-state.md`
|
||||
- 主要表、状态归属、缓存与事件模型
|
||||
- `workers-and-runtime.md`
|
||||
- CLI 角色、outbox dispatcher、Redis Streams consumer、运行时约束
|
||||
- `billing-architecture.md`
|
||||
- 计费链路专项说明,重点看 Flux / Stripe / outbox / Redis Streams
|
||||
|
||||
## 快速结论
|
||||
|
||||
- `apps/server/src/app.ts` 是唯一的 API 应用装配入口。
|
||||
- 服务端采用 `Hono + injeca + Drizzle + Redis + better-auth`。
|
||||
- 路由层整体较薄,业务逻辑主要在 `src/services/`。
|
||||
- **Postgres 是所有余额与计费状态的唯一真相源**,Redis 只做缓存、KV、Pub/Sub、Streams。
|
||||
- WebSocket 只用于聊天同步,跨实例广播依赖 Redis Pub/Sub。
|
||||
- 对外 LLM 能力不是本地推理,而是转发到配置里的 gateway,再按 usage / fallback rate 扣 Flux。
|
||||
|
||||
## 修改代码前建议先看
|
||||
|
||||
- 改 API 入口或新增依赖:先看 `architecture-overview.md`
|
||||
- 改某个接口行为:先看 `transport-and-routes.md`
|
||||
- 改表结构、缓存或幂等:先看 `data-model-and-state.md`
|
||||
- 改 worker、部署角色、事件处理:先看 `workers-and-runtime.md`
|
||||
- 改扣费、充值、Stripe:先看 `billing-architecture.md`
|
||||
@@ -0,0 +1,163 @@
|
||||
# Server Architecture Overview
|
||||
|
||||
## 一句话总结
|
||||
|
||||
`apps/server` 是一个基于 `Hono` 的 Node 服务端,负责认证、角色/聊天/Provider 配置、Flux 余额、Stripe 充值和面向 gateway 的 LLM 代理。整体模式是:
|
||||
|
||||
- 路由层负责参数校验、鉴权、错误映射
|
||||
- 服务层负责业务逻辑和数据库事务
|
||||
- `Postgres` 负责持久化与账本真相
|
||||
- `Redis` 负责缓存、配置 KV、Pub/Sub、Streams
|
||||
- `injeca` 负责把这些依赖组装成一个可启动应用
|
||||
|
||||
## 入口与装配
|
||||
|
||||
核心入口在 `src/app.ts`:
|
||||
|
||||
- `createApp()`
|
||||
- 初始化 logger
|
||||
- 解析环境变量
|
||||
- 初始化 OpenTelemetry
|
||||
- 建立 Postgres / Redis 连接
|
||||
- 执行数据库迁移
|
||||
- 构建各个 service
|
||||
- 注册路由和中间件
|
||||
- `runApiServer()`
|
||||
- 启动 HTTP 服务
|
||||
- 注入 WebSocket
|
||||
- 绑定 `uncaughtException` / `unhandledRejection`
|
||||
|
||||
CLI 入口在 `src/bin/run.ts`,支持三种角色:
|
||||
|
||||
- `api`
|
||||
- `cache-sync-consumer`
|
||||
- `outbox-dispatcher`
|
||||
|
||||
## 依赖注入结构
|
||||
|
||||
`app.ts` 使用 `injeca.provide()` 注册依赖,依赖关系大致如下:
|
||||
|
||||
- 基础设施
|
||||
- `env`
|
||||
- `otel`
|
||||
- `db`
|
||||
- `redis`
|
||||
- `configKV`
|
||||
- 服务
|
||||
- `outboxService`
|
||||
- `auth`
|
||||
- `characterService`
|
||||
- `providerService`
|
||||
- `chatService`
|
||||
- `stripeService`
|
||||
- `fluxAuditService`
|
||||
- `fluxService`
|
||||
- `requestLogService`
|
||||
- `billingService`
|
||||
|
||||
这个装配顺序说明了几个事实:
|
||||
|
||||
- `billingService` 依赖 `db + redis + outboxService`
|
||||
- `fluxService` 只读余额,不承担余额写入职责
|
||||
- `auth` 直接绑定数据库 schema,不是外部独立服务
|
||||
|
||||
## 应用层边界
|
||||
|
||||
### 1. HTTP / WS 传输层
|
||||
|
||||
在 `src/routes/` 和 `src/middlewares/`:
|
||||
|
||||
- 参数校验使用 `valibot`
|
||||
- 用户身份来自 `sessionMiddleware` 和 `authGuard`
|
||||
- 业务异常统一抛 `ApiError`
|
||||
- 全局 `onError` 转成标准 JSON 错误响应
|
||||
|
||||
### 2. 业务服务层
|
||||
|
||||
在 `src/services/`:
|
||||
|
||||
- `characters.ts`
|
||||
- `chats.ts`
|
||||
- `providers.ts`
|
||||
- `flux.ts`
|
||||
- `billing-service.ts`
|
||||
- `stripe.ts`
|
||||
- `outbox-service.ts`
|
||||
|
||||
这里是主要改动面。大多数业务改动都不应该直接写进 route handler。
|
||||
|
||||
### 3. 持久化层
|
||||
|
||||
在 `src/schemas/`:
|
||||
|
||||
- Drizzle schema 基本覆盖了所有核心表
|
||||
- 数据迁移由 `@proj-airi/server-schema` 提供
|
||||
- `app.ts` 和 `run-outbox-dispatcher.ts` 启动时都会执行迁移
|
||||
|
||||
## 中间件与通用约束
|
||||
|
||||
全局中间件链路大致是:
|
||||
|
||||
1. `/api/*` 启用 CORS
|
||||
2. `hono/logger`
|
||||
3. 可选的 `otelMiddleware`
|
||||
4. `sessionMiddleware`
|
||||
5. `bodyLimit(1MB)`
|
||||
6. 各 route 的局部 guard
|
||||
|
||||
需要记住的行为:
|
||||
|
||||
- WebSocket `/ws/chat` 在 `bodyLimit` 之前注册
|
||||
- `sessionMiddleware` 不会阻断匿名请求,只是往 context 填 `user/session`
|
||||
- `authGuard` 才会真正返回 401
|
||||
- `rate-limit.ts` 目前是**内存限流**,不是分布式限流
|
||||
|
||||
## 错误模型
|
||||
|
||||
统一错误类型在 `src/utils/error.ts`:
|
||||
|
||||
- `ApiError(statusCode, errorCode, message, details)`
|
||||
|
||||
约定:
|
||||
|
||||
- 业务层可以直接抛 `ApiError`
|
||||
- 未知异常会被包装成 `500 INTERNAL_SERVER_ERROR`
|
||||
- 参数错误、权限错误、余额不足都已有明确 helper
|
||||
|
||||
## 关键设计取舍
|
||||
|
||||
### Flux 读写分离
|
||||
|
||||
- `FluxService`
|
||||
- 面向读取
|
||||
- Redis cache-aside
|
||||
- 新用户首次读取时初始化余额
|
||||
- `BillingService`
|
||||
- 面向写入
|
||||
- 事务内更新余额、流水、审计、outbox
|
||||
|
||||
这是服务端最重要的边界之一,尽量不要把写余额逻辑重新塞回 `flux.ts`。
|
||||
|
||||
### LLM 网关代理而不是本地 provider 编排
|
||||
|
||||
`/api/v1` 并不直接调具体模型 provider,而是转发到 `config: GATEWAY_BASE_URL`。因此:
|
||||
|
||||
- 服务端关心的是鉴权、限流、计费、日志、观测
|
||||
- 具体模型执行和 usage 返回格式由 gateway 决定
|
||||
|
||||
### Redis 有多种职责,但都不是余额真相源
|
||||
|
||||
Redis 在这里同时承担:
|
||||
|
||||
- Flux 余额缓存
|
||||
- 运行时配置 KV
|
||||
- WebSocket 跨实例广播 Pub/Sub
|
||||
- 计费事件 Streams
|
||||
|
||||
但余额真相仍然在 Postgres。
|
||||
|
||||
## 当前值得注意的实现信号
|
||||
|
||||
- `src/services/request-log.ts` 和 `src/services/llm-request-log.ts` 职责重复,当前实际注入的是前者。
|
||||
- `src/schemas/accounts.ts` 和 `src/schemas/auth.ts` 内容重复,`createAuth()` 使用的是 `accounts.ts`。
|
||||
- `v1completions.ts` 已实现 `handleTTS` / `handleTranscription`,但路由仍被注释掉,当前只开放 chat completions。
|
||||
@@ -0,0 +1,269 @@
|
||||
# Data Model And State
|
||||
|
||||
## 真相源原则
|
||||
|
||||
这套服务端最关键的状态归属如下:
|
||||
|
||||
- `Postgres`
|
||||
- 用户认证数据
|
||||
- 角色、聊天、Provider 配置
|
||||
- Flux 余额与账本
|
||||
- Stripe 业务镜像
|
||||
- LLM 请求日志
|
||||
- outbox 事件
|
||||
- `Redis`
|
||||
- Flux 余额缓存
|
||||
- 服务配置 KV
|
||||
- 聊天跨实例广播
|
||||
- 计费事件队列
|
||||
|
||||
如果要判断“改哪个地方才算真的改成功”,大多数场景答案都是 Postgres。
|
||||
|
||||
## 主要表分组
|
||||
|
||||
### 认证
|
||||
|
||||
- `user`
|
||||
- `session`
|
||||
- `account`
|
||||
- `verification`
|
||||
|
||||
来源文件:
|
||||
|
||||
- `src/schemas/accounts.ts`
|
||||
|
||||
说明:
|
||||
|
||||
- `better-auth` 直接用这组表
|
||||
- `src/schemas/auth.ts` 基本是重复副本,目前不是主要依赖入口
|
||||
|
||||
### 角色与用户交互
|
||||
|
||||
- `characters`
|
||||
- `character_covers`
|
||||
- `avatar_model`
|
||||
- `character_capabilities`
|
||||
- `character_i18n`
|
||||
- `character_prompts`
|
||||
- `user_character_likes`
|
||||
- `user_character_bookmarks`
|
||||
|
||||
来源文件:
|
||||
|
||||
- `src/schemas/characters.ts`
|
||||
- `src/schemas/user-character.ts`
|
||||
|
||||
说明:
|
||||
|
||||
- 角色实体采用软删除
|
||||
- 点赞与收藏通过中间表建模
|
||||
- 计数值冗余保存在 `characters` 表上
|
||||
|
||||
### 聊天
|
||||
|
||||
- `chats`
|
||||
- `chat_members`
|
||||
- `messages`
|
||||
- `media`
|
||||
- `stickers`
|
||||
- `sticker_packs`
|
||||
|
||||
来源文件:
|
||||
|
||||
- `src/schemas/chats.ts`
|
||||
|
||||
说明:
|
||||
|
||||
- `messages.seq` 是会话内顺序字段
|
||||
- 写消息时通过 `SELECT ... FOR UPDATE` 锁 chat 以串行生成 seq
|
||||
- `senderId` 是宽松字段,不强制外键
|
||||
|
||||
### Provider 配置
|
||||
|
||||
- `user_provider_configs`
|
||||
- `system_provider_configs`
|
||||
|
||||
来源文件:
|
||||
|
||||
- `src/schemas/providers.ts`
|
||||
|
||||
说明:
|
||||
|
||||
- 运行时查询时会把系统配置和用户配置拼接成一个结果集
|
||||
- `config` 是 `jsonb`
|
||||
|
||||
### Flux / 账本 / 审计
|
||||
|
||||
- `user_flux`
|
||||
- `flux_ledger`
|
||||
- `flux_audit_log`
|
||||
|
||||
来源文件:
|
||||
|
||||
- `src/schemas/flux.ts`
|
||||
- `src/schemas/flux-ledger.ts`
|
||||
- `src/schemas/flux-audit-log.ts`
|
||||
|
||||
职责边界:
|
||||
|
||||
- `user_flux`
|
||||
- 当前余额快照
|
||||
- `flux_ledger`
|
||||
- append-only 账本流水
|
||||
- 偏系统真相源
|
||||
- `flux_audit_log`
|
||||
- 用户可见历史
|
||||
- 偏产品展示
|
||||
|
||||
关键约束:
|
||||
|
||||
- `flux_ledger` 对 `(userId, requestId)` 有部分唯一索引
|
||||
- 用来做扣费 / 充值幂等
|
||||
|
||||
### Stripe 业务镜像
|
||||
|
||||
- `stripe_customer`
|
||||
- `stripe_checkout_session`
|
||||
- `stripe_subscription`
|
||||
- `stripe_invoice`
|
||||
|
||||
来源文件:
|
||||
|
||||
- `src/schemas/stripe.ts`
|
||||
|
||||
说明:
|
||||
|
||||
- 这些表是 Stripe 状态的本地镜像
|
||||
- 真正的余额变化仍由 `billingService` 写入 `user_flux + flux_ledger`
|
||||
- `fluxCredited` 字段用于避免重复入账
|
||||
|
||||
### LLM 请求日志
|
||||
|
||||
- `llm_request_log`
|
||||
|
||||
来源文件:
|
||||
|
||||
- `src/schemas/llm-request-log.ts`
|
||||
|
||||
说明:
|
||||
|
||||
- 只做追加写入
|
||||
- 明确不加 user 外键,以避免高并发写入的额外约束成本
|
||||
|
||||
### Outbox
|
||||
|
||||
- `outbox_events`
|
||||
|
||||
来源文件:
|
||||
|
||||
- `src/schemas/outbox-events.ts`
|
||||
|
||||
说明:
|
||||
|
||||
- 本质上是 DB 内事件暂存区
|
||||
- 通过 `claimedBy + claimExpiresAt + publishedAt` 实现 lease/claim 分发
|
||||
|
||||
## 服务与状态写入边界
|
||||
|
||||
### `createFluxService()`
|
||||
|
||||
负责:
|
||||
|
||||
- 余额读取
|
||||
- 新用户首次读取时初始化 `user_flux`
|
||||
- Redis cache-aside
|
||||
|
||||
不负责:
|
||||
|
||||
- 扣费
|
||||
- 充值
|
||||
- ledger / audit / outbox 写入
|
||||
|
||||
### `createBillingService()`
|
||||
|
||||
负责:
|
||||
|
||||
- 所有余额写操作
|
||||
- DB 事务
|
||||
- ledger / audit / outbox 联动
|
||||
- 事务完成后 best-effort 更新 Redis
|
||||
|
||||
这是所有 Flux 写路径应收敛到的中心。
|
||||
|
||||
### `createStripeService()`
|
||||
|
||||
负责:
|
||||
|
||||
- Stripe 实体 upsert
|
||||
|
||||
不负责:
|
||||
|
||||
- 最终 Flux 入账
|
||||
|
||||
真正入账通过 `billingService.creditFluxFromStripeCheckout()` 或相关 credit 方法完成。
|
||||
|
||||
## Redis 中的数据类型
|
||||
|
||||
### Flux 缓存
|
||||
|
||||
- key: `flux:<userId>`
|
||||
- value: 字符串化整数
|
||||
|
||||
写入来源:
|
||||
|
||||
- `fluxService.getFlux()` cache miss 后回填
|
||||
- `billingService` 余额事务成功后 best-effort 更新
|
||||
- `cache-sync-consumer` 消费 `flux.debited` / `flux.credited` 后同步
|
||||
|
||||
### 配置 KV
|
||||
|
||||
- key: `config:<CONFIG_NAME>`
|
||||
|
||||
由 `config-kv.ts` 管理,支持:
|
||||
|
||||
- 数值
|
||||
- 字符串
|
||||
- `FLUX_PACKAGES` JSON
|
||||
|
||||
### 聊天跨实例广播
|
||||
|
||||
- channel: `chat:broadcast:<userId>`
|
||||
|
||||
### 计费事件流
|
||||
|
||||
- stream: 默认 `billing-events`
|
||||
|
||||
## 幂等与并发控制
|
||||
|
||||
### 余额并发
|
||||
|
||||
`billingService` 在事务中:
|
||||
|
||||
1. `SELECT user_flux FOR UPDATE`
|
||||
2. 计算新余额
|
||||
3. 写余额
|
||||
4. 写 ledger / audit / outbox
|
||||
|
||||
这保证同一用户余额更新是串行化的。
|
||||
|
||||
### Stripe 幂等
|
||||
|
||||
主要依赖:
|
||||
|
||||
- `stripe_checkout_session.fluxCredited`
|
||||
- `stripe_invoice.fluxCredited`
|
||||
- `flux_ledger(userId, requestId)` 唯一约束
|
||||
|
||||
### outbox 并发
|
||||
|
||||
`outbox-service.ts` 使用:
|
||||
|
||||
- `FOR UPDATE SKIP LOCKED`
|
||||
- `claimExpiresAt`
|
||||
|
||||
这允许多个 dispatcher 并行拉取待发布事件。
|
||||
|
||||
## 现有代码中的结构信号
|
||||
|
||||
- `request-log.ts` 与 `llm-request-log.ts` 完全重叠,后者更像旧名残留。
|
||||
- `accounts.ts` 与 `auth.ts` 也是重复 schema,后续如果做整理,应先统一真实使用入口再删副本。
|
||||
@@ -0,0 +1,258 @@
|
||||
# Transport And Routes
|
||||
|
||||
## 路由总览
|
||||
|
||||
应用在 `src/app.ts` 中挂载以下路由:
|
||||
|
||||
- `GET /health`
|
||||
- `/api/auth/*`
|
||||
- `/api/characters`
|
||||
- `/api/providers`
|
||||
- `/api/chats`
|
||||
- `/api/v1`
|
||||
- `/api/flux`
|
||||
- `/api/stripe`
|
||||
- `GET /ws/chat`
|
||||
|
||||
## 鉴权链路
|
||||
|
||||
### HTTP
|
||||
|
||||
- `sessionMiddleware(auth)`
|
||||
- 通过 `better-auth` 解析当前 session
|
||||
- 把 `user` / `session` 注入 Hono context
|
||||
- `authGuard`
|
||||
- 检查 `c.get('user')`
|
||||
- 未登录直接 401
|
||||
|
||||
### WebSocket
|
||||
|
||||
`GET /ws/chat` 走 query token:
|
||||
|
||||
- 读取 `token`
|
||||
- 用 `auth.api.getSession()` 验证 Bearer token
|
||||
- 校验通过后为该 `user.id` 建立 Eventa peer
|
||||
|
||||
这意味着聊天 WS 的鉴权方式和普通 cookie session 路径不完全相同。
|
||||
|
||||
## 路由到服务映射
|
||||
|
||||
### `/api/auth/*`
|
||||
|
||||
实现位置:
|
||||
|
||||
- 路由注册:`src/app.ts`
|
||||
- 实际处理:`auth.handler(c.req.raw)`
|
||||
- 服务工厂:`src/libs/auth.ts`
|
||||
|
||||
特点:
|
||||
|
||||
- 基于 `better-auth`
|
||||
- 开启 email/password、Google、GitHub
|
||||
- Bearer plugin 已启用
|
||||
- `/api/auth/*` 有独立 IP 限流,每分钟 20 次
|
||||
|
||||
### `/api/characters`
|
||||
|
||||
实现位置:
|
||||
|
||||
- route: `src/routes/characters.ts`
|
||||
- service: `src/services/characters.ts`
|
||||
|
||||
主要能力:
|
||||
|
||||
- `GET /`
|
||||
- 默认返回当前用户拥有的角色
|
||||
- `?all=true` 返回全部未删除角色
|
||||
- `GET /:id`
|
||||
- `POST /`
|
||||
- `PATCH /:id`
|
||||
- `DELETE /:id`
|
||||
- `POST /:id/like`
|
||||
- `POST /:id/bookmark`
|
||||
|
||||
特点:
|
||||
|
||||
- 路由层做 `valibot` 校验
|
||||
- 更新和删除会额外校验 `ownerId === user.id`
|
||||
- 点赞和收藏是 toggle 语义
|
||||
|
||||
### `/api/providers`
|
||||
|
||||
实现位置:
|
||||
|
||||
- route: `src/routes/providers.ts`
|
||||
- service: `src/services/providers.ts`
|
||||
|
||||
主要能力:
|
||||
|
||||
- 用户 Provider Config CRUD
|
||||
- 查询时会合并:
|
||||
- `user_provider_configs`
|
||||
- `system_provider_configs`
|
||||
|
||||
特点:
|
||||
|
||||
- `findAll(ownerId)` 通过 `unionAll` 合并系统配置和用户配置
|
||||
- 用户只能改自己的 user config,不能改 system config
|
||||
|
||||
### `/api/chats`
|
||||
|
||||
实现位置:
|
||||
|
||||
- route: `src/routes/chats.ts`
|
||||
- service: `src/services/chats.ts`
|
||||
|
||||
主要能力:
|
||||
|
||||
- Chat CRUD
|
||||
- 成员增删
|
||||
|
||||
聊天核心约束:
|
||||
|
||||
- 所有操作都会先校验用户是否属于 chat member
|
||||
- 删除是软删除,写 `deletedAt`
|
||||
- 消息序号 `seq` 在写消息时通过锁 chat 行串行分配
|
||||
|
||||
### `GET /ws/chat`
|
||||
|
||||
实现位置:
|
||||
|
||||
- route 注册:`src/app.ts`
|
||||
- handler factory: `src/routes/chat-ws.ts`
|
||||
- 底层事件适配:`src/libs/eventa-hono-adapter.ts`
|
||||
|
||||
主要 RPC:
|
||||
|
||||
- `sendMessages`
|
||||
- 调 `chatService.pushMessages()`
|
||||
- 再调 `chatService.pullMessages()` 生成广播 payload
|
||||
- `pullMessages`
|
||||
- 调 `chatService.pullMessages()`
|
||||
|
||||
广播策略:
|
||||
|
||||
- 同实例:内存 `Map<userId, Set<EventContext>>`
|
||||
- 跨实例:Redis Pub/Sub,channel 前缀 `chat:broadcast:`
|
||||
|
||||
### `/api/v1`
|
||||
|
||||
实现位置:
|
||||
|
||||
- route: `src/routes/v1completions.ts`
|
||||
- 依赖服务:
|
||||
- `fluxService`
|
||||
- `billingService`
|
||||
- `configKV`
|
||||
- `requestLogService`
|
||||
|
||||
当前已开放:
|
||||
|
||||
- `POST /api/v1/chat/completions`
|
||||
- `POST /api/v1/chat/completion`
|
||||
|
||||
已实现但暂未挂载:
|
||||
|
||||
- `handleTTS`
|
||||
- `handleTranscription`
|
||||
|
||||
请求流程:
|
||||
|
||||
1. 校验已登录
|
||||
2. 检查相关配置是否存在
|
||||
3. 检查用户 Flux 是否大于 0
|
||||
4. 代理请求到 `GATEWAY_BASE_URL`
|
||||
5. 解析 usage,计算扣费
|
||||
6. 记录 metrics
|
||||
7. 调 `billingService.debitFlux()`
|
||||
8. 异步写 `llm_request_log`
|
||||
|
||||
重要取舍:
|
||||
|
||||
- non-streaming
|
||||
- 先拿完整响应
|
||||
- 再扣费
|
||||
- 扣费失败会阻断响应
|
||||
- streaming
|
||||
- 先把流回给客户端
|
||||
- 流结束后再 best-effort 扣费
|
||||
- 扣费失败只打 error log,不回滚给客户端
|
||||
|
||||
### `/api/flux`
|
||||
|
||||
实现位置:
|
||||
|
||||
- route: `src/routes/flux.ts`
|
||||
- services:
|
||||
- `fluxService`
|
||||
- `fluxAuditService`
|
||||
|
||||
主要能力:
|
||||
|
||||
- `GET /api/flux`
|
||||
- 读取当前用户余额
|
||||
- `GET /api/flux/history`
|
||||
- 读取用户可见流水
|
||||
|
||||
### `/api/stripe`
|
||||
|
||||
实现位置:
|
||||
|
||||
- route: `src/routes/stripe.ts`
|
||||
- services:
|
||||
- `fluxService`
|
||||
- `stripeService`
|
||||
- `billingService`
|
||||
- `configKV`
|
||||
|
||||
主要能力:
|
||||
|
||||
- `GET /packages`
|
||||
- `POST /checkout`
|
||||
- `GET /orders`
|
||||
- `GET /invoices`
|
||||
- `POST /portal`
|
||||
- `POST /webhook`
|
||||
|
||||
主要职责拆分:
|
||||
|
||||
- `stripeService`
|
||||
- 负责把 Stripe customer / session / subscription / invoice 持久化
|
||||
- `billingService`
|
||||
- 负责真正改余额
|
||||
|
||||
## 参数校验方式
|
||||
|
||||
输入 schema 位于 `src/api/*.schema.ts`:
|
||||
|
||||
- `characters.schema.ts`
|
||||
- `chats.schema.ts`
|
||||
- `providers.schema.ts`
|
||||
|
||||
route 层统一使用 `safeParse`,失败时抛:
|
||||
|
||||
- `createBadRequestError('Invalid Request', 'INVALID_REQUEST', result.issues)`
|
||||
|
||||
## 中间件与 guard
|
||||
|
||||
### `configGuard`
|
||||
|
||||
作用:
|
||||
|
||||
- 检查某些 Redis 配置项是否已写入
|
||||
- 缺失时返回 503
|
||||
|
||||
使用场景:
|
||||
|
||||
- LLM chat
|
||||
- Stripe checkout
|
||||
- 未来的 TTS / ASR
|
||||
|
||||
### `rateLimiter`
|
||||
|
||||
封装自 `hono-rate-limiter`,当前默认是单实例内存存储。
|
||||
|
||||
影响:
|
||||
|
||||
- 多实例部署下不是全局一致限流
|
||||
- 适合作为基础保护,不适合作为严格额度控制
|
||||
@@ -0,0 +1,207 @@
|
||||
# Workers And Runtime
|
||||
|
||||
## 进程角色
|
||||
|
||||
统一入口在 `src/bin/run.ts`:
|
||||
|
||||
- `api`
|
||||
- 启动 Hono HTTP + WebSocket 服务
|
||||
- `cache-sync-consumer`
|
||||
- 消费 Redis Streams 中的计费事件,回写 Flux Redis 缓存
|
||||
- `outbox-dispatcher`
|
||||
- 从 Postgres `outbox_events` 拉取未发布事件,投递到 Redis Streams
|
||||
|
||||
这三个角色已经是当前服务端部署拆分的基本单位。
|
||||
|
||||
## API 角色
|
||||
|
||||
启动路径:
|
||||
|
||||
- `src/bin/run.ts`
|
||||
- `runApiServer()`
|
||||
- `createApp()`
|
||||
|
||||
启动时会做的事情:
|
||||
|
||||
- 解析 env
|
||||
- 初始化日志
|
||||
- 可选初始化 OTel
|
||||
- 连接 Postgres / Redis
|
||||
- 跑数据库迁移
|
||||
- 装配服务
|
||||
- 启动 HTTP server
|
||||
- 注入 WebSocket
|
||||
|
||||
## Outbox Dispatcher
|
||||
|
||||
实现位置:
|
||||
|
||||
- 入口:`src/bin/run-outbox-dispatcher.ts`
|
||||
- 服务:`src/services/outbox-dispatcher.ts`
|
||||
- 存储:`src/services/outbox-service.ts`
|
||||
- MQ:`src/services/billing-mq.ts`
|
||||
|
||||
工作流程:
|
||||
|
||||
1. 从 `outbox_events` claim 一批未发布事件
|
||||
2. 逐条发布到 Redis Stream
|
||||
3. 发布成功后写 `publishedAt` 和 `streamMessageId`
|
||||
4. 失败则释放 claim,等待下一轮处理
|
||||
|
||||
关键机制:
|
||||
|
||||
- 支持多实例并发 dispatcher
|
||||
- claim 通过 TTL 失效,避免 worker 崩掉后永久锁死
|
||||
|
||||
相关环境变量:
|
||||
|
||||
- `OUTBOX_DISPATCHER_NAME`
|
||||
- `OUTBOX_DISPATCHER_BATCH_SIZE`
|
||||
- `OUTBOX_DISPATCHER_CLAIM_TTL_MS`
|
||||
- `OUTBOX_DISPATCHER_POLL_MS`
|
||||
- `BILLING_EVENTS_STREAM`
|
||||
|
||||
## Billing Events Consumer
|
||||
|
||||
实现位置:
|
||||
|
||||
- 入口:`src/bin/run-billing-events-consumer.ts`
|
||||
- worker:`src/services/billing-mq-worker.ts`
|
||||
- stream adapter:`src/services/billing-mq.ts`
|
||||
|
||||
当前默认 handler:
|
||||
|
||||
- `handleCacheSyncMessage()`
|
||||
|
||||
它只处理:
|
||||
|
||||
- `flux.credited`
|
||||
- `flux.debited`
|
||||
|
||||
并把 `payload.balanceAfter` 写回 Redis:
|
||||
|
||||
- key: `flux:<userId>`
|
||||
|
||||
这说明当前 consumer 的目标非常克制:
|
||||
|
||||
- 不是账务真相处理器
|
||||
- 不是分析流水处理器
|
||||
- 只是缓存一致性补偿器
|
||||
|
||||
相关环境变量:
|
||||
|
||||
- `BILLING_EVENTS_STREAM`
|
||||
- `BILLING_EVENTS_CONSUMER_NAME`
|
||||
- `BILLING_EVENTS_BATCH_SIZE`
|
||||
- `BILLING_EVENTS_BLOCK_MS`
|
||||
- `BILLING_EVENTS_MIN_IDLE_MS`
|
||||
|
||||
## Redis Streams 语义
|
||||
|
||||
`billing-mq.ts` 把 Redis Streams 抽象成:
|
||||
|
||||
- `publish()`
|
||||
- `ensureConsumerGroup()`
|
||||
- `consume()`
|
||||
- `claimIdleMessages()`
|
||||
- `ack()`
|
||||
|
||||
这层约束了消息处理语义:
|
||||
|
||||
- 使用 consumer group
|
||||
- 使用 pending reclaim
|
||||
- handler 抛错时不 ack,消息保持 pending
|
||||
|
||||
因此新增新的 stream consumer 时,最安全的方式通常是复用这层,不要自己裸写 `XREADGROUP`。
|
||||
|
||||
## 聊天 WebSocket 运行时
|
||||
|
||||
`src/routes/chat-ws.ts` 还有一套独立于 Redis Streams 的运行时机制:
|
||||
|
||||
- 同实例连接保存在进程内 `Map`
|
||||
- 跨实例 fan-out 通过 Redis Pub/Sub
|
||||
|
||||
这意味着:
|
||||
|
||||
- WS 广播不具备持久化和重放能力
|
||||
- 真正补齐消息还是靠 `pullMessages`
|
||||
- 广播只是加速客户端同步
|
||||
|
||||
## OpenTelemetry
|
||||
|
||||
初始化在 `src/libs/otel.ts`。
|
||||
|
||||
启用条件:
|
||||
|
||||
- `OTEL_EXPORTER_OTLP_ENDPOINT` 存在
|
||||
|
||||
覆盖面:
|
||||
|
||||
- HTTP
|
||||
- Auth
|
||||
- Chat engagement
|
||||
- Revenue
|
||||
- LLM
|
||||
- DB / Redis instrumentation
|
||||
|
||||
重要实现细节:
|
||||
|
||||
- `sdk.start()` 必须发生在 `metrics.getMeter()` 之前
|
||||
- `/health` 会被 HTTP instrumentation 忽略
|
||||
|
||||
## 环境变量分层
|
||||
|
||||
### 基础运行
|
||||
|
||||
- `HOST`
|
||||
- `PORT`
|
||||
- `API_SERVER_URL`
|
||||
- `CLIENT_URL`
|
||||
- `DATABASE_URL`
|
||||
- `REDIS_URL`
|
||||
|
||||
### Auth
|
||||
|
||||
- `AUTH_GOOGLE_CLIENT_ID`
|
||||
- `AUTH_GOOGLE_CLIENT_SECRET`
|
||||
- `AUTH_GITHUB_CLIENT_ID`
|
||||
- `AUTH_GITHUB_CLIENT_SECRET`
|
||||
|
||||
### Stripe
|
||||
|
||||
- `STRIPE_SECRET_KEY`
|
||||
- `STRIPE_WEBHOOK_SECRET`
|
||||
|
||||
### Billing MQ / Outbox
|
||||
|
||||
- `BILLING_EVENTS_STREAM`
|
||||
- `BILLING_EVENTS_CONSUMER_NAME`
|
||||
- `BILLING_EVENTS_BATCH_SIZE`
|
||||
- `BILLING_EVENTS_BLOCK_MS`
|
||||
- `BILLING_EVENTS_MIN_IDLE_MS`
|
||||
- `OUTBOX_DISPATCHER_NAME`
|
||||
- `OUTBOX_DISPATCHER_BATCH_SIZE`
|
||||
- `OUTBOX_DISPATCHER_CLAIM_TTL_MS`
|
||||
- `OUTBOX_DISPATCHER_POLL_MS`
|
||||
|
||||
### OTel
|
||||
|
||||
- `OTEL_SERVICE_NAMESPACE`
|
||||
- `OTEL_SERVICE_NAME`
|
||||
- `OTEL_TRACES_SAMPLING_RATIO`
|
||||
- `OTEL_EXPORTER_OTLP_ENDPOINT`
|
||||
- `OTEL_EXPORTER_OTLP_HEADERS`
|
||||
- `OTEL_DEBUG`
|
||||
|
||||
## 运行时修改建议
|
||||
|
||||
如果你要改:
|
||||
|
||||
- 新增 worker
|
||||
- 先看 `run.ts` 的角色模型和 `billing-mq-worker.ts`
|
||||
- 改事件分发
|
||||
- 先看 outbox,而不是直接在业务事务里调用 Redis Streams
|
||||
- 改聊天同步
|
||||
- 先区分“持久化消息”与“广播通知”两层
|
||||
- 改部署限流
|
||||
- 注意当前 `rate-limit.ts` 仍是单实例内存模型
|
||||
Reference in New Issue
Block a user