Files
Alfred/docs/development/backend.md
lanyuanxiaoyao db40d04dc5 refactor(db): 统一数据库 schema — 软删除、命名规范、约束标准化
- 全表新增 deleted_at 列,统一软删除替代硬删除+archived_at
- models.model_id 重命名为 external_id,消除语义混淆
- conversations.model_id 改为可空(模型为建议而非绑定)
- messages 新增 updated_at,移除 CASCADE 改为 DAO 层级联
- 移除 DB 层 UNIQUE 约束,改为应用层检查(配合软删除)
- 新增 helpers.ts(baseColumns + 构造层防御)、ESLint 规则、契约测试
- 迁移 0004 补全 CHECK 约束(providers.type/materials.status/messages.role)
- DAO 层全面重写:级联软删除、应用层唯一、provider 删除保护
- 路由/前端/测试全量适配 externalId 重命名及类型变更
2026-06-05 01:02:23 +08:00

108 lines
7.6 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.
# 后端开发
开发规范见 [开发规范文档](README.md)。
## 共享工具
`src/server/helpers/`:
- `response.ts``createApiError(error, status)``createHeaders(mode, init)``createMetaResponse(version)``formatDuration(ms)``jsonResponse(body, options)`
- `url.ts``parseIdFromUrl(url)`
- `list-params.ts``parseListParams(url, mode, options?)` — 统一校验分页/排序参数,替代 validatePagination
- `pagination.ts``paginateQuery()` — Drizzle 分页查询封装
`src/server/middleware/`:
- `validate.ts``validateIdParam(idStr, mode)` — 校验 ID 格式(字母数字 + `_-``validatePagination(pageParam, pageSizeParam, mode)` — page≥1, pageSize≤200`validateTimeRange(from, to, mode)`
- `error-handler.ts``AppError` — 业务异常类(含 statusCode`withErrorHandler(fn, mode, logger?)` — 包裹 handler 捕获异常
## 数据库
SQLite + bun:sqlite + Drizzle ORM。
- `src/server/db/schema.ts`Drizzle 表结构,列名 snake_caseTS 类型 camelCase。所有业务表通过 `helpers.ts``baseColumns` 获取 id/created_at/updated_at/deleted_at。
- `src/server/db/helpers.ts``baseColumns` 常量id、createdAt、updatedAt、deletedAt+ Drizzle 构建器再导出。`src/server/db/` 内禁止直接从 `drizzle-orm/sqlite-core` 导入 `sqliteTable`ESLint 强制)。
- `src/server/db/connection.ts``createDatabase(dataDir, logger)` 打开 `alfred.db`PRAGMAforeign_keys=ON、journal_mode=WAL、busy_timeout=5000。`wrap(db)` 转 Drizzle 实例(`DrizzleDB` 类型)。工具函数:`timestamp()``notDeleted(table)``softDeleteRecord(db, table, id)``paginateQuery()`(支持 `softDelete` 参数自动过滤已删除行)。
- Migration开发期 `drizzle-kit generate` 产出到 `drizzle/`;生产期嵌入可执行文件,启动时自动应用。备份到 `<dataDir>/backups/`,事务中执行(迁移期间临时关闭外键检查),失败回滚。
### 软删除
所有业务表projects、providers、models、conversations、materials、messages使用 `deleted_at` 列实现软删除,不暴露给 API 层。DAO 查询通过 `notDeleted(table)``paginateQuery({ softDelete })` 自动过滤已删除行。唯一性校验在应用层完成(同名 + `deleted_at IS NULL`),无数据库级 UNIQUE 约束。级联软删除:删除项目 → 级联软删除会话(→ 消息)+ 素材;删除会话 → 级联软删除消息;删除供应商 → 需无未删除模型。
### 数据访问函数
| 文件 | 函数 |
| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `projects.ts` | createProject、getProject、listProjects、updateProject、deleteProject、archiveProject、restoreProject |
| `providers.ts` | createProvider、getProvider、listProviders、listProviderOptions、updateProvider、deleteProvider |
| `models.ts` | createModel、getModel、listModels、getModelWithProvider、getModelsByProviderId、updateModel、deleteModel |
| `conversations.ts` | createConversation、getConversation、listConversations、updateConversation、updateConversationTimestamp、deleteConversation、createMessage、createMessages、listMessages |
| `materials.ts` | createMaterial、getMaterial、listMaterials、deleteMaterial |
输入输出类型来自 `src/shared/api.ts`
## AI 服务层
- `src/server/ai/types.ts``AIProviderConfig`name、type、baseUrl、apiKey`AIModelConfig`providerId、modelId、capabilities。注AI 层 `modelId` 对应 DB 层 `Model.externalId`
- `src/server/ai/registry.ts`
- `buildProviderRegistry(db)` — 从 DB 查询供应商构建 AI SDK Provider Registry每次调用重建不缓存。通过 `registry.languageModel('providerId:modelId')` 获取模型实例。
- `testProviderConnection(config, logger)` — 测试 Base URL 可达性 + `/models` 接口
- `testModelConnection(config, logger)` — 测试模型连通性(需传入含 modelId 的合并配置)
- `src/server/ai/agents/alfred-agent.ts``createAlfredAgent(model)` — ToolLoopAgent + `stepCountIs(20)` + `getCurrentTime` 工具。
- `src/server/ai/tools/`AI 工具定义。
### 供应商类型
| type | AI SDK factory |
| ------------------- | --------------------------------------------------- |
| `openai` | `createOpenAI({ apiKey, baseURL })` |
| `anthropic` | `createAnthropic({ apiKey, baseURL })` |
| `openai-compatible` | `createOpenAICompatible({ name, apiKey, baseURL })` |
### 连通性测试
- `POST /api/providers/test` — 用未保存配置测试,不写入 DB不阻止保存。Base URL 不可达或 API Key 无效返回 `ok: false``/models` 不支持返回 `ok: true` + 提示。
- `POST /api/models/test` — 用模型关联供应商 + externalId 测试。
## 素材 API
| 方法 | 路径 | 说明 |
| ------ | ---------------------------------- | ---------------------- |
| GET | `/api/projects/:id/materials` | 列出项目下素材(分页) |
| POST | `/api/projects/:id/materials` | 创建素材 |
| GET | `/api/projects/:id/materials/:mid` | 获取素材详情 |
| DELETE | `/api/projects/:id/materials/:mid` | 删除素材(软删除) |
校验description 必填非空associatedDate 必填 YYYY-MM-DD项目须存在且 active 且未删除,素材归属校验不匹配返回 403。
## 聊天 API
| 方法 | 路径 | 说明 |
| ------ | ----------------------------------------------- | ------------------ |
| GET | `/api/projects/:id/conversations` | 列出项目下所有会话 |
| POST | `/api/projects/:id/conversations` | 创建新会话 |
| GET | `/api/projects/:id/conversations/:cid` | 获取会话详情 |
| PATCH | `/api/projects/:id/conversations/:cid` | 更新会话 |
| DELETE | `/api/projects/:id/conversations/:cid` | 删除会话及消息 |
| GET | `/api/projects/:id/conversations/:cid/messages` | 获取消息列表 |
| POST | `/api/projects/:id/chat` | 发送消息SSE 回复 |
`send.ts`:验证会话归属 → 保存用户消息 → `createAlfredAgent``createAgentUIStreamResponse``onFinish` 持久化 AI 回复。
## 日志
| 实现 | 用途 |
| --------------------- | -------------- |
| PinoLoggerWrapper | 生产运行时 |
| ConsoleFallbackLogger | 配置加载前降级 |
| NoopLogger | 静默丢弃 |
| MemoryLogger | 测试替身 |
## 版本管理
唯一来源:`package.json`。开发模式从 package.json 运行时读取;生产模式构建时烘焙为字面量。
## 更新触发条件
修改后端模块 API、共享工具、数据库 schema、AI 服务层、聊天 API 或列表查询参数解析时,必须更新本文档。管理页面 CRUD 通用模式的详细约定见 [crud.md](crud.md)。