# 后端开发 开发规范见 [开发规范文档](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_case,TS 类型 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`,PRAGMA:foreign_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/`;生产期嵌入可执行文件,启动时自动应用。备份到 `/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)。