Why
- src/services/ was an unordered mix of single-file services and module
directories with no shared classification axis, plus several long-dead
admin batch helpers that survived the move to the simpler synchronous
admin-flux-grants flow.
What
- services/ now has two top-level layers:
domain/ — DB state + business rules (billing, characters, chats,
flux, flux-transaction, llm-router, providers, request-log,
stripe, user-deletion, admin/{flux-grants,router-config})
adapters/ — thin wrappers over external SDKs / infra (config-kv, email,
posthog, tts/)
- admin/* moved under domain/admin/ with consistent plural names
(flux-grants, router-config).
- tts-adapters/ collapsed to adapters/tts/ (no redundant -adapters suffix
once nested under adapters/).
- 63 src files + scripts/e2e-llm-router.ts + tests/verifications/_harness.ts
had relative imports rewritten; git mv preserves blame.
- apps/server/CLAUDE.md and docs/ai-context/*.md updated to match new paths.
Dead code removed
- services/admin-flux-grant-batches/ (service + worker + tests, 1090 LOC) —
superseded by admin-flux-grants and never wired into app.ts.
- routes/admin/flux-grant-batches/ — same.
- utils/redis-compressed.ts + test — zero production call sites.
- llm-router/index.ts re-exports trimmed from 26 to 6; only symbols with
external consumers are kept.
Intentionally kept
- schemas/flux-grant-batch.ts and its schemas/index.ts export remain so the
drizzle-kit generate diff stays empty. Removing them is a separate PR
that owns the drop-table migration for flux_grant_batch /
flux_grant_batch_recipient.
Verification
- pnpm -F @proj-airi/server typecheck: passes.
- pnpm exec eslint apps/server: 49 errors, identical to main baseline
(all are pre-existing node/prefer-global/buffer in envelope-crypto and
scripts/e2e-llm-router; untouched by this change).
- Vitest passes per-file; the 6 mockDB hook timeouts under full-parallel
run are the known pushSchema-per-worker infra cost, not a regression.
6.9 KiB
6.9 KiB
Transport And Routes
路由总览
应用在 src/app.ts 中挂载以下路由:
GET /livez— K8s 风格 liveness 探针,纯静态 200,不碰任何外部依赖GET /readyz— K8s 风格 readiness 探针,并发 ping Postgres + Redis;任一失败回 503。不检查上游 LLM key 健康(R14)GET /— 服务标识 JSON,避免邮件链接拼错落到框架默认 404/api/auth/*/api/v1/characters/api/v1/providers/api/v1/chats/api/v1/openai/api/v1/flux/api/v1/stripe/api/admin/flux-grants— adminGuard 守卫,详见admin-flux-grants.mdGET /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/* 及 /sign-in
实现位置:
- 路由入口:
src/routes/auth/index.ts(通过.route('/')挂载到根路径) - token auth 辅助路由:
src/routes/oidc/token-auth.ts - Electron 回调中继:
src/routes/oidc/electron-callback.ts - better-auth 配置:
src/libs/auth.ts - Bearer 解析:
src/libs/request-auth.ts - 登录页渲染:
src/utils/sign-in-page.ts
特点:
- 基于
better-auth+oauthProvider插件 - 开启 email/password、Google、GitHub 社交登录
- Bearer plugin + JWT plugin 已启用
/api/auth/*有独立 IP 限流GET /api/auth/get-session、POST /api/auth/sign-out、GET /api/auth/list-sessions由本地路由处理- Bearer token 会先尝试 better-auth session,再回退到受信任 OIDC access token
- 详见
auth-and-oidc.md
/api/v1/characters
实现位置:
- route:
src/routes/characters/index.ts - service:
src/services/domain/characters.ts
主要能力:
GET /- 默认返回当前用户拥有的角色
?all=true返回全部未删除角色
GET /:idPOST /PATCH /:idDELETE /:idPOST /:id/likePOST /:id/bookmark
特点:
- 路由层做
valibot校验 - 更新和删除会额外校验
ownerId === user.id - 点赞和收藏是 toggle 语义
/api/v1/providers
实现位置:
- route:
src/routes/providers/index.ts - service:
src/services/domain/providers.ts
主要能力:
- 用户 Provider Config CRUD
- 查询时会合并:
user_provider_configssystem_provider_configs
特点:
findAll(ownerId)通过unionAll合并系统配置和用户配置- 用户只能改自己的 user config,不能改 system config
/api/v1/chats
实现位置:
- route:
src/routes/chats/index.ts - service:
src/services/domain/chats.ts
主要能力:
- Chat CRUD
- 成员增删
聊天核心约束:
- 所有操作都会先校验用户是否属于 chat member
- 删除是软删除,写
deletedAt - 消息序号
seq在写消息时通过锁 chat 行串行分配
GET /ws/chat
实现位置:
- route 注册:
src/app.ts(在bodyLimit之前注册) - handler factory:
src/routes/chat-ws/index.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:
实现约束:
- Redis Pub/Sub 只承担通知职责,不承担持久化和重放职责
- key / channel 与 payload 边界应集中收口,不要在调用点散落模板字符串和裸
JSON.parse - 具体规范见
redis-boundaries-and-pubsub.md
/api/v1/openai
实现位置:
- route:
src/routes/openai/v1/index.ts - 依赖服务:
fluxServicebillingServiceconfigKVrequestLogService
当前已开放:
POST /api/v1/openai/chat/completionsPOST /api/v1/openai/chat/completionPOST /api/v1/openai/audio/speechGET /api/v1/openai/audio/voices(按 model 缓存上游响应,TTL 600s,仅 200 入缓存)
handleTranscription(STT)目前未挂载,需要时参考 /audio/speech 接入方式。
请求流程:
- 校验已登录
- 检查相关配置是否存在
- 检查用户 Flux 是否大于 0
- 交给
llmRouter.route/llmRouter.routeTts:读LLM_ROUTER_CONFIG,按 upstream 链路 + key rotator + envelope crypto 调上游 - 解析 usage,计算扣费
- 记录 metrics
- 调
billingService.debitFlux() - 异步写
llm_request_log
重要取舍:
- non-streaming
- 先拿完整响应
- 再扣费
- 扣费失败会阻断响应
- streaming
- 先把流回给客户端
- 流结束后再 best-effort 扣费
- 扣费失败只打 error log,不回滚给客户端
/api/v1/flux
实现位置:
- route:
src/routes/flux/index.ts - services:
fluxServicefluxTransactionService
主要能力:
GET /api/v1/flux- 读取当前用户余额
GET /api/v1/flux/history- 读取用户可见流水
/api/v1/stripe
实现位置:
- route:
src/routes/stripe/index.ts - services:
fluxServicestripeServicebillingServiceconfigKV
主要能力:
GET /packagesPOST /checkoutGET /ordersGET /invoicesPOST /portalPOST /webhook
主要职责拆分:
stripeService- 负责把 Stripe customer / session / subscription / invoice 持久化
billingService- 负责真正改余额
/api/admin/flux-grants
实现位置:
- route:
src/routes/admin/flux-grants/index.ts - service:
src/services/domain/admin/flux-grants/index.ts - guard:
src/middlewares/admin-guard.ts
主要能力:
POST /api/admin/flux-grants?dryRun=true|false— 同步给一组邮箱发 FLUX,请求线程内顺序调creditFlux,返回每条 outcome- 鉴权:
authGuard+adminGuard(ADMIN_EMAILSallowlist +email_verified=true) - 没有 batch 表 / 状态机 / 后台 loop;详见
admin-flux-grants.md
参数校验方式
输入 schema 位于各资源路由目录下的 schema.ts:
characters.schema.tschats.schema.tsproviders.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,当前默认是单实例内存存储。
影响:
- 多实例部署下不是全局一致限流
- 适合作为基础保护,不适合作为严格额度控制