Skip to main content
LibreChat is joining ClickHouse to power the open-source Agentic Data Stack 🎉 Learn more
LibreChat

代码标准与规范

LibreChat 的编码标准、工作区边界以及贡献规范。

工作区边界

LibreChat 是一个 monorepo。所有新代码都应针对正确的工作区:

工作区语言端侧用途
/apiJS (旧版)后端Express 服务器 — 尽量减少在此处的更改
/packages/apiTypeScript后端新的后端代码存放于此(仅限 TS,由 /api 调用)
/packages/data-schemasTypeScript后端数据库模型/模式以及特定于数据库的共享逻辑
/packages/data-providerTypeScript共享API 类型、endpoint、数据服务 — 由前端和后端使用
/clientTypeScript/React前端前端 SPA
/packages/clientTypeScript前端共享前端工具库
  • 所有新的后端代码必须使用 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.tscapabilities.tsservice.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 — 避免使用 unknownRecord<string, unknown> 以及 as unknown as T 断言。Record<string, unknown> 几乎总是意味着缺少显式的类型定义。
  • 不要重复定义类型 —— 在定义新类型之前,请检查项目中是否已经存在该类型(特别是 packages/data-provider 中)。请复用并扩展现有类型。
  • 适当地使用联合类型、泛型和接口。

注释与文档

  • 编写自解释代码;不要使用行内注释来叙述代码的功能。
  • 仅针对复杂/非直观逻辑或公共 API 的智能感知使用 JSDoc。
  • 单行 JSDoc 用于简要说明,多行 JSDoc 用于复杂情况。
  • 除非绝对必要,否则请避免使用独立的 // 注释。

导入顺序

导入内容按以下三个部分组织(按顺序排列):

  1. 包导入 — 按行长度从短到长排序(react 始终是第一个导入)。
  2. import type 导入 — 按从长到短的顺序排序(先包类型,后本地类型;长度排序在子组之间重置)。
  3. 本地/项目导入 — 按从长到短的顺序排列。
  • 尽可能将来自同一模块的值导入合并在一起。
  • 始终使用独立的 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.jsusers 集合)。
  • 在模型中仅包含必要的字段、索引和验证。
  • 通过避免直接引用请求/响应对象,保持模型与 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,以便查询和模式验证能够模拟真实的数据库行为。

这篇指南怎么样?