Files
moeka-project/apps/server/docs/ai-context/architecture-overview.md
T
RainbowBird 4da4a72703 feat(server): finalize in-process LLM/TTS router cutover
End-state of the multi-step KTD-5 / KTD-6 / U8 work. The knoway sidecar
is no longer reachable from server code; the router is required at boot
and now owns chat completions, TTS synthesis, and voice catalog listing.

Highlights:
- LLM_ROUTER_MASTER_KEY becomes required; app.ts drops the graceful-
  skip branch and the chat fallback fetch path is gone.
- /audio/speech and /audio/voices route through new routeTts /
  listTtsVoices entries that reuse the chat key-rotator + per-attempt
  timeout + abort propagation.
- DEFAULT_CHAT_MODEL / DEFAULT_TTS_MODEL move from env to configKV so
  default-model swaps are hot-reloadable via Pub/Sub.
- GATEWAY_BASE_URL removed from env schema, .env, .env.local, smoke,
  verification harness. Redis upstream-voices cache deleted — catalogs
  come from in-process adapter JSON.
- routeTts splits adapter error contract by ApiError statusCode:
  4xx propagates without fallback; 5xx folds into the network-failure
  fallback path. handleTTS wraps billing + span attribute in try/finally
  to plug a span leak when ttsMeter.accumulate() throws.
- seed-router-config.ts rewritten with --merge (default) / --reset /
  --dry-run modes and env-var key handoff (OPENROUTER_KEY / AZURE_KEY /
  DASHSCOPE_KEY) so prod seed flows never put plaintext on the CLI.
  Adds DashScope CosyVoice seeding.

Docs (CLAUDE.md, architecture-overview.md, transport-and-routes.md)
reflect the new boundary. verifications/llm-router.md replaces the
overstated "U1-U9 shipped" line with an evidence-vs-pending table.

Tests: full 40-file / 343-case server suite green. New regressions pin
ApiError 4xx → no-fallback, ApiError 5xx → fallback, TTS billing
failure → span closed and error propagated.
2026-05-18 23:32:33 +08:00

164 lines
5.1 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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`HTTP/WS;没有常驻后台 loop,也没有 fire-and-forget 异步任务。admin flux grant 在 POST 请求线程内同步处理完返回;详见 `workers-and-runtime.md`
## 依赖注入结构
`app.ts` 使用 `injeca.provide()` 注册依赖,依赖关系大致如下:
- 基础设施
- `env`
- `otel`
- `db`
- `redis`
- `configKV`
- 服务
- `auth`
- `characterService`
- `providerService`
- `chatService`
- `stripeService`
- `fluxTransactionService`
- `fluxService`
- `requestLogService`
- `billingService`
- `adminFluxGrantsService`
- `ttsMeter`
- `userDeletionService`
- `emailService`
这个装配顺序说明了几个事实:
- `billingService` 依赖 `db + redis`
- `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`
这里是主要改动面。大多数业务改动都不应该直接写进 route handler。
### 3. 持久化层
`src/schemas/`
- Drizzle schema 基本覆盖了所有核心表
- 数据迁移由 `@proj-airi/server-schema` 提供
- `app.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`
- 面向写入
- debitFlux / credit 方法:事务内同步更新余额并写 `flux_transaction` ledger;事务提交后 best-effort 刷 Redis 余额缓存
这是服务端最重要的边界之一,尽量不要把写余额逻辑重新塞回 `flux.ts`
### LLM/TTS 路由在进程内,而不是本地 provider 编排
`/api/v1/openai``services/llm-router` 读取 `LLM_ROUTER_CONFIG` 后按 upstream 链路 + key rotator 直接调 providerOpenRouter、Azure Speech、阿里云 DashScope、火山引擎 等),不再依赖外部 knoway sidecar。因此:
- 服务端关心的是鉴权、限流、计费、日志、观测、上游路由与 key 健康
- 具体模型协议翻译由 `services/llm-router``services/tts-adapters` 的 adapter 完成
### Redis 有多种职责,但都不是余额真相源
Redis 在这里同时承担:
- Flux 余额缓存
- 运行时配置 KV
- WebSocket 跨实例广播 Pub/Sub
- Sub-Flux 计量债务账本(TTS 字符等,TTL 抹零,详见 `flux-meter.md`
- TTS voices 上游响应缓存
但余额真相仍然在 Postgres。Redis Streams 已全部移除,详见 `redis-boundaries-and-pubsub.md` 的 NOTICE。
## 当前值得注意的实现信号
- `/api/v1/openai` 当前开放:`POST /chat/completions``POST /chat/completion``POST /audio/speech``GET /audio/voices``handleTranscription` 路由尚未挂载。
- `flux_grant_batch` schema / service / route 已被简化版 `admin-flux-grants` 取代,但旧的 `src/schemas/flux-grant-batch.ts``src/services/admin-flux-grant-batch/``src/routes/admin/flux-grant-batches/` 仍以 dead code 形态残留在仓库里,没有在 `app.ts` 装配。改这块前直接删旧文件,不要继续往里面加东西。