代码标准与规范
LibreChat 的编码标准、工作区边界以及贡献规范。
工作区边界
LibreChat 是一个 monorepo。所有新代码都应针对正确的工作区:
| 工作区 | 语言 | 端侧 | 用途 |
|---|---|---|---|
/api | JS (旧版) | 后端 | Express 服务器 — 尽量减少在此处的更改 |
/packages/api | TypeScript | 后端 | 新的后端代码存放于此(仅限 TS,由 /api 调用) |
/packages/data-schemas | TypeScript | 后端 | 数据库模型/模式以及特定于数据库的共享逻辑 |
/packages/data-provider | TypeScript | 共享 | API 类型、endpoint、数据服务 — 由前端和后端使用 |
/client | TypeScript/React | 前端 | 前端 SPA |
/packages/client | TypeScript | 前端 | 共享前端工具库 |
- 所有新的后端代码必须使用 TypeScript,位于
/packages/api中。 - 将
/api的更改保持在绝对最低限度(仅保留调用/packages/api的精简 JS 包装器)。 - 数据库特定的共享逻辑位于
/packages/data-schemas中。 - 前端/后端共享的 API 逻辑(endpoints、types、data-service)位于
/packages/data-provider中。 - 从项目根目录构建所有已编译代码:
npm run build。 - 在 API/类型更改后重新构建共享 data-provider 代码:
npm run build:data-provider。
通用指南
- 遵循“整洁代码”原则:保持函数和模块短小精悍,坚持单一职责原则,并编写具有表现力且易于阅读的代码。
- 使用有意义且具描述性的变量名和函数名。
- 在代码的可读性和可维护性与简洁性之间,优先考虑前者。
- 使用提供的
.eslintrc和.prettierrc文件以保持代码格式的一致性。 - 使用自动修复功能修复所有格式化 lint 错误(如果可用)。必须解决所有 TypeScript/ESLint 警告和错误。
命名与文件组织
- 尽可能使用单单词文件名,例如
permissions.ts、capabilities.ts或service.ts。 - 当需要多个单词时,优先使用能够提供文件上下文的单单词目录,例如使用
admin/capabilities.ts而不是adminCapabilities.ts。 - 让目录结构提供上下文。优先使用
app/service.ts而非app/appConfigService.ts。
代码结构
- Never-nesting(避免嵌套):使用提前返回(early returns)、扁平化代码、最小化缩进。将复杂操作拆分为命名清晰的辅助函数。
- 函数式优先:使用纯函数、不可变数据,以及
map/filter/reduce来替代命令式循环。仅在能显著改善领域建模或状态封装时才考虑使用 OOP。 - 禁止使用动态导入,除非绝对必要。
- 将重复的逻辑提取到专门的工具函数中(DRY 原则)。优先使用参数化辅助函数、常量、共享验证器、集中式错误处理和共享类型,而不是使用近乎重复的实现。
迭代与性能
- 最小化循环 —— 尤其是针对消息数组等共享数据结构,这些结构会被频繁迭代。在规模化场景下,每一次额外的遍历都会增加开销。
- 尽可能将连续的 O(n) 操作合并为单次遍历;如果工作可以合并,切勿对同一个集合进行两次循环。
- 选择能够减少迭代需求的数据结构(例如,使用
Map/Set进行查找,而不是使用Array.find/Array.includes)。 - 避免不必要的对象创建;考虑空间与时间的权衡。
- 防止内存泄漏:请谨慎使用闭包,及时释放资源/事件监听器,并避免循环引用。
类型安全
- 严禁使用
any。所有参数、返回值和变量必须使用显式类型。 - 限制
unknown— 避免使用unknown、Record<string, unknown>以及as unknown as T断言。Record<string, unknown>几乎总是意味着缺少显式的类型定义。 - 不要重复定义类型 —— 在定义新类型之前,请检查项目中是否已经存在该类型(特别是
packages/data-provider中)。请复用并扩展现有类型。 - 适当地使用联合类型、泛型和接口。
注释与文档
- 编写自解释代码;不要使用行内注释来叙述代码的功能。
- 仅针对复杂/非直观逻辑或公共 API 的智能感知使用 JSDoc。
- 单行 JSDoc 用于简要说明,多行 JSDoc 用于复杂情况。
- 除非绝对必要,否则请避免使用独立的
//注释。
导入顺序
导入内容按以下三个部分组织(按顺序排列):
- 包导入 — 按行长度从短到长排序(
react始终是第一个导入)。 import type导入 — 按从长到短的顺序排序(先包类型,后本地类型;长度排序在子组之间重置)。- 本地/项目导入 — 按从长到短的顺序排列。
- 尽可能将来自同一模块的值导入合并在一起。
- 始终使用独立的
import type { ... }进行类型导入;切勿在值导入中使用内联type关键字(例如,import { Foo, type Bar }是错误的)。
循环偏好设置
- 尽可能限制循环。 优先使用单次遍历转换,并避免对同一数据进行重复迭代。
- 对于性能关键或依赖索引的操作,请使用
for (let i = 0; ...)。 - 使用
for...of进行简单的数组迭代。 for...in仅用于对象属性枚举。
Node.js API Server
API 设计
- 在设计 API 时遵循 RESTful 原则。
- 为路由、控制器、服务和模型使用具有意义且描述性的名称。
- 为每个路由使用适当的 HTTP 方法(GET、POST、PUT、DELETE)。
- 使用适当的状态码和响应结构以确保 API 响应的一致性(2xx 表示成功,4xx 表示客户端请求错误,5xx 表示服务器错误)。
- 使用 try-catch 代码块来捕获并优雅地处理异常。
- 实现适当的错误处理并始终返回相应的错误响应。
- 使用
utils目录中包含的日志系统来记录重要事件和错误。 - 使用
requireJWTAuth中间件进行基于 JWT 的无状态身份验证。
文件结构
新的后端代码应以 TypeScript 形式存放在 /packages/api 中。旧有的 /api 目录遵循以下结构:
路由
指定每个 HTTP 请求方法、要使用的任何中间件,以及为每个路由调用的控制器函数。
- 使用 Express Router 在单独的文件中为每个资源或逻辑分组定义路由。
- 使用具有描述性的路由名称并遵循 RESTful 规范。
- 保持路由简洁并专注于单一职责。
- 为所有路由添加
/api命名空间前缀。
控制器
包含每个路由的逻辑,包括调用相应的服务函数并返回适当的响应状态码和 JSON 正文。
- 为每个路由创建一个单独的控制器文件,以处理请求/响应逻辑。
- 使用 PascalCase 命名规范命名控制器文件,并在文件名后添加 "Controller" 后缀(例如
UserController.js)。 - 通过将复杂操作委托给 service 或 model 文件,保持 controller 的精简。
服务
包含跨多个控制器共享的复杂业务逻辑或操作。
- 使用 PascalCase 命名约定命名服务文件,并在文件名后添加 "Service"(例如
AuthService.js)。 - 避免将服务与特定模型或数据库紧密耦合,以提高可重用性。
- 在每个服务中保持单一职责原则。
模型
定义 Mongoose 模型以表示数据实体及其关系。
- 使用单数、PascalCase 命名格式来命名模型文件及其关联的集合(例如
User.js和users集合)。 - 在模型中仅包含必要的字段、索引和验证。
- 通过避免直接引用请求/响应对象,保持模型与 API 层相互独立。
数据库访问 (MongoDB 和 Mongoose)
- 使用 Mongoose (https://mongoosejs.com) 作为 MongoDB ODM。
- 为每个实体创建单独的模型文件,并确保关注点分离。
- 使用 Mongoose 模式验证来强制执行数据完整性。
- 高效处理数据库连接并避免连接泄漏。
- 使用 Mongoose 查询构建器来创建简洁且易于阅读的数据库查询。
React Client
TypeScript 和 React 通用最佳实践
- 使用 TypeScript best practices 以受益于静态类型检查和改进的工具支持。
- 将相关文件归类在功能目录中(例如
SidePanel/Memories/)。 - 使用 PascalCase 命名约定来命名组件。
- 使用简洁且具有描述性的名称,以准确反映组件的用途。
- 在适当的情况下,将复杂的组件拆分为更小、可复用的组件。
- 保持组件内的渲染逻辑尽可能简洁。
- 将可重用部分提取为独立的函数或 hooks。
- 使用 TypeScript 类型或接口应用 prop 类型定义。
- 在适当的地方使用表单验证(我们使用 React Hook Form 进行表单验证和提交)。
本地化
- 所有面向客户端的文本必须使用
useLocalize()hook 进行本地化。 - 仅更新
client/src/locales/en/translation.json中的英文键(其他语言由外部自动处理)。 - 使用语义化本地化键前缀:
com_ui_、com_assistants_等。 - 始终为新的本地化键提供有意义的后备文本。
数据服务
- 在
client/src/data-provider/[Feature]/queries.ts中创建数据提供程序钩子 (hooks)。 - 从
client/src/data-provider/[Feature]/index.ts导出所有 hooks。 - 将功能导出添加到主
client/src/data-provider/index.ts中。 - 使用 React Query (
@tanstack/react-query) 处理所有 API 交互。 - 在变更(mutations)上实现正确的查询失效(query invalidation)。
- 在
packages/data-provider/src/keys.ts中添加 QueryKeys 和 MutationKeys。
当添加共享 API 集成时,请更新:
packages/data-provider/src/api-endpoints.ts(endpoints)packages/data-provider/src/data-service.ts(数据服务函数)packages/data-provider/src/types/queries.ts(TypeScript 类型)
性能
- 在大规模部署时优先考虑内存和速度效率。
- 为大型数据集实现适当的游标分页 (cursor pagination)。
- 通过正确使用依赖数组来避免不必要的重新渲染。
- 利用 React Query 的缓存和后台重新获取(refetching)功能。
测试与文档
- 使用 Jest 为所有关键和复杂的功能编写单元测试。
- 使用 Supertest 为所有 API endpoint 编写集成测试。
- 使用 Playwright 为所有客户端功能编写端到端测试。
- 使用具有描述性的测试用例和函数名称,以清晰地表达测试的目的。
- 在各自的工作区目录中运行测试:
cd api && npx jest <pattern>,cd packages/api && npx jest <pattern>等。 - 涵盖 UI/数据流的加载、成功和错误状态。
- 在前端测试中渲染组件时,请使用
test/layout-test-utils。 - 优先使用真实逻辑而非模拟(mock)。仅对无法在本地控制的内容进行模拟,例如外部 HTTP API、受速率限制的服务以及非确定性的系统调用。
- 当您需要断言调用情况而不替换底层实现时,请使用 spies。
- 在基于 MongoDB 的测试中使用
mongodb-memory-server,以便查询和模式验证能够模拟真实的数据库行为。
这篇指南怎么样?