Files
Alfred/docs/development/backend.md

9.3 KiB
Raw Blame History

后端开发

本文档说明 alfred 后端的 API、配置加载、日志、版本管理和后端测试开发约定。

适用场景:修改 src/server/、src/shared/api.ts、后端测试、配置契约、API 响应或日志模块。

库使用优先级

优先级 来源 典型用途
1 Bun 内置 API Bun.serve、Bun.file、Bun.YAML、Bun.spawn、bun:sqlite
2 es-toolkit 类型判断、深度比较、并发控制
3 标准 Web API Headers、fetch、AbortController
4 主流三方库 pino、@sinclair/typebox、ajv、drizzle-orm
5 自行实现 仅在以上都无法满足时

新增依赖前必须先检查上述每一层是否已有可用方案。

API 路由开发

路由文件位于 src/server/routes/,每个端点一个文件。路由通过 server.ts 的 Bun.serve({ routes }) 声明式注册。

新增路由步骤:

  1. 在 src/server/routes/ 下创建 .ts
  2. 实现 handler 函数并 export
  3. 在 server.ts 的 routes 对象中注册路径和 method handler
  4. 在 tests/server/ 中添加对应测试

共享工具

helpers.ts 提供跨路由共用的响应工具:

  • createApiError(error, status) — 构造 API 错误体
  • createHeaders(mode, init) — 创建响应 Headers
  • jsonResponse(body, options) — JSON 响应构造

middleware.ts 提供 API 参数校验函数:

  • validateIdParam(idStr, mode) — 校验 ID 参数格式
  • validatePagination(pageParam, pageSizeParam, mode) — 校验分页参数
  • validateTimeRange(from, to, mode) — 校验时间范围参数

数据库

项目使用 SQLite 作为存储后端,通过 bun:sqlite + Drizzle ORM 实现类型安全的数据访问。

schema 定义

src/server/db/schema.ts 使用 Drizzle ORM 定义表结构,列名使用 snake_caseTypeScript 类型使用 camelCaseDrizzle schema 负责映射。

数据库连接

src/server/db/connection.tscreateDatabase(dataDir, logger) 打开 <dataDir>/alfred.db,设置 PRAGMAforeign_keys=ON、journal_mode=WAL、busy_timeout=5000

migration 机制

  • 开发期:使用 drizzle-kit generate 从 TS schema 生成 SQL migration 文件到 drizzle/ 目录
  • 生产期:构建时将 drizzle/*.sql 嵌入可执行文件,启动时自动应用 pending migrations
  • 每次 migration 前自动备份现有 DB 到 <dataDir>/backups/alfred-<timestamp>.db
  • migration 在事务中执行,失败则回滚并停止启动

数据访问

src/server/db/projects.ts 提供项目数据访问函数,src/server/db/providers.ts 提供供应商数据访问函数,src/server/db/models.ts 提供模型数据访问函数,src/server/db/conversations.ts 提供会话和消息数据访问函数。输入输出使用 src/shared/api.ts 的类型。函数内部使用 Drizzle query builder 包装 bun:sqlite Database。

AI 服务层

src/server/ai/ 提供 AI Provider Registry 构建与连接测试能力。

类型定义

src/server/ai/types.ts 定义 AI 配置类型:

  • AIProviderConfig — 供应商配置name、type、baseUrl、apiKey
  • AIModelConfig — 模型配置providerId、modelId、capabilities
  • AIRegistryConfig — Registry 构建配置providers、models供后续 AI 调用层组合使用

Registry 构建

src/server/ai/registry.ts 提供:

  • buildProviderRegistry(db) — 从 DB 查询所有供应商,构建 Vercel AI SDK Provider Registry
  • testProviderConnection(config) — 先测试 Base URL 可达性,再请求 /models 验证 API Key 和模型列表接口

每次 AI 调用时从 DB 查询 providers构建 registry 后通过 registry.languageModel('providerId:modelId') 获取模型实例。不使用缓存层。模型是否存在以及业务能力标签由调用方基于 models 表先行校验registry 只负责将 providerId/modelId 映射到 AI SDK 模型实例。

Agent 流式调用

src/server/ai/agent-stream.ts 提供 agentStream(options) 函数,封装 Vercel AI SDK streamText 调用。接收数据库实例、消息数组和模型 DB ID从 DB 查询模型与供应商信息后构建 Provider Registry使用 : 作为 provider 和 modelId 的分隔符。默认使用 stepCountIs(1) 限制单步调用。返回 StreamTextResult,路由层通过 result.toUIMessageStreamResponse() 转为 SSE 响应。

供应商连通性测试

供应商连通性测试返回 { providerTestResponse: { ok, message } },前端根据 ok 展示成功或失败提示。

  • POST /api/providers/test — 使用表单中尚未保存的供应商配置测试连接
  • POST /api/models/test — 使用模型关联供应商配置和 modelId 测试模型连接

测试连接不会写入数据库也不会阻止保存。Base URL 不可达或 API Key 无效返回 ok: falseBase URL 可达但 /models 不支持、非标准或返回非鉴权错误时返回 ok: true 并在 message 中提示用户可检查 URL 或忽略提醒。

支持的供应商类型

type AI SDK factory
openai createOpenAI({ apiKey, baseURL })
anthropic createAnthropic({ apiKey, baseURL })
openai-compatible createOpenAICompatible({ name, apiKey, baseURL })

聊天 API

聊天 API 按项目维度组织会话和消息:

方法 路径 说明
GET /api/projects/:id/conversations 列出项目下所有会话
POST /api/projects/:id/conversations 创建新会话
GET /api/projects/:id/conversations/:cid 获取会话详情
DELETE /api/projects/:id/conversations/:cid 删除会话及其消息
GET /api/projects/:id/conversations/:cid/messages 获取会话消息列表
POST /api/projects/:id/chat 发送消息并流式回复

聊天路由处理器位于 src/server/routes/chat/,遵循统一的 handler 模式。send.ts 处理发送消息:验证会话归属后保存用户消息到 DB调用 agentStream 获取流式响应,返回 SSE UI 消息流,流结束后后台保存 AI 回复到 DB。

类型规范

  • 共享类型以 src/shared/api.ts 为唯一源头
  • 应用常量以 src/shared/app.ts 为唯一源头
  • 版本号以 package.json.version 为唯一源头
  • 前端不得 import src/server/ 下的任何文件
  • 严格联合类型优先于宽类型

配置契约

配置加载流程固定为unknown -> AuthoringConfig -> NormalizedConfig -> ValidatedConfig -> ServerConfig。

Ajv 保持严格拒绝模式allErrors: true、不启用类型强制转换、不注入默认值、不自动删除未知字段。

新增或修改配置字段时必须同步更新 TypeBox schema fragments、config.schema.json、测试和对应用户文档。

日志模块

后端运行时代码统一通过 Logger 接口输出日志,禁止直接使用 console.*。

实现 用途
PinoLoggerWrapper 生产运行时
ConsoleFallbackLogger 配置加载失败前的降级日志
NoopLogger 静默丢弃日志
MemoryLogger 测试替身

敏感信息会自动 redact authorization、cookie、password 等字段。

版本管理

项目使用 package.json.version 作为版本号唯一来源。

版本获取方式:

  • 开发模式src/server/version.ts 运行时从 package.json 读取
  • 生产模式scripts/build.ts 在构建时将版本号烘焙为字面量注入

版本升迁命令:

bun run version:patch   # 升迁 patch 版本
bun run version:minor   # 升迁 minor 版本
bun run version:major   # 升迁 major 版本
bun run version:set     # 显式设置版本号

后端测试

变更类型 测试重点
API 路由 tests/server/app.test.ts 集成行为
配置 schema schema 导出、合法/非法配置
helpers/middleware 单元测试

后端测试约定:

  • API 路由集成测试必须通过真实 startServer 覆盖路由注册、HTTP method、fallback、响应 header 和核心错误路径。
  • SQLite 相关测试必须复用 tests/helpers.ts 中的测试数据库 helper不要在测试文件内分散实现临时目录清理或直接裸用 rmSync(dir, { recursive: true })
  • DAO 和路由边界测试应优先使用真实 migration 初始化测试库,只有 migration 执行器单测可以使用最小 fake migration。
  • logger/bootstrap 中预期的 fallback 输出必须在测试中捕获并断言,正常通过的测试不应污染 stdout/stderr。

更新触发条件

修改后端 API、共享类型、配置契约、日志模块、版本管理或后端测试规范时必须更新本文档。