ARTICLE DETAIL

资讯详情

深耕商务建站与企业官网运营的一线实战洞察。

FastGPT 代码规范实战:从 DDD 分层目录到 TypeScript 与 Zod 工程实践的全面解析

FastGPT 代码规范实战:从 DDD 分层目录到 TypeScript 与 Zod 工程实践的全面解析 FastGPT 代码规范实战从 DDD 分层目录到 TypeScript 与 Zod 工程实践的全面解析【免费下载链接】FastGPTFastGPT is a knowledge-based platform built on the LLMs, offers a comprehensive suite of out-of-the-box capabilities such as data processing, RAG retrieval, and visual AI workflow orchestration, letting you easily develop and deploy complex question-answering systems without the need for extensive setup or configuration.项目地址: https://gitcode.com/GitHub_Trending/fa/FastGPTFastGPT 是一个基于 LLM 的知识库问答平台核心能力涵盖数据处理、RAG 检索与可视化 AI 工作流编排。其前端与后端共享大量类型与常量代码规模横跨packages/global、packages/service、packages/web等多个包因此建立统一、可维护的代码规范尤为重要。本文以仓库内 .agents/code/syntax.md 为骨架结合 packages/service/core/app 等真实源码系统讲解 FastGPT 的 DDD 目录组织、文件职责划分、层级依赖约束以及 TypeScript 类型体系、Zod 校验与 OpenAPI 风格的落地方式帮助你写出符合社区规范、易于评审与维护的代码。基础代码组织模式按业务域 → 子功能 → 固定文件三层划分FastGPT 采用 DDD领域驱动设计架构包内代码按业务域 → 子功能 → 固定文件三层划分。以packages/为例核心划分如下packages/ ├── global/core/ # 类型、常量前后端共享 │ ├── app/ │ │ ├── type.ts # 顶层聚合类型 │ │ ├── constants.ts │ │ ├── workflow/ │ │ │ ├── type.ts │ │ │ └── constants.ts │ │ ├── version/ │ │ │ └── type.ts │ │ └── evaluation/ │ │ └── type.ts │ ├── chat/ │ ├── dataset/ │ └── plugin/ │ └── service/core/ # 后端业务逻辑不可在前端引用 ├── app/ │ ├── schema.ts # App 主表 Mongoose Schema │ ├── entity.ts # findById / create / updateById 等基础操作封装 │ ├── service.ts # 聚合业务逻辑跨子功能协调不允许互相引用只允许单向依赖 │ ├── auth.ts # 鉴权相关如有 │ ├── utils.ts # 纯函数工具无副作用可独立单测 │ ├── version/ │ │ ├── schema.ts │ │ ├── entity.ts │ │ ├── service.ts │ │ └── utils.ts │ ├── evaluation/ │ │ ├── schema.ts # 合并多个 schema 到单文件 │ │ ├── entity.ts │ │ ├── service.ts │ │ └── utils.ts │ ├── logs/ │ └── tool/ │ ├── service.ts │ └── utils.ts ├── chat/ ├── dataset/ └── plugin/在仓库中可以看到这一约定已落实到实际代码packages/service/core/app 目录下存在schema.ts、controller.ts、utils.ts以及version/、evaluation/、tool/等子目录与文档中的目录骨架一一对应。叶子目录固定文件说明每个叶子目录不再细分子功能的目录统一放置四个固定文件职责严格分离文件职责schema.tsMongoose Schema 定义导出 Model 和 SchemaTypeentity.ts数据访问封装findById、create、updateById等基础操作service.ts业务逻辑调用 entity跨模块协调处理业务规则utils.ts纯函数工具无副作用可独立单测entity.ts与service.ts的分工示例// entity.ts 示例 —— 只做数据访问不含业务判断 export const findAppById (id: string) MongoApp.findById(id).lean(); export const createApp (data: AppCreateParams, session?: ClientSession) MongoApp.create([data], { session }); // service.ts 示例 —— 调用 entity处理业务规则 export const createAppAndInitVersion async (data: AppCreateParams, session?: ClientSession) { const app await createApp(data, session); await createVersion({ appId: app._id, ... }, session); return app; }; // service 需协同通过 props 传入另一个 service 或者衍生方法。 const service1 xxxx const service2 (props: {id:string; service1: typeof service1 }) { const data findAppById(id) return props.service1(data); };核心原则是entity.ts只做数据访问、不含业务判断service.ts调用 entity 并处理业务规则utils.ts保持纯函数、无副作用因此可以脱离数据库独立做单元测试。层级约束global/core/只放类型和常量禁止引入 mongoose、服务端 SDKservice/core/只在服务端使用禁止被packages/web/或前端页面直接引用子功能目录不超过3 层嵌套一个目录内无需拆子功能时直接放schema.tsentity.tsservice.tsutils.ts多个 schema 文件如evalSchema.tsevalItemSchema.ts合并到单个schema.ts。从 packages/service/core/app/schema.ts 可以看到AppSchema定义了parentId、teamId、tmbId、name、type、version等字段并通过AppCollectionName apps统一管理集合名符合schema 只定义数据模型的定位。代码风格贯穿类型安全与可读性的具体约定禁止 re-export禁止使用export { ... } from ...、export type { ... } from ...或export * from ...转导其他模块的成员包括index.tsbarrel、目录聚合入口和兼容旧路径的转发文件。每个导出只能由其实际定义文件提供使用方直接从定义模块导入index.ts可以包含自身的实现和定义但不能聚合导出其他文件移动定义时直接修改所有使用方的 import确认旧路径无引用后删除旧文件不为缩短 import 路径、隐藏目录结构或兼容旧路径新增转导层避免依赖来源不明确、循环依赖和无效模块加载。// ❌ 不好的实践通过目录入口转导其他模块 export { createLLMResponse } from ./createLLMResponse; export type { LLMResponse } from ./type; export * from ./constants; // ✅ 好的实践使用方直接引用成员的定义模块 import { createLLMResponse } from fastgpt/service/core/ai/llm/createLLMResponse; import type { LLMResponse } from fastgpt/global/core/ai/llm/type;使用type进行类型声明不使用interface// ❌ 不好的实践 interface User { id: string; name: string; } // ✅ 好的实践 type User { id: string; name: string; }使用type的考量在于联合类型、交叉类型、映射类型等能力只有type具备且统一使用type可以避免interface声明合并带来的隐式行为。使用 IIFE 写法取代 if/else 进行变量条件赋值// ❌ 不好的实践 if (condition) { value true; } else { value false; } // ✅ 好的实践 const value (() { if (condition) { return true; } return false; })();IIFE 将条件分支收敛在表达式内部变量声明与赋值在一条语句中完成避免先声明后赋值造成的中间状态与作用域泄漏。类型推导Zod schema 同时承担校验和类型用z.infer从 schema 推导类型不重复手写相同结构的 type避免校验逻辑与类型定义出现两处维护点导致漂移。// ❌ 不好的实践 type MessageParam { role: user | assistant; content: string }; const MessageParamSchema z.object({ role: z.enum([user, assistant]), content: z.string() }); // ✅ 好的实践 export const MessageParamSchema z.discriminatedUnion(role, [...]); export type MessageParam z.infertypeof MessageParamSchema;Zod Schema 与 OpenAPI 风格一套 schema 三处复用Zod schema 在 FastGPT 中同时承担运行时校验、TypeScript 类型推导和 OpenAPI 生成来源。新增或调整 API 时按以下规则组织业务通用结构放在业务归属目录例如packages/global/core/app/type.ts、packages/global/core/workflow/type/node.ts、packages/global/support/permission/**/controller.ts。packages/global/openapi/**只声明接口 query/body/response/path、接口专用兼容处理和 OpenAPI 文档信息不把通用配置类型、权限对象、工具配置等只为文档复制到 openapi 目录。OpenAPI schema 优先复用业务 schema。需要字段说明时优先在业务 schema 上补齐meta只属于某个接口视角的说明可以在 openapi schema 里用SomeSchema.shape.field.meta(...)补充。不要重复建立同构 schema也不要用export const A B这种重命名 alias 当作新 schema 导出。只有接口边界确实需要特殊兼容时才在 openapi 目录定义专用 wrapper例如把{}兼容为undefined、或去掉 JSON Schema 不支持的 function 字段。此类 wrapper 附近要写清楚原因。API response schema 默认声明业务data结构不重复声明统一响应 envelope例如code、statusText。只有路由实际直接返回这些字段时才把它们写进 schema。只为实际存在的业务入参和业务出参定义 Schema。请求完全没有 query、body 或 params 时不创建z.object({})占位 Schema也不调用parseApiInputOpenAPI 省略对应的requestParams/requestBody。没有业务返回数据的成功响应不创建z.undefined()、z.null()或z.object({})占位 Schemahandler 直接不返回值或返回undefinedOpenAPI 只保留状态码和说明统一响应中间件会把undefined包成data: null。存在实际业务数据时仍必须在 API 边界执行 Zod 校验。每个对外字段补齐description关键入参和返回值补example。如果字段语义属于业务通用结构优先把meta写到通用 schema如果只是某接口视角写到 openapi schema。API 入参、客户端传输结构和需要容错解析的配置字段优先使用packages/global/common/zod里的BoolSchema、NumSchema、IntSchema。不要直接写z.coerce.number()普通数值用NumSchema非负整数、数量、分页、limit 用IntSchema布尔配置和查询参数用BoolSchema。只有明确需要严格拒绝字符串/数字形式时才保留z.number()或z.boolean()。废弃字段用.meta({ deprecated: true })标记可同时保留业务说明例如description: 旧版团队标签。不要只写/** deprecated */也不要只把已废弃写进description。Tag 归属按能力复用判断通用模块接口被业务模块使用时同时加通用模块 tag 和业务模块 tag业务模块自己的状态查询或状态操作只加业务模块 tag。API key 文档只给实际开放接口加 apikey 专用 tag不开放的接口不要为了分类加 tag。仅客户端使用的 API 也要以客户端实际传参为准定义 schema避免schema.parse因number、boolean的字符串形态导致业务不可用。理解 BoolSchema / NumSchema / IntSchema 的容错语义上述规则提到的三个通用 Schema 定义在 packages/global/common/zod/index.ts 中其实现揭示了为什么不要手写z.coerce.number()import z from zod; import { stripUrlTrailingSlash } from ../string/url; const truthyBoolStrs [true, 1, yes, y, on]; export const BoolSchema z.preprocess((val) { if (typeof val boolean) return val; if (typeof val string) { return truthyBoolStrs.includes(val.trim().toLowerCase()); } if (typeof val number) { if (val 1) return true; if (val 0) return false; } return val; }, z.boolean()); export const NumSchema z.coerce.numbernumber(); export const IntSchema NumSchema.int().nonnegative(); export const UrlSchema z.string().url().transform(stripUrlTrailingSlash);可以看到BoolSchema通过z.preprocess把字符串true/1/yes/y/on忽略大小写与首尾空格和数字1/0统一归一化为布尔值避免查询参数以字符串形态传入时校验失败NumSchema是对z.coerce.number()的统一封装负责把字符串数字安全转换为 numberIntSchema在NumSchema基础上追加.int().nonnegative()专用于非负整数场景如数量、分页、limit只有明确需要严格拒绝字符串/数字形式时才直接使用z.number()或z.boolean()。在实际 API 定义中的用法如下import { BoolSchema, IntSchema, NumSchema } from fastgpt/global/common/zod; export const UpdateConfigSchema z.object({ enabled: BoolSchema.meta({ description: 是否启用 }), limit: IntSchema.optional().meta({ description: 最大数量 }), temperature: NumSchema.optional().meta({ description: 温度参数 }), teamTags: z.array(z.string()).optional().meta({ description: 旧版团队标签, deprecated: true }) });其他 TypeScript 编码约定可选链调用回调用?.()调用可选回调取代if (fn) fn()的冗余写法。// ❌ 不好的实践 if (onProgress) { onProgress({ phase: creatingContainer }); } // ✅ 好的实践 onProgress?.({ phase: creatingContainer });空值合并取默认值用??取代||处理默认值避免0、false、被错误覆盖。// ❌ 不好的实践 const version lastVersion?.version || 0; // version 为 0 时被误覆盖 const text item?.value || ; // ✅ 好的实践 const version (lastVersion?.version ?? -1) 1; const text item?.value ?? ;这是||的经典陷阱0、、false都是合法值却被||当作假值错误替换成默认值??只在null/undefined时兜底语义更精确。解构重命名同名变量来自多个来源时解构时重命名避免命名冲突。// ❌ 不好的实践 const r1 await getSkillGuidance(...); const r2 await createLLMResponse(...); const inputTokens r1.usage.inputTokens r2.usage.inputTokens; // ✅ 好的实践 const { usage: guidanceUsage } await getSkillGuidance(...); const { usage: generateUsage } await createLLMResponse(...); const inputTokens guidanceUsage.inputTokens generateUsage.inputTokens;类型守卫用is关键字收窄unknown/any类型替代强制断言。// ❌ 不好的实践 function process(value: unknown) { const n value as number; // 不安全 } // ✅ 好的实践 const isValidNumber (value: unknown): value is number typeof value number Number.isFinite(value); if (isValidNumber(value)) { // 此处 value 安全收窄为 number }value is number是 TypeScript 的类型谓词语法配合Number.isFinite可同时排除NaN、Infinity比裸as number更安全。非关键清理用.catch()链次要的清理操作不影响主流程用.catch()吞掉错误不污染主 try/catch。// ❌ 不好的实践 try { await client.delete(); } catch { // 清理失败主流程中断 } // ✅ 好的实践 await client.delete().catch(() {});适用于资源释放、临时文件删除、日志上报等非关键路径避免清理失败把主流程的异常处理逻辑搅乱。函数参数不超过 2 个多参数用对象传递独立参数不超过 2 个超过时改为对象参数便于扩展且无需关心顺序。// ❌ 不好的实践 function createVersion(skillId: string, teamId: string, tmbId: string, version: number) {} // ✅ 好的实践 function createVersion(data: { skillId: string; teamId: string; tmbId: string; version: number }) {}数据写操作函数支持可选 session 参数涉及数据库写操作的函数统一支持可选的session参数便于上层组合事务。事务统一通过mongoSessionRun发起内部自动处理 startTransaction / commit / abort / retry。import { mongoSessionRun } from fastgpt/service/common/mongo/sessionRun; import { type ClientSession } from fastgpt/service/common/mongo; // entity.ts —— 基础操作透传 session export const createVersion (data: CreateVersionData, session?: ClientSession) MongoAppVersion.create([data], { session }); // service.ts —— 需要事务时用 mongoSessionRun 包裹外部已有 session 时直接传入 export const createAppAndInitVersion async ( data: AppCreateParams, session?: ClientSession ) { const create async (session: ClientSession) { const app await createApp(data, session); await createVersion({ appId: app._id, version: 0 }, session); return app; }; if (session) { return create(session); } else { return mongoSessionRun(create); } };这一约定与底层实现 packages/service/common/mongo/sessionRun.ts 完全对应mongoSessionRun通过connectionMongo.startSession()开启会话调用session.withTransaction()执行事务回调MongoDB driver 会处理TransientTransactionError事务级重试与UnknownTransactionCommitResult并设置maxCommitTimeMS超时上限60 秒针对 ACL 增量写入的并发冲突定义了MongoTransactionConflictError在maxConflictRetries3 次范围内记录 warn 日志并用新 session 重试业务错误则保持原样抛出最后在finally中endSession()释放会话。使用该模式时需注意entity.ts层面的写操作只需透传session不自行开启事务service.ts需要组合多个写操作时优先检查外部是否已传入session避免嵌套事务否则用mongoSessionRun自建事务事务适合多表一致性写入场景例如创建 App 后同步初始化 version 记录若中途失败可整体回滚。总结FastGPT 的代码规范是一套面向大型 monorepo 的实战工程约定DDD 三层目录与固定文件职责划分保证了类型共享、服务端隔离的清晰边界type、IIFE、可选链、??、解构重命名、类型守卫等 TypeScript 细节约定统一了团队代码风格Zod 单源 schema 同时服务运行时校验、类型推导与 OpenAPI 生成配合BoolSchema/NumSchema/IntSchema的容错语义与meta描述让 API 边界健壮且文档自洽而session透传与mongoSessionRun事务封装则让复杂业务可以在不引入嵌套事务的前提下安全组合写操作。无论是为 FastGPT 贡献代码还是在类似规模的项目中制定工程规范这套约定都值得作为参考基线。如需深入了解目录落地情况可继续阅读 packages/service/core/app/schema.tsApp 主表 Schema 与集合名定义、packages/global/common/zod/index.ts通用容错 Schema 实现以及 packages/service/common/mongo/sessionRun.ts事务运行器实现。【免费下载链接】FastGPTFastGPT is a knowledge-based platform built on the LLMs, offers a comprehensive suite of out-of-the-box capabilities such as data processing, RAG retrieval, and visual AI workflow orchestration, letting you easily develop and deploy complex question-answering systems without the need for extensive setup or configuration.项目地址: https://gitcode.com/GitHub_Trending/fa/FastGPT创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表
PREV
查看更多资讯
NEXT
返回资讯列表