docs(server): ai context
This commit is contained in:
@@ -0,0 +1,27 @@
|
||||
# `@proj-airi/server`
|
||||
|
||||
HTTP and WebSocket backend for AIRI. This app owns auth, billing, chat synchronization, gateway forwarding, and server-side observability export.
|
||||
|
||||
## What It Does
|
||||
|
||||
- Serves the Hono-based API and WebSocket endpoints.
|
||||
- Uses Postgres as the source of truth for users, billing, and durable state.
|
||||
- Uses Redis for cache, KV, Pub/Sub, and Streams.
|
||||
- Forwards GenAI requests to the configured upstream gateway and records billing from usage.
|
||||
- Exports traces, metrics, and logs through OpenTelemetry.
|
||||
|
||||
## How To Use It
|
||||
|
||||
Install dependencies from the repo root and run scoped commands:
|
||||
|
||||
```sh
|
||||
pnpm -F @proj-airi/server typecheck
|
||||
pnpm -F @proj-airi/server exec vitest run
|
||||
pnpm -F @proj-airi/server build
|
||||
```
|
||||
|
||||
For local observability infrastructure, use:
|
||||
|
||||
```sh
|
||||
docker compose -f apps/server/docker-compose.otel.yml up -d
|
||||
```
|
||||
@@ -17,8 +17,14 @@
|
||||
- 主要表、状态归属、缓存与事件模型
|
||||
- `workers-and-runtime.md`
|
||||
- CLI 角色、outbox dispatcher、Redis Streams consumer、运行时约束
|
||||
- `redis-boundaries-and-pubsub.md`
|
||||
- Redis key / channel 收口、Pub/Sub / Streams 边界、运行时校验约束
|
||||
- `config-and-naming-conventions.md`
|
||||
- `configKV` 默认值来源、Redis key 命名、HTTP route 命名、后续收敛 TODO
|
||||
- `billing-architecture.md`
|
||||
- 计费链路专项说明,重点看 Flux / Stripe / outbox / Redis Streams
|
||||
- `observability-conventions.md`
|
||||
- traces / metrics 命名规则,标准 OTel 字段与 `airi.*` 自定义字段边界
|
||||
|
||||
## 快速结论
|
||||
|
||||
@@ -35,4 +41,7 @@
|
||||
- 改某个接口行为:先看 `transport-and-routes.md`
|
||||
- 改表结构、缓存或幂等:先看 `data-model-and-state.md`
|
||||
- 改 worker、部署角色、事件处理:先看 `workers-and-runtime.md`
|
||||
- 改 Redis key、Pub/Sub、Streams 边界:先看 `redis-boundaries-and-pubsub.md`
|
||||
- 改配置默认值、Redis key 命名、HTTP route 命名:先看 `config-and-naming-conventions.md`
|
||||
- 改扣费、充值、Stripe:先看 `billing-architecture.md`
|
||||
- 改 trace / metric attributes、OTel 命名:先看 `observability-conventions.md`
|
||||
|
||||
@@ -0,0 +1,161 @@
|
||||
# Config And Naming Conventions
|
||||
|
||||
## 目标
|
||||
|
||||
这篇文档收口三类容易逐步漂移的约定:
|
||||
|
||||
- `configKV` 的默认值和读取语义
|
||||
- Redis key / channel 的命名规则
|
||||
- HTTP route 的资源命名规则
|
||||
|
||||
这些约定不是“代码风格建议”,而是为了减少:
|
||||
|
||||
- 默认值写两份导致的配置漂移
|
||||
- Redis key 命名混用导致的排障成本
|
||||
- HTTP route 语义不稳定导致的版本化困难
|
||||
|
||||
## `configKV` 约定
|
||||
|
||||
### 单一真相源
|
||||
|
||||
`src/services/config-kv.ts` 中的 `ConfigEntrySchemas` 是以下三件事的单一真相源:
|
||||
|
||||
- 配置值的运行时校验
|
||||
- 配置值的默认值
|
||||
- Redis 中的序列化 / 反序列化 shape
|
||||
|
||||
这意味着:
|
||||
|
||||
- 默认值必须定义在 `ConfigEntrySchemas`
|
||||
- 业务代码不要再写第二份 `?? defaultValue`
|
||||
- `configKV.get()` / `configKV.getOrThrow()` 应直接依赖 schema 默认值
|
||||
|
||||
### 读取语义
|
||||
|
||||
- `getOptional(key)`
|
||||
- 用于“这个 key 合法地可以不存在”的场景
|
||||
- 对 required key,未配置时返回 `null`
|
||||
- 对带 schema 默认值的 key,返回默认值
|
||||
- `getOrThrow(key)`
|
||||
- 用于“缺失就是配置错误”的场景
|
||||
- required key 未配置时抛 `CONFIG_NOT_SET`
|
||||
- `get(key)`
|
||||
- 是 `getOrThrow(key)` 的别名
|
||||
- 默认用于业务代码
|
||||
|
||||
### 禁止事项
|
||||
|
||||
- 不要给 `getOptional` 增加调用点默认值参数,例如 `getOptional(key, fallback)`
|
||||
- 不要同时在 schema 和调用侧维护两份默认值
|
||||
- 不要绕过 `configKV` 直接从 Redis 读配置
|
||||
|
||||
### 当前例子
|
||||
|
||||
推荐:
|
||||
|
||||
```ts
|
||||
const fluxPer1kTokens = await configKV.get('FLUX_PER_1K_TOKENS')
|
||||
const maxCheckoutAmount = await configKV.get('MAX_CHECKOUT_AMOUNT_CENTS')
|
||||
```
|
||||
|
||||
不推荐:
|
||||
|
||||
```ts
|
||||
const fluxPer1kTokens = (await configKV.getOptional('FLUX_PER_1K_TOKENS')) ?? 1
|
||||
const maxCheckoutAmount = (await configKV.getOptional('MAX_CHECKOUT_AMOUNT_CENTS')) ?? 1_000_000
|
||||
```
|
||||
|
||||
## Redis key / channel 命名
|
||||
|
||||
### 命名规则
|
||||
|
||||
Redis key 和 channel 统一采用“分段命名”,推荐使用冒号 `:` 作为分隔符:
|
||||
|
||||
```txt
|
||||
{scope}:{id}:{resource}
|
||||
{scope}:{id}:{subscope}:{subid}:{resource}
|
||||
lock:{domain}:{id}
|
||||
config:{key}
|
||||
```
|
||||
|
||||
推荐例子:
|
||||
|
||||
```txt
|
||||
user:{userId}:flux
|
||||
config:{key}
|
||||
chat:{userId}:broadcast
|
||||
lock:user:{userId}:flux
|
||||
```
|
||||
|
||||
不推荐例子:
|
||||
|
||||
```txt
|
||||
flux:{userId}
|
||||
chat:broadcast:{userId}
|
||||
userFlux:{userId}
|
||||
userUidFlux1
|
||||
```
|
||||
|
||||
### 设计原则
|
||||
|
||||
- 前缀表达 namespace,而不是随手缩写
|
||||
- 真实 Redis key 不要出现 `1`、`2` 这种占位编号
|
||||
- 参数占位编号只用于文档里的模板名,不用于运行时 key
|
||||
- key / channel 必须通过 helper 收口,不要在业务代码里散落模板字符串
|
||||
|
||||
### 文档里的模板命名
|
||||
|
||||
如果要在文档中表示“这个 key 有几个参数位”,可以用编号描述模板:
|
||||
|
||||
- `userUserId1Flux`
|
||||
- `configKey1`
|
||||
- `lockDomain1Id1`
|
||||
|
||||
但最终真实 key 仍然必须是:
|
||||
|
||||
```txt
|
||||
user:{userId}:flux
|
||||
config:{key}
|
||||
lock:{domain}:{id}
|
||||
```
|
||||
|
||||
## HTTP route 命名
|
||||
|
||||
### 资源命名原则
|
||||
|
||||
- 优先使用复数资源名
|
||||
- 从属资源优先挂在父资源下
|
||||
- 当前登录用户资源优先使用 `me`
|
||||
|
||||
推荐:
|
||||
|
||||
```txt
|
||||
/api/users/me/flux
|
||||
/api/users/me/flux/history
|
||||
```
|
||||
|
||||
不推荐:
|
||||
|
||||
```txt
|
||||
/api/user/flux
|
||||
/api/flux
|
||||
```
|
||||
|
||||
### 版本化约束
|
||||
|
||||
如果某类 HTTP API 需要长期稳定对外契约,优先在一个明确子树下版本化,例如:
|
||||
|
||||
```txt
|
||||
/api/v1/...
|
||||
/api/openai/v1/...
|
||||
```
|
||||
|
||||
不要让“部分资源版本化、部分资源裸挂”长期并存而没有说明。
|
||||
|
||||
## TODO
|
||||
|
||||
- 把 `apps/server` 中现有 Redis key / channel 继续向 helper 收口,避免业务代码里散落模板字符串。
|
||||
- 统一把旧式 key 命名迁移到分段命名风格,优先处理 Flux cache、chat broadcast、lock key。
|
||||
- 给 `configKV` 增补一份“哪些配置属于 infra、哪些属于运营策略”的清单,避免继续模糊放置位置。
|
||||
- 把所有 `configKV.getOptional(...) ?? defaultValue` 模式清理掉,默认值统一回到 `ConfigEntrySchemas`。
|
||||
- 评估是否把 `/api/v1` 收口为兼容层 API,并明确业务资源是否也需要统一版本化。
|
||||
@@ -0,0 +1,159 @@
|
||||
# Observability Conventions
|
||||
|
||||
这份约定定义 AIRI 服务端新增 trace / metric attributes 时应该遵守的命名规则,目标是减少自定义前缀扩散,并让 Grafana / Tempo / Loki 查询尽量对齐 OpenTelemetry 语义约定。
|
||||
|
||||
## 总原则
|
||||
|
||||
- 能直接映射到 OpenTelemetry semantic conventions 的字段,优先使用标准字段。
|
||||
- 不能映射到标准字段、但确实属于 AIRI 业务语义的字段,统一放到 `airi.*` 命名空间下。
|
||||
- 不要新增新的顶级前缀,例如 `llm.*`、`gateway.*`、`telegram.*` 之类的 attribute key。
|
||||
- span name、event name、metric name 不等于 attribute key;是否迁移它们要单独评估兼容性。
|
||||
- 代码里不要继续散落新的 observability key 字符串字面量;统一从 [packages/server-shared/src/observability.ts](/Users/luoling8192/Git/moeru-ai/airi/packages/server-shared/src/observability.ts) 引用。
|
||||
|
||||
## 标准字段优先级
|
||||
|
||||
### GenAI
|
||||
|
||||
优先使用:
|
||||
|
||||
- `GEN_AI_ATTR_OPERATION_NAME`
|
||||
- `GEN_AI_ATTR_REQUEST_MODEL`
|
||||
- `GEN_AI_ATTR_USAGE_INPUT_TOKENS`
|
||||
- `GEN_AI_ATTR_USAGE_OUTPUT_TOKENS`
|
||||
- `SERVER_ATTR_ADDRESS`
|
||||
- `SERVER_ATTR_PORT`
|
||||
|
||||
适用场景:
|
||||
|
||||
- chat completion
|
||||
- embeddings
|
||||
- 其他能明确归类到 GenAI 上游调用的请求
|
||||
|
||||
注意:
|
||||
|
||||
- 当前 OpenTelemetry GenAI semantic conventions 仍处于 `Development` 状态,因此只在“语义明确匹配”时采用。
|
||||
- 没有明确标准归属的字段不要硬塞进 `gen_ai.*`。
|
||||
|
||||
### Database / Redis
|
||||
|
||||
优先使用:
|
||||
|
||||
- `db.system.name`
|
||||
- `db.operation.name`
|
||||
- `db.namespace`
|
||||
- `db.query.text`
|
||||
- `db.response.status_code`
|
||||
- `server.address`
|
||||
- `server.port`
|
||||
|
||||
Redis 相关优先复用 instrumentation 自动产生的标准属性,不要重复造一套并行命名。
|
||||
|
||||
## AIRI 自定义字段
|
||||
|
||||
以下场景使用 `airi.*`:
|
||||
|
||||
- 计费或余额语义
|
||||
- 仅 AIRI 内部存在的流式控制字段
|
||||
- 临时调试但仍需要进入可观测系统的业务字段
|
||||
|
||||
当前示例:
|
||||
|
||||
- `AIRI_ATTR_BILLING_FLUX_CONSUMED`
|
||||
- `AIRI_ATTR_GEN_AI_STREAM`
|
||||
- `AIRI_ATTR_GEN_AI_STREAM_INTERRUPTED`
|
||||
- `AIRI_ATTR_GEN_AI_OPERATION_KIND`
|
||||
- `AIRI_ATTR_GEN_AI_INPUT_MESSAGES`
|
||||
- `AIRI_ATTR_GEN_AI_INPUT_TEXT`
|
||||
- `AIRI_ATTR_GEN_AI_OUTPUT_TEXT`
|
||||
|
||||
## Metric Name 策略
|
||||
|
||||
当前 `apps/server` 仍保留以下 metric name:
|
||||
|
||||
- `llm.request.duration`
|
||||
- `llm.request.count`
|
||||
- `llm.tokens.prompt`
|
||||
- `llm.tokens.completion`
|
||||
- `flux.consumed`
|
||||
|
||||
这是有意为之,不是遗漏。
|
||||
|
||||
原因:
|
||||
|
||||
- metric name 改动比 attribute 改动更容易破坏现有 Prometheus 查询、Grafana 面板和告警。
|
||||
- 目前更高价值的是先统一 metric attributes,使查询维度稳定。
|
||||
- 如需迁移 metric name,应该走兼容迁移方案,而不是在普通功能改动里直接重命名。
|
||||
|
||||
## Grafana / Prometheus 查询策略
|
||||
|
||||
面板和告警查询优先依赖 metric labels,对齐我们已经统一的 attributes。
|
||||
|
||||
### GenAI 面板应该查什么
|
||||
|
||||
优先使用这些 Prometheus label:
|
||||
|
||||
- `gen_ai_request_model`
|
||||
- `gen_ai_operation_name`
|
||||
- `airi_gen_ai_operation_kind`
|
||||
- `http_response_status_code`
|
||||
|
||||
说明:
|
||||
|
||||
- Prometheus 暴露时会把 attribute key 里的 `.` 转成 `_`,所以 `gen_ai.request.model` 会变成 `gen_ai_request_model`。
|
||||
- `gen_ai_operation_name` 适合 chat、embeddings 这类有明确 semconv 的操作。
|
||||
- `airi_gen_ai_operation_kind` 适合当前没有明确 semconv 的 AIRI 自定义操作类型,例如 `tts`、`asr`。
|
||||
|
||||
### 不再新增使用的旧查询维度
|
||||
|
||||
新增 dashboard、录制规则、告警时,不要再新增依赖这些旧 label:
|
||||
|
||||
- `model`
|
||||
- `type`
|
||||
|
||||
旧面板可以渐进迁移,不要求一次性全部替换,但新改动必须直接使用新标签。
|
||||
|
||||
### 当前已落地的 dashboard 例子
|
||||
|
||||
[apps/server/otel/grafana/dashboards/airi-server-overview-cloud.json](/Users/luoling8192/Git/moeru-ai/airi/apps/server/otel/grafana/dashboards/airi-server-overview-cloud.json) 已经按以下方式查询:
|
||||
|
||||
- Request rate by model: `gen_ai_request_model`
|
||||
- Request rate by operation: `gen_ai_operation_name` + `airi_gen_ai_operation_kind`
|
||||
- Latency by model: `gen_ai_request_model`
|
||||
- Flux consumed by model: `gen_ai_request_model`
|
||||
- Token throughput by model: `gen_ai_request_model`
|
||||
|
||||
如果未来新增本地 dashboard 或新的 cloud dashboard,默认按这一套 label 维度来。
|
||||
|
||||
## Span Name 策略
|
||||
|
||||
span name 目前允许保留业务可读格式,例如:
|
||||
|
||||
- `llm.gateway.chat`
|
||||
- `llm.gateway.tts`
|
||||
- `llm.gateway.asr`
|
||||
|
||||
原因:
|
||||
|
||||
- span name 主要服务于人工浏览和局部检索。
|
||||
- 语义筛选应优先依赖 attributes,而不是依赖 span name 文本。
|
||||
|
||||
如果未来统一 span name,也应保证查询主要依赖 `gen_ai.*` / `db.*` / `airi.*` attributes。
|
||||
|
||||
## 修改前检查
|
||||
|
||||
新增 observability 字段前,先问自己:
|
||||
|
||||
1. 这个字段能否映射到已有 OTel semconv?
|
||||
2. 如果不能,它是否明确属于 AIRI 业务语义?
|
||||
3. 如果属于 AIRI,是否应该挂到 `airi.*`,而不是新造顶级前缀?
|
||||
4. 我改的是 attribute key 还是 metric name / span name?
|
||||
5. 如果是 metric name,是否已经评估 Prometheus / Grafana / alerting 兼容性?
|
||||
6. 如果要改 dashboard,我是否优先用了 `gen_ai_request_model`、`gen_ai_operation_name`、`airi_gen_ai_operation_kind`,而不是旧的 `model` / `type`?
|
||||
|
||||
## 当前参考实现
|
||||
|
||||
- [packages/server-shared/src/observability.ts](/Users/luoling8192/Git/moeru-ai/airi/packages/server-shared/src/observability.ts)
|
||||
- [apps/server/src/routes/v1completions.ts](/Users/luoling8192/Git/moeru-ai/airi/apps/server/src/routes/v1completions.ts)
|
||||
- [apps/server/src/libs/otel.ts](/Users/luoling8192/Git/moeru-ai/airi/apps/server/src/libs/otel.ts)
|
||||
- [services/telegram-bot/src/llm/actions.ts](/Users/luoling8192/Git/moeru-ai/airi/services/telegram-bot/src/llm/actions.ts)
|
||||
- [services/telegram-bot/src/bots/telegram/agent/actions/read-message.ts](/Users/luoling8192/Git/moeru-ai/airi/services/telegram-bot/src/bots/telegram/agent/actions/read-message.ts)
|
||||
@@ -0,0 +1,188 @@
|
||||
# Redis Boundaries And Pub/Sub
|
||||
|
||||
## 目标
|
||||
|
||||
这篇文档约束服务端使用 Redis 时的几个高风险边界:
|
||||
|
||||
- key / channel 拼接
|
||||
- Pub/Sub payload 序列化与反序列化
|
||||
- Redis 返回值的运行时校验
|
||||
- Redis 与 Postgres 的职责边界
|
||||
|
||||
这不是“推荐写法”集合,而是后续改代码时应该默认遵守的约束。
|
||||
|
||||
## 一句话规则
|
||||
|
||||
- 不要在业务代码里到处手写 Redis key / channel 模板字符串。
|
||||
- 不要把 TypeScript 类型注解当成 Redis 边界的运行时校验。
|
||||
- 不要把 Pub/Sub 当持久化通道。
|
||||
- 不要让 Redis 承担余额、账本、订单这类真相源职责。
|
||||
|
||||
## Redis 职责边界
|
||||
|
||||
当前 `apps/server` 中 Redis 主要承担四类职责:
|
||||
|
||||
- cache
|
||||
- 例如 `user:{userId}:flux`
|
||||
- config KV
|
||||
- 例如 `config:{key}`
|
||||
- Pub/Sub
|
||||
- 例如聊天跨实例广播 `chat:{userId}:broadcast`
|
||||
- Streams
|
||||
- 例如 `billing-events`
|
||||
|
||||
其中:
|
||||
|
||||
- Postgres 是余额、账本、订单、聊天消息等持久状态的唯一真相源
|
||||
- Redis Pub/Sub 只负责降低跨实例通知延迟,不提供持久化、回放、补偿
|
||||
- Redis Streams 用于异步事件消费,但也必须在边界层做输入输出校验
|
||||
|
||||
## Key / Channel 收口规则
|
||||
|
||||
### 必须收口
|
||||
|
||||
Redis key 和 Pub/Sub channel 必须通过单独 helper 构造,不要在多个调用点重复写模板字符串。
|
||||
|
||||
推荐模式:
|
||||
|
||||
```ts
|
||||
function fluxRedisKey(userId: string): string {
|
||||
return `user:${userId}:flux`
|
||||
}
|
||||
|
||||
function userBroadcastChannel(userId: string): string {
|
||||
if (typeof userId !== 'string' || userId.length === 0) {
|
||||
throw new TypeError('user broadcast channel requires a non-empty string userId')
|
||||
}
|
||||
|
||||
return `chat:${userId}:broadcast`
|
||||
}
|
||||
```
|
||||
|
||||
这样做的原因不是“风格统一”,而是为了避免:
|
||||
|
||||
- key 前缀分散在多个文件
|
||||
- 某个调用点把对象、空串、错误 id 拼进 channel
|
||||
- 后续重构前缀或路由粒度时漏改
|
||||
|
||||
### 禁止依赖模板字符串兜底
|
||||
|
||||
不要假设 `` `${value}` `` 可以安全把任意值转成 Redis key。
|
||||
|
||||
原因:
|
||||
|
||||
- 如果 `value` 在运行时是对象,会得到 `[object Object]`
|
||||
- 这类错误不会在 TypeScript 编译期暴露
|
||||
- 一旦写进 Redis channel / key,排查成本很高
|
||||
|
||||
## Pub/Sub Payload 规则
|
||||
|
||||
### 发布侧
|
||||
|
||||
发布侧必须显式构造消息对象,不要把“业务对象刚好长得像 payload”当成协议。
|
||||
|
||||
推荐模式:
|
||||
|
||||
```ts
|
||||
interface BroadcastMessage {
|
||||
userId: string
|
||||
payload: {
|
||||
chatId: string
|
||||
messages: unknown[]
|
||||
fromSeq: number
|
||||
toSeq: number
|
||||
}
|
||||
}
|
||||
|
||||
function createBroadcastMessage(
|
||||
userId: string,
|
||||
payload: BroadcastMessage['payload'],
|
||||
): BroadcastMessage {
|
||||
if (typeof userId !== 'string' || userId.length === 0) {
|
||||
throw new TypeError('broadcast message requires a non-empty string userId')
|
||||
}
|
||||
|
||||
return { userId, payload }
|
||||
}
|
||||
```
|
||||
|
||||
### 消费侧
|
||||
|
||||
消费侧不要只写:
|
||||
|
||||
```ts
|
||||
const data = JSON.parse(message) as BroadcastMessage
|
||||
```
|
||||
|
||||
因为这只是类型断言,不是校验。
|
||||
|
||||
至少要验证:
|
||||
|
||||
- `userId` 是非空字符串
|
||||
- `payload.chatId` 是字符串
|
||||
- `payload.messages` 是数组
|
||||
- `fromSeq` / `toSeq` 是数字
|
||||
|
||||
如果消息不合法,应该记录错误并丢弃,而不是继续广播到本地连接。
|
||||
|
||||
## Streams 边界规则
|
||||
|
||||
Redis Streams 的参考实现已经在 `src/libs/mq/stream.ts` 里。
|
||||
|
||||
这层模式值得复用的点有两个:
|
||||
|
||||
- 输入通过 `serialize()` 收口
|
||||
- 输出通过 `deserialize()` 和运行时检查收口
|
||||
|
||||
也就是说:
|
||||
|
||||
- 不要在业务代码里裸写 `XADD` / `XREADGROUP`
|
||||
- 不要相信 Redis 返回值一定符合你期望的 shape
|
||||
- 边界校验失败时应该尽早抛错,而不是继续传播脏数据
|
||||
|
||||
## Chat WS 当前约束
|
||||
|
||||
`src/routes/chat-ws.ts` 当前采用:
|
||||
|
||||
- 同实例内存连接表
|
||||
- 跨实例 Redis Pub/Sub
|
||||
|
||||
这个设计的语义必须明确:
|
||||
|
||||
- 广播通知不是持久化消息
|
||||
- 丢广播不会丢聊天真相数据
|
||||
- 客户端补齐消息仍然依赖 `pullMessages`
|
||||
|
||||
因此后续如果改聊天同步:
|
||||
|
||||
- 需要“可重放”时,不要继续堆在 Pub/Sub 上
|
||||
- 需要“跨实例即时通知”时,可以继续用 Pub/Sub
|
||||
- 需要“持久事件消费”时,应优先考虑 Streams
|
||||
|
||||
## 修改 Redis 代码时的检查清单
|
||||
|
||||
- 这个 Redis 数据是 cache、KV、Pub/Sub 还是 Stream?
|
||||
- 它是不是被误当成真相源?
|
||||
- key / channel 是否通过 helper 统一构造?
|
||||
- 是否校验了关键标识符,例如 `userId`、`chatId`、`streamMessageId`?
|
||||
- Pub/Sub payload 是否有显式创建函数和解析函数?
|
||||
- 解析失败时是否会安全丢弃,而不是继续传播?
|
||||
- 这个需求是否其实应该用 Streams,而不是 Pub/Sub?
|
||||
|
||||
## 当前代码可直接参考的位置
|
||||
|
||||
- key helper
|
||||
- `src/services/flux.ts`
|
||||
- Streams 边界封装
|
||||
- `src/libs/mq/stream.ts`
|
||||
- Pub/Sub 聊天广播
|
||||
- `src/routes/chat-ws.ts`
|
||||
- 命名规范和待迁移事项
|
||||
- `config-and-naming-conventions.md`
|
||||
|
||||
## 对 AI / 后续修改者的直接要求
|
||||
|
||||
- 新增 Redis key / channel 时,先写 helper,再写调用点
|
||||
- 新增 Pub/Sub payload 时,先定义消息 shape 和 parse / create 边界,再接业务逻辑
|
||||
- 如果看到业务代码里散落 `` `prefix:${id}` ``,优先做小范围收口
|
||||
- 如果看到 `JSON.parse(...) as SomeType` 出现在 Redis 边界,默认把它视为待修复点
|
||||
@@ -10,7 +10,7 @@
|
||||
- `/api/providers`
|
||||
- `/api/chats`
|
||||
- `/api/v1`
|
||||
- `/api/flux`
|
||||
- `/api/users/me/flux`
|
||||
- `/api/stripe`
|
||||
- `GET /ws/chat`
|
||||
|
||||
@@ -135,6 +135,12 @@
|
||||
- 同实例:内存 `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`
|
||||
|
||||
实现位置:
|
||||
@@ -150,11 +156,8 @@
|
||||
|
||||
- `POST /api/v1/chat/completions`
|
||||
- `POST /api/v1/chat/completion`
|
||||
|
||||
已实现但暂未挂载:
|
||||
|
||||
- `handleTTS`
|
||||
- `handleTranscription`
|
||||
- `POST /api/v1/audio/speech`
|
||||
- `POST /api/v1/audio/transcriptions`
|
||||
|
||||
请求流程:
|
||||
|
||||
@@ -178,7 +181,7 @@
|
||||
- 流结束后再 best-effort 扣费
|
||||
- 扣费失败只打 error log,不回滚给客户端
|
||||
|
||||
### `/api/flux`
|
||||
### `/api/users/me/flux`
|
||||
|
||||
实现位置:
|
||||
|
||||
@@ -189,9 +192,9 @@
|
||||
|
||||
主要能力:
|
||||
|
||||
- `GET /api/flux`
|
||||
- `GET /api/users/me/flux`
|
||||
- 读取当前用户余额
|
||||
- `GET /api/flux/history`
|
||||
- `GET /api/users/me/flux/history`
|
||||
- 读取用户可见流水
|
||||
|
||||
### `/api/stripe`
|
||||
|
||||
@@ -85,6 +85,8 @@
|
||||
- 真正补齐消息还是靠 `pullMessages`
|
||||
- 广播只是为了降低拉取延迟,不代表存在旧式 `sync` 端点
|
||||
|
||||
如果要改 Redis key / channel 构造、Pub/Sub payload 或 Streams 边界,先看 `redis-boundaries-and-pubsub.md`。
|
||||
|
||||
## OpenTelemetry
|
||||
|
||||
初始化在 `src/libs/otel.ts`。
|
||||
|
||||
@@ -8,17 +8,8 @@
|
||||
"ESNext"
|
||||
],
|
||||
"useDefineForClassFields": true,
|
||||
"baseUrl": ".",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Bundler",
|
||||
"paths": {
|
||||
"@proj-airi/server-sdk-shared": [
|
||||
"../../packages/server-sdk-shared/src/index.ts"
|
||||
],
|
||||
"@proj-airi/server-sdk-shared/*": [
|
||||
"../../packages/server-sdk-shared/src/*"
|
||||
]
|
||||
},
|
||||
"resolveJsonModule": true,
|
||||
"types": [
|
||||
"vitest",
|
||||
@@ -29,7 +20,6 @@
|
||||
"skipLibCheck": true
|
||||
},
|
||||
"include": [
|
||||
"../../packages/server-sdk-shared/src/**/*.ts",
|
||||
"src/**/*.ts",
|
||||
"src/**/*.d.ts"
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user