diff --git a/.claude/design/api/index.md b/.claude/design/api/index.md new file mode 100644 index 0000000000..9157b7c1b8 --- /dev/null +++ b/.claude/design/api/index.md @@ -0,0 +1,351 @@ +# FastGPT API 设计规范 + +## 核心原则 + +### 必须遵守 + +1. **所有 API 必须使用 zod schema 定义入参和出参** +2. **Schema 定义在 `packages/global/openapi/` 中,类型从 schema 导出** +3. **API 路由中使用 `schema.parse()` 验证入参和出参** +4. **路由文件不导出类型别名** — 消费者直接从 openapi 包导入 +5. **必须在 OpenAPI index 中注册路由** + +## 文件位置约定 + +``` +packages/global/openapi/ +├── api.ts # 公共 Schema (分页等) +├── type.ts # OpenAPIPath 类型 +├── tag.ts # API 标签定义 +├── index.ts # 用户 API 文档入口 +├── admin.ts # 管理员 API 文档入口 +└── core/ + └── dataset/ + ├── api.ts # ← Schema 定义 (入参/出参) + ├── index.ts # ← OpenAPI 路由注册 + ├── collection/ + │ ├── api.ts + │ └── index.ts + └── data/ + ├── api.ts + └── index.ts +``` + +## 开发流程 (5 步) + +### 步骤 1: 定义 Zod Schema + +**文件位置**: `packages/global/openapi/[module]/api.ts` + +```typescript +import { z } from 'zod'; +import { ObjectIdSchema } from '../../../common/type/mongo'; +import { ParentIdSchema } from '../../../common/parentFolder/type'; + +/* ============================================================================ + * API: 创建知识库 + * Route: POST /api/core/dataset/create + * ============================================================================ */ +// 入参 Schema +export const CreateDatasetBodySchema = z.object({ + parentId: ParentIdSchema.meta({ + example: '68ad85a7463006c963799a05', + description: '父级文件夹 ID' + }), + name: z.string().meta({ + example: '我的知识库', + description: '知识库名称' + }) +}); +export type CreateDatasetBody = z.infer; + +// 出参 Schema +export const CreateDatasetResponseSchema = ObjectIdSchema.meta({ + example: '68ad85a7463006c963799a05', + description: '新创建的知识库 ID' +}); +export type CreateDatasetResponse = z.infer; +``` + +### 步骤 2: 实现 API 路由 + +**文件位置**: `projects/app/src/pages/api/[path]/[route].ts` + +```typescript +import { NextAPI } from '@/service/middleware/entry'; +import type { ApiRequestProps } from '@fastgpt/service/type/next'; +import { + CreateDatasetBodySchema, + CreateDatasetResponseSchema, + type CreateDatasetResponse +} from '@fastgpt/global/openapi/core/dataset/api'; + +// ❌ 不要在路由文件中重导出类型别名 +// export type DatasetCreateBody = CreateDatasetBody; + +async function handler(req: ApiRequestProps): Promise { + // 1. 入参验证 + const { parentId, name } = CreateDatasetBodySchema.parse(req.body); + + // 2. 业务逻辑 + const datasetId = await createDataset({ parentId, name }); + + // 3. 出参验证 + return CreateDatasetResponseSchema.parse(datasetId); +} + +export default NextAPI(handler); +``` + +### 步骤 3: 注册 OpenAPI 路由 + +**3a. 添加标签** (如果是新模块): `packages/global/openapi/tag.ts` + +```typescript +export const TagsMap = { + // Dataset + datasetCommon: '知识库管理', // ← 新增 + datasetCollection: '集合管理', + // ... +}; +``` + +**3b. 注册路由**: `packages/global/openapi/[module]/index.ts` + +```typescript +import type { OpenAPIPath } from '../../type'; +import { TagsMap } from '../../tag'; +import { CreateDatasetBodySchema } from './api'; + +export const DatasetPath: OpenAPIPath = { + '/core/dataset/create': { + post: { + summary: '创建知识库', + description: '创建新的知识库,支持多种类型', + tags: [TagsMap.datasetCommon], + requestBody: { + content: { + 'application/json': { + schema: CreateDatasetBodySchema + } + } + }, + responses: { + 200: { + description: '成功返回新创建的知识库 ID' + } + } + } + }, + ...DatasetCollectionPath, + ...DatasetDataPath +}; +``` + +### 步骤 4: 前端类型引用 + +前端直接从 openapi 包导入类型,不从 API 路由文件导入。 + +```typescript +// ✅ 正确: 从 openapi 导入 +import type { CreateDatasetBody } from '@fastgpt/global/openapi/core/dataset/api'; + +// ❌ 错误: 从路由文件导入 +import type { DatasetCreateBody } from '@/pages/api/core/dataset/create'; + +// ❌ 错误: 从旧的 global 文件导入 +import type { CreateDatasetParams } from '@/global/core/dataset/api'; +``` + +### 步骤 5: 测试文件 + +测试同样直接从 openapi 导入类型。 + +```typescript +import createHandler from '@/pages/api/core/dataset/create'; +import type { + CreateDatasetBody, + CreateDatasetResponse +} from '@fastgpt/global/openapi/core/dataset/api'; +import { Call } from '@test/utils/request'; + +const res = await Call(createHandler, { + auth: users.members[0], + body: { name: 'test', intro: 'intro', avatar: 'avatar', type: DatasetTypeEnum.dataset } +}); +``` + +## 公共 Schema 参考 + +使用已有的公共 Schema,避免重复定义。 + +### ID 类型 + +| Schema | 说明 | 导入路径 | +|--------|------|----------| +| `ObjectIdSchema` | MongoDB ObjectId (24位hex,自动将 ObjectId 对象转为 string) | `@fastgpt/global/common/type/mongo` | +| `ParentIdSchema` | 父级 ID (`z.string().nullish()`) | `@fastgpt/global/common/parentFolder/type` | + +**ObjectId 陷阱**: MongoDB 返回的 `_id` 是 ObjectId 对象,不是 string。直接用 `z.string()` 验证出参会报错。必须使用 `ObjectIdSchema`(内置 preprocess 自动转换)。 + +```typescript +// ❌ 出参验证会失败 — ObjectId 不是 string +export const ResponseSchema = z.string(); +return ResponseSchema.parse(document._id); // ZodError! + +// ✅ ObjectIdSchema 内置 preprocess,自动转换 +import { ObjectIdSchema } from '../../../common/type/mongo'; +export const ResponseSchema = ObjectIdSchema; +return ResponseSchema.parse(document._id); // "68ad85a7463006c963799a05" +``` + +### 分页 Schema + +| Schema | 说明 | 导入路径 | +|--------|------|----------| +| `PaginationSchema` | 偏移分页 (pageSize, offset, pageNum) | `@fastgpt/global/openapi/api` | +| `PaginationResponseSchema` | 分页响应 (total, list) | `@fastgpt/global/openapi/api` | +| `LinkedPaginationSchema` | 游标分页 (pageSize, nextId, prevId) | `@fastgpt/global/openapi/api` | +| `LinkedListResponseSchema` | 游标分页响应 (list, hasMorePrev, hasMoreNext) | `@fastgpt/global/openapi/api` | + +### 认证 Schema + +| Schema | 说明 | 导入路径 | +|--------|------|----------| +| `OutLinkChatAuthSchema` | 外部链接认证 (shareId, outLinkUid, teamId, teamToken) | `@fastgpt/global/support/permission/chat` | + +### 业务 Schema + +已有的业务级 Schema 可以直接在出入参中复用: + +| Schema | 说明 | 导入路径 | +|--------|------|----------| +| `ApiDatasetServerSchema` | 第三方知识库服务器配置 | `@fastgpt/global/core/dataset/apiDataset/type` | +| `EmbeddingModelItemSchema` | 向量模型信息 | `@fastgpt/global/core/ai/model.schema` | + +遇到复杂嵌套类型时,优先查找是否已有对应的 zod schema 可复用。 + +## Schema 字段定义规范 + +### meta 信息 + +所有字段必须有 `description`,推荐有 `example`: + +```typescript +z.string().meta({ + example: 'alice@example.com', + description: '用户邮箱' +}) +``` + +### 可选字段 + +```typescript +// 可选 (undefined) +z.string().optional().meta({ description: '...' }) + +// 可空 (undefined | null) — 用于 parentId 等场景 +z.string().nullish().meta({ description: '...' }) +``` + +### 枚举字段 + +```typescript +import { DatasetTypeEnum } from '../../../core/dataset/constants'; + +// TypeScript enum — 使用 z.enum +z.enum(DatasetTypeEnum).meta({ + example: DatasetTypeEnum.dataset, + description: '知识库类型' +}) +``` + +### 嵌套对象 + +嵌套对象优先抽取为独立 Schema 复用: + +```typescript +const FileItemSchema = z.object({ + fileId: z.string().meta({ example: 'temp/abc.pdf', description: '文件 ID' }), + name: z.string().meta({ example: '文档.pdf', description: '文件名' }) +}); + +export const CreateWithFilesBodySchema = z.object({ + files: z.array(FileItemSchema).meta({ description: '文件列表' }) +}); +``` + +## 旧 API 改造指南 + +将旧 API 迁移到 zod schema 规范时,除了上述 5 步外,还需要: + +### 1. 清理旧类型定义 + +旧类型通常在 `projects/app/src/global/` 或路由文件中。迁移后: +- 如果旧类型已无其他引用,可删除 +- 如果仍有其他 API 引用,暂保留,逐步迁移 + +### 2. 更新所有引用点 + +使用全局搜索找到所有旧类型的引用,逐一更新: + +```bash +# 搜索旧类型名 +grep -r "CreateDatasetParams" projects/app/src/ +``` + +需要更新的典型位置: +- `projects/app/src/web/` — 前端 API 调用 +- `projects/app/src/pageComponents/` — 页面组件 +- `projects/app/test/` — 测试文件 + +### 3. 移除路由文件中的类型别名 + +```typescript +// ❌ 删除这些 +export type DatasetCreateQuery = {}; +export type DatasetCreateBody = CreateDatasetBody; +export type DatasetCreateResponse = CreateDatasetResponse; +``` + +## 审查检查清单 + +### 必须检查项 + +**Schema 文件** (`packages/global/openapi/.../api.ts`): +- [ ] 文件头部有 API 声明注释 (路由、方法、描述、标签) +- [ ] 入参和出参都用 zod schema 定义 +- [ ] 导出 `z.infer` 类型 +- [ ] 所有字段有 `description` +- [ ] ID 字段使用 `ObjectIdSchema`,父级 ID 使用 `ParentIdSchema` + +**API 路由文件** (`projects/app/src/pages/api/.../route.ts`): +- [ ] 入参使用 `Schema.parse(req.body)` 或 `Schema.parse(req.query)` +- [ ] 出参使用 `Schema.parse(result)` +- [ ] 函数返回值类型声明为 openapi 导出的类型 +- [ ] **没有**重导出类型别名 + +**OpenAPI 注册** (`packages/global/openapi/.../index.ts`): +- [ ] 路由已在 index.ts 中注册 +- [ ] 使用了正确的 `TagsMap` 标签 +- [ ] 新模块已在 `tag.ts` 中添加标签 + +**类型引用**: +- [ ] 前端代码从 `@fastgpt/global/openapi/` 导入类型 +- [ ] 测试文件从 `@fastgpt/global/openapi/` 导入类型 +- [ ] 没有从路由文件 (`@/pages/api/...`) 导入类型 + +## 项目内参考示例 + +- **Schema 定义**: `packages/global/openapi/core/dataset/api.ts` +- **OpenAPI 注册**: `packages/global/openapi/core/dataset/index.ts` +- **API 路由实现**: `projects/app/src/pages/api/core/dataset/create.ts` +- **前端调用**: `projects/app/src/web/core/dataset/api.ts` +- **测试文件**: `projects/app/test/api/core/dataset/create.test.ts` +- **分页 Schema**: `packages/global/openapi/api.ts` +- **标签定义**: `packages/global/openapi/tag.ts` + +--- + +**Version**: 2.0 +**Last Updated**: 2026-04-10 diff --git a/.claude/design/common/api/index.md b/.claude/design/common/api/index.md deleted file mode 100644 index b2fb2d4550..0000000000 --- a/.claude/design/common/api/index.md +++ /dev/null @@ -1,743 +0,0 @@ -# FastGPT API 设计规范 - -## 核心原则 - -### 🔴 必须遵守的规则 - -1. **所有 API 必须使用 zod schema 定义入参和出参** -2. **必须导出 schema 的 TypeScript 类型** -3. **必须在 schema 文件头部声明 API 信息(路由、方法、描述、标签)** -4. **入参必须使用 schema.parse() 验证** -5. **函数返回值必须使用 schema.parse() 验证** -6. **必须编写完整的 OpenAPI 文档** - -## 开发流程 - -### 步骤 1: 定义 Zod Schema 并声明 API - -**文件位置**: `packages/global/openapi/[module]/[api].ts` - -**文件头部必须声明 API 信息**: - -```typescript -import { z } from 'zod'; - -/* ============================================================================ - * API: 获取应用对话日志列表 - * Route: POST /api/core/app/logs/list - * Method: POST - * Description: 获取指定应用的对话日志列表,支持分页和多种筛选条件 - * Tags: ['App', 'Log', 'Read'] - * ============================================================================ */ - -// 入参 Schema -export const GetAppChatLogsBodySchema = PaginationSchema.extend({ - appId: z.string().meta({ - example: '68ad85a7463006c963799a05', - description: '应用 ID' - }), - dateStart: z.union([z.string(), z.date()]).meta({ - example: '2024-01-01T00:00:00.000Z', - description: '开始时间' - }), - dateEnd: z.union([z.string(), z.date()]).meta({ - example: '2024-12-31T23:59:59.999Z', - description: '结束时间' - }), - sources: z.array(z.nativeEnum(ChatSourceEnum)).optional().meta({ - example: [ChatSourceEnum.api, ChatSourceEnum.online], - description: '对话来源筛选' - }) -}); - -// 导出入参类型 -export type getAppChatLogsBody = z.infer; - -// 出参 Schema -export const GetAppChatLogsResponseSchema = z.object({ - total: z.number().meta({ example: 100, description: '总记录数' }), - list: z.array(ChatLogItemSchema) -}); - -// 导出出参类型 -export type getAppChatLogsResponseType = z.infer; -``` - -**API 声明规范**: - -```typescript -/** - * 每个 API 文件必须在文件头部声明以下信息: - * - * 1. API 名称 (API): 简短的功能描述 - * 2. 路由 (Route): 完整的 API 路径 - * 3. 方法 (Method): HTTP 方法 (GET/POST/PUT/DELETE) - * 4. 描述 (Description): API 的详细功能说明 - * 5. 标签 (Tags): API 的分类标签数组 - * - * 标签示例: - * - 'App': 应用相关 API - * - 'User': 用户相关 API - * - 'Log': 日志相关 API - * - 'Read': 只读操作 - * - 'Write': 写入操作 - * - 'Delete': 删除操作 - */ -``` - -**Schema 定义规范**: - -#### ✅ 字段定义规范 - -```typescript -// ✅ 好的实践: 完整的 meta 信息 -export const GetUserSchema = z.object({ - userId: z.string().meta({ - example: '68ad85a7463006c963799a05', - description: '用户 ID' - }), - email: z.string().email().meta({ - example: 'user@example.com', - description: '用户邮箱' - }), - age: z.number().int().positive().meta({ - example: 25, - description: '用户年龄' - }), - status: z.enum(['active', 'inactive']).meta({ - example: 'active', - description: '用户状态' - }) -}); - -// ❌ 不好的实践: 缺少 meta 信息 -export const GetUserSchemaBad = z.object({ - userId: z.string(), - email: z.string(), - age: z.number(), - status: z.string() -}); -``` - -#### ✅ 嵌套对象定义 - -```typescript -// 嵌套对象应该定义为独立的 Schema -export const AddressSchema = z.object({ - street: z.string().meta({ description: '街道地址' }), - city: z.string().meta({ description: '城市' }), - country: z.string().meta({ description: '国家' }) -}); - -export const CreateUserSchema = z.object({ - name: z.string().meta({ description: '用户名' }), - address: AddressSchema.meta({ description: '地址信息' }) -}); -``` - -#### ✅ 数组定义 - -```typescript -export const GetUserListResponseSchema = z.object({ - total: z.number().meta({ example: 100, description: '总数' }), - list: z.array( - z.object({ - id: z.string().meta({ description: '用户 ID' }), - name: z.string().meta({ description: '用户名' }) - }) - ).meta({ description: '用户列表' }) -}); -``` - -#### ✅ 可选字段 - -```typescript -export const UpdateUserSchema = z.object({ - userId: z.string().meta({ description: '用户 ID' }), - // 可选字段使用 .optional() - name: z.string().optional().meta({ description: '用户名' }), - // 或使用 .nullish() 允许 null 和 undefined - email: z.string().email().nullish().meta({ description: '用户邮箱' }) -}); -``` - -#### ✅ 分页 Schema - -```typescript -import { PaginationSchema } from '@fastgpt/global/openapi/api'; - -// 继承分页 Schema -export const GetUserListSchema = PaginationSchema.extend({ - // 添加额外的筛选字段 - keyword: z.string().optional().meta({ description: '搜索关键词' }), - status: z.enum(['active', 'inactive']).optional().meta({ description: '状态筛选' }) -}); -``` - -#### ✅ 多个 API 的 Schema 文件 - -```typescript -/* ============================================================================ - * API: 获取日志键 - * Route: GET /api/core/app/logs/keys - * Method: GET - * Description: 获取应用的日志配置键列表 - * Tags: ['App', 'Log', 'Read'] - * ============================================================================ */ - -export const GetLogKeysQuerySchema = z.object({ - appId: z.string().meta({ description: '应用 ID' }) -}); - -export const GetLogKeysResponseSchema = z.object({ - logKeys: z.array(AppLogKeysSchema).meta({ description: '日志键列表' }) -}); - -/* ============================================================================ - * API: 更新日志键 - * Route: POST /api/core/app/logs/keys - * Method: POST - * Description: 更新应用的日志配置键 - * Tags: ['App', 'Log', 'Write'] - * ============================================================================ */ - -export const UpdateLogKeysBodySchema = z.object({ - appId: z.string().meta({ description: '应用 ID' }), - logKeys: z.array(AppLogKeysSchema).meta({ description: '日志键列表' }) -}); -``` - -### 步骤 2: 实现 API 路由 - -**文件位置**: `projects/app/src/pages/api/[path]/[route].ts` - -**标准实现模板**: - -```typescript -import type { NextApiResponse } from 'next'; -import { NextAPI } from '@/service/middleware/entry'; -import type { ApiRequestProps } from '@fastgpt/service/type/next'; -import { - GetAppChatLogsBodySchema, - GetAppChatLogsResponseSchema, - type getAppChatLogsResponseType -} from '@fastgpt/global/openapi/...'; - -async function handler( - req: ApiRequestProps, - _res: NextApiResponse -): Promise { - // 🔴 步骤 1: 使用 schema.parse() 验证入参 - const { appId, dateStart, dateEnd, sources } = GetAppChatLogsBodySchema.parse(req.body); - - // 或对于 query 参数 - // const { param1, param2 } = YourAPIQuerySchema.parse(req.query); - - // 🔴 步骤 2: 业务逻辑处理 - const result = await yourBusinessLogic({ appId, dateStart, dateEnd, sources }); - - // 🔴 步骤 3: 使用 schema.parse() 验证出参 - return GetAppChatLogsResponseSchema.parse({ - list: result.list, - total: result.total - }); -} - -export default NextAPI(handler); -``` - -**完整示例**: - -```typescript -import type { NextApiResponse } from 'next'; -import type { ApiRequestProps } from '@fastgpt/service/type/next'; -import { NextAPI } from '@/service/middleware/entry'; -import { authApp } from '@fastgpt/service/support/permission/app/auth'; -import { - GetAppChatLogsBodySchema, - GetAppChatLogsResponseSchema, - type getAppChatLogsResponseType -} from '@fastgpt/global/openapi/core/app/log/api'; - -async function handler( - req: ApiRequestProps, - _res: NextApiResponse -): Promise { - // 🔴 1. 验证入参 - const { appId, dateStart, dateEnd, sources } = GetAppChatLogsBodySchema.parse(req.body); - - // 2. 权限验证 (如果需要) - await authApp({ - req, - authToken: true, - appId, - per: AppReadChatLogPerVal - }); - - // 3. 业务逻辑 - const { list, total } = await getChatLogsFromDB({ - appId, - dateStart, - dateEnd, - sources - }); - - // 🔴 4. 验证出参 - return GetAppChatLogsResponseSchema.parse({ - list, - total - }); -} - -export default NextAPI(handler); -``` - -### 步骤 3: 权限验证 (如需要) - -**使用 `authApp` 或其他权限验证函数**: - -```typescript -import { authApp } from '@fastgpt/service/support/permission/app/auth'; -import { AppWritePerVal } from '@fastgpt/global/support/permission/app/constant'; - -async function handler(req: ApiRequestProps, res: NextApiResponse) { - const { appId } = YourAPIBodySchema.parse(req.body); - - // 权限验证 - await authApp({ - req, - authToken: true, - appId, - per: AppWritePerVal // 权限常量 - }); - - // 继续处理... -} -``` - -### 步骤 4: 错误处理 - -**使用统一的错误处理**: - -```typescript -import { APIError } from '@fastgpt/service/core/error/controller'; -import { CommonErrEnum } from '@fastgpt/global/common/error/code/common'; - -async function handler(req: ApiRequestProps, res: NextApiResponse) { - try { - const { appId } = YourAPIBodySchema.parse(req.body); - - if (!appId) { - return Promise.reject(CommonErrEnum.missingParams); - } - - // 业务逻辑... - - } catch (error) { - // 统一错误处理 - return APIError(error)(req, res); - } -} -``` - -## 完整开发示例 - -### 场景: 创建用户 API - -**1. 定义 Schema** (`packages/global/openapi/core/user/api.ts`): - -```typescript -import { z } from 'zod'; - -/* ============================================================================ - * API: 创建用户 - * Route: POST /api/core/user/create - * Method: POST - * Description: 创建新用户,返回创建的用户信息 - * Tags: ['User', 'Write'] - * ============================================================================ */ - -// 入参 -export const CreateUserBodySchema = z.object({ - name: z.string().min(2).max(50).meta({ - example: 'Alice', - description: '用户名 (2-50 字符)' - }), - email: z.string().email().meta({ - example: 'alice@example.com', - description: '用户邮箱' - }), - age: z.number().int().positive().optional().meta({ - example: 25, - description: '用户年龄' - }), - avatar: z.string().url().optional().meta({ - example: 'https://example.com/avatar.jpg', - description: '头像 URL' - }) -}); - -export type createUserBodyType = z.infer; - -// 出参 -export const CreateUserResponseSchema = z.object({ - userId: z.string().meta({ example: '68ad85a7463006c963799a05', description: '用户 ID' }), - name: z.string().meta({ example: 'Alice', description: '用户名' }), - email: z.string().meta({ example: 'alice@example.com', description: '用户邮箱' }), - createdAt: z.date().meta({ example: '2024-01-01T00:00:00.000Z', description: '创建时间' }) -}); - -export type createUserResponseType = z.infer; -``` - -**2. 实现 API** (`projects/app/src/pages/api/core/user/create.ts`): - -```typescript -import type { NextApiResponse } from 'next'; -import { NextAPI } from '@/service/middleware/entry'; -import type { ApiRequestProps } from '@fastgpt/service/type/next'; -import { MongoUser } from '@fastgpt/service/core/user/schema'; -import { - CreateUserBodySchema, - CreateUserResponseSchema, - type createUserResponseType -} from '@fastgpt/global/openapi/core/user/api'; - -async function handler( - req: ApiRequestProps, - _res: NextApiResponse -): Promise { - // 🔴 验证入参 - const { name, email, age, avatar } = CreateUserBodySchema.parse(req.body); - - // 检查邮箱是否已存在 - const existingUser = await MongoUser.findOne({ email }); - if (existingUser) { - return Promise.reject('Email already exists'); - } - - // 创建用户 - const user = await MongoUser.create({ - name, - email, - age, - avatar, - createdAt: new Date() - }); - - // 🔴 验证出参 - return CreateUserResponseSchema.parse({ - userId: user._id.toString(), - name: user.name, - email: user.email, - createdAt: user.createdAt - }); -} - -export default NextAPI(handler); -``` - -## 审查检查清单 - -### 🔴 必须检查项 (阻塞性) - -**Schema 文件** (`packages/global/openapi/.../api.ts`): -- [ ] **API 声明**: 文件头部有 API 信息(路由、方法、描述、标签) -- [ ] **Schema 定义**: 入参和出参都使用 zod 定义 -- [ ] **类型导出**: 导出 `z.infer` 类型 -- [ ] **Meta 信息**: 所有字段都有 `description` 和 `example` - -**API 路由文件** (`projects/app/src/pages/api/.../route.ts`): -- [ ] **入参验证**: 使用 `Schema.parse(req.body)` 或 `parse(req.query)` -- [ ] **出参验证**: 使用 `Schema.parse(responseData)` -- [ ] **函数返回类型**: 函数返回值声明为导出的类型 -- [ ] **权限验证**: API 路由有相应的权限检查 (如需要) - -### 🟡 推荐检查项 (建议性) - -- [ ] **错误处理**: 使用 `APIError` 统一错误处理 -- [ ] **字段验证**: 使用 zod 的验证方法 (.min(), .max(), .email() 等) -- [ ] **可空字段**: 正确使用 `.optional()` 或 `.nullish()` -- [ ] **复用 Schema**: 相同结构抽取为独立 Schema -- [ ] **分页支持**: 列表 API 继承 `PaginationSchema` - -### 🟢 可选检查项 (优化性) - -- [ ] **字段顺序**: 字段按重要性排序 -- [ ] **Schema 复用**: 复用现有 Schema 减少重复 -- [ ] **注释**: 复杂逻辑添加注释 - -## 常见问题和解决方案 - -### 问题 1: 缺少 API 声明 - -**错误示例**: -```typescript -// ❌ 错误: 缺少 API 声明 -import { z } from 'zod'; - -export const GetUserSchema = z.object({ - id: z.string() -}); -``` - -**正确做法**: -```typescript -// ✅ 正确: 包含完整的 API 声明 -import { z } from 'zod'; - -/* ============================================================================ - * API: 获取用户信息 - * Route: GET /api/core/user/detail - * Method: GET - * Description: 根据 userId 获取用户详细信息 - * Tags: ['User', 'Read'] - * ============================================================================ */ - -export const GetUserSchema = z.object({ - id: z.string().meta({ - example: '68ad85a7463006c963799a05', - description: '用户 ID' - }) -}); -``` - -### 问题 2: 类型不匹配 - -**错误示例**: -```typescript -// ❌ 错误: 函数返回类型未声明 -async function handler(req: ApiRequestProps, res: NextApiResponse) { - const data = YourAPIBodySchema.parse(req.body); - return { success: true, data }; // 类型未声明 -} -``` - -**正确做法**: -```typescript -// ✅ 正确: 声明返回类型 -async function handler( - req: ApiRequestProps, - _res: NextApiResponse -): Promise { - const data = YourAPIBodySchema.parse(req.body); - - return YourAPIResponseSchema.parse({ - success: true, - data - }); -} -``` - -### 问题 3: 缺少 Meta 信息 - -**错误示例**: -```typescript -// ❌ 错误: 缺少 meta 信息 -export const UserSchema = z.object({ - id: z.string(), - name: z.string(), - email: z.string() -}); -``` - -**正确做法**: -```typescript -// ✅ 正确: 完整的 meta 信息 -export const UserSchema = z.object({ - id: z.string().meta({ - example: '68ad85a7463006c963799a05', - description: '用户 ID' - }), - name: z.string().meta({ - example: 'Alice', - description: '用户名' - }), - email: z.string().email().meta({ - example: 'alice@example.com', - description: '用户邮箱' - }) -}); -``` - -### 问题 4: 未验证出参 - -**错误示例**: -```typescript -// ❌ 错误: 直接返回数据 -async function handler(req: ApiRequestProps, res: NextApiResponse) { - const { appId } = YourAPIBodySchema.parse(req.body); - const result = await getData(appId); - - return result; // 未验证出参结构 -} -``` - -**正确做法**: -```typescript -// ✅ 正确: 验证出参 -async function handler(req: ApiRequestProps, res: NextApiResponse) { - const { appId } = YourAPIBodySchema.parse(req.body); - const result = await getData(appId); - - return YourAPIResponseSchema.parse(result); -} -``` - -### 问题 5: Schema 复用不当 - -**不好做法**: -```typescript -// ❌ 重复定义相同的结构 -export const Schema1 = z.object({ - id: z.string(), - name: z.string(), - email: z.string() -}); - -export const Schema2 = z.object({ - id: z.string(), - name: z.string(), - email: z.string() -}); -``` - -**正确做法**: -```typescript -// ✅ 抽取公共 Schema -export const BaseUserSchema = z.object({ - id: z.string().meta({ description: 'ID' }), - name: z.string().meta({ description: '名称' }), - email: z.string().email().meta({ description: '邮箱' }) -}); - -export const Schema1 = z.object({ - user: BaseUserSchema -}); - -export const Schema2 = z.object({ - users: z.array(BaseUserSchema) -}); -``` - -## 快速参考 - -### API 声明模板 - -```typescript -/* ============================================================================ - * API: [简短功能描述] - * Route: [HTTP 方法] [完整路由路径] - * Method: [GET/POST/PUT/DELETE] - * Description: [详细功能说明] - * Tags: [['模块', '子模块', '操作类型']] - * ============================================================================ */ -``` - -### 常用标签 - -- **模块标签**: `App`, `User`, `Chat`, `Workflow`, `Dataset` -- **操作类型**: `Read`, `Write`, `Delete`, `Update` -- **其他**: `Admin`, `Public`, `Internal` - -### 常用 Zod 验证方法 - -```typescript -// 字符串 -z.string() // 字符串 - .min(2) // 最小长度 - .max(50) // 最大长度 - .email() // 邮箱格式 - .url() // URL 格式 - .uuid() // UUID 格式 - -// 数字 -z.number() // 数字 - .int() // 整数 - .positive() // 正数 - .min(0) // 最小值 - .max(100) // 最大值 - -// 布尔 -z.boolean() // 布尔值 - -// 日期 -z.date() // 日期对象 - .or(z.string()) // 或日期字符串 - -// 枚举 -z.enum(['active', 'inactive']) // 枚举值 -z.nativeEnum(MyEnum) // TypeScript 枚举 - -// 数组 -z.array(z.string()) // 字符串数组 - .min(1) // 最小长度 - .max(10) // 最大长度 - -// 可选 -z.string().optional() // 可选 (undefined) -z.string().nullish() // 可空 (undefined | null) - -// 对象 -z.object({ // 对象 - name: z.string(), - age: z.number() -}) - -// 继承 -PaginationSchema.extend({ // 扩展 - keyword: z.string() -}) - -// 联合类型 -z.union([z.string(), z.number()]) // 字符串或数字 -z.discriminator('type', { // 判别联合 - type1: Type1Schema, - type2: Type2Schema -}) -``` - -### Meta 字段说明 - -```typescript -z.string().meta({ - example: 'value', // 示例值 (必填) - description: '字段说明' // 字段描述 (必填) -}) -``` - -### TypeScript 类型导出 - -```typescript -// Schema 定义 -export const UserSchema = z.object({ - id: z.string(), - name: z.string() -}); - -// 导出类型 (命名规范: camelCase) -export type userType = z.infer; - -// 或使用 PascalCase -export type UserType = z.infer; -``` - -## 参考资源 - -### 项目内示例 - -- **API Schema 示例**: `/Volumes/code/fastgpt-pro/FastGPT/packages/global/openapi/core/app/log/api.ts` -- **API 实现示例**: `/Volumes/code/fastgpt-pro/FastGPT/projects/app/src/pages/api/core/app/logs/list.ts` -- **分页 Schema**: `packages/global/openapi/api.ts` - -### 相关文档 - -- **Zod 官方文档**: https://zod.dev/ -- **FastGPT API 规范**: `.claude/skills/pr-review/fastgpt-style-guide.md` -- **PR Review 审查维度**: `.claude/skills/pr-review/code-quality-standards.md` - ---- - -**Version**: 1.0 -**Last Updated**: 2026-01-27 -**Maintainer**: FastGPT Development Team diff --git a/.claude/design/core/workflow/index.md b/.claude/design/core/workflow/index.md index ef6b970e9c..f902040895 100644 --- a/.claude/design/core/workflow/index.md +++ b/.claude/design/core/workflow/index.md @@ -446,7 +446,7 @@ interface DebugDataType { runtimeEdges: RuntimeEdgeItemType[]; entryNodeIds: string[]; variables: Record; - history?: ChatItemType[]; + history?: ChatItemMiniType[]; query?: UserChatItemValueItemType[]; workflowInteractiveResponse?: WorkflowInteractiveResponseType; } diff --git a/.claude/design/outlink/wechat-clawbot.md b/.claude/design/outlink/wechat-clawbot.md index 16b14e0882..579c993e30 100644 --- a/.claude/design/outlink/wechat-clawbot.md +++ b/.claude/design/outlink/wechat-clawbot.md @@ -128,10 +128,10 @@ export type WechatPollJobData = { shareId: string }; ```typescript // packages/service/support/outLink/schema.ts -OutLinkSchema.index({ shareId: -1 }); -OutLinkSchema.index({ teamId: 1, tmbId: 1, appId: 1 }); +OutLinkSchemaType.index({ shareId: -1 }); +OutLinkSchemaType.index({ teamId: 1, tmbId: 1, appId: 1 }); // 条件索引: 仅索引 wechat online 渠道,用于服务重启恢复 -OutLinkSchema.index( +OutLinkSchemaType.index( { type: 1, 'app.status': 1 }, { partialFilterExpression: { type: 'wechat', 'app.status': 'online' } } ); diff --git a/.claude/design/outlink/wechat-file-support.md b/.claude/design/outlink/wechat-file-support.md index 751e8e1b90..c1f2f282a1 100644 --- a/.claude/design/outlink/wechat-file-support.md +++ b/.claude/design/outlink/wechat-file-support.md @@ -401,7 +401,7 @@ export async function downloadAndStoreMedia(params: { ```typescript async function processUserGroup( - outLink: OutLinkSchema, + outLink: OutLinkSchemaType, group: ParsedMessageGroup ): Promise { const app = outLink.app; diff --git a/.claude/skills/api-development/SKILL.md b/.claude/skills/api-development/SKILL.md index f01a9b20f6..635d1a2a90 100644 --- a/.claude/skills/api-development/SKILL.md +++ b/.claude/skills/api-development/SKILL.md @@ -16,4 +16,4 @@ description: FastGPT API 开发规范。重点强调使用 zod schema 定义入 ## 说明文档 -[API 设计规范](../design/common/api/index.md) \ No newline at end of file +[API 设计规范](../../design/api/index.md) \ No newline at end of file diff --git a/.claude/skills/system-pr_review/SKILL.md b/.claude/skills/system-pr_review/SKILL.md index cd2032b915..a33902b496 100644 --- a/.claude/skills/system-pr_review/SKILL.md +++ b/.claude/skills/system-pr_review/SKILL.md @@ -3,258 +3,182 @@ name: pr-review description: 当用户传入一个 review 的 pr 链接时候,触发该 skill,对 pr 进行代码审查。 --- -# When to Use This Skill - -用户传入一个 review 的 pr 链接 - # PR Review 代码审查技能 -> 全面审查 Pull Request 的代码质量、安全性、性能和架构设计,提供专业的改进建议 +> 按阶段对 Pull Request 进行系统性审查,先验证需求理解与逻辑正确性,再并行进行多维度质量检测,最后提交审查报告。 -## 快速开始 +--- -```bash -# 审查当前分支的 PR -gh pr view - -# 审查指定 PR -gh pr view 6324 - -# 查看变更内容 -gh pr diff 6324 -``` - -## 工具集成 - -### 使用 gh CLI 加速审查 - -```bash -# 查看并审查 PR -gh pr view && gh pr diff - -# 添加审查评论 -gh pr review --comment -b "我的审查意见" - -# 批准 PR -gh pr review --approve - -# 请求修改 -gh pr review --request-changes -``` - -### 本地测试 PR +## 步骤 0:拉取代码 ```bash # 检出 PR 分支到本地 gh pr checkout -# 运行测试 -pnpm test +# 获取 PR 基本信息 +gh pr view --json number,title,body,author,state,headRefName,baseRefName,additions,deletions,files -# 运行 lint -pnpm lint - -# 类型检查 -pnpm tsc --noEmit - -# 启动开发服务器验证 -pnpm dev -``` - -### 常见命令参考 - -```bash -# PR 信息查看 -gh pr view --json title,body,author,state,files,additions,deletions - -# PR diff 查看 +# 获取完整 diff gh pr diff -gh pr diff > /tmp/pr.diff # 保存到文件 -# PR commits 查看 +# 查看 commit 历史 gh pr view --json commits --jq '.commits[].messageHeadline' -# PR checks 状态 -gh pr checks - -# PR 评论 -gh pr comment --body "评论内容" - -# PR 审查提交 -gh pr review --approve -gh pr review --request-changes -gh pr review --comment -b "评论内容" - -# PR 操作 -gh pr merge --squash # Squash merge -gh pr close # 关闭 PR -``` - -## 审查流程 - -### 1. 信息收集阶段 - -自动执行以下步骤: - -```bash -# 1. 获取 PR 基本信息 -gh pr view --json title,body,author,state,headRefName,baseRefName,additions,deletions,files - -# 2. 获取 PR 变更 diff -gh pr diff - -# 3. 获取 PR 的 commit 历史 -gh pr view --json commits - -# 4. 检查 CI/CD 状态 +# 检查 CI 状态 gh pr checks ``` -### 2. 多维度代码审查 +--- -按照以下三个维度进行系统性审查: +## 第一阶段:需求理解与逻辑验证 -#### 维度 1: 基本代码质量标准 📐 +**目标**:理解本次 PR 的意图,并通过阅读代码来推理测试用例是否能通过。 -通用的代码质量标准,适用于所有项目: +### 1.1 需求总结 -- **安全性**: 输入验证、权限检查、注入防护、敏感信息保护 -- **正确性**: 错误处理、边界条件、类型安全 -- **性能**: 算法复杂度、数据库优化、内存管理 -- **可测试性**: 测试覆盖、测试质量、Mock 使用 +阅读 PR 标题、描述和 diff,用自己的语言总结: +- 本次 PR 的核心目的是什么? +- 改动了哪些关键模块? +- 对外部接口或数据结构是否有变更? -📖 **详细指南**: [code-quality-standards.md](./code-quality-standards.md) +### 1.2 测试推理 -#### 维度 2: FastGPT 风格规范 🎨 +充当测试角色,针对 PR 的核心改动,**提出 3~5 个关键测例**,然后在代码中找到对应逻辑进行推理校验: -FastGPT 项目特定的代码规范和约定: +- 正常路径:主流程是否按预期运行? +- 边界条件:空值、极大值、并发等边界是否被处理? +- 异常路径:错误输入或依赖失败时行为是否正确? -- **API 路由开发**: 路由定义、权限验证、错误处理: [API 路由开发规范](./style/api.md) -- **前端组件开发**: TypeScript + React、Chakra UI、状态管理: [前端组件开发规范](./style/front.md) -- **数据库操作**: Model 定义、查询优化、索引设计: [数据库操作规范](./style/db.md) -- **包结构与依赖**: 依赖方向、导入规范、类型导出: [包结构与依赖规范](./style/package.md) -- **日志与可观测性**: 统一日志分类、结构化字段、敏感信息、OTEL 导出等审查标准: [日志review 标准](./style/logger.md) +**校验方式**:直接阅读相关代码,推理每个测例的执行路径,确认逻辑能通过。如果代码中存在对应单元测试,也一并检查。 -#### 维度 3: 常见问题检查清单 🔍 +### ⚠️ 阶段门控 -快速识别和修复常见问题模式: +如果第一阶段发现**需求理解存在严重歧义**或**核心逻辑存在明显错误**(如测例推理无法通过),**立即跳过后续阶段,直接进入"提交评论"步骤**,在报告中标明阻塞原因,请求作者澄清或修复后再继续审查。 -- **TypeScript 问题**: any 类型滥用、类型定义不完整、不安全断言 -- **异步错误处理**: 未处理 Promise、错误信息丢失、静默失败 -- **React 性能**: 不必要的重渲染、渲染中创建对象、缺少 memoization -- **安全漏洞**: 注入攻击、XSS、文件上传漏洞 +--- -📖 **详细清单**: [common-issues-checklist.md](./common-issues-checklist.md) +## 第二到第六阶段:并行深度审查 -### 3. 生成并提交审查报告 +第一阶段通过后,**以下五个阶段可以并行执行**,彼此独立,互不依赖。 -PR 审查输出分为两个部分: -1. **整体审查报告**: 提交为 PR 顶部的总体评论 -2. **行级代码评论**: 直接在代码行的位置添加具体评论 +--- -#### 步骤 1: 分析代码并准备评论 +### 第二阶段:后端代码质量 🔒 -在审查过程中,需要为每个问题记录: -- **文件路径**: 如 `packages/service/core/workflow/dispatch.ts` -- **行号**: 如 `L142-L150` -- **问题类型**: 🔴严重 / 🟡改进 / 🟢优化 -- **评论内容**: 具体的问题描述和建议 +聚焦后端(`packages/service/`、`projects/app/src/pages/api/`、`projects/app/src/service/`)的质量问题,完成以下检查清单: +- [] [后端安全](./backend-quality/security.md) +- [] [后端错误处理](./backend-quality/error-handling.md) +- [] [后端性能](./backend-quality/performance.md) -#### 步骤 2: 提交代码审查评论 +--- -GitHub CLI 的 `gh pr review` 命令不支持直接提交行级评论,需要使用 GitHub API。 +### 第三阶段:前端代码质量 🎨 + +聚焦前端(`projects/app/src/`、`packages/web/`)的质量问题,完成以下检查清单: + +- [] [React 性能](./frontend-quality/react-performance.md) +- [] [前端安全](./frontend-quality/security.md) +- [] [TypeScript 质量](./frontend-quality/typescript.md) + +### 第四阶段:代码风格规范 📐 + +对照 FastGPT 各项规范逐一检查,完成以下检查清单: + +- [] [API 路由开发规范](../../design/api/index.md) +- [] [前端组件规范](./style/front.md) +- [] [数据库规范](./style/db.md) +- [] [包结构规范](./style/package.md) +- [] [日志规范](./style/logger.md) +- [] [Service 解耦规范](./style/service-decoupling.md) + +--- + +### 第五阶段:测试覆盖 🧪 + +- 新增的核心业务逻辑是否有对应单元测试(`test/` 或 `projects/*/test/`)? +- 测试是否覆盖了正常路径、边界条件和错误路径? +- 如果没有测试,评估缺失测试的风险等级(高风险逻辑无测试应标记为 🔴)。 + +--- + +### 第六阶段:回归风险检测 🔄 + +- **接口兼容性**:对外 API 是否有 breaking change(字段删除、类型变更、行为变更)? +- **数据库兼容性**:schema 变更是否向后兼容?旧数据是否需要迁移? +- **依赖影响**:修改的公共模块(`packages/global/`、`packages/service/`)是否会影响其他调用方? +- **配置变更**:是否新增了必填配置项,且未提供默认值或迁移说明? + +--- + +## 最终步骤:提交审查报告 + +### 收集所有阶段的问题 + +汇总各阶段发现的问题,按严重程度分类: +- 🔴 **严重**(必须修复才能合并) +- 🟡 **建议**(改进代码质量) +- 🟢 **可选**(优化建议) + +### 提交行级代码评论 + +GitHub CLI 不支持行级评论,需通过 GitHub API 提交: ```bash -# 1. 获取仓库信息 REPO=$(gh repo view --json nameWithOwner -q .nameWithOwner) -# 2. 准备 review 数据(包含行级评论) cat > /tmp/review-data.json << 'EOF' { - "body": "## 📊 代码审查总结\n\n详细的审查意见请查看下方的行级评论。", + "body": "## 📊 代码审查总结\n\n详细意见请查看下方行级评论。", "event": "COMMENT", "comments": [ { - "path": "packages/service/core/workflow/dispatch.ts", - "line": 142, - "body": "🔴 **严重问题**: 这里缺少错误处理,如果 runtimeNode 为 null 会导致运行时错误。\n\n**建议**:\n```typescript\nif (!runtimeNode) {\n throw new Error(`Runtime node not found: ${nodeId}`);\n}\n```" - }, - { - "path": "packages/service/core/workflow/dispatch.ts", - "line": 150, - "body": "🟡 **性能优化**: 建议将此正则表达式编译提取到函数外部,避免每次调用都重新编译。\n\n**建议**:\n```typescript\nconst NODE_ID_PATTERN = /^node_([a-f0-9]+)$/; // 在模块顶部定义\n```" + "path": "文件路径", + "line": 行号, + "body": "🔴 **问题描述**\n\n**建议**:\n```typescript\n// 修复示例\n```" } ] } EOF -# 3. 使用 GitHub API 提交 review gh api repos/$REPO/pulls//reviews \ --method POST \ --input /tmp/review-data.json ``` - -#### 步骤 3: 生成整体审查报告 +### 审查报告模板 ```markdown # PR Review: {PR Title} -## 📊 变更概览 -- **PR 编号**: #{number} -- **作者**: @author -- **分支**: {baseRefName} ← {headRefName} -- **变更统计**: +{additions} -{deletions} 行 -- **涉及文件**: {files.length} 个文件 +## 📋 需求理解 +{第一阶段总结:PR 的核心目的与改动范围} -## ✅ 优点 -{列出做得好的地方} +## 🧪 逻辑验证 +{列出提出的测例及推理结果,标明是否通过} ## ⚠️ 问题汇总 -### 🔴 严重问题 ({count} 个,必须修复) -{简要列出每个严重问题,并在下方添加行级评论} +### 🔴 严重问题({count} 个,必须修复) +{问题列表,行级评论已标注} -### 🟡 建议改进 ({count} 个) -{简要列出每个建议} +### 🟡 建议改进({count} 个) +{问题列表} -### 🟢 可选优化 ({count} 个) -{简要列出优化建议} +### 🟢 可选优化({count} 个) +{问题列表} -## 🧪 测试建议 -{建议的测试方法} - -## 💬 总体评价 -- **代码质量**: ⭐⭐⭐⭐☆ (4/5) -- **安全性**: ⭐⭐⭐⭐⭐ (5/5) -- **性能**: ⭐⭐⭐⭐☆ (4/5) -- **可维护性**: ⭐⭐⭐⭐☆ (4/5) +## ✅ 做得好的地方 +{列出值得肯定的实现} ## 🚀 审查结论 -{建议: 通过/需修改/拒绝} - ---- - -## 📍 详细代码评论 -已在以下位置添加了具体的行级评论: -{列出所有添加了行级评论的位置} +{通过 / 需修改 / 阻塞(说明原因)} ``` -#### 步骤 4: 提交整体审查报告 - -通过 GitHub CLI 提交整体审查报告到评论区。 - -#### 审查命令快速参考: +### 命令参考 | 场景 | 命令 | |------|------| +| 请求修改 | `gh pr review --request-changes --body-file /tmp/review.md` | | 批准 PR | `gh pr review --approve` | -| 请求修改 | `gh pr review --request-changes` | -| 一般评论 | `gh pr review --comment` | -| 从文件提交 | `gh pr review --body-file /tmp/review.md` | -| 添加普通评论 | `gh pr comment --body "内容"` | -| 撤销审查 | `gh pr review --dismiss` | - - - +| 仅评论 | `gh pr review --comment --body-file /tmp/review.md` | diff --git a/.claude/skills/system-pr_review/backend-quality/error-handling.md b/.claude/skills/system-pr_review/backend-quality/error-handling.md new file mode 100644 index 0000000000..0e3a08d98f --- /dev/null +++ b/.claude/skills/system-pr_review/backend-quality/error-handling.md @@ -0,0 +1,104 @@ +# 后端错误处理检查标准 + +## 1. 异步操作未覆盖错误处理 🔴 + +所有 `async/await` 操作都可能抛出异常,未捕获的错误会导致 Promise 静默失败或未处理的拒绝。 + +```typescript +// ❌ 无 try-catch,错误向上抛出且无上下文 +async function deleteUser(userId: string) { + await db.users.deleteOne({ id: userId }); +} + +// ✅ 捕获错误,记录上下文,重新抛出 +async function deleteUser(userId: string): Promise { + try { + const result = await db.users.deleteOne({ id: userId }); + if (result.deletedCount === 0) { + throw new Error(`User not found: ${userId}`); + } + } catch (error) { + addLog.error(error, `Failed to delete user: ${userId}`); + throw error; + } +} +``` + +**例外情况**:API 路由的顶层 handler 通常由框架统一捕获,内部 service 可以直接 throw,不需要每层都 try-catch。重点检查的是**没有上层兜底的独立异步调用**。 + +--- + +## 2. 错误信息丢失 🟡 + +catch 中创建新 Error 但不保留原始错误,导致问题难以排查。 + +```typescript +// ❌ 原始错误信息丢失 +catch (error) { + throw new Error('Save failed'); // 为什么失败?不知道 +} + +// ❌ 空 catch,静默失败 +catch (error) { + // 什么都不做 +} + +// ✅ 保留错误链 +catch (error) { + addLog.error(error, 'Save user failed'); + throw new Error(`Save user failed: ${String(error)}`, { cause: error }); +} + +// ✅ 确需忽略时,必须写明原因 +catch (error) { + // 清理临时文件失败不影响主流程,记录日志后忽略 + addLog.warn(error, 'Temp file cleanup failed'); +} +``` + +--- + +## 3. Fire-and-Forget 未挂 catch 🔴 + +不 await 的 Promise 如果抛出错误,会成为未处理的 rejection,在 Node.js 中可能导致进程崩溃。 + +```typescript +// ❌ 错误无处捕获 +sendNotification(userId); +updateLastLogin(userId); + +// ✅ 如不需要等待,至少挂 .catch +sendNotification(userId).catch(err => + addLog.warn(err, 'Notification send failed, non-critical') +); + +// ✅ 或用 void 明确表示有意忽略(需团队约定) +void sendNotification(userId).catch(err => + addLog.warn(err, 'Notification failed') +); +``` + +--- + +## 4. 业务错误与系统错误混淆 🟡 + +业务错误(如"用户不存在")应返回明确的业务状态码,而不是 500。 + +```typescript +// ❌ 业务错误被当成系统错误 +async function getUser(userId: string) { + const user = await db.users.findById(userId); + if (!user) throw new Error('User not found'); // 被框架捕获后返回 500 +} + +// ✅ 使用业务错误类(FastGPT 中使用 ERROR_ENUM) +import { ERROR_ENUM } from '@fastgpt/global/common/error/errorCode'; + +async function getUser(userId: string) { + const user = await db.users.findById(userId); + if (!user) { + throw new Error(ERROR_ENUM.unAuthUser); // 框架识别为 4xx 业务错误 + } + return user; +} +``` diff --git a/.claude/skills/system-pr_review/backend-quality/performance.md b/.claude/skills/system-pr_review/backend-quality/performance.md new file mode 100644 index 0000000000..0764bd3f9c --- /dev/null +++ b/.claude/skills/system-pr_review/backend-quality/performance.md @@ -0,0 +1,139 @@ +# 后端性能检查标准 + +## 1. CPU 密集型同步操作阻塞事件循环 🔴 + +Node.js 是单线程的,长时间的同步计算会阻塞所有并发请求。即使是几百毫秒的阻塞,在高并发下也是严重问题。 + +**高危模式**: +```typescript +// ❌ 同步解析/序列化超大 JSON(几十MB会卡住几百ms) +const data = JSON.parse(fs.readFileSync('/path/to/huge.json', 'utf8')); + +// ❌ 大数组的同步复杂计算 +const result = largeArray.reduce((acc, item) => { + return acc + heavyComputation(item); // 10万条数据×复杂计算 +}, 0); + +// ❌ 同步读取大文件 +const content = fs.readFileSync('/path/to/large/file'); +``` + +**修复方案**: +```typescript +// ✅ 拆分到多个 tick,释放事件循环 +async function processLargeArray(items: Item[]) { + const CHUNK_SIZE = 1000; + const results: Result[] = []; + for (let i = 0; i < items.length; i += CHUNK_SIZE) { + const chunk = items.slice(i, i + CHUNK_SIZE); + results.push(...chunk.map(item => process(item))); + await new Promise(resolve => setImmediate(resolve)); // 释放事件循环 + } + return results; +} + +// ✅ CPU 密集型任务使用 worker_threads +import { Worker } from 'worker_threads'; + +// ✅ 文件使用流式处理 +import { createReadStream } from 'fs'; +const stream = createReadStream('/path/to/large/file'); +``` + +**判断标准**:单次操作耗时预计超过 50ms 的同步计算都应考虑异步化或分片处理。 + +--- + +## 2. N+1 查询问题 🟡 + +在循环中对每条记录单独查询数据库,导致 N 条记录触发 N+1 次数据库请求。 + +```typescript +// ❌ 循环内查询:100条用户 → 101次数据库请求 +const users = await db.users.find({}).toArray(); +for (const user of users) { + user.posts = await db.posts.find({ userId: user._id }).toArray(); +} + +// ✅ 一次查询 + 内存关联:2次数据库请求 +const users = await db.users.find({}).toArray(); +const userIds = users.map(u => u._id); +const allPosts = await db.posts + .find({ userId: { $in: userIds } }) + .toArray(); + +const postsByUser = new Map(); +allPosts.forEach(post => { + const key = String(post.userId); + postsByUser.set(key, [...(postsByUser.get(key) ?? []), post]); +}); + +users.forEach(user => { + user.posts = postsByUser.get(String(user._id)) ?? []; +}); +``` + +**识别信号**:循环体内出现 `await db.xxx.find/findOne/findById` 即为高度可疑。 + +--- + +## 3. 全量加载未分页 🟡 + +不加 `limit` 地拉取集合数据,当数据量增长到几万甚至几十万条时,单次请求会消耗大量内存和时间。 + +```typescript +// ❌ 无限制全量拉取 +const allLogs = await db.logs.find({ userId }).toArray(); +const allItems = await db.collection.find({}).toArray(); + +// ✅ 强制分页 +const PAGE_SIZE = 50; +const logs = await db.logs + .find({ userId }) + .sort({ createdAt: -1 }) + .skip((page - 1) * PAGE_SIZE) + .limit(PAGE_SIZE) + .toArray(); + +// ✅ 如确实需要全量(如后台任务),使用游标流式处理 +const cursor = db.collection.find({}).batchSize(100); +for await (const doc of cursor) { + await processDoc(doc); +} +``` + +--- + +## 4. 查询未投影(返回多余字段)🟢 + +MongoDB 默认返回文档所有字段。当文档包含大字段(如 `content`、`vectorData`、`fileContent`)时,不投影会显著增加网络传输和内存开销。 + +```typescript +// ❌ 返回整个文档(包含大字段) +const datasets = await db.datasets.find({ teamId }).toArray(); + +// ✅ 只取需要的字段 +const datasets = await db.datasets + .find({ teamId }) + .project({ name: 1, type: 1, createdAt: 1 }) // 排除 vectorData 等大字段 + .toArray(); +``` + +--- + +## 5. 缺少索引的高频查询 🟡 + +在没有索引的字段上进行 `find`/`findOne` 会触发全表扫描,随数据量线性增长。 + +**检查方法**:查看查询条件中的字段,对照 Model 定义确认是否有对应索引。 + +```typescript +// 检查 Model 定义是否有索引 +const DatasetSchema = new Schema({ + teamId: { type: String, required: true, index: true }, // ✅ 有索引 + name: { type: String }, // ❌ 无索引但被频繁查询? +}); + +// 高频查询条件 +db.datasets.find({ teamId, name }); // name 无索引 → 需要添加 +``` diff --git a/.claude/skills/system-pr_review/backend-quality/security.md b/.claude/skills/system-pr_review/backend-quality/security.md new file mode 100644 index 0000000000..3fdcb09066 --- /dev/null +++ b/.claude/skills/system-pr_review/backend-quality/security.md @@ -0,0 +1,175 @@ +# 后端安全检查标准 + +## 1. NoSQL 注入 🔴 + +**核心风险**:MongoDB 操作符(`$gt`、`$where`、`$regex`、`$ne` 等)可通过 HTTP 请求体注入。接口将入参直接透传到查询条件时,攻击者可绕过权限校验或泄露数据。 + +**典型攻击**: +``` +POST /api/login +{ "username": { "$gt": "" }, "password": { "$gt": "" } } +→ db.users.findOne({ username: { $gt: "" }, password: { $gt: "" } }) +→ 匹配所有用户,绕过密码校验 +``` + +**高危模式**: +```typescript +// ❌ 入参直接作为查询条件 +const { username, password } = req.body; +await db.users.findOne({ username, password }); + +// ❌ 对象字段透传进查询 +async function getUser({ filter }: { filter: object }) { + return db.users.findOne(filter); +} + +// ❌ updateOne 条件字段未校验 +await db.collection.updateOne( + { _id: req.body.id }, // id 可能是 { $gt: "" } + { $set: req.body.update } // update 可能注入 $where +); +``` + +**修复方案**: +```typescript +// ✅ 方案1:zod schema 严格校验(推荐) +const LoginSchema = z.object({ + username: z.string().min(1).max(50), + password: z.string().min(1).max(100) +}); +const { username, password } = LoginSchema.parse(req.body); + +// ✅ 方案2:动态查询使用字段白名单 +const ALLOWED_FIELDS = ['status', 'type', 'teamId'] as const; +function buildSafeFilter(raw: Record) { + return ALLOWED_FIELDS.reduce((acc, key) => { + if (raw[key] !== undefined && typeof raw[key] === 'string') { + acc[key] = raw[key] as string; + } + return acc; + }, {} as Record); +} + +// ✅ _id 字段强制 ObjectId 转换 +await db.collection.findOne({ _id: new Types.ObjectId(id) }); +``` + +**检查清单**: +- [ ] 接口入参经过 zod schema 校验 +- [ ] 查询条件字段均为原始类型(`string`/`number`/`boolean`) +- [ ] `req.body` 对象字段未直接传入 MongoDB 操作符位置 +- [ ] `_id` 字段使用 `new Types.ObjectId(id)` 转换 + +--- + +## 2. 命令注入 / 路径遍历 🔴 + +```typescript +// ❌ 危险:用户输入拼入 shell 命令 +exec(`convert ${req.body.filename} output.png`); +// filename = "; rm -rf /" → 执行恶意命令 + +// ❌ 危险:路径拼接未过滤 ../ +const filePath = path.join('/uploads', req.body.path); +// path = "../../etc/passwd" + +// ✅ 使用 execFile 并传数组参数 +execFile('convert', [sanitizedFilename, 'output.png']); + +// ✅ 校验路径在允许目录内 +const resolved = path.resolve('/uploads', req.body.path); +if (!resolved.startsWith(path.resolve('/uploads'))) { + throw new Error('Invalid path'); +} +``` + +--- + +## 3. 死循环风险 🔴 + +**高危模式**: +```typescript +// ❌ 递归无终止条件 +async function processNode(nodeId: string) { + const node = await getNode(nodeId); + await processNode(node.parentId); // parentId 可能形成环形引用 +} + +// ❌ while 无退出条件 +while (queue.length > 0) { + const item = queue.shift(); + queue.push(...item.children); // children 可能重新推入导致无限循环 +} +``` + +**修复方案**: +```typescript +// ✅ 递归:深度限制 + 访问集合 +async function processNode(nodeId: string, visited = new Set(), depth = 0) { + if (depth > 100 || visited.has(nodeId)) return; + visited.add(nodeId); + const node = await getNode(nodeId); + await processNode(node.parentId, visited, depth + 1); +} + +// ✅ 循环:最大迭代次数 +const MAX_ITER = 10000; +let iter = 0; +while (queue.length > 0) { + if (++iter > MAX_ITER) throw new Error('Max iterations exceeded'); + // ... +} +``` + +--- + +## 4. 数据膨胀 🟡 + +无约束的数据积累导致集合无限增长,查询变慢、内存溢出或磁盘耗尽。 + +```typescript +// ❌ 数组字段无上限 push +await db.collection.updateOne( + { _id: id }, + { $push: { logs: newLog } } // 日志数组可无限增长 +); + +// ❌ 批量写入无上限 +await db.collection.insertMany(items); // items 可能有几十万条 + +// ✅ $push + $slice 保留最近 N 条 +await db.collection.updateOne( + { _id: id }, + { $push: { logs: { $each: [newLog], $slice: -100 } } } +); + +// ✅ 批量写入加上限校验 +const MAX_BATCH = 1000; +if (items.length > MAX_BATCH) { + throw new Error(`Batch size exceeds limit: ${items.length}`); +} +``` + +--- + +## 5. 敏感信息保护 🔴 + +```typescript +// ❌ 硬编码密钥 +const API_KEY = 'sk-1234567890abcdef'; + +// ❌ 日志包含密码/token +addLog.info('User login', { userId, email, password }); + +// ✅ 使用环境变量 +const API_KEY = process.env.OPENAI_API_KEY; +if (!API_KEY) throw new Error('OPENAI_API_KEY is required'); + +// ✅ 日志过滤敏感字段 +const { password, token, ...safeUser } = user; +addLog.info('User login', safeUser); + +// ✅ API 响应过滤敏感字段 +const { password: _, ...safeResponse } = userData; +res.json(safeResponse); +``` diff --git a/.claude/skills/system-pr_review/code-quality-standards.md b/.claude/skills/system-pr_review/code-quality-standards.md deleted file mode 100644 index 8de0039576..0000000000 --- a/.claude/skills/system-pr_review/code-quality-standards.md +++ /dev/null @@ -1,612 +0,0 @@ -# 维度 1: 代码质量标准 - -> 通用的代码质量标准,适用于所有项目。这些标准关注代码的正确性、安全性、性能和可维护性。 - -## 目录 - -- [1. 安全性标准](#1-安全性标准) -- [2. 正确性标准](#2-正确性标准) -- [3. 性能标准](#3-性能标准) -- [4. 可测试性标准](#4-可测试性标准) -- [5. 可维护性标准](#5-可维护性标准) -- [6. 文档标准](#6-文档标准) - ---- - -## 1. 安全性标准 - -### 1.1 输入验证 🔴 **必须检查** - -**原则**: 永远不要信任用户输入,所有输入必须验证 - -**检查清单**: -- [ ] 所有用户输入都经过验证和清理 -- [ ] 文件上传验证类型、大小、扩展名 -- [ ] URL 参数和查询参数验证 -- [ ] 使用白名单而不是黑名单 -- [ ] 数组/对象参数验证长度和结构 - -**示例**: -```typescript -// ❌ 不安全: 直接使用用户输入 -async function searchUsers(query: string) { - return await db.users.find({ name: query }); -} - -// ✅ 安全: 验证和清理输入 -async function searchUsers(query: string): Promise { - // 验证输入 - if (!query || query.length > 100) { - throw new Error('Invalid query parameter'); - } - - // 清理输入: 移除特殊字符 - const sanitizedQuery = query.replace(/[^\w\s]/g, ''); - - return await db.users - .find({ - name: { $regex: sanitizedQuery, $options: 'i' } - }) - .limit(10) // 限制结果数量 - .toArray(); -} -``` - -### 1.2 权限检查 🔴 **必须检查** - -**原则**: 所有需要授权的操作都必须验证用户权限 - -**检查清单**: -- [ ] 所有 API 路由都有权限验证 -- [ ] 验证用户对资源的所有权 -- [ ] 敏感操作需要额外验证 (2FA, 确认密码) -- [ ] 遵循最小权限原则 - -**示例**: -```typescript -// ❌ 不安全: 没有权限验证 -export default async function handler(req: NextAPIRequest, res: NextAPIResponse) { - const userId = req.body.userId; - const user = await db.users.findById(userId); - res.json(user); -} - -// ✅ 安全: 验证权限 -import { parseHeaderCert } from '@fastgpt/global/support/permission/controller'; - -export default async function handler(req: NextAPIRequest, res: NextAPIResponse) { - // 1. 验证身份 - const { userId: authUserId } = await parseHeaderCert(req); - - // 2. 验证权限 (只能访问自己的数据) - const requestedUserId = req.body.userId; - - // 管理员可以访问所有用户,普通用户只能访问自己 - if (authUserId !== requestedUserId && !isAdmin(authUserId)) { - throw new Error('Permission denied'); - } - - const user = await db.users.findById(requestedUserId); - - // 3. 过滤敏感字段 - const { password, ...safeUser } = user; - res.json(safeUser); -} -``` - -### 1.3 注入防护 🔴 **必须检查** - -**原则**: 防止 SQL/NoSQL 注入、命令注入、XSS 等攻击 - -**检查清单**: -- [ ] 使用参数化查询,不拼接字符串 -- [ ] 避免直接使用 `eval` 或 `Function` 构造函数 -- [ ] 对用户输出进行 HTML 转义 -- [ ] 使用 DOMPurify 等库清理 HTML - -**示例**: -```typescript -// ❌ NoSQL 注入风险 -async function findUser(query: any) { - return await db.users.findOne(query); - // 如果 query = { "$gt": "" }, 会返回所有用户 -} - -// ✅ 使用参数化和验证 -async function findUser(email: string): Promise { - // 验证 email 格式 - if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) { - throw new Error('Invalid email format'); - } - - return await db.users.findOne({ email }); -} -``` - -### 1.4 敏感信息保护 🔴 **必须检查** - -**原则**: 不要在代码中硬编码敏感信息,不要在日志中暴露敏感数据 - -**检查清单**: -- [ ] 无硬编码的密钥、token、密码 -- [ ] 敏感信息使用环境变量 -- [ ] 错误日志不包含敏感信息 -- [ ] API 响应过滤敏感字段 -- [ ] 密码使用哈希存储 - -**示例**: -```typescript -// ❌ 不安全: 硬编码密钥 -const API_KEY = 'sk-1234567890abcdef'; -const DB_PASSWORD = 'mypassword'; - -// ✅ 安全: 使用环境变量 -const API_KEY = process.env.OPENAI_API_KEY; -if (!API_KEY) { - throw new Error('OPENAI_API_KEY is required'); -} - -// ❌ 不安全: 日志包含敏感信息 -console.log('User logged in:', { - userId: user.id, - email: user.email, - password: user.password // 密码被记录! -}); - -// ✅ 安全: 过滤敏感字段 -const { password, ...safeUser } = user; -console.log('User logged in:', { - userId: safeUser.id, - email: safeUser.email -}); -``` - ---- - -## 2. 正确性标准 - -### 2.1 错误处理 🔴 **必须检查** - -**原则**: 所有可能失败的操作都必须有错误处理 - -**检查清单**: -- [ ] 所有 async/await 都有 try-catch -- [ ] Promise 都有 .catch() 处理 -- [ ] 错误信息清晰且有用 -- [ ] 区分业务错误和系统错误 -- [ ] 错误日志包含上下文信息 - -**示例**: -```typescript -// ❌ 不好的错误处理 -async function deleteUser(userId: string) { - await db.users.deleteOne({ id: userId }); - // 没有错误处理,如果用户不存在会怎样? -} - -// ✅ 好的错误处理 -async function deleteUser(userId: string): Promise { - try { - const result = await db.users.deleteOne({ id: userId }); - - if (result.deletedCount === 0) { - throw new Error(`User not found: ${userId}`); - } - - console.log(`User ${userId} deleted successfully`); - } catch (error) { - if (error instanceof Error) { - console.error(`Failed to delete user ${userId}:`, error); - throw new Error(`Delete user failed: ${error.message}`); - } - throw error; - } -} -``` - -### 2.2 类型安全 🟡 **推荐检查** - -**原则**: 充分利用 TypeScript 类型系统,避免类型错误 - -**检查清单**: -- [ ] 避免使用 `any` 类型 -- [ ] 函数参数和返回值有明确的类型 -- [ ] 复杂类型使用 interface 或 type 定义 -- [ ] 使用类型守卫而不是类型断言 -- [ ] 启用 strict 模式 - -**示例**: -```typescript -// ❌ 不好的类型使用 -async function fetchData(id: any): any { - const result: any = await db.collection('data').findOne({ id }); - return result; -} - -// ✅ 好的类型使用 -interface UserData { - id: string; - name: string; - email: string; - createdAt: Date; -} - -async function fetchData(id: string): Promise { - const result = await db.collection('data').findOne({ id }); - return result; -} -``` - -### 2.3 边界条件 🟡 **推荐检查** - -**原则**: 考虑边界情况和异常输入 - -**检查清单**: -- [ ] 空值处理 (null, undefined, '') -- [ ] 空数组/空对象处理 -- [ ] 极限值处理 (0, 最大值, 最小值) -- [ ] 并发和竞争条件 -- [ ] 资源耗尽情况 - -**示例**: -```typescript -// ❌ 未处理边界条件 -function getFirstItem(items: T[]): T { - return items[0]; // 如果数组为空会返回 undefined -} - -// ✅ 处理边界条件 -function getFirstItem(items: T[]): T | undefined { - if (items.length === 0) { - return undefined; - } - return items[0]; -} - -// 或使用可选链 -function getFirstItem(items: T[]): T | undefined { - return items[0]; -} - -// 使用时 -const first = getFirstItem(items); -if (first) { - // 安全使用 first -} -``` - ---- - -## 3. 性能标准 - -### 3.1 算法复杂度 🟡 **推荐检查** - -**原则**: 避免不必要的嵌套循环,使用合适的数据结构 - -**检查清单**: -- [ ] 避免嵌套循环 (O(n²) 或更差) -- [ ] 大数据集使用合适的算法 -- [ ] 使用 Set/Map 优化查找操作 -- [ ] 分页处理大数据集 - -**示例**: -```typescript -// ❌ 性能问题: O(n²) -function findDuplicates(arr: string[]): string[] { - const duplicates: string[] = []; - for (let i = 0; i < arr.length; i++) { - for (let j = i + 1; j < arr.length; j++) { - if (arr[i] === arr[j]) { - duplicates.push(arr[i]); - } - } - } - return duplicates; -} - -// ✅ 优化后: O(n) -function findDuplicates(arr: string[]): string[] { - const seen = new Set(); - const duplicates: string[] = []; - - for (const item of arr) { - if (seen.has(item)) { - duplicates.push(item); - } else { - seen.add(item); - } - } - - return duplicates; -} -``` - -### 3.2 数据库查询 🟡 **推荐检查** - -**原则**: 避免 N+1 查询,使用索引优化查询 - -**检查清单**: -- [ ] 避免 N+1 查询问题 -- [ ] 使用索引优化查询 -- [ ] 只查询需要的字段 -- [ ] 使用分页 (skip + limit) -- [ ] 批量操作使用 bulkWrite - -**示例**: -```typescript -// ❌ N+1 查询问题 -const users = await db.users.find({}).toArray(); -for (const user of users) { - const posts = await db.posts.find({ userId: user.id }).toArray(); - user.posts = posts; -} - -// ✅ 使用 $in 操作 -const users = await db.users.find({}).toArray(); -const userIds = users.map(u => u.id); -const posts = await db.posts.find({ userId: { $in: userIds } }).toArray(); - -// 构建映射 -const postsByUser = new Map(); -posts.forEach(post => { - if (!postsByUser.has(post.userId)) { - postsByUser.set(post.userId, []); - } - postsByUser.get(post.userId)!.push(post); -}); - -// 关联数据 -users.forEach(user => { - user.posts = postsByUser.get(user.id) || []; -}); -``` - -### 3.3 内存管理 🟢 **可选检查** - -**原则**: 避免内存泄漏,及时清理资源 - -**检查清单**: -- [ ] 避免内存泄漏 (事件监听器、定时器) -- [ ] 及时清理不再使用的大对象 -- [ ] 使用流处理大文件 -- [ ] 避免不必要的闭包 - ---- - -## 4. 可测试性标准 - -### 4.1 测试覆盖 🟡 **推荐检查** - -**原则**: 新功能必须有测试,核心功能要有充分测试,确保单元测试覆盖率在 90% 以上。 - -**检查清单**: -- [ ] 新功能有对应的单元测试 -- [ ] 核心业务逻辑有集成测试 -- [ ] 关键路径有 E2E 测试 -- [ ] 测试覆盖主要场景 (正常和异常) - -**示例**: -```typescript -describe('UserService', () => { - describe('createUser', () => { - it('should create user successfully with valid data', async () => { - // Arrange - const userData = { - name: 'Test User', - email: 'test@example.com' - }; - - // Act - const user = await createUser(userData); - - // Assert - expect(user).toBeDefined(); - expect(user.id).toBeDefined(); - expect(user.name).toBe(userData.name); - }); - - it('should throw error with duplicate email', async () => { - // Arrange - const userData = { - name: 'Test User', - email: 'existing@example.com' - }; - await createUser(userData); - - // Act & Assert - await expect(createUser(userData)).rejects.toThrow('Duplicate email'); - }); - - it('should throw error with invalid email', async () => { - // Arrange - const userData = { - name: 'Test User', - email: 'invalid-email' - }; - - // Act & Assert - await expect(createUser(userData)).rejects.toThrow('Invalid email'); - }); - }); -}); -``` - -### 4.2 测试质量 🟢 **可选检查** - -**原则**: 测试应该是独立的、可重复的、快速的 - -**检查清单**: -- [ ] 测试用例独立,不依赖执行顺序 -- [ ] 测试用例可重复执行 -- [ ] 测试运行快速 (隔离慢操作) -- [ ] 测试命名清晰描述测试意图 -- [ ] 使用 AAA 模式 (Arrange, Act, Assert) - ---- - -## 5. 可维护性标准 - -### 5.1 代码组织 🟡 **推荐检查** - -**原则**: 代码应该易于理解、修改和扩展 - -**检查清单**: -- [ ] 函数/模块职责单一 -- [ ] 代码重复已抽取 -- [ ] 函数长度合理 (一般 < 50 行) -- [ ] 文件结构清晰 -- [ ] 命名清晰表达意图 - -**示例**: -```typescript -// ❌ 职责不单一,函数过长 -async function processUser(userId: string) { - const user = await db.users.findById(userId); - if (!user) throw new Error('User not found'); - - const orders = await db.orders.find({ userId }).toArray(); - const totalAmount = orders.reduce((sum, order) => sum + order.amount, 0); - - const recommendations = await generateRecommendations(user); - const notifications = await buildNotifications(user, recommendations); - - await sendEmail(user.email, notifications); - await updateLastLogin(userId); - - return { user, orders, totalAmount, recommendations }; -} - -// ✅ 职责单一,易于测试 -async function getUserProfile(userId: string) { - const user = await db.users.findById(userId); - if (!user) throw new Error('User not found'); - return user; -} - -async function getUserOrders(userId: string) { - return await db.orders.find({ userId }).toArray(); -} - -async function calculateTotalAmount(orders: Order[]) { - return orders.reduce((sum, order) => sum + order.amount, 0); -} - -async function processUser(userId: string) { - const user = await getUserProfile(userId); - const orders = await getUserOrders(userId); - const totalAmount = calculateTotalAmount(orders); - - return { user, orders, totalAmount }; -} -``` - -### 5.2 命名规范 🟢 **可选检查** - -**原则**: 命名应该清晰表达意图,遵循团队约定 - -**检查清单**: -- [ ] 变量名清晰表达用途 -- [ ] 函数名使用动词开头 -- [ ] 布尔值变量使用 is/has/should 前缀 -- [ ] 常量使用 UPPER_SNAKE_CASE -- [ ] 类/接口使用 PascalCase - -**示例**: -```typescript -// ❌ 不好的命名 -const d = new Date(); -const temp1 = getUser(); -const flag = checkUser(); - -// ✅ 好的命名 -const currentDate = new Date(); -const currentUser = getUser(); -const isAuthenticated = checkUser(); -``` - ---- - -## 6. 文档标准 - -### 6.1 注释质量 🟢 **可选检查** - -**原则**: 注释应该解释"为什么"而不是"是什么" - -**检查清单**: -- [ ] 复杂逻辑有清晰注释 -- [ ] 注释解释设计决策 -- [ ] 公共 API 有 JSDoc 注释 -- [ ] TODO/FIXME 有跟踪 issue - -**示例**: -```typescript -// ❌ 不好的注释: 重复代码 -// 获取用户 -const user = await getUser(userId); - -// ✅ 好的注释: 解释原因 -// 使用缓存避免重复查询数据库 -const user = await getUserWithCache(userId); - -// ❌ 不好的注释: 没有解释 -// 重试 3 次 -for (let i = 0; i < 3; i++) { - try { - return await operation(); - } catch (error) { - // 继续尝试 - } -} - -// ✅ 好的注释: 解释设计决策 -// 重试 3 次处理临时网络故障 -// 使用指数退避避免服务器过载 -for (let attempt = 1; attempt <= 3; attempt++) { - try { - return await operation(); - } catch (error) { - if (attempt === 3) throw error; - await sleep(Math.pow(2, attempt) * 1000); - } -} -``` - -### 6.2 API 文档 🔴 **必选检查** - -所有修改到的 API 都需要用 zod 来进行类型声明以及编写对应的 OpenAPI 文档。 - -参考: [api-development.md](../common/skills/api-development/SKILL.md) - ---- - -## 快速检查表 - -### 🔴 必须检查项 (阻塞性) - -- [ ] **输入验证**: 所有用户输入都经过验证 -- [ ] **权限验证**: API 路由都有权限检查 -- [ ] **注入防护**: 使用参数化查询 -- [ ] **敏感信息**: 无硬编码密钥 -- [ ] **错误处理**: 所有异步操作有错误处理 - -### 🟡 推荐检查项 (建议性) - -- [ ] **类型安全**: 避免使用 `any` -- [ ] **边界条件**: 处理空值和边界情况 -- [ ] **算法复杂度**: 避免嵌套循环 -- [ ] **数据库查询**: 避免 N+1 查询 -- [ ] **测试覆盖**: 新功能有测试 -- [ ] **代码组织**: 职责单一,无重复代码 - -### 🟢 可选检查项 (优化性) - -- [ ] **命名规范**: 命名清晰表达意图 -- [ ] **注释质量**: 复杂逻辑有注释 -- [ ] **API 文档**: 公共 API 有 JSDoc -- [ ] **内存管理**: 避免内存泄漏 - ---- - -**Version**: 1.0 -**Last Updated**: 2026-01-27 -**Maintainer**: FastGPT Development Team diff --git a/.claude/skills/system-pr_review/common-issues-checklist.md b/.claude/skills/system-pr_review/common-issues-checklist.md deleted file mode 100644 index d249427765..0000000000 --- a/.claude/skills/system-pr_review/common-issues-checklist.md +++ /dev/null @@ -1,667 +0,0 @@ -# 维度 3: 常见问题检查清单 - -> 快速识别和修复常见问题模式。这个清单帮助审查者快速发现代码中的典型问题和反模式。 - -## 目录 - -- [1. TypeScript 问题](#1-typescript-问题) -- [2. 异步错误处理问题](#2-异步错误处理问题) -- [3. React 性能问题](#3-react-性能问题) -- [4. 安全漏洞问题](#4-安全漏洞问题) -- [5. 环境配置问题](#5-环境配置问题) - ---- - -## 1. TypeScript 问题 - -### 🔴 1.1 滥用 any 类型 - -**问题识别**: -- 变量声明为 `any` 类型 -- 函数参数或返回值使用 `any` -- 类型断言过度使用 - -**快速修复**: -```typescript -// ❌ 问题代码 -async function fetchData(id: any): any { - const result: any = await db.collection('data').findOne({ id }); - return result; -} - -// ✅ 修复方案 -interface UserData { - id: string; - name: string; - email: string; -} - -async function fetchData(id: string): Promise { - const result = await db.collection('data').findOne({ id }); - return result; -} -``` - -**审查建议**: 🔴 严重问题,必须修复 - ---- - -### 🟡 1.2 类型定义不完整 - -**问题识别**: -- 使用 `object` 作为类型 -- 参数结构不明确 -- 缺少必要的类型定义 - -**快速修复**: -```typescript -// ❌ 问题代码 -function updateUser(id: string, data: object) { - return db.users.updateOne({ id }, { $set: data }); -} - -// ✅ 修复方案 -type UpdateUserData = { - name?: string; - email?: string; - avatar?: string; -}; - -function updateUser(id: string, data: UpdateUserData) { - return db.users.updateOne({ id }, { $set: data }); -} -``` - -**审查建议**: 🟡 建议改进 - ---- - -### 🟡 1.3 不安全的类型断言 - -**问题识别**: -- 双重断言 (`as any as Type`) -- 断言后没有验证 -- 过度依赖类型断言 - -**快速修复**: -```typescript -// ❌ 问题代码 -const value = data as any as User; - -// ✅ 修复方案 1: 类型守卫 -function isUser(value: unknown): value is User { - return ( - typeof value === 'object' && - value !== null && - 'id' in value && - 'name' in value - ); -} - -if (isUser(data)) { - // 安全使用 data 作为 User -} - -// ✅ 修复方案 2: 使用 zod 验证 -import { z } from 'zod'; - -const UserSchema = z.object({ - id: z.string(), - name: z.string() -}); - -const result = UserSchema.parse(data); -``` - -**审查建议**: 🟡 建议改进 - ---- - -## 2. 异步错误处理问题 - -### 🔴 2.1 未处理的 Promise rejection - -**问题识别**: -- async 函数没有 try-catch -- 没有 .catch() 处理 -- 错误可能静默失败 - -**快速修复**: -```typescript -// ❌ 问题代码 -async function fetchUserData(userId: string) { - const response = await fetch(`/api/users/${userId}`); - const data = await response.json(); - return data; -} - -// ✅ 修复方案 -async function fetchUserData(userId: string): Promise { - try { - const response = await fetch(`/api/users/${userId}`); - - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); - } - - const data = await response.json(); - return data; - } catch (error) { - if (error instanceof Error) { - console.error(`Failed to fetch user ${userId}:`, error); - throw new Error(`User fetch failed: ${error.message}`); - } - throw error; - } -} -``` - -**审查建议**: 🔴 严重问题,必须修复 - ---- - -### 🟡 2.2 错误信息丢失 - -**问题识别**: -- catch 中创建新的错误但不保留原始错误 -- 错误日志信息不完整 -- 难以调试和追踪问题 - -**快速修复**: -```typescript -// ❌ 问题代码 -async function saveUser(user: User) { - try { - await db.users.insertOne(user); - } catch (error) { - throw new Error('Save failed'); // 原始错误丢失 - } -} - -// ✅ 修复方案 -async function saveUser(user: User) { - try { - await db.users.insertOne(user); - } catch (error) { - if (error instanceof Error) { - console.error('Database error:', error); - throw new Error(`Save user failed: ${error.message}`, { - cause: error - }); - } - throw error; - } -} -``` - -**审查建议**: 🟡 建议改进 - ---- - -### 🟡 2.3 静默忽略错误 - -**问题识别**: -- 空的 catch 块 -- 使用 void 忽略 Promise -- 没有说明原因的忽略 - -**快速修复**: -```typescript -// ❌ 问题代码 -async function cleanup() { - try { - await deleteTempFiles(); - } catch (error) { - // 空的 catch,错误被忽略 - } -} - -// ✅ 修复方案 -async function cleanup() { - try { - await deleteTempFiles(); - } catch (error) { - // 至少记录错误日志 - console.error('Cleanup failed:', error); - // 如果确实需要忽略,添加注释说明原因 - // 错误被忽略是因为清理失败不应影响主流程 - } -} -``` - -**审查建议**: 🟡 建议改进 (必须有明确的注释说明) - ---- - -## 3. React 性能问题 - -### 🟢 3.1 不必要的组件重渲染 - -**问题识别**: -- 父组件状态变化导致子组件不必要的重渲染 -- 子组件是昂贵的计算或渲染 -- 没有使用 React.memo - -**快速修复**: -```typescript -// ❌ 问题代码 -const Parent = ({ items }: { items: Item[] }) => { - const [count, setCount] = useState(0); - - return ( - <> - - {items.map(item => ( - - ))} - - ); -}; - -// ✅ 修复方案 -const ExpensiveChild = React.memo(function ExpensiveChild({ data }: { data: Item }) { - // 昂贵的计算或渲染 - return
{/* ... */}
; -}); - -const Parent = ({ items }: { items: Item[] }) => { - const [count, setCount] = useState(0); - - return ( - <> - - {items.map(item => ( - - ))} - - ); -}; -``` - -**审查建议**: 🟢 可选优化 - ---- - -### 🟡 3.2 渲染中创建新对象/函数 - -**问题识别**: -- JSX 中使用箭头函数 -- JSX 中创建对象字面量 -- 导致子组件不必要的重渲染 - -**快速修复**: -```typescript -// ❌ 问题代码 -const MyComponent = ({ items }: { items: Item[] }) => { - return ( - <> - {items.map(item => ( - handleClick(item.id)} // 每次渲染创建新函数 - options={{ enable: true, mode: 'edit' }} // 每次渲染创建新对象 - /> - ))} - - ); -}; - -// ✅ 修复方案 -const MyComponent = ({ items }: { items: Item[] }) => { - const handleClick = useCallback((id: string) => { - // 处理逻辑 - }, []); - - const options = useMemo(() => ({ - enable: true, - mode: 'edit' - }), []); - - return ( - <> - {items.map(item => ( - handleClick(item.id)} - options={options} - /> - ))} - - ); -}; -``` - -**审查建议**: 🟡 建议改进 - ---- - -### 🟡 3.3 昂贵计算未缓存 - -**问题识别**: -- 复杂的数组操作 (sort, filter, map 链式调用) -- 每次渲染都重新计算 -- 计算结果在渲染间不变 - -**快速修复**: -```typescript -// ❌ 问题代码 -const ExpensiveList = ({ items }: { items: Item[] }) => { - // 每次渲染都重新计算 - const sortedItems = items.sort((a, b) => a.value - b.value); - const filteredItems = sortedItems.filter(item => item.active); - - return
    {filteredItems.map(item =>
  • {item.name}
  • )}
; -}; - -// ✅ 修复方案 -const ExpensiveList = ({ items }: { items: Item[] }) => { - const sortedItems = useMemo(() => - [...items].sort((a, b) => a.value - b.value), - [items] - ); - - const filteredItems = useMemo(() => - sortedItems.filter(item => item.active), - [sortedItems] - ); - - return
    {filteredItems.map(item =>
  • {item.name}
  • )}
; -}; -``` - -**审查建议**: 🟡 建议改进 - ---- - - -## 4. 安全漏洞问题 - -### 🔴 4.1 NoSQL 注入 (接口入参风险) - -**核心风险**: MongoDB 查询操作符 (`$gt`、`$where`、`$regex`、`$ne` 等) 可以通过 HTTP 请求体注入。当接口直接将入参透传到数据库查询时,攻击者可以构造恶意对象绕过权限校验或泄露数据。 - -**典型攻击场景**: -``` -// 攻击者发送的请求体 -POST /api/login -{ "username": { "$gt": "" }, "password": { "$gt": "" } } -// → MongoDB 查询变为 { username: { $gt: "" }, password: { $gt: "" } } -// → 匹配所有用户,绕过密码校验 -``` - -**问题识别**: -- 接口入参未经 zod/类型校验直接传入查询条件 -- 查询字段类型声明为 `any` 或 `object` -- 使用 `req.body.xxx` 直接拼入 `find()`、`findOne()`、`updateOne()` 等 -- 动态构建查询对象时未限制字段类型为原始值 - -**高危模式**: -```typescript -// ❌ 危险: 入参直接作为查询字段值 -const { username, password } = req.body; -await db.users.findOne({ username, password }); - -// ❌ 危险: 对象字段透传进查询 -async function getUser({ filter }: { filter: object }) { - return db.users.findOne(filter); // filter 可以是任意操作符 -} - -// ❌ 危险: updateOne 条件字段未校验 -await db.collection.updateOne( - { _id: req.body.id }, // id 可能是 { $gt: "" } - { $set: req.body.update } // update 可能注入 $where 等 -); -``` - -**快速修复**: -```typescript -// ✅ 方案 1: zod schema 严格约束入参类型(推荐) -import { z } from 'zod'; - -const LoginSchema = z.object({ - username: z.string().min(1).max(50), - password: z.string().min(1).max(100) -}); - -async function login(req: Request) { - // parse 失败直接抛出,不会进入查询逻辑 - const { username, password } = LoginSchema.parse(req.body); - return db.users.findOne({ username, password }); -} - -// ✅ 方案 2: 显式提取原始值,拒绝对象类型 -async function searchUsers(query: unknown): Promise { - if (typeof query !== 'string' || query.length > 100) { - throw new Error('Invalid query'); - } - return db.users.find({ - name: { $regex: query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), $options: 'i' } - }).limit(10).toArray(); -} - -// ✅ 方案 3: 动态查询条件使用白名单字段 -const ALLOWED_FILTER_FIELDS = ['status', 'type', 'teamId'] as const; - -function buildSafeFilter(raw: Record) { - return ALLOWED_FILTER_FIELDS.reduce((acc, key) => { - if (raw[key] !== undefined && typeof raw[key] === 'string') { - acc[key] = raw[key]; - } - return acc; - }, {} as Record); -} -``` - -**审查重点**: -- [ ] 所有接口入参是否经过 zod schema 或等效校验 -- [ ] 查询条件字段是否均为原始类型 (`string`、`number`、`boolean`) -- [ ] 是否存在将 `req.body` 的对象字段直接传入 MongoDB 操作符位置的情况 -- [ ] `_id` 字段是否使用 `new Types.ObjectId(id)` 强制转换 - -**审查建议**: 🔴 严重问题,必须修复 - ---- - -### 🔴 4.2 XSS 攻击 - -**问题识别**: -- 使用 `dangerouslySetInnerHTML` -- 用户输入直接渲染到 HTML -- 没有 HTML 转义 - -**快速修复**: -```typescript -// ❌ 问题代码 -const UserProfile = ({ user }: { user: User }) => { - return ( -
-

{user.name}

-

-

- ); -}; - -// ✅ 修复方案 -import DOMPurify from 'dompurify'; - -const UserProfile = ({ user }: { user: User }) => { - const cleanBio = DOMPurify.sanitize(user.bio); - - return ( -
-

{user.name}

-

-

- ); -}; - -// 或更安全的方案 -const UserProfile = ({ user }: { user: User }) => { - return ( -
-

{user.name}

-

{user.bio}

// React 自动转义 -
- ); -}; -``` - -**审查建议**: 🔴 严重问题,必须修复 - ---- - -### 🔴 4.3 文件上传漏洞 - -**问题识别**: -- 没有文件类型验证 -- 没有文件大小限制 -- 没有扩展名白名单 - -**快速修复**: -```typescript -// ❌ 问题代码 -app.post('/upload', async (req, res) => { - const file = req.body.file; - await fs.writeFile(`/uploads/${file.name}`, file.data); - res.json({ success: true }); -}); - -// ✅ 修复方案 -import { extname } from 'path'; - -const ALLOWED_EXTENSIONS = ['.jpg', '.jpeg', '.png', '.gif', '.pdf']; -const ALLOWED_MIMES = ['image/jpeg', 'image/png', 'image/gif', 'application/pdf']; -const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB - -app.post('/upload', async (req, res) => { - const file = req.body.file; - - // 验证文件大小 - if (file.size > MAX_FILE_SIZE) { - return res.status(400).json({ error: 'File too large' }); - } - - // 验证 MIME 类型 - if (!ALLOWED_MIMES.includes(file.mimetype)) { - return res.status(400).json({ error: 'Invalid file type' }); - } - - // 验证扩展名 - const ext = extname(file.name).toLowerCase(); - if (!ALLOWED_EXTENSIONS.includes(ext)) { - return res.status(400).json({ error: 'Invalid file extension' }); - } - - const safeName = `${Date.now()}-${Math.random().toString(36).substr(2)}${ext}`; - await fs.writeFile(`/uploads/${safeName}`, file.data); - - res.json({ success: true, filename: safeName }); -}); -``` - -**审查建议**: 🔴 严重问题,必须修复 - ---- - - -## 5. 环境配置问题 - -### 🔴 5.1 硬编码配置 - -**问题识别**: -- 配置值直接写在代码中 -- 密钥、token 硬编码 -- 不同环境无法灵活配置 - -**快速修复**: -```typescript -// ❌ 问题代码 -const API_KEY = 'sk-1234567890abcdef'; -const DB_URL = 'mongodb://localhost:27017/myapp'; - -// ✅ 修复方案 -const API_KEY = process.env.OPENAI_API_KEY; -const DB_URL = process.env.MONGODB_URL; - -if (!API_KEY) { - throw new Error('OPENAI_API_KEY is required'); -} -``` - -**审查建议**: 🔴 严重问题 (特别是敏感信息),必须修复 - ---- - -### 🟡 5.2 环境变量未验证 - -**问题识别**: -- 直接使用环境变量而不验证 -- 没有默认值或类型转换 -- 缺少必需的环境变量检查 - -**快速修复**: -```typescript -// ❌ 问题代码 -const config = { - apiKey: process.env.API_KEY, - port: parseInt(process.env.PORT), - debug: process.env.DEBUG === 'true' -}; - -// ✅ 修复方案 -const getConfig = () => { - const apiKey = process.env.API_KEY; - if (!apiKey) { - throw new Error('API_KEY environment variable is required'); - } - - const port = parseInt(process.env.PORT || '3000', 10); - if (isNaN(port)) { - throw new Error('PORT must be a valid number'); - } - - return { - apiKey, - port, - debug: process.env.DEBUG === 'true' - }; -}; - -const config = getConfig(); -``` - -**审查建议**: 🟡 建议改进 - ---- - -## 快速识别检查表 - -### 🔴 严重问题 (必须修复) - -- [ ] 滥用 `any` 类型 -- [ ] 未处理的 Promise rejection -- [ ] 硬编码敏感信息 -- [ ] SQL/NoSQL 注入漏洞 -- [ ] XSS 攻击漏洞 -- [ ] 文件上传无验证 - -### 🟡 建议改进 (推荐修复) - -- [ ] 类型定义不完整 -- [ ] 错误信息丢失 -- [ ] React 不必要的重渲染 -- [ ] 环境变量未验证 - -### 🟢 可选优化 (锦上添花) - -- [ ] 进一步性能优化 -- [ ] 代码简化 -- [ ] 类型守卫优化 - ---- - -**Version**: 1.0 -**Last Updated**: 2026-01-27 -**Maintainer**: FastGPT Development Team diff --git a/.claude/skills/system-pr_review/frontend-quality/react-performance.md b/.claude/skills/system-pr_review/frontend-quality/react-performance.md new file mode 100644 index 0000000000..0bca36b6b9 --- /dev/null +++ b/.claude/skills/system-pr_review/frontend-quality/react-performance.md @@ -0,0 +1,138 @@ +# 前端 React 性能检查标准 + +## 1. 不必要的组件重渲染 🟡 + +父组件状态变化导致子组件不必要地重新渲染,在昂贵组件(复杂列表、图表、编辑器)上会造成明显卡顿。 + +**识别信号**:子组件接收的 props 在父组件状态变化时并未改变,但子组件仍然重渲染。 + +```typescript +// ❌ 父组件 count 变化 → ExpensiveChild 不必要地重渲染 +const Parent = ({ items }: { items: Item[] }) => { + const [count, setCount] = useState(0); + return ( + <> + + {items.map(item => )} + + ); +}; + +// ✅ 用 React.memo 跳过 props 未变化的渲染 +const ExpensiveChild = React.memo(function ExpensiveChild({ data }: { data: Item }) { + return
{/* 昂贵渲染 */}
; +}); +``` + +**注意**:`React.memo` 对 props 做浅比较,如果传入的是每次新建的对象/函数引用,memo 无效——需要配合下面的优化。 + +--- + +## 2. 渲染函数中创建对象或函数 🟡 + +每次渲染都创建新的对象/数组/函数引用,导致子组件的 `React.memo` 失效,或 `useEffect` 依赖项频繁触发。 + +```typescript +// ❌ 每次渲染都创建新的函数和对象引用 +const MyComponent = ({ items }: { items: Item[] }) => { + return ( + <> + {items.map(item => ( + handleClick(item.id)} // 每次渲染新函数 + options={{ enable: true, mode: 'edit' }} // 每次渲染新对象 + /> + ))} + + ); +}; + +// ✅ 用 useCallback/useMemo 稳定引用 +const MyComponent = ({ items }: { items: Item[] }) => { + const handleClick = useCallback((id: string) => { + // 处理逻辑 + }, []); // 依赖项为空,引用永远稳定 + + const options = useMemo(() => ({ enable: true, mode: 'edit' }), []); + + return ( + <> + {items.map(item => ( + handleClick(item.id)} + options={options} + /> + ))} + + ); +}; +``` + +**判断标准**:只有当子组件是 `React.memo` 包裹的,或该引用是某个 `useEffect`/`useCallback` 的依赖项时,才需要稳定引用。普通非 memo 组件的 props 无需此优化。 + +--- + +## 3. 昂贵计算未缓存 🟡 + +在渲染函数中进行复杂的数组操作(sort、filter、reduce 的链式调用),每次渲染都重新计算,即使输入数据未变化。 + +```typescript +// ❌ 每次渲染都重新排序和过滤 +const ExpensiveList = ({ items }: { items: Item[] }) => { + const sortedItems = [...items].sort((a, b) => a.value - b.value); + const filteredItems = sortedItems.filter(item => item.active); + return
    {filteredItems.map(item =>
  • {item.name}
  • )}
; +}; + +// ✅ useMemo 缓存计算结果,只在 items 变化时重新计算 +const ExpensiveList = ({ items }: { items: Item[] }) => { + const sortedItems = useMemo( + () => [...items].sort((a, b) => a.value - b.value), + [items] + ); + const filteredItems = useMemo( + () => sortedItems.filter(item => item.active), + [sortedItems] + ); + return
    {filteredItems.map(item =>
  • {item.name}
  • )}
; +}; +``` + +**判断标准**:操作数组长度超过 100 条,或包含复杂排序/计算逻辑时,值得用 `useMemo` 缓存。简单的几条数据无需优化。 + +--- + +## 4. 大列表缺少虚拟化 🟢 + +一次性渲染几百上千个 DOM 节点,会导致首次渲染慢、滚动卡顿、内存占用高。 + +```typescript +// ❌ 渲染1000条数据 → 1000个真实DOM节点 +const List = ({ items }: { items: Item[] }) => ( +
+ {items.map(item => )} +
+); + +// ✅ 虚拟化(仅渲染可见区域的节点) +import { VariableSizeList } from 'react-window'; + +const VirtualList = ({ items }: { items: Item[] }) => ( + 50} + width="100%" + > + {({ index, style }) => ( +
+ +
+ )} +
+); +``` + +**判断标准**:列表超过 200 条且需要同时显示在页面上时,建议虚拟化。 diff --git a/.claude/skills/system-pr_review/frontend-quality/security.md b/.claude/skills/system-pr_review/frontend-quality/security.md new file mode 100644 index 0000000000..ea6da2c257 --- /dev/null +++ b/.claude/skills/system-pr_review/frontend-quality/security.md @@ -0,0 +1,130 @@ +# 前端安全检查标准 + +## 1. XSS 攻击 🔴 + +跨站脚本攻击(XSS)通过注入恶意脚本到页面中执行,可窃取用户 Cookie、Session 或劫持页面行为。 + +### 1.1 dangerouslySetInnerHTML 未净化 + +```typescript +// ❌ 直接渲染用户输入的 HTML,攻击者可注入 +const UserProfile = ({ user }: { user: User }) => ( +

+); + +// ✅ 方案1:避免 dangerouslySetInnerHTML,用 React 文本节点渲染(自动转义) +const UserProfile = ({ user }: { user: User }) => ( +

{user.bio}

// React 自动转义,安全 +); + +// ✅ 方案2:确实需要渲染富文本时,使用 DOMPurify 净化 +import DOMPurify from 'dompurify'; +const UserProfile = ({ user }: { user: User }) => { + const cleanBio = DOMPurify.sanitize(user.bio, { + ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'p', 'br'] + }); + return

; +}; +``` + +### 1.2 URL 注入 + +```typescript +// ❌ 危险:href 使用用户输入,可能注入 javascript:alert(1) +const Link = ({ href, text }: { href: string; text: string }) => ( + {text} +); + +// ✅ 校验 URL 协议 +function sanitizeHref(href: string): string { + try { + const url = new URL(href); + if (!['http:', 'https:'].includes(url.protocol)) { + return '#'; // 拒绝 javascript: 等协议 + } + return href; + } catch { + return '#'; + } +} +const Link = ({ href, text }: { href: string; text: string }) => ( + {text} +); +``` + +### 1.3 eval / 动态代码执行 + +```typescript +// ❌ 危险:执行用户输入的代码 +eval(userInput); +new Function('return ' + userInput)(); + +// ✅ 永远不要对用户输入执行 eval/Function +// 如需动态计算,使用安全的表达式解析库(如 mathjs) +``` + +--- + +## 2. 敏感信息暴露在前端 🔴 + +### 2.1 API Key 硬编码在前端代码 + +```typescript +// ❌ 危险:密钥打包进前端 bundle,用户可通过 DevTools 获取 +const API_KEY = 'sk-1234567890abcdef'; +const response = await fetch('/api/endpoint', { + headers: { 'Authorization': `Bearer ${API_KEY}` } +}); + +// ✅ 密钥只存在于服务端,前端调用自己的 API 路由 +const response = await fetch('/api/my-proxy-route', { + headers: { 'Authorization': `Bearer ${userToken}` } // 用户 token,非服务密钥 +}); +``` + +### 2.2 敏感数据存储在 localStorage + +```typescript +// ❌ localStorage 可被 XSS 攻击读取 +localStorage.setItem('userToken', token); +localStorage.setItem('apiKey', apiKey); + +// ✅ 敏感 token 存储在 httpOnly Cookie(无法被 JS 读取) +// 由服务端 Set-Cookie: token=xxx; HttpOnly; Secure; SameSite=Strict +// 前端无需手动操作,自动随请求发送 +``` + +### 2.3 用户敏感信息打印到控制台 + +```typescript +// ❌ 生产环境日志暴露敏感信息 +console.log('User data:', { userId, email, token, password }); + +// ✅ 生产环境避免打印敏感字段 +if (process.env.NODE_ENV !== 'production') { + console.log('Debug:', { userId }); +} +``` + +--- + +## 3. CSRF 防护 🟡 + +跨站请求伪造(CSRF)诱导用户在已登录状态下执行恶意操作。 + +**检查点**: +- 状态变更接口(POST/PUT/DELETE)是否验证了 CSRF Token 或依赖 `SameSite` Cookie? +- 是否接受来自任意 Origin 的跨域请求? + +```typescript +// ✅ FastGPT 中通过 Authorization header 携带 token 天然防 CSRF +// (CSRF 攻击无法读取其他域的 Cookie/Header) +fetch('/api/endpoint', { + method: 'POST', + headers: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json' + }, + body: JSON.stringify(data) +}); +``` diff --git a/.claude/skills/system-pr_review/frontend-quality/typescript.md b/.claude/skills/system-pr_review/frontend-quality/typescript.md new file mode 100644 index 0000000000..04ba1242cd --- /dev/null +++ b/.claude/skills/system-pr_review/frontend-quality/typescript.md @@ -0,0 +1,150 @@ +# 前端 TypeScript 质量检查标准 + +## 1. any 类型滥用 🔴 + +`any` 类型会关闭 TypeScript 的类型检查,使得类型错误只能在运行时暴露,而非编译时发现。 + +**识别信号**: +- 变量、参数、返回值声明为 `any` +- 使用 `as any` 进行类型断言 +- API 响应数据直接使用,无类型约束 + +```typescript +// ❌ any 让类型系统形同虚设 +async function fetchData(id: any): any { + const result: any = await db.collection('data').findOne({ id }); + return result; +} + +// ✅ 明确的类型定义 +type UserData = { + id: string; + name: string; + email: string; + createdAt: Date; +}; + +async function fetchData(id: string): Promise { + const result = await db.collection('data').findOne({ id }); + return result; +} +``` + +**例外情况**: +- 第三方库确实没有类型定义时,可以用 `unknown` 代替 `any`,然后通过类型守卫收窄 +- 临时调试代码(但合并前必须移除) + +--- + +## 2. 不安全的类型断言 🟡 + +类型断言(`as Type`)绕过了编译器检查,如果断言错误,运行时会产生难以调试的问题。 + +```typescript +// ❌ 双重断言(最危险,完全跳过类型检查) +const user = data as any as User; + +// ❌ 无根据的断言(data 可能不是 User) +const user = data as User; +user.profile.avatar; // 如果 data 不符合 User 结构,运行时报错 + +// ✅ 使用类型守卫(编译时 + 运行时都安全) +function isUser(value: unknown): value is User { + return ( + typeof value === 'object' && + value !== null && + 'id' in value && + typeof (value as User).id === 'string' + ); +} + +if (isUser(data)) { + console.log(data.id); // 安全 +} + +// ✅ 使用 zod 验证外部数据(API 响应、localStorage 读取等) +import { z } from 'zod'; + +const UserSchema = z.object({ + id: z.string(), + name: z.string(), + email: z.string().email() +}); + +const user = UserSchema.parse(apiResponse); // 解析失败自动抛出 +``` + +--- + +## 3. 类型定义不完整 🟡 + +使用 `object`、`{}` 或过于宽泛的联合类型,导致 TypeScript 无法提供有效的自动补全和错误提示。 + +```typescript +// ❌ object 类型无任何约束 +function updateUser(id: string, data: object) { + return db.users.updateOne({ id }, { $set: data }); +} + +// ❌ 函数参数数量超过2个,未用对象收拢 +function createItem(name: string, type: string, teamId: string, createdBy: string) { + // ... +} + +// ✅ 明确类型定义 +type UpdateUserData = { + name?: string; + email?: string; + avatar?: string; +}; +function updateUser(id: string, data: UpdateUserData) { + return db.users.updateOne({ id }, { $set: data }); +} + +// ✅ 多参数用对象收拢(FastGPT 规范:超过2个参数必须用对象) +type CreateItemParams = { + name: string; + type: string; + teamId: string; + createdBy: string; +}; +function createItem({ name, type, teamId, createdBy }: CreateItemParams) { + // ... +} +``` + +--- + +## 4. 非空断言过度使用 🟡 + +非空断言(`!`)告诉 TypeScript"这个值不可能是 null/undefined",如果判断错误,会在运行时抛出 `Cannot read properties of null`。 + +```typescript +// ❌ 危险:如果 user 为 null,运行时崩溃 +const email = user!.email; +const name = data!.profile!.name; + +// ✅ 方案1:提前校验 +if (!user) throw new Error('User not found'); +const email = user.email; // 此后 TypeScript 知道 user 非空 + +// ✅ 方案2:可选链(不确定是否存在时) +const name = data?.profile?.name ?? 'Unknown'; + +// ✅ 方案3:只在确实不可能为空的地方使用(需加注释说明原因) +// teamId 在此处由 authMiddleware 保证非空 +const teamId = req.headers.teamId!; +``` + +--- + +## 快速检查表 + +| 检查项 | 级别 | +|--------|------| +| 无 `any` 类型声明 | 🔴 | +| 无 `as any as Type` 双重断言 | 🔴 | +| 函数参数有明确类型约束 | 🟡 | +| 超过2个参数使用对象收拢 | 🟡 | +| `!` 非空断言有校验或注释支撑 | 🟡 | +| 外部数据(API响应)经过 zod 或类型守卫处理 | 🟡 | diff --git a/.claude/skills/system-pr_review/style/api.md b/.claude/skills/system-pr_review/style/api.md deleted file mode 100644 index 97537de10d..0000000000 --- a/.claude/skills/system-pr_review/style/api.md +++ /dev/null @@ -1,96 +0,0 @@ - -# API 路由开发规范 - -FastGPT 使用 Next.js API Routes,需要遵循特定的开发模式。 - -### 2.1 路由定义 - -**文件位置**: `projects/app/src/pages/api/` - -**审查要点**: -- ✅ 路由文件使用命名导出,不支持默认导出 -- ✅ 使用 `NextAPIRequest` 和 `NextAPIResponse` 类型 -- ✅ 支持的 HTTP 方法明确 (`GET`, `POST`, `PUT`, `DELETE`) -- ✅ 返回统一的响应格式 - -**示例**: -```typescript -import type { NextAPIRequest, NextAPIResponse } from '@fastgpt/service/type/next'; -import { APIError } from '@fastgpt/service/core/error/controller'; - -export default async function handler(req: NextAPIRequest, res: NextAPIResponse) { - try { - if (req.method !== 'POST') { - throw new Error('Method not allowed'); - } - - // 处理逻辑... - const result = await processData(req.body); - - res.json(result); - } catch (error) { - APIError(error)(req, res); - } -} -``` - -### 2.2 类型合约 - -**文件位置**: `packages/global/openapi/` - -**审查要点**: -- ✅ API 合约定义在 OpenAPI 规范文件中 -- ✅ 请求参数有完整的类型定义 -- ✅ 响应格式有完整的类型定义 -- ✅ 错误响应有说明 - -### 2.3 业务逻辑 - -**文件位置**: -- 通用逻辑: `packages/service/` -- 项目特定逻辑: `projects/app/src/service/` - -**审查要点**: -- ✅ 业务逻辑与 API 路由分离 -- ✅ 服务函数有明确的类型定义 -- ✅ 错误处理统一 - -### 2.4 权限验证 - -**审查要点**: -- ✅ 所有 API 路由都有权限验证 (除了公开端点) -- ✅ 使用 `parseHeaderCert` 解析认证头 -- ✅ 验证用户对资源的所有权 -- ✅ 敏感操作需要额外验证 - -**示例**: -```typescript -import { parseHeaderCert } from '@fastgpt/global/support/permission/controller'; - -export default async function handler(req: NextAPIRequest, res: NextAPIResponse) { - try { - // 解析认证头 - const { userId, teamId } = await parseHeaderCert(req); - - // 验证权限 - const resource = await Resource.findById(resourceId); - if (!resource || resource.userId !== userId) { - throw new Error('Permission denied'); - } - - // 继续处理... - } catch (error) { - APIError(error)(req, res); - } -} -``` - -### 2.5 错误处理 - -**审查要点**: -- ✅ 使用 try-catch 包裹所有异步操作 -- ✅ 使用 `APIError` 统一错误响应 -- ✅ 错误信息不暴露敏感数据 -- ✅ HTTP 状态码正确 - ---- diff --git a/.claude/skills/system-pr_review/style/db.md b/.claude/skills/system-pr_review/style/db.md index d41edb8c83..0ecb9b050e 100644 --- a/.claude/skills/system-pr_review/style/db.md +++ b/.claude/skills/system-pr_review/style/db.md @@ -63,6 +63,4 @@ const users = await User.find({}) - ✅ 数据库操作使用 try-catch - ✅ 处理重复键错误 (code 11000) - ✅ 处理连接错误 -- ✅ 错误日志包含上下文信息 - ---- +- ✅ 错误日志包含上下文信息 \ No newline at end of file diff --git a/.claude/skills/system-pr_review/style/front.md b/.claude/skills/system-pr_review/style/front.md index f45b685417..12bf7e279a 100644 --- a/.claude/skills/system-pr_review/style/front.md +++ b/.claude/skills/system-pr_review/style/front.md @@ -65,17 +65,21 @@ export const YourComponent = React.memo(function YourComponent({ ## 3.4 国际化 **审查要点**: -- ✅ 所有用户可见文本使用 `i18nT` +- ✅ 所有用户可见文本使用 `t`, 服务端使用`i18nT` - ✅ 翻译 key 使用命名空间 - ✅ 动态文本使用插值 **示例**: ```typescript import { i18nT } from '@fastgpt/web/i18n/utils'; - const message = i18nT('user:welcome', { name: userName }); ``` +```typescript +import { useTranslation } from 'next-i18next'; +const { t } = useTranslation(); +const message = t('user:welcome', { name: userName }); +``` ## 3.5 性能优化 **审查要点**: @@ -83,6 +87,4 @@ const message = i18nT('user:welcome', { name: userName }); - ✅ 大列表使用虚拟化 - ✅ 避免在渲染中创建新对象/函数 - ✅ 使用 `useMemo` 缓存计算结果 -- ✅ 使用 `useCallback` 缓存函数 - ---- +- ✅ 使用 `useCallback` 缓存函数 \ No newline at end of file diff --git a/.claude/skills/system-pr_review/style/package.md b/.claude/skills/system-pr_review/style/package.md index f75185f64d..9d65f2202a 100644 --- a/.claude/skills/system-pr_review/style/package.md +++ b/.claude/skills/system-pr_review/style/package.md @@ -47,6 +47,4 @@ import { UserType } from '@fastgpt/global/core/user/type'; - ✅ 公共类型必须导出 - ✅ 类型文件使用 `.d.ts` 扩展名 - ✅ 复杂类型放在独立的类型文件 -- ✅ 使用 `export type` 导出类型 - ---- +- ✅ 使用 `export type` 导出类型 \ No newline at end of file diff --git a/.claude/skills/system-pr_review/style/service-decoupling.md b/.claude/skills/system-pr_review/style/service-decoupling.md new file mode 100644 index 0000000000..57a55d06ec --- /dev/null +++ b/.claude/skills/system-pr_review/style/service-decoupling.md @@ -0,0 +1,216 @@ +# Service 解耦规范 + +## 核心原则 + +**后端 service 之间不允许互相引用,只允许单向依赖,跨 service 的协调由上层 controller 完成。** + +如果 serviceA 确实需要 serviceB 的数据,**由 controller 提前调用 serviceB,将结果以参数形式传入 serviceA**,而不是让 serviceA 自行 import serviceB。 + +--- + +## 依赖方向 + +``` +Controller / API Handler + ↓ 调用 serviceB → 得到结果 + ↓ 将结果作为参数传入 serviceA + Service A Service B Service C + ↓ 调用 ↓ 调用 ↓ 调用 + Repository Repository Repository + (DB Model) (DB Model) (DB Model) +``` + +**合法方向**: +- `controller` → `service`(上层调用下层,允许) +- `service` → 本模块的 DB Model(允许) +- `service` → `packages/service/common/`(公共工具,允许) +- `service` 接收其他 service 的**数据结果**(以参数形式传入,允许) + +**违规方向**: +- `serviceA` import `serviceB`(同级 service 互相引用,禁止) +- `serviceA` → `controllerB`(service 引用上层,禁止) + +--- + +## 违规模式识别 + +### 1. 同级 service 直接 import + +```typescript +// ❌ 违规:datasetService 直接引用 workflowService +// packages/service/core/dataset/service.ts +import { dispatchWorkflow } from '../workflow/service'; + +export async function deleteDataset(datasetId: string) { + await MongoDataset.deleteOne({ _id: datasetId }); + await dispatchWorkflow({ datasetId }); // 违规:跨 service 调用 +} +``` + +**识别方式**:在 `packages/service/core/xxx/` 目录下的文件中,import 了同级其他模块的 service 文件。 + +--- + +### 2. 循环依赖 + +```typescript +// ❌ serviceA 引用 serviceB,serviceB 也引用 serviceA → 循环依赖 +// dataset/service.ts +import { updateAppDataset } from '../app/service'; + +// app/service.ts +import { getDatasetInfo } from '../dataset/service'; +``` + +循环依赖会导致模块加载时出现 `undefined` 错误,且极难排查。 + +--- + +### 3. 在 service 内触发副作用业务 + +```typescript +// ❌ service 内部触发了属于其他业务域的逻辑 +export async function updateDatasetCollection(collectionId: string, data: UpdateData) { + await MongoDatasetCollection.updateOne({ _id: collectionId }, { $set: data }); + + // 违规:更新数据集集合后,直接触发通知或工作流——这是其他业务域的职责 + await sendTeamNotification(teamId, 'collection_updated'); + await triggerRebuildIndex(collectionId); +} +``` + +--- + +## 正确模式:由 Controller 协调 + +### 模式1:Controller 顺序调用多个 service + +```typescript +// ✅ controller 层负责协调多个 service +// projects/app/src/pages/api/core/dataset/collection/update.ts + +import { updateDatasetCollection } from '@fastgpt/service/core/dataset/collection/controller'; +import { sendTeamNotification } from '@fastgpt/service/support/user/team/controller'; +import { triggerRebuildIndex } from '@fastgpt/service/core/dataset/training/controller'; + +export default async function handler(req, res) { + const { collectionId, ...data } = req.body; + + // 1. 更新集合(dataset service 只做自己的事) + await updateDatasetCollection(collectionId, data); + + // 2. 发送通知(由 controller 层调用通知 service) + await sendTeamNotification(teamId, 'collection_updated'); + + // 3. 触发重建索引(由 controller 层调用训练 service) + await triggerRebuildIndex(collectionId); + + res.json({ success: true }); +} +``` + +### 模式2:serviceA 需要 serviceB 的数据 → Controller 提前获取并以参数传入 + +当 serviceA 的某个函数需要用到 serviceB 的查询结果时,**不要让 serviceA 内部去调用 serviceB**,而是由 controller 先查询,再将结果作为参数传给 serviceA。 + +```typescript +// ❌ 违规:serviceA 内部自己去查 serviceB 的数据 +// packages/service/core/dataset/service.ts +import { getTeamInfo } from '../support/user/team/service'; // 跨 service import + +export async function checkDatasetQuota(datasetId: string) { + const dataset = await MongoDataset.findById(datasetId); + const team = await getTeamInfo(dataset.teamId); // 违规:直接调用其他 service + return dataset.usedSize < team.maxDatasetSize; +} + +// ✅ 正确:controller 提前获取 team 信息,以参数形式传入 +// packages/service/core/dataset/service.ts +export async function checkDatasetQuota( + datasetId: string, + teamMaxSize: number // ← 由 controller 传入,service 不关心数据从哪来 +) { + const dataset = await MongoDataset.findById(datasetId); + return dataset.usedSize < teamMaxSize; +} + +// projects/app/src/pages/api/core/dataset/xxx.ts(controller 层) +import { getTeamInfo } from '@fastgpt/service/support/user/team/controller'; +import { checkDatasetQuota } from '@fastgpt/service/core/dataset/service'; + +export default async function handler(req, res) { + const { datasetId, teamId } = req.body; + + // controller 负责获取跨域数据 + const team = await getTeamInfo(teamId); + + // 将所需数据作为参数传入 service + const hasQuota = await checkDatasetQuota(datasetId, team.maxDatasetSize); + + // ... +} +``` + +**这个模式的好处**: +- `checkDatasetQuota` 可以独立测试,只需传入 `datasetId` 和 `teamMaxSize`,无需 mock 整个 team service +- service 的职责更纯粹:只处理本模块的数据,不感知其他模块的存在 +``` + +--- + +## 公共逻辑的处理 + +如果多个 service 都需要某个逻辑,不应让它们互相引用,而应将公共逻辑**下沉到 common 层**。 + +``` +packages/service/ +├── common/ ← 公共工具层,可被所有 service 引用 +│ ├── error/ +│ ├── file/ +│ └── string/ +├── core/ +│ ├── dataset/ +│ │ └── service.ts ← 只引用 common/,不引用 core/workflow/ +│ └── workflow/ +│ └── service.ts ← 只引用 common/,不引用 core/dataset/ +``` + +```typescript +// ✅ 公共逻辑下沉到 common 层 +// packages/service/common/permission/utils.ts +export function checkTeamPermission(teamId: string, userId: string) { ... } + +// dataset/service.ts 和 workflow/service.ts 都可以引用 common +import { checkTeamPermission } from '../../common/permission/utils'; +``` + +--- + +## 审查检查清单 + +审查新增或修改的 service 文件时,重点检查: + +- [ ] 文件顶部的 `import` 列表中,是否有引用同级或跨域的 service 文件? +- [ ] 是否出现了 `import xxx from '../otherModule/service'` 或 `from '../otherModule/controller'`? +- [ ] 若有跨模块调用,是否可以通过**上移到 controller 层**来解决? +- [ ] 公共逻辑是否应该下沉到 `common/` 而不是通过互相引用实现? + +**快速 grep 方法**: + +```bash +# 找出 service 文件中可能的跨 service 引用 +grep -rn "from '\.\./[^/]*/service" packages/service/core/ +grep -rn "from '\.\./[^/]*/controller" packages/service/core/ +``` + +--- + +## 为什么这样设计 + +**可测试性**:service 不依赖其他 service,可以独立 mock 测试,无需构造复杂的依赖图。 + +**可维护性**:修改一个 service 不会意外影响另一个 service,降低改动的波及范围。 + +**可读性**:阅读 controller 代码时,业务流程一目了然——"先做 A,再做 B,再做 C",而不是藏在 service 内部的隐式调用链。 + +**避免循环依赖**:互相引用极易形成循环依赖,导致运行时 `undefined` 错误,且 TypeScript 编译不会报错,只会在运行时暴露。 diff --git a/document/content/docs/openapi/share.en.mdx b/document/content/docs/openapi/share.en.mdx index 34614036f3..e2ff6a6266 100644 --- a/document/content/docs/openapi/share.en.mdx +++ b/document/content/docs/openapi/share.en.mdx @@ -243,7 +243,7 @@ type ResponseType = { temperature?: number; // Temperature maxToken?: number; // Model max tokens quoteList?: SearchDataResponseItemType[]; // Citation list - historyPreview?: ChatItemType[]; // Context preview (history may be truncated) + historyPreview?: ChatItemMiniType[]; // Context preview (history may be truncated) similarity?: number; // Minimum similarity threshold limit?: number; // Max citation tokens diff --git a/document/content/docs/openapi/share.mdx b/document/content/docs/openapi/share.mdx index 44f94e7622..937013a7f4 100644 --- a/document/content/docs/openapi/share.mdx +++ b/document/content/docs/openapi/share.mdx @@ -243,7 +243,7 @@ type ResponseType = { temperature?: number; // 温度 maxToken?: number; // 模型的最大token quoteList?: SearchDataResponseItemType[]; // 引用列表 - historyPreview?: ChatItemType[]; // 上下文预览(历史记录会被裁剪) + historyPreview?: ChatItemMiniType[]; // 上下文预览(历史记录会被裁剪) similarity?: number; // 最低相关度 limit?: number; // 引用上限token diff --git a/document/data/doc-last-modified.json b/document/data/doc-last-modified.json index 9cd5a4d297..0a95908929 100644 --- a/document/data/doc-last-modified.json +++ b/document/data/doc-last-modified.json @@ -222,7 +222,7 @@ "document/content/docs/self-host/upgrading/4-14/4141.mdx": "2026-03-03T17:39:47+08:00", "document/content/docs/self-host/upgrading/4-14/41410.en.mdx": "2026-03-31T23:15:29+08:00", "document/content/docs/self-host/upgrading/4-14/41410.mdx": "2026-04-08T16:15:25+08:00", - "document/content/docs/self-host/upgrading/4-14/41411.mdx": "2026-04-09T15:12:39+08:00", + "document/content/docs/self-host/upgrading/4-14/41411.mdx": "2026-04-10T13:58:10+08:00", "document/content/docs/self-host/upgrading/4-14/4142.en.mdx": "2026-03-03T17:39:47+08:00", "document/content/docs/self-host/upgrading/4-14/4142.mdx": "2026-03-03T17:39:47+08:00", "document/content/docs/self-host/upgrading/4-14/4143.en.mdx": "2026-03-03T17:39:47+08:00", diff --git a/packages/global/core/ai/type.ts b/packages/global/core/ai/type.ts index 9ca87d10b3..32b9adc34e 100644 --- a/packages/global/core/ai/type.ts +++ b/packages/global/core/ai/type.ts @@ -10,6 +10,7 @@ import type { } from 'openai/resources'; import type { WorkflowInteractiveResponseType } from '../workflow/template/system/interactive/type'; import type { Stream } from 'openai/streaming'; +import z from 'zod'; // Extension of ChatCompletionMessageParam, Add file url type export type ChatCompletionContentPartFile = { @@ -74,16 +75,14 @@ export type ChatCompletion = SdkChatCompletion & { error?: any; }; -export type CompletionFinishReason = - | 'error' - | 'close' - | 'stop' - | 'length' - | 'tool_calls' - | 'content_filter' - | 'function_call' - | null - | undefined; +export const CompletionFinishReasonSchema = z + .union([ + z.enum(['error', 'close', 'stop', 'length', 'tool_calls', 'content_filter', 'function_call']), + z.literal(null), + z.undefined() + ]) + .meta({ description: '模型完成原因' }); +export type CompletionFinishReason = z.infer; export type { Stream }; diff --git a/packages/global/core/chat/adapt.ts b/packages/global/core/chat/adapt.ts index 1896d5ff08..67e3bd513d 100644 --- a/packages/global/core/chat/adapt.ts +++ b/packages/global/core/chat/adapt.ts @@ -1,6 +1,6 @@ import type { AIChatItemValueItemType, - ChatItemType, + ChatItemMiniType, ChatItemValueItemType, RuntimeUserPromptType, SystemChatItemValueItemType, @@ -46,7 +46,7 @@ export const chats2GPTMessages = ({ reserveId, reserveTool = false }: { - messages: ChatItemType[]; + messages: ChatItemMiniType[]; reserveId: boolean; reserveTool?: boolean; }): ChatCompletionMessageParam[] => { @@ -218,7 +218,7 @@ export const GPTMessages2Chats = ({ reserveTool?: boolean; reserveReason?: boolean; getToolInfo?: (name: string) => { name: string; avatar: string }; -}): ChatItemType[] => { +}): ChatItemMiniType[] => { const chatMessages = messages .map((item) => { const obj = GPT2Chat[item.role]; @@ -402,7 +402,7 @@ export const GPTMessages2Chats = ({ .filter((item) => item.value.length > 0); // Merge data with the same dataId(Sequential obj merging) - const result = chatMessages.reduce((result: ChatItemType[], currentItem) => { + const result = chatMessages.reduce((result: ChatItemMiniType[], currentItem) => { const lastItem = result[result.length - 1]; if (lastItem && lastItem.dataId === currentItem.dataId && lastItem.obj === currentItem.obj) { @@ -455,7 +455,7 @@ export const runtimePrompt2ChatsValue = (prompt: { return value; }; -export const getSystemPrompt_ChatItemType = (prompt?: string): ChatItemType[] => { +export const getSystemPrompt_ChatItemType = (prompt?: string): ChatItemMiniType[] => { if (!prompt) return []; return [ { diff --git a/packages/global/core/chat/constants.ts b/packages/global/core/chat/constants.ts index 89cc4aefdd..2c98cdb560 100644 --- a/packages/global/core/chat/constants.ts +++ b/packages/global/core/chat/constants.ts @@ -88,3 +88,10 @@ export enum ChatStatusEnum { running = 'running', finish = 'finish' } + +export enum GetChatTypeEnum { + normal = 'normal', + outLink = 'outLink', + team = 'team', + home = 'home' +} diff --git a/packages/global/core/chat/type.ts b/packages/global/core/chat/type.ts index 050506809d..1ec4876286 100644 --- a/packages/global/core/chat/type.ts +++ b/packages/global/core/chat/type.ts @@ -1,15 +1,23 @@ -import type { SearchDataResponseItemType } from '../dataset/type'; -import type { ChatSourceEnum, ChatStatusEnum } from './constants'; +import { SearchDataResponseItemSchema } from '../dataset/type'; +import type { ChatSourceEnum } from './constants'; import { ChatFileTypeEnum, ChatRoleEnum } from './constants'; -import type { FlowNodeTypeEnum } from '../workflow/node/constant'; -import type { DispatchNodeResponseKeyEnum } from '../workflow/runtime/constants'; -import type { AppSchemaType, VariableItemType } from '../app/type'; -import type { DispatchNodeResponseType } from '../workflow/runtime/type'; +import { FlowNodeTypeEnum } from '../workflow/node/constant'; +import { DispatchNodeResponseKeyEnum } from '../workflow/runtime/constants'; +import { AppSchemaTypeSchema, type AppSchemaType, type VariableItemType } from '../app/type'; +import { DispatchNodeResponseSchema } from '../workflow/runtime/type'; import { WorkflowInteractiveResponseTypeSchema } from '../workflow/template/system/interactive/type'; import type { FlowNodeInputItemType } from '../workflow/type/io'; import z from 'zod'; import { AgentPlanSchema } from '../ai/agent/type'; +export const ChatHistoryItemResSchema = DispatchNodeResponseSchema.extend({ + nodeId: z.string(), + id: z.string(), + moduleType: z.enum(FlowNodeTypeEnum), + moduleName: z.string() +}); +export type ChatHistoryItemResType = z.infer; + /* One tool run response */ export type ToolRunResponseItemType = any; /* tool module response */ @@ -200,43 +208,51 @@ export const AIChatItemValueSchema = z.object({ export type AIChatItemValueItemType = z.infer; -// TODO 待迁移成 zod -export type AIChatItemType = { - obj: ChatRoleEnum.AI; - value: AIChatItemValueItemType[]; - memories?: Record; - userGoodFeedback?: string; - userBadFeedback?: string; - customFeedbacks?: string[]; - adminFeedback?: AdminFbkType; - isFeedbackRead?: boolean; +export const AIChatItemSchema = z.object({ + obj: z.literal(ChatRoleEnum.AI), + value: z.array(AIChatItemValueSchema), + memories: z.record(z.string(), z.any()).optional(), + userGoodFeedback: z.string().optional(), + userBadFeedback: z.string().optional(), + customFeedbacks: z.array(z.string()).optional(), + adminFeedback: AdminFbkSchema.optional(), + isFeedbackRead: z.boolean().optional(), + durationSeconds: z.number().optional(), + errorMsg: z.string().optional(), + citeCollectionIds: z.array(z.string()).optional(), + [DispatchNodeResponseKeyEnum.nodeResponse]: z.array(ChatHistoryItemResSchema).optional().meta({ + description: '节点响应' + }) +}); +export type AIChatItemType = z.infer; - durationSeconds?: number; - errorMsg?: string; - citeCollectionIds?: string[]; +export const ChatItemValueItemSchema = z.union([ + UserChatItemValueItemSchema, + SystemChatItemValueItemSchema, + AIChatItemValueSchema +]); +export type ChatItemValueItemType = z.infer; - /** - * 不再存储在 chatItemSchema 里,分别存储到 chatItemResponseSchema - */ - [DispatchNodeResponseKeyEnum.nodeResponse]?: ChatHistoryItemResType[]; -}; +export const ChatItemObjItemSchema = z.union([ + UserChatItemSchema, + SystemChatItemSchema, + AIChatItemSchema +]); +export type ChatItemObjItemType = z.infer; -export type ChatItemValueItemType = - | UserChatItemValueItemType - | SystemChatItemValueItemType - | AIChatItemValueItemType; -export type ChatItemObjItemType = UserChatItemType | SystemChatItemType | AIChatItemType; - -export type ChatItemSchemaType = ChatItemObjItemType & { - dataId: string; - chatId: string; - userId: string; - teamId: string; - tmbId: string; - appId: string; - time: Date; - deleteTime?: Date | null; -}; +export const ChatItemDBSchema = ChatItemObjItemSchema.and( + z.object({ + dataId: z.string(), + chatId: z.string(), + userId: z.string(), + teamId: z.string(), + tmbId: z.string(), + appId: z.string(), + time: z.date(), + deleteTime: z.date().nullish() + }) +); +export type ChatItemDBSchemaType = z.infer; // Client error show const ErrorTextItemSchema = z.object({ @@ -245,67 +261,66 @@ const ErrorTextItemSchema = z.object({ }); export type ErrorTextItemType = z.infer; -export type ResponseTagItemType = { - useAgentSandbox?: boolean; - totalQuoteList?: SearchDataResponseItemType[]; - toolCiteLinks?: ToolCiteLinksType[]; - errorText?: ErrorTextItemType; - - /** @deprecated */ - llmModuleAccount?: number; - /** @deprecated */ - historyPreviewLength?: number; -}; - -export type ChatItemType = ChatItemObjItemType & { - dataId?: string; -} & ResponseTagItemType; - /* --------- chat item response ---------- */ -export type ChatItemResponseSchemaType = { - teamId: string; - appId: string; - chatId: string; - chatItemDataId: string; - data: ChatHistoryItemResType; -}; +export const ChatItemResponseSchema = z.object({ + teamId: z.string(), + appId: z.string(), + chatId: z.string(), + chatItemDataId: z.string(), + data: ChatHistoryItemResSchema +}); +export type ChatItemResponseSchemaType = z.infer; /* --------- team chat --------- */ -export type ChatAppListSchema = { - apps: AppSchemaType[]; - teamInfo: any; - uid?: string; -}; +export const ChatAppListSchema = z.object({ + apps: z.array(AppSchemaTypeSchema), + teamInfo: z.any(), + uid: z.string().optional() +}); +export type ChatAppListSchemaType = z.infer; /* ---------- history ------------- */ -export type HistoryItemType = { - chatId: string; - updateTime: Date; - customTitle?: string; - title: string; -}; -export type ChatHistoryItemType = HistoryItemType & { - appId: string; - top?: boolean; -}; +export const HistoryItemSchema = z.object({ + chatId: z.string(), + updateTime: z.date(), + customTitle: z.string().optional(), + title: z.string() +}); +export type HistoryItemType = z.infer; + +export const ChatHistoryItemSchema = HistoryItemSchema.extend({ + appId: z.string(), + top: z.boolean().optional() +}); +export type ChatHistoryItemType = z.infer; /* ------- response data ------------ */ -export type ChatHistoryItemResType = DispatchNodeResponseType & { - nodeId: string; - id: string; - moduleType: FlowNodeTypeEnum; - moduleName: string; -}; - export const ToolCiteLinksSchema = z.object({ name: z.string(), url: z.string() }); export type ToolCiteLinksType = z.infer; +export const ResponseTagItemSchema = z.object({ + useAgentSandbox: z.boolean().optional(), + totalQuoteList: z.array(SearchDataResponseItemSchema).optional(), + toolCiteLinks: z.array(ToolCiteLinksSchema).optional(), + errorText: ErrorTextItemSchema.optional(), + llmModuleAccount: z.number().optional().meta({ deprecated: true }), + historyPreviewLength: z.number().optional().meta({ deprecated: true }) +}); +export type ResponseTagItemType = z.infer; + /* dispatch run time */ export const RuntimeUserPromptSchema = z.object({ files: z.array(UserChatItemFileItemSchema), text: z.string() }); export type RuntimeUserPromptType = z.infer; + +export const ChatItemMiniSchema = ChatItemObjItemSchema.and( + z.object({ + dataId: z.string().optional() + }) +).and(ResponseTagItemSchema); +export type ChatItemMiniType = z.infer; diff --git a/packages/global/core/chat/utils.ts b/packages/global/core/chat/utils.ts index dca33a1f83..fa9bbd15eb 100644 --- a/packages/global/core/chat/utils.ts +++ b/packages/global/core/chat/utils.ts @@ -4,7 +4,7 @@ import { ChatRoleEnum, ChatSourceEnum } from './constants'; import { type AIChatItemValueItemType, type ChatHistoryItemResType, - type ChatItemType, + type ChatItemMiniType, type UserChatItemValueItemType } from './type'; import { sliceStrStartEnd } from '../../common/string/tools'; @@ -96,7 +96,7 @@ ${stepText}`; }; // Concat 2 -> 1, and sort by role -export const concatHistories = (histories1: ChatItemType[], histories2: ChatItemType[]) => { +export const concatHistories = (histories1: ChatItemMiniType[], histories2: ChatItemMiniType[]) => { const newHistories = [...histories1, ...histories2]; return newHistories.sort((a, b) => { if (a.obj === ChatRoleEnum.System) { @@ -106,7 +106,10 @@ export const concatHistories = (histories1: ChatItemType[], histories2: ChatItem }); }; -export const getChatTitleFromChatMessage = (message?: ChatItemType, defaultValue = '新对话') => { +export const getChatTitleFromChatMessage = ( + message?: ChatItemMiniType, + defaultValue = '新对话' +) => { // @ts-ignore const textMsg = message?.value.find((item) => 'text' in item && item.text); @@ -119,11 +122,11 @@ export const getChatTitleFromChatMessage = (message?: ChatItemType, defaultValue // Keep the first n and last n characters export const getHistoryPreview = ( - completeMessages: ChatItemType[], + completeMessages: ChatItemMiniType[], size = 100, useVision = false ): { - obj: `${ChatRoleEnum}`; + obj: ChatRoleEnum; value: string; }[] => { return completeMessages.map((item, i) => { diff --git a/packages/global/core/dataset/api.ts b/packages/global/core/dataset/api.ts index d7444b2eab..2126ef8102 100644 --- a/packages/global/core/dataset/api.ts +++ b/packages/global/core/dataset/api.ts @@ -1,35 +1,12 @@ -import type { ChunkSettingsType, DatasetDataIndexItemType, DatasetSchemaType } from './type'; +import type { ChunkSettingsType, DatasetDataIndexItemType } from './type'; import type { DatasetCollectionTypeEnum, DatasetCollectionDataProcessModeEnum } from './constants'; import type { ParentIdType } from '../../common/parentFolder/type'; import type { APIFileItemType } from './apiDataset/type'; -/* ================= dataset ===================== */ -export type DatasetUpdateBody = { - id: string; - - apiDatasetServer?: DatasetSchemaType['apiDatasetServer']; - - parentId?: ParentIdType; - name?: string; - avatar?: string; - intro?: string; - - agentModel?: string; - vlmModel?: string; - - websiteConfig?: DatasetSchemaType['websiteConfig']; - externalReadUrl?: DatasetSchemaType['externalReadUrl']; - defaultPermission?: DatasetSchemaType['defaultPermission']; - chunkSettings?: DatasetSchemaType['chunkSettings']; - - // sync schedule - autoSync?: boolean; -}; - /* ================= collection ===================== */ // Input + store params type DatasetCollectionStoreDataType = ChunkSettingsType & { - parentId?: string; + parentId?: ParentIdType; metadata?: Record; customPdfParse?: boolean; diff --git a/packages/global/core/dataset/apiDataset/type.ts b/packages/global/core/dataset/apiDataset/type.ts index 1e87584115..058f001ec7 100644 --- a/packages/global/core/dataset/apiDataset/type.ts +++ b/packages/global/core/dataset/apiDataset/type.ts @@ -1,49 +1,63 @@ -import { RequireOnlyOne } from '../../../common/type/utils'; -import type { ParentIdType } from '../../../common/parentFolder/type'; +import { ParentIdSchema } from '../../../common/parentFolder/type'; +import z from 'zod'; -export type APIFileItemType = { - id: string; - rawId: string; - parentId: ParentIdType; - name: string; - type: 'file' | 'folder'; - updateTime: Date; - createTime: Date; - hasChild?: boolean; -}; +export const APIFileItemSchema = z.object({ + id: z.string(), + rawId: z.string(), + parentId: ParentIdSchema, + name: z.string(), + type: z.enum(['file', 'folder']), + updateTime: z.date(), + createTime: z.date(), + hasChild: z.boolean().optional() +}); +export type APIFileItemType = z.infer; // Api dataset config -export type APIFileServer = { - baseUrl: string; - authorization?: string; - basePath?: string; -}; -export type FeishuServer = { - appId: string; - appSecret?: string; - folderToken: string; -}; -export type YuqueServer = { - userId: string; - token?: string; - basePath?: string; -}; +export const APIFileServerSchema = z + .object({ + baseUrl: z.string(), + authorization: z.string().optional(), + basePath: z.string().optional() + }) + .meta({ description: 'API 服务器配置' }); +export type APIFileServerType = z.infer; +export const FeishuServerSchema = z + .object({ + appId: z.string(), + appSecret: z.string().optional(), + folderToken: z.string() + }) + .meta({ description: '飞书服务器配置' }); +export type FeishuServerType = z.infer; +export const YuqueServerSchema = z + .object({ + userId: z.string(), + token: z.string().optional(), + basePath: z.string().optional() + }) + .meta({ description: '语雀服务器配置' }); +export type YuqueServerType = z.infer; -export type ApiDatasetServerType = { - apiServer?: APIFileServer; - feishuServer?: FeishuServer; - yuqueServer?: YuqueServer; -}; +export const ApiDatasetServerSchema = z + .object({ + apiServer: APIFileServerSchema.optional(), + feishuServer: FeishuServerSchema.optional(), + yuqueServer: YuqueServerSchema.optional() + }) + .meta({ description: '第三方知识库配置' }); +export type ApiDatasetServerType = z.infer; // Api dataset api +export const ApiFileReadContentResponseSchema = z.object({ + title: z.string().optional(), + rawText: z.string() +}); +export type ApiFileReadContentResponseType = z.infer; -export type ApiFileReadContentResponse = { - title?: string; - rawText: string; -}; - -export type APIFileReadResponse = { - url: string; -}; +export const APIFileReadResponseSchema = z.object({ + url: z.string() +}); +export type APIFileReadResponseType = z.infer; export type ApiDatasetDetailResponse = APIFileItemType; diff --git a/packages/global/core/dataset/type.ts b/packages/global/core/dataset/type.ts index 60d79d97ac..c20710970e 100644 --- a/packages/global/core/dataset/type.ts +++ b/packages/global/core/dataset/type.ts @@ -1,7 +1,5 @@ -import type { LLMModelItemType, EmbeddingModelItemType } from '../ai/model.schema'; -import { PermissionTypeEnum } from '../../support/permission/constant'; -import { PushDatasetDataChunkProps } from './api'; -import type { +import { EmbeddingModelItemSchema, LLMModelItemSchema } from '../ai/model.schema'; +import { DataChunkSplitModeEnum, DatasetCollectionDataProcessModeEnum, DatasetCollectionTypeEnum, @@ -13,141 +11,151 @@ import type { ChunkTriggerConfigTypeEnum, ParagraphChunkAIModeEnum } from './constants'; -import type { DatasetPermission } from '../../support/permission/dataset/controller'; -import type { - ApiDatasetServerType, - APIFileServer, - FeishuServer, - YuqueServer +import { + ApiDatasetServerSchema, + APIFileServerSchema, + FeishuServerSchema, + YuqueServerSchema } from './apiDataset/type'; -import type { SourceMemberType } from '../../support/user/type'; +import { SourceMemberSchema } from '../../support/user/type'; import { DatasetDataIndexTypeEnum } from './data/constants'; -import type { ParentIdType } from '../../common/parentFolder/type'; +import { ParentIdSchema } from '../../common/parentFolder/type'; import z from 'zod'; import { ObjectIdSchema } from '../../common/type/mongo'; +import { PermissionSchema } from '../../support/permission/controller'; -export type ChunkSettingsType = { - trainingType?: DatasetCollectionDataProcessModeEnum; +/* ===== Chunk ===== */ +export const ChunkSettingsSchema = z.object({ + trainingType: z + .enum(DatasetCollectionDataProcessModeEnum) + .optional() + .meta({ description: '训练类型' }), - // Chunk trigger - chunkTriggerType?: ChunkTriggerConfigTypeEnum; - chunkTriggerMinSize?: number; // maxSize from agent model, not store + chunkTriggerType: z + .enum(ChunkTriggerConfigTypeEnum) + .optional() + .meta({ description: '分块触发时机' }), + chunkTriggerMinSize: z.number().optional().meta({ description: '分块触发最小大小' }), - // Data enhance - dataEnhanceCollectionName?: boolean; // Auto add collection name to data + dataEnhanceCollectionName: z.boolean().optional().meta({ description: '增加集合名到分块里' }), + imageIndex: z.boolean().optional().meta({ description: '图片索引' }), + autoIndexes: z.boolean().optional().meta({ description: '自动生成索引' }), + indexPrefixTitle: z.boolean().optional().meta({ description: '索引前缀标题' }), - // Index enhance - imageIndex?: boolean; - autoIndexes?: boolean; - indexPrefixTitle?: boolean; + chunkSettingMode: z + .enum(ChunkSettingModeEnum) + .optional() + .meta({ description: '系统参数/自定义参数' }), + chunkSplitMode: z.enum(DataChunkSplitModeEnum).optional().meta({ description: '分块拆分模式' }), + paragraphChunkAIMode: z + .enum(ParagraphChunkAIModeEnum) + .optional() + .meta({ description: '段落分块 AI 模式' }), + paragraphChunkDeep: z.number().optional().meta({ description: '段落分块深度' }), + paragraphChunkMinSize: z.number().optional().meta({ description: '段落分块最小大小' }), + chunkSize: z.number().optional().meta({ description: '分块大小' }), + chunkSplitter: z.string().optional().meta({ description: '自定义最高优先分割符号' }), + indexSize: z.number().optional().meta({ description: '索引大小' }), + qaPrompt: z.string().optional().meta({ description: 'QA 拆分提示词' }) +}); +export type ChunkSettingsType = z.infer; - // Chunk setting - chunkSettingMode?: ChunkSettingModeEnum; // 系统参数/自定义参数 - chunkSplitMode?: DataChunkSplitModeEnum; - // Paragraph split - paragraphChunkAIMode?: ParagraphChunkAIModeEnum; - paragraphChunkDeep?: number; // Paragraph deep - paragraphChunkMinSize?: number; // Paragraph min size, if too small, it will merge - // Size split - chunkSize?: number; // chunk/qa chunk size, Paragraph max chunk size. - // Char split - chunkSplitter?: string; // chunk/qa chunk splitter - indexSize?: number; +/* ===== Dataset ===== */ +export const DatasetSchema = z + .object({ + _id: ObjectIdSchema.meta({ description: '数据集 ID' }), + parentId: ParentIdSchema.meta({ description: '父级 ID' }), + userId: ObjectIdSchema.meta({ description: '用户 ID' }), + teamId: ObjectIdSchema.meta({ description: '团队 ID' }), + tmbId: ObjectIdSchema.meta({ description: '团队成员 ID' }), + updateTime: z.date().meta({ description: '更新时间' }), + inheritPermission: z.boolean().meta({ description: '继承权限' }), - qaPrompt?: string; -}; + avatar: z.string().meta({ description: '头像' }), + name: z.string().meta({ description: '名称' }), + intro: z.string().meta({ description: '简介' }), + type: z.enum(DatasetTypeEnum).meta({ description: '数据集类型' }), -export type DatasetSchemaType = { - _id: string; - parentId: ParentIdType; - userId: string; - teamId: string; - tmbId: string; - updateTime: Date; + vectorModel: z.string().meta({ description: '向量模型' }), + agentModel: z.string().meta({ description: 'AI 模型' }), + vlmModel: z.string().optional().meta({ description: '视觉语言模型' }), - avatar: string; - name: string; - intro: string; - type: `${DatasetTypeEnum}`; + websiteConfig: z + .object({ + url: z.string().meta({ description: '网站 URL' }), + selector: z.string().meta({ description: '网站选择器' }) + }) + .optional() + .meta({ description: '网站配置' }), + chunkSettings: ChunkSettingsSchema.optional().meta({ description: '分块配置' }), - vectorModel: string; - agentModel: string; - vlmModel?: string; + apiDatasetServer: ApiDatasetServerSchema.optional().meta({ description: 'API 服务器配置' }), - websiteConfig?: { - url: string; - selector: string; - }; + deleteTime: z.date().nullish().meta({ description: '删除时间' }), - chunkSettings?: ChunkSettingsType; + autoSync: z.boolean().optional().meta({ description: '自动同步', deprecated: true }), + externalReadUrl: z.string().optional().meta({ description: '外部读取 URL', deprecated: true }), + defaultPermission: z.number().optional().meta({ description: '默认权限', deprecated: true }), + apiServer: APIFileServerSchema.optional().meta({ + description: 'API 服务器配置', + deprecated: true + }), + feishuServer: FeishuServerSchema.optional().meta({ + description: '飞书服务器配置', + deprecated: true + }), + yuqueServer: YuqueServerSchema.optional().meta({ + description: '语雀服务器配置', + deprecated: true + }) + }) + .meta({ description: '知识库' }); +export type DatasetSchemaType = z.infer; - inheritPermission: boolean; +/* ===== Collection ===== */ +export const DatasetCollectionSchema = ChunkSettingsSchema.omit({ + trainingType: true +}).extend({ + _id: ObjectIdSchema.meta({ description: '集合 ID' }), + teamId: ObjectIdSchema.meta({ description: '团队 ID' }), + tmbId: ObjectIdSchema.meta({ description: '团队成员 ID' }), + datasetId: ObjectIdSchema.meta({ description: '数据集 ID' }), + parentId: ParentIdSchema.meta({ description: '父级 ID' }), + name: z.string().meta({ description: '名称' }), + type: z.enum(DatasetCollectionTypeEnum).meta({ description: '集合类型' }), + tags: z.array(z.string()).optional().meta({ description: '标签' }), - apiDatasetServer?: ApiDatasetServerType; + createTime: z.date().meta({ description: '创建时间' }), + updateTime: z.date().meta({ description: '更新时间' }), - // 软删除字段 - deleteTime?: Date | null; + forbid: z.boolean().optional().meta({ description: '是否禁用' }), - /** @deprecated */ - autoSync?: boolean; - /** @deprecated */ - externalReadUrl?: string; - /** @deprecated */ - defaultPermission?: number; - /** @deprecated */ - apiServer?: APIFileServer; - /** @deprecated */ - feishuServer?: FeishuServer; - /** @deprecated */ - yuqueServer?: YuqueServer; -}; + fileId: z.string().optional().meta({ description: '文件 ID' }), + rawLink: z.string().optional().meta({ description: '原始链接' }), + externalFileId: z.string().optional().meta({ description: '外部文件 ID' }), + apiFileId: z.string().optional().meta({ description: 'API 文件 ID' }), + apiFileParentId: z.string().optional().meta({ description: 'API 文件父级 ID' }), + externalFileUrl: z.string().optional().meta({ description: '外部文件 URL' }), -export type DatasetCollectionSchemaType = ChunkSettingsType & { - _id: string; - teamId: string; - tmbId: string; - datasetId: string; - parentId?: string; - name: string; - type: DatasetCollectionTypeEnum; - tags?: string[]; + rawTextLength: z.number().optional().meta({ description: '原始文本长度' }), + hashRawText: z.string().optional().meta({ description: '文本哈希' }), - createTime: Date; - updateTime: Date; + metadata: z.record(z.string(), z.any()).optional().meta({ description: '其他元数据' }), - // Status - forbid?: boolean; + customPdfParse: z.boolean().optional().meta({ description: '自定义 PDF 解析' }), + trainingType: z.enum(DatasetCollectionDataProcessModeEnum).meta({ description: '训练类型' }) +}); +export type DatasetCollectionSchemaType = z.infer; - // Collection metadata - fileId?: string; // local file id - rawLink?: string; // link url - externalFileId?: string; //external file id - apiFileId?: string; // api file id - apiFileParentId?: string; - externalFileUrl?: string; // external import url - - rawTextLength?: number; - hashRawText?: string; - - metadata?: { - webPageSelector?: string; - relatedImgId?: string; // The id of the associated image collections - - [key: string]: any; - }; - - // Parse settings - customPdfParse?: boolean; - trainingType: DatasetCollectionDataProcessModeEnum; -}; - -export type DatasetCollectionTagsSchemaType = { - _id: string; - teamId: string; - datasetId: string; - tag: string; -}; +export const DatasetCollectionTagsSchema = z.object({ + _id: ObjectIdSchema.meta({ description: '标签 ID' }), + teamId: ObjectIdSchema.meta({ description: '团队 ID' }), + datasetId: ObjectIdSchema.meta({ description: '数据集 ID' }), + tag: z.string().meta({ description: '标签' }) +}); +export type DatasetCollectionTagsSchemaType = z.infer; +/* ===== Data ===== */ export const DatasetDataIndexItemSchema = z.object({ type: z.enum(DatasetDataIndexTypeEnum).meta({ description: '索引类型' }), dataId: z.string().meta({ description: 'vectorDB ID' }), @@ -185,137 +193,173 @@ export const DatasetDataSchema = DatasetDataFieldSchema.extend({ }); export type DatasetDataSchemaType = z.infer; -export type DatasetDataTextSchemaType = { - _id: string; - teamId: string; - datasetId: string; - collectionId: string; - dataId: string; - fullTextToken: string; -}; +export const DatasetDataTextSchema = z.object({ + _id: ObjectIdSchema.meta({ description: '数据文本 ID' }), + teamId: ObjectIdSchema.meta({ description: '团队 ID' }), + datasetId: ObjectIdSchema.meta({ description: '数据集 ID' }), + collectionId: ObjectIdSchema.meta({ description: '集合 ID' }), + dataId: ObjectIdSchema.meta({ description: '数据 ID' }), + fullTextToken: z.string().meta({ description: '全文 token' }) +}); +export type DatasetDataTextSchemaType = z.infer; -export type DatasetTrainingSchemaType = { - _id: string; - userId: string; - teamId: string; - tmbId: string; - datasetId: string; - collectionId: string; - billId: string; - expireAt: Date; - lockTime: Date; - mode: TrainingModeEnum; - dataId?: string; - q: string; - a: string; - imageId?: string; - imageDescMap?: Record; - chunkIndex: number; - indexSize?: number; - weight: number; - indexes: Omit[]; - retryCount: number; - errorMsg?: string; -}; +/* ===== Training ===== */ +export const DatasetTrainingSchema = z.object({ + _id: ObjectIdSchema.meta({ description: '训练 ID' }), + userId: ObjectIdSchema.meta({ description: '用户 ID' }), + teamId: ObjectIdSchema.meta({ description: '团队 ID' }), + tmbId: ObjectIdSchema.meta({ description: '团队成员 ID' }), + datasetId: ObjectIdSchema.meta({ description: '数据集 ID' }), + collectionId: ObjectIdSchema.meta({ description: '集合 ID' }), + billId: z.string().meta({ description: '账单 ID' }), + expireAt: z.date().meta({ description: '过期时间' }), + lockTime: z.date().meta({ description: '锁定时间' }), + mode: z.enum(TrainingModeEnum).meta({ description: '训练模式' }), + dataId: z.string().optional().meta({ description: '数据 ID' }), + q: z.string().meta({ description: '问题/主文本' }), + a: z.string().meta({ description: '回答/补充文本' }), + imageId: z.string().optional().meta({ description: '图片 ID' }), + imageDescMap: z.record(z.string(), z.string()).optional().meta({ description: '图片描述映射' }), + chunkIndex: z.number().meta({ description: '块索引' }), + indexSize: z.number().optional().meta({ description: '索引大小' }), + weight: z.number().meta({ description: '权重' }), + indexes: z + .array(DatasetDataIndexItemSchema.omit({ dataId: true })) + .meta({ description: '向量索引' }), + retryCount: z.number().meta({ description: '重试次数' }), + errorMsg: z.string().optional().meta({ description: '错误信息' }) +}); +export type DatasetTrainingSchemaType = z.infer; -export type CollectionWithDatasetType = DatasetCollectionSchemaType & { - dataset: DatasetSchemaType; -}; +export const CollectionWithDatasetSchema = DatasetCollectionSchema.extend({ + dataset: DatasetSchema +}); +export type CollectionWithDatasetType = z.infer; + +/* ====== service type ===== */ /* ================= dataset ===================== */ -export type DatasetSimpleItemType = { - _id: string; - avatar: string; - name: string; - vectorModel: EmbeddingModelItemType; -}; -export type DatasetListItemType = { - _id: string; - tmbId: string; - avatar: string; - updateTime: Date; - name: string; - intro: string; - type: `${DatasetTypeEnum}`; - permission: DatasetPermission; - vectorModel: EmbeddingModelItemType; - inheritPermission: boolean; - private?: boolean; - sourceMember?: SourceMemberType; -}; +export const DatasetSimpleItemSchema = z.object({ + _id: ObjectIdSchema.meta({ description: '数据集 ID' }), + avatar: z.string().meta({ description: '头像' }), + name: z.string().meta({ description: '名称' }), + vectorModel: EmbeddingModelItemSchema.meta({ description: '向量模型' }) +}); +export type DatasetSimpleItemType = z.infer; +export const DatasetListItemSchema = z.object({ + _id: ObjectIdSchema.meta({ description: '数据集 ID' }), + tmbId: ObjectIdSchema.meta({ description: '团队成员 ID' }), + avatar: z.string().meta({ description: '头像' }), + updateTime: z.date().meta({ description: '更新时间' }), + name: z.string().meta({ description: '名称' }), + intro: z.string().meta({ description: '简介' }), + type: z.enum(DatasetTypeEnum).meta({ description: '数据集类型' }), + permission: PermissionSchema, + vectorModel: EmbeddingModelItemSchema.meta({ description: '向量模型' }), + inheritPermission: z.boolean().meta({ description: '继承权限' }), + private: z.boolean().optional().meta({ description: '是否私有' }), + sourceMember: SourceMemberSchema.optional().meta({ description: '来源成员' }) +}); +export type DatasetListItemType = z.infer; -export type DatasetItemType = Omit & { - status: `${DatasetStatusEnum}`; - errorMsg?: string; - vectorModel: EmbeddingModelItemType; - agentModel: LLMModelItemType; - vlmModel?: LLMModelItemType; - permission: DatasetPermission; -}; +export const DatasetItemSchema = DatasetSchema.omit({ + vectorModel: true, + agentModel: true, + vlmModel: true +}).extend({ + status: z.enum(DatasetStatusEnum).meta({ description: '状态' }), + errorMsg: z.string().optional().meta({ description: '错误信息' }), + vectorModel: EmbeddingModelItemSchema.meta({ description: '向量模型' }), + agentModel: LLMModelItemSchema.meta({ description: 'AI 模型' }), + vlmModel: LLMModelItemSchema.optional().meta({ description: '视觉语言模型' }), + permission: PermissionSchema +}); +export type DatasetItemType = z.infer; /* ================= tag ===================== */ -export type DatasetTagType = { - _id: string; - tag: string; -}; +export const DatasetTagSchema = z.object({ + _id: ObjectIdSchema.meta({ description: '标签 ID' }), + tag: z.string().meta({ description: '标签' }) +}); +export type DatasetTagType = z.infer; -export type TagUsageType = { - tagId: string; - collections: string[]; -}; +export const TagUsageSchema = z.object({ + tagId: z.string().meta({ description: '标签 ID' }), + collections: z.array(z.string()).meta({ description: '集合 ID' }) +}); +export type TagUsageType = z.infer; /* ================= collection ===================== */ -export type DatasetCollectionItemType = CollectionWithDatasetType & { - sourceName: string; - sourceId?: string; - file?: { - filename?: string; - contentLength?: number; - }; - permission: DatasetPermission; - indexAmount: number; - errorCount?: number; -}; +export const DatasetCollectionItemSchema = CollectionWithDatasetSchema.extend({ + sourceName: z.string().meta({ description: '来源名称' }), + sourceId: z.string().optional().meta({ description: '来源 ID' }), + file: z + .object({ + filename: z.string().optional().meta({ description: '文件名' }), + contentLength: z.number().optional().meta({ description: '文件长度' }) + }) + .optional() + .meta({ description: '文件信息' }), + permission: PermissionSchema, + indexAmount: z.number().meta({ description: '索引数量' }), + errorCount: z.number().optional().meta({ description: '错误数量' }) +}); +export type DatasetCollectionItemType = z.infer; /* ================= data ===================== */ -export type DatasetDataItemType = DatasetDataFieldType & { - id: string; - teamId: string; - datasetId: string; - imagePreivewUrl?: string; - updateTime: Date; - collectionId: string; - sourceName: string; - sourceId?: string; - chunkIndex: number; - indexes: DatasetDataIndexItemType[]; - isOwner: boolean; -}; +export const DatasetDataItemSchema = DatasetDataFieldSchema.extend({ + id: ObjectIdSchema.meta({ description: '数据 ID' }), + teamId: ObjectIdSchema.meta({ description: '团队 ID' }), + datasetId: ObjectIdSchema.meta({ description: '数据集 ID' }), + collectionId: ObjectIdSchema.meta({ description: '集合 ID' }), + imagePreivewUrl: z.string().optional().meta({ description: '图片预览 URL' }), + updateTime: z.date().meta({ description: '更新时间' }), + sourceName: z.string().meta({ description: '来源名称' }), + sourceId: z.string().optional().meta({ description: '来源 ID' }), + chunkIndex: z.number().meta({ description: '块索引' }), + indexes: z.array(DatasetDataIndexItemSchema).meta({ description: '向量索引' }), + isOwner: z.boolean().meta({ description: '是否为 owner' }) +}); +export type DatasetDataItemType = z.infer; /* --------------- file ---------------------- */ -export type DatasetFileSchema = { - _id: string; - length: number; - chunkSize: number; - uploadDate: Date; - filename: string; - contentType: string; - metadata: { - teamId: string; - tmbId?: string; - uid: string; - encoding?: string; - }; -}; +export const DatasetFileSchema = z.object({ + _id: ObjectIdSchema.meta({ description: '文件 ID' }), + length: z.number().meta({ description: '文件长度' }), + chunkSize: z.number().meta({ description: '块大小' }), + uploadDate: z.date().meta({ description: '上传时间' }), + filename: z.string().meta({ description: '文件名' }), + contentType: z.string().meta({ description: '文件类型' }), + metadata: z + .object({ + teamId: ObjectIdSchema.meta({ description: '团队 ID' }), + tmbId: ObjectIdSchema.meta({ description: '团队成员 ID' }), + uid: z.string().meta({ description: '用户 ID' }), + encoding: z.string().optional().meta({ description: '编码' }) + }) + .meta({ description: '其他元数据' }) +}); +export type DatasetFileSchemaType = z.infer; /* ============= search =============== */ -export type SearchDataResponseItemType = Omit< - DatasetDataItemType, - 'teamId' | 'indexes' | 'isOwner' -> & { - score: { type: `${SearchScoreTypeEnum}`; value: number; index: number }[]; - // score: number; -}; +export const SearchDataResponseItemSchema = DatasetDataItemSchema.omit({ + teamId: true, + indexes: true, + isOwner: true +}) + .extend({ + score: z + .array( + z.object({ + type: z.enum(SearchScoreTypeEnum).meta({ description: '评分类型' }), + value: z.number().meta({ description: '评分值' }), + index: z.number().meta({ description: '索引' }) + }) + ) + .meta({ description: '评分列表' }) + }) + .meta({ description: '搜索数据响应项' }); +export type SearchDataResponseItemType = z.infer; export const DatasetCiteItemSchema = z .object({ diff --git a/packages/global/core/workflow/runtime/type.ts b/packages/global/core/workflow/runtime/type.ts index e2b218cdec..98413fced1 100644 --- a/packages/global/core/workflow/runtime/type.ts +++ b/packages/global/core/workflow/runtime/type.ts @@ -1,6 +1,6 @@ import type { ChatNodeUsageType } from '../../../support/wallet/bill/type'; import type { - ChatItemType, + ChatItemMiniType, ToolRunResponseItemType, AIChatItemValueItemType, ChatHistoryItemResType @@ -10,24 +10,24 @@ import type { StoreNodeItemType } from '../type/node'; import type { DispatchNodeResponseKeyEnum } from './constants'; import type { NodeInputKeyEnum } from '../constants'; import { NodeOutputKeyEnum } from '../constants'; -import type { ClassifyQuestionAgentItemType } from '../template/system/classifyQuestion/type'; +import { ClassifyQuestionAgentItemSchema } from '../template/system/classifyQuestion/type'; import type { NextApiResponse } from 'next'; import type { AppSchemaType } from '../../app/type'; import type { RuntimeEdgeItemType } from '../type/edge'; -import { type ReadFileNodeResponseType } from '../template/system/readFiles/type'; +import { ReadFileNodeResponseSchema } from '../template/system/readFiles/type'; import type { WorkflowResponseType } from '../../../../service/core/workflow/dispatch/type'; import type { AiChatQuoteRoleType } from '../template/system/aiChat/type'; import type { OpenaiAccountType } from '../../../support/user/team/type'; -import type { CompletionFinishReason } from '../../ai/type'; +import { CompletionFinishReasonSchema } from '../../ai/type'; import type { InteractiveNodeResponseType, WorkflowInteractiveResponseType } from '../template/system/interactive/type'; -import type { SearchDataResponseItemType } from '../../dataset/type'; +import { SearchDataResponseItemSchema } from '../../dataset/type'; import type { localeType } from '../../../common/i18n/type'; import { type UserChatItemValueItemType } from '../../chat/type'; -import type { DatasetSearchModeEnum } from '../../dataset/constants'; -import type { ChatRoleEnum } from '../../chat/constants'; +import { DatasetSearchModeEnum } from '../../dataset/constants'; +import { ChatRoleEnum } from '../../chat/constants'; import z from 'zod'; /* @@ -75,7 +75,7 @@ export type ChatDispatchProps = { chatId: string; responseChatItemId?: string; - histories: ChatItemType[]; + histories: ChatItemMiniType[]; variables: Record; // global variable query: UserChatItemValueItemType[]; // trigger query chatConfig: AppSchemaType['chatConfig']; @@ -112,7 +112,7 @@ export type SystemVariablesType = { appId: string; chatId?: string; responseChatItemId?: string; - histories: ChatItemType[]; + histories: ChatItemMiniType[]; cTime: string; }; @@ -141,280 +141,181 @@ export type RuntimeNodeItemType = { }; // 知识库未 schema 改造,这里用不了 -// export const DispatchNodeResponseSchema = z.object({ -// // common -// moduleLogo: z.string().nullish(), -// runningTime: z.number().nullish(), -// query: z.string().nullish(), -// textOutput: z.string().nullish(), +export const DispatchNodeResponseSchema = z + .object({ + // common + moduleLogo: z.string().optional().meta({ description: '模块 logo' }), + runningTime: z.number().optional().meta({ description: '运行时间: 毫秒' }), + query: z.string().optional().meta({ description: '查询语句' }), + textOutput: z.string().optional().meta({ description: '文本输出' }), -// error: z.record(z.string(), z.any()).nullish(), // Client will toast -// errorText: z.string().nullish(), // Just show + llmRequestIds: z.array(z.string()).optional().meta({ description: 'LLM 请求追踪 ID 列表' }), -// customInputs: z.record(z.string(), z.any()).nullish(), -// customOutputs: z.record(z.string(), z.any()).nullish(), -// nodeInputs: z.record(z.string(), z.any()).nullish(), -// nodeOutputs: z.record(z.string(), z.any()).nullish(), -// mergeSignId: z.string().nullish(), + error: z + .union([z.record(z.string(), z.any()), z.string()]) + .optional() + .meta({ description: '错误信息' }), + errorText: z.string().optional().meta({ description: '错误文本' }), // Just show -// llmRequestIds: z.array(z.string()).nullish(), // LLM 请求追踪 ID 列表 + customInputs: z.record(z.string(), z.any()).optional().meta({ description: '自定义输入' }), + customOutputs: z.record(z.string(), z.any()).optional().meta({ description: '自定义输出' }), + nodeInputs: z.record(z.string(), z.any()).optional().meta({ description: '节点输入' }), + nodeOutputs: z.record(z.string(), z.any()).optional().meta({ description: '节点输出' }), + mergeSignId: z.string().optional().meta({ description: '合并签名 ID' }), -// // bill -// inputTokens: z.number().nullish(), -// outputTokens: z.number().nullish(), -// model: z.string().nullish(), -// contextTotalLen: z.number().nullish(), -// totalPoints: z.string().nullish(), -// childTotalPoints: z.string().nullish(), + // bill + tokens: z.number().optional().meta({ description: '总 token' }), + inputTokens: z.number().optional().meta({ description: '输入 token' }), + outputTokens: z.number().optional().meta({ description: '输出 token' }), + model: z.string().optional().meta({ description: '模型' }), + contextTotalLen: z.number().optional().meta({ description: '上下文总长度' }), + totalPoints: z.number().optional().meta({ description: '总积分' }), + childTotalPoints: z.number().optional().meta({ description: '子节点总积分' }), -// // chat -// temperature: z.number().nullish(), -// maxToken: z.number().nullish(), -// quoteList: z.array(SearchDataResponseItemTypeSchema).nullish(), -// reasoningText: z.string().nullish(), -// historyPreview: z -// .array( -// z.object({ -// obj: z.enum(Object.values(ChatRoleEnum)), -// value: z.string() -// }) -// ) -// .nullish(), // completion context array. history will slice -// finishReason: z.enum(Object.values(CompletionFinishReason)).nullish(), + // LLM chat + temperature: z.number().optional().meta({ description: '温度' }), + maxToken: z.number().optional().meta({ description: '最大 token' }), + quoteList: z + .array(SearchDataResponseItemSchema) + .optional() + .meta({ description: '知识库引用列表' }), + reasoningText: z.string().optional().meta({ description: '思考文本' }), + historyPreview: z + .array( + z.object({ + obj: z.enum(ChatRoleEnum), + value: z.string() + }) + ) + .optional() + .meta({ description: '上下文预览' }), // completion context array. history will slice + finishReason: CompletionFinishReasonSchema.optional(), -// // dataset search -// embeddingModel: z.string().nullish(), -// embeddingTokens: z.number().nullish(), -// similarity: z.number().nullish(), -// limit: z.number().nullish(), -// searchMode: z.enum(Object.values(DatasetSearchModeEnum)).nullish(), -// embeddingWeight: z.number().nullish(), -// rerankModel: z.string().nullish(), -// rerankWeight: z.number().nullish(), -// reRankInputTokens: z.number().nullish(), -// searchUsingReRank: z.boolean().nullish(), -// queryExtensionResult: z -// .object({ -// model: z.string(), -// inputTokens: z.number(), -// outputTokens: z.number(), -// query: z.string() -// }) -// .nullish(), -// deepSearchResult: z -// .object({ -// model: z.string(), -// inputTokens: z.number(), -// outputTokens: z.number() -// }) -// .nullish(), + // dataset search + embeddingModel: z.string().optional().meta({ description: '嵌入模型' }), + embeddingTokens: z.number().optional().meta({ description: '嵌入 token' }), + similarity: z.number().optional().meta({ description: '相似度' }), + limit: z.number().optional().meta({ description: '限制' }), + searchMode: z.enum(DatasetSearchModeEnum).optional().meta({ description: '搜索模式' }), + embeddingWeight: z.number().optional().meta({ description: '嵌入权重' }), + rerankModel: z.string().optional().meta({ description: '重排模型' }), + rerankWeight: z.number().optional().meta({ description: '重排权重' }), + reRankInputTokens: z.number().optional().meta({ description: '重排输入 token' }), + searchUsingReRank: z.boolean().optional().meta({ description: '使用重排' }), + queryExtensionResult: z + .object({ + model: z.string().meta({ description: '模型' }), + inputTokens: z.number().meta({ description: '输入 token' }), + outputTokens: z.number().meta({ description: '输出 token' }), + query: z.string().meta({ description: '查询内容' }) + }) + .optional() + .meta({ description: '查询扩展结果' }), + deepSearchResult: z + .object({ + model: z.string().meta({ description: '模型' }), + inputTokens: z.number().meta({ description: '输入 token' }), + outputTokens: z.number().meta({ description: '输出 token' }) + }) + .optional(), -// // dataset concat -// concatLength: z.number().nullish(), + // dataset concat + concatLength: z.number().optional(), -// // cq -// cqList: z.array(ClassifyQuestionAgentItemTypeSchema).nullish(), -// cqResult: z.string().nullish(), + // cq + cqList: z + .array(ClassifyQuestionAgentItemSchema) + .optional() + .meta({ description: '分类问题列表' }), + cqResult: z.string().optional().meta({ description: '分类结果' }), -// // content extract -// extractDescription: z.string().nullish(), -// extractResult: z.record(z.string(), z.any()).nullish(), + // content extract + extractDescription: z.string().optional().meta({ description: '提取描述' }), + extractResult: z.record(z.string(), z.any()).optional().meta({ description: '提取结果' }), -// // http -// params: z.record(z.string(), z.any()).nullish(), -// body: z.record(z.string(), z.any()).nullish(), -// headers: z.record(z.string(), z.any()).nullish(), -// httpResult: z.record(z.string(), z.any()).nullish(), + // http + params: z.record(z.string(), z.any()).optional().meta({ description: '请求参数' }), + body: z + .union([z.record(z.string(), z.any()), z.string()]) + .optional() + .meta({ description: '请求体' }), + headers: z.record(z.string(), z.any()).optional().meta({ description: '请求头' }), + httpResult: z.record(z.string(), z.any()).optional().meta({ description: '请求结果' }), -// // Tool -// toolInput: z.record(z.string(), z.any()).nullish(), -// pluginOutput: z.record(z.string(), z.any()).nullish(), -// pluginDetail: z.array(ChatHistoryItemResTypeSchema).nullish(), + // Tool + toolInput: z.record(z.string(), z.any()).optional().meta({ description: '工具输入' }), + pluginOutput: z.record(z.string(), z.any()).optional().meta({ description: '插件输出' }), + pluginDetail: z.array(z.any()).optional(), + toolParamsResult: z + .record(z.string(), z.any()) + .optional() + .meta({ description: '工具参数结果' }), + toolRes: z.any().optional().meta({ description: '工具响应' }), -// // if-else -// ifElseResult: z.string().nullish(), + // if-else + ifElseResult: z.string().optional().meta({ description: '判断器结果' }), -// // tool call -// toolCallInputTokens: z.number().nullish(), -// toolCallOutputTokens: z.number().nullish(), -// toolDetail: z.array(ChatHistoryItemResTypeSchema).nullish(), -// toolStop: z.boolean().nullish(), + // tool call + toolCallInputTokens: z.number().optional().meta({ description: '工具调用输入 token' }), + toolCallOutputTokens: z.number().optional().meta({ description: '工具调用输出 token' }), + toolDetail: z.array(z.any()).optional(), + toolStop: z.boolean().optional(), -// // code -// codeLog: z.string().nullish(), + // Agent call + stepQuery: z.string().optional().meta({ description: '步骤查询' }), -// // read files -// readFilesResult: z.string().nullish(), -// readFiles: ReadFileNodeResponseSchema.nullish(), + // Compress chunk + compressTextAgent: z + .object({ + inputTokens: z.number().meta({ description: '输入 token' }), + outputTokens: z.number().meta({ description: '输出 token' }), + totalPoints: z.number().meta({ description: '总积分' }) + }) + .optional() + .meta({ description: '压缩文本Agent' }), -// // user select -// userSelectResult: z.string().nullish(), + // code + codeLog: z.string().optional().meta({ description: '代码日志' }), -// // update var -// updateVarResult: z.array(z.any()).nullish(), + // read files + readFilesResult: z.string().optional().meta({ description: '文件读取结果' }), + readFiles: ReadFileNodeResponseSchema.optional(), -// // loop -// loopResult: z.array(z.any()).nullish(), -// loopInput: z.array(z.any()).nullish(), -// loopDetail: z.array(ChatHistoryItemResTypeSchema).nullish(), -// loopInputValue: z.any().nullish(), -// loopOutputValue: z.any().nullish(), + // user select + userSelectResult: z.string().optional().meta({ description: '用户选择结果' }), + // form input + formInputResult: z.record(z.string(), z.any()).optional().meta({ description: '表单输入结果' }), -// // form input -// formInputResult: z.record(z.string(), z.any()).nullish(), + // update var + updateVarResult: z.array(z.any()).optional().meta({ description: '更新变量结果' }), -// // tool params -// toolParamsResult: z.record(z.string(), z.any()).nullish(), + // loop + loopResult: z.array(z.any()).optional().meta({ description: '循环结果' }), + loopInput: z.array(z.any()).optional().meta({ description: '循环输入' }), + loopDetail: z.array(z.any()).optional().meta({ description: '循环详情' }), + loopInputValue: z.any().optional().meta({ description: '循环输入值' }), + loopOutputValue: z.any().optional().meta({ description: '循环输出值' }), -// toolRes: z.any().nullish(), + childrenResponses: z.array(z.any()).optional().meta({ description: '子节点响应' }), -// // @deprecated -// extensionModel: z.string().nullish(), -// extensionResult: z.string().nullish(), -// extensionTokens: z.number().nullish(), -// tokens: z.number().nullish() -// }); -export type DispatchNodeResponseType = { - // common - moduleLogo?: string; - runningTime?: number; - query?: string; - textOutput?: string; - // LLM request tracking - llmRequestIds?: string[]; // LLM 请求追踪 ID 列表 + // Tools + toolId: z.string().optional().meta({ description: '工具 ID' }), - // Client will toast - error?: Record | string; - // Just show - errorText?: string; + extensionModel: z.string().optional().meta({ description: '扩展模型', deprecated: true }), + extensionResult: z.string().optional().meta({ description: '扩展结果', deprecated: true }), + extensionTokens: z.number().optional().meta({ description: '扩展 token', deprecated: true }) + }) + .meta({ description: '节点响应' }); - customInputs?: Record; - customOutputs?: Record; - nodeInputs?: Record; - nodeOutputs?: Record; - mergeSignId?: string; - - // bill - tokens?: number; // deprecated - inputTokens?: number; - outputTokens?: number; - model?: string; - contextTotalLen?: number; - totalPoints?: number; - childTotalPoints?: number; - - // chat - temperature?: number; - maxToken?: number; - quoteList?: SearchDataResponseItemType[]; - reasoningText?: string; - historyPreview?: { - obj: `${ChatRoleEnum}`; - value: string; - }[]; // completion context array. history will slice - finishReason?: CompletionFinishReason; - - // dataset search - embeddingModel?: string; - embeddingTokens?: number; - similarity?: number; - limit?: number; - searchMode?: `${DatasetSearchModeEnum}`; - embeddingWeight?: number; - rerankModel?: string; - rerankWeight?: number; - reRankInputTokens?: number; - searchUsingReRank?: boolean; - queryExtensionResult?: { - model: string; - inputTokens: number; - outputTokens: number; - query: string; - }; - deepSearchResult?: { - model: string; - inputTokens: number; - outputTokens: number; - }; - - // dataset concat - concatLength?: number; - - // cq - cqList?: ClassifyQuestionAgentItemType[]; - cqResult?: string; - - // content extract - extractDescription?: string; - extractResult?: Record; - - // http - params?: Record; - body?: Record | string; - headers?: Record; - httpResult?: Record; - - // Tool - toolInput?: Record; - pluginOutput?: Record; - pluginDetail?: ChatHistoryItemResType[]; - toolParamsResult?: Record; - toolRes?: any; - - // if-else - ifElseResult?: string; - - // tool call - toolCallInputTokens?: number; - toolCallOutputTokens?: number; - toolDetail?: ChatHistoryItemResType[]; - toolStop?: boolean; - // Agent call - stepQuery?: string; - // Compress chunk - compressTextAgent?: { - inputTokens: number; - outputTokens: number; - totalPoints: number; - }; - - // code - codeLog?: string; - - // read files - readFilesResult?: string; - readFiles?: ReadFileNodeResponseType; - - // user select - userSelectResult?: string; - - // update var - updateVarResult?: any[]; - - // loop - loopResult?: any[]; - loopInput?: any[]; - loopDetail?: ChatHistoryItemResType[]; - // loop start - loopInputValue?: any; - // loop end - loopOutputValue?: any; - - // form input - formInputResult?: Record; - - // Children node responses - childrenResponses?: ChatHistoryItemResType[]; - - // Tools - toolId?: string; - - /** @deprecated */ - extensionModel?: string; - /** @deprecated */ - extensionResult?: string; - /** @deprecated */ - extensionTokens?: number; +type Tmp_DispatchNodeResponseType = z.infer; +export type DispatchNodeResponseType = Omit< + Tmp_DispatchNodeResponseType, + 'childrenResponses' | 'loopDetail' | 'pluginDetail' | 'toolDetail' +> & { + childrenResponses?: DispatchNodeResponseType[]; + loopDetail?: DispatchNodeResponseType[]; + pluginDetail?: DispatchNodeResponseType[]; + toolDetail?: DispatchNodeResponseType[]; }; export type DispatchNodeResultType = { @@ -426,7 +327,7 @@ export type DispatchNodeResultType; [DispatchNodeResponseKeyEnum.memories]?: Record; diff --git a/packages/global/core/workflow/runtime/utils.ts b/packages/global/core/workflow/runtime/utils.ts index 5da00e430d..2b7e55553d 100644 --- a/packages/global/core/workflow/runtime/utils.ts +++ b/packages/global/core/workflow/runtime/utils.ts @@ -1,7 +1,7 @@ import json5 from 'json5'; import { checkStrOversize, replaceVariable, valToStr } from '../../../common/string/tools'; import { ChatRoleEnum } from '../../../core/chat/constants'; -import type { ChatItemType } from '../../../core/chat/type'; +import type { ChatItemMiniType } from '../../../core/chat/type'; import type { NodeOutputItemType } from './type'; import { ChatCompletionRequestMessageRoleEnum } from '../../ai/constants'; import { @@ -161,7 +161,7 @@ export const valueTypeFormat = (value: any, valueType?: WorkflowIOValueTypeEnum) 2. Check that the workflow starts at the interaction node */ export const getLastInteractiveValue = ( - histories: ChatItemType[] + histories: ChatItemMiniType[] ): WorkflowInteractiveResponseType | undefined => { const lastAIMessage = [...histories].reverse().find((item) => item.obj === ChatRoleEnum.AI); diff --git a/packages/global/core/workflow/template/system/classifyQuestion/type.ts b/packages/global/core/workflow/template/system/classifyQuestion/type.ts index 8c40f75ffe..bff0b9d5de 100644 --- a/packages/global/core/workflow/template/system/classifyQuestion/type.ts +++ b/packages/global/core/workflow/template/system/classifyQuestion/type.ts @@ -1,4 +1,9 @@ -export type ClassifyQuestionAgentItemType = { - value: string; - key: string; -}; +import z from 'zod'; + +export const ClassifyQuestionAgentItemSchema = z + .object({ + value: z.string().meta({ description: '分类值' }), + key: z.string().meta({ description: '分类键' }) + }) + .meta({ description: '分类问题Agent项' }); +export type ClassifyQuestionAgentItemType = z.infer; diff --git a/packages/global/openapi/api.ts b/packages/global/openapi/api.ts index a3f953ef43..e08ea67350 100644 --- a/packages/global/openapi/api.ts +++ b/packages/global/openapi/api.ts @@ -1,6 +1,7 @@ import type { RequireOnlyOne } from '../common/type/utils'; import { z } from 'zod'; +/* 按 offset 分页 */ export const PaginationSchema = z.object({ pageSize: z.union([z.number(), z.string()]).optional().describe('每页条数'), offset: z.union([z.number(), z.string()]).optional().describe('偏移量(与页码二选一)'), @@ -24,3 +25,61 @@ export type PaginationResponseType = { total: number; list: T[]; }; +export type PaginationResponse = PaginationResponseType; + +/* 按 cursor 分页 */ + +export const LinkedPaginationSchema = (extraShape?: TShape) => + z.object({ + pageSize: z + .int() + .positive() + .optional() + .default(10) + .meta({ example: 15, description: '每页条数' }), + anchor: z.any().optional().meta({ description: '当前锚点(如 chunkIndex)' }), + initialId: z.string().optional().meta({ + example: '68ad85a7463006c963799a05', + description: '初始定位数据 ID' + }), + nextId: z.string().optional().meta({ + example: '68ad85a7463006c963799a06', + description: '向后翻页的游标 ID' + }), + prevId: z.string().optional().meta({ + example: '68ad85a7463006c963799a04', + description: '向前翻页的游标 ID' + }), + ...(extraShape ?? ({} as TShape)) + }); + +export type LinkedPaginationProps = T & { + pageSize: number; + anchor?: A; + initialId?: string; + nextId?: string; + prevId?: string; +}; + +export const LinkedListResponseSchema = (itemSchema: T) => + z.object({ + list: z + .array( + z.intersection( + itemSchema, + z.object({ + id: z.string().meta({ example: '68ad85a7463006c963799a05', description: '数据 ID' }), + anchor: z.any().optional().meta({ description: '锚点值' }) + }) + ) + ) + .meta({ description: '数据列表' }), + hasMorePrev: z.boolean().meta({ example: false, description: '是否还有更多前置数据' }), + hasMoreNext: z.boolean().meta({ example: true, description: '是否还有更多后置数据' }) + }); + +export type LinkedListResponse = { + list: Array; + hasMorePrev: boolean; + hasMoreNext: boolean; +}; diff --git a/packages/global/openapi/core/chat/controler/api.ts b/packages/global/openapi/core/chat/controler/api.ts index 76d1b4314d..f982efe3b3 100644 --- a/packages/global/openapi/core/chat/controler/api.ts +++ b/packages/global/openapi/core/chat/controler/api.ts @@ -1,6 +1,9 @@ import { OutLinkChatAuthSchema } from '../../../../support/permission/chat'; import { ObjectIdSchema } from '../../../../common/type/mongo'; import z from 'zod'; +import { AppChatConfigTypeSchema } from '../../../../core/app/type'; +import { AppTypeEnum } from '../../../../core/app/constants'; +import { FlowNodeInputItemTypeSchema } from '../../../../core/workflow/type/io'; /* Init */ // Online chat @@ -19,13 +22,25 @@ export const InitChatQuerySchema = z }); export type InitChatQueryType = z.infer; export const InitChatResponseSchema = z.object({ - chatId: z.string().min(1).describe('对话ID'), + chatId: z.string().optional().describe('对话ID'), appId: ObjectIdSchema.describe('应用ID'), userAvatar: z.string().optional().describe('用户头像'), - title: z.string().min(1).describe('对话标题'), + title: z.string().describe('对话标题'), variables: z.record(z.string(), z.any()).optional().describe('全局变量值'), - app: z.object({}).describe('应用配置') + app: z + .object({ + chatConfig: AppChatConfigTypeSchema.optional().describe('聊天配置'), + chatModels: z.array(z.string()).optional().describe('聊天模型'), + name: z.string().min(1).describe('应用名称'), + avatar: z.string().describe('应用头像'), + intro: z.string().describe('应用简介'), + canUse: z.boolean().optional().describe('是否可用'), + type: z.enum(AppTypeEnum).describe('应用类型'), + pluginInputs: z.array(FlowNodeInputItemTypeSchema).describe('插件输入') + }) + .describe('应用配置') }); +export type InitChatResponseType = z.infer; /* ============ v2/chat/stop ============ */ export const StopV2ChatSchema = z @@ -56,42 +71,3 @@ export const StopV2ChatResponseSchema = z } }); export type StopV2ChatResponse = z.infer; - -/* ============ chat file ============ */ -export const PresignChatFileGetUrlSchema = z - .object({ - key: z.string().min(1).describe('文件key'), - appId: ObjectIdSchema.describe('应用ID'), - outLinkAuthData: OutLinkChatAuthSchema.optional().describe('外链鉴权数据') - }) - .meta({ - example: { - key: '1234567890', - appId: '1234567890', - outLinkAuthData: { - shareId: '1234567890', - outLinkUid: '1234567890' - } - } - }); -export type PresignChatFileGetUrlParams = z.infer; - -export const PresignChatFilePostUrlSchema = z - .object({ - filename: z.string().min(1).describe('文件名'), - appId: ObjectIdSchema.describe('应用ID'), - chatId: z.string().min(1).describe('对话ID'), - outLinkAuthData: OutLinkChatAuthSchema.optional().describe('外链鉴权数据') - }) - .meta({ - example: { - filename: '1234567890', - appId: '1234567890', - chatId: '1234567890', - outLinkAuthData: { - shareId: '1234567890', - outLinkUid: '1234567890' - } - } - }); -export type PresignChatFilePostUrlParams = z.infer; diff --git a/packages/global/openapi/core/chat/controler/index.ts b/packages/global/openapi/core/chat/controler/index.ts index 075294e24f..17403b883c 100644 --- a/packages/global/openapi/core/chat/controler/index.ts +++ b/packages/global/openapi/core/chat/controler/index.ts @@ -3,13 +3,31 @@ import { TagsMap } from '../../../tag'; import { StopV2ChatSchema, StopV2ChatResponseSchema, - PresignChatFilePostUrlSchema, - PresignChatFileGetUrlSchema + InitChatQuerySchema, + InitChatResponseSchema } from './api'; -import { CreatePostPresignedUrlResultSchema } from '../../../../../service/common/s3/type'; -import { z } from 'zod'; export const ChatControllerPath: OpenAPIPath = { + '/core/chat/init': { + get: { + summary: '初始化聊天', + description: '初始化聊天', + tags: [TagsMap.chatController], + requestParams: { + query: InitChatQuerySchema + }, + responses: { + 200: { + description: '成功返回聊天初始化信息', + content: { + 'application/json': { + schema: InitChatResponseSchema + } + } + } + } + } + }, '/v2/chat/stop': { post: { summary: '停止 Agent 运行', @@ -34,53 +52,5 @@ export const ChatControllerPath: OpenAPIPath = { } } } - }, - '/core/chat/file/presignChatFilePostUrl': { - post: { - summary: '获取文件上传 URL', - description: '获取文件上传 URL', - tags: [TagsMap.chatController], - requestBody: { - content: { - 'application/json': { - schema: PresignChatFilePostUrlSchema - } - } - }, - responses: { - 200: { - description: '成功上传对话文件预签名 URL', - content: { - 'application/json': { - schema: CreatePostPresignedUrlResultSchema - } - } - } - } - } - }, - '/core/chat/file/presignChatFileGetUrl': { - post: { - summary: '获取文件预览地址', - description: '获取文件预览地址', - tags: [TagsMap.chatController], - requestBody: { - content: { - 'application/json': { - schema: PresignChatFileGetUrlSchema - } - } - }, - responses: { - 200: { - description: '成功获取对话文件预签名 URL', - content: { - 'application/json': { - schema: z.string() - } - } - } - } - } } }; diff --git a/packages/global/openapi/core/chat/file/api.ts b/packages/global/openapi/core/chat/file/api.ts new file mode 100644 index 0000000000..6bc617594c --- /dev/null +++ b/packages/global/openapi/core/chat/file/api.ts @@ -0,0 +1,42 @@ +import { OutLinkChatAuthSchema } from '../../../../support/permission/chat'; +import { ObjectIdSchema } from '../../../../common/type/mongo'; +import z from 'zod'; + +/* ============ chat file ============ */ +export const PresignChatFileGetUrlSchema = z + .object({ + key: z.string().min(1).describe('文件key'), + appId: ObjectIdSchema.describe('应用ID'), + outLinkAuthData: OutLinkChatAuthSchema.optional().describe('外链鉴权数据') + }) + .meta({ + example: { + key: '1234567890', + appId: '1234567890', + outLinkAuthData: { + shareId: '1234567890', + outLinkUid: '1234567890' + } + } + }); +export type PresignChatFileGetUrlParams = z.infer; + +export const PresignChatFilePostUrlSchema = z + .object({ + filename: z.string().min(1).describe('文件名'), + appId: ObjectIdSchema.describe('应用ID'), + chatId: z.string().min(1).describe('对话ID'), + outLinkAuthData: OutLinkChatAuthSchema.optional().describe('外链鉴权数据') + }) + .meta({ + example: { + filename: '1234567890', + appId: '1234567890', + chatId: '1234567890', + outLinkAuthData: { + shareId: '1234567890', + outLinkUid: '1234567890' + } + } + }); +export type PresignChatFilePostUrlParams = z.infer; diff --git a/packages/global/openapi/core/chat/file/index.ts b/packages/global/openapi/core/chat/file/index.ts new file mode 100644 index 0000000000..239035e43d --- /dev/null +++ b/packages/global/openapi/core/chat/file/index.ts @@ -0,0 +1,56 @@ +import type { OpenAPIPath } from '../../../type'; +import { TagsMap } from '../../../tag'; +import { PresignChatFilePostUrlSchema, PresignChatFileGetUrlSchema } from './api'; +import { CreatePostPresignedUrlResultSchema } from '../../../../../service/common/s3/type'; +import { z } from 'zod'; + +export const ChatFilePath: OpenAPIPath = { + '/core/chat/file/presignChatFilePostUrl': { + post: { + summary: '获取文件上传 URL', + description: '获取文件上传 URL', + tags: [TagsMap.chatFile], + requestBody: { + content: { + 'application/json': { + schema: PresignChatFilePostUrlSchema + } + } + }, + responses: { + 200: { + description: '成功上传对话文件预签名 URL', + content: { + 'application/json': { + schema: CreatePostPresignedUrlResultSchema + } + } + } + } + } + }, + '/core/chat/file/presignChatFileGetUrl': { + post: { + summary: '获取文件预览地址', + description: '获取文件预览地址', + tags: [TagsMap.chatFile], + requestBody: { + content: { + 'application/json': { + schema: PresignChatFileGetUrlSchema + } + } + }, + responses: { + 200: { + description: '成功获取对话文件预签名 URL', + content: { + 'application/json': { + schema: z.string() + } + } + } + } + } + } +}; diff --git a/packages/global/openapi/core/chat/helperBot/api.ts b/packages/global/openapi/core/chat/helperBot/api.ts index e2039d8d58..f76251e337 100644 --- a/packages/global/openapi/core/chat/helperBot/api.ts +++ b/packages/global/openapi/core/chat/helperBot/api.ts @@ -8,7 +8,7 @@ import { } from '../../../../core/chat/helperBot/type'; import { topAgentParamsSchema } from '../../../../core/chat/helperBot/topAgent/type'; import { z } from 'zod'; -import type { PaginationResponse } from '../../../../../web/common/fetch/type'; +import type { PaginationResponse } from '../../../api'; import { ChatFileTypeEnum } from '../../../../core/chat/constants'; // 分页获取记录 diff --git a/packages/global/openapi/core/chat/index.ts b/packages/global/openapi/core/chat/index.ts index 4f5e818f47..63a2c7ab6d 100644 --- a/packages/global/openapi/core/chat/index.ts +++ b/packages/global/openapi/core/chat/index.ts @@ -7,18 +7,22 @@ import { GetRecentlyUsedAppsResponseSchema } from './api'; import { TagsMap } from '../../tag'; import { ChatControllerPath } from './controler'; import { HelperBotPath } from './helperBot'; -import { ChatQuotePath } from './quote/index'; import { ChatInputGuidePath } from './inputGuide/index'; +import { OutLinkChatPath } from './outLink/index'; +import { ChatRecordPath } from './record/index'; +import { ChatFilePath } from './file'; export const ChatPath: OpenAPIPath = { + ...ChatFeedbackPath, + ...ChatFilePath, ...ChatSettingPath, ...ChatFavouriteAppPath, - ...ChatFeedbackPath, ...ChatHistoryPath, ...ChatControllerPath, ...HelperBotPath, - ...ChatQuotePath, ...ChatInputGuidePath, + ...OutLinkChatPath, + ...ChatRecordPath, '/core/chat/recentlyUsed': { get: { diff --git a/packages/global/openapi/core/chat/inputGuide/api.ts b/packages/global/openapi/core/chat/inputGuide/api.ts index 509522a20a..f0611a4346 100644 --- a/packages/global/openapi/core/chat/inputGuide/api.ts +++ b/packages/global/openapi/core/chat/inputGuide/api.ts @@ -1,6 +1,7 @@ import { z } from 'zod'; import { PaginationSchema } from '../../../api'; import { ObjectIdSchema } from '../../../../common/type/mongo'; +import { OutLinkChatAuthSchema } from '../../../../support/permission/chat'; /* ============================================================================ * API: 获取对话输入引导列表 @@ -30,3 +31,127 @@ export const ChatInputGuideListResponseSchema = z.object({ total: z.number().meta({ example: 10, description: '总数' }) }); export type ChatInputGuideListResponseType = z.infer; + +/* ============================================================================ + * API: 统计对话输入引导总数 + * Route: GET /api/core/chat/inputGuide/countTotal + * Method: GET + * Description: 获取指定应用的对话输入引导总数 + * Tags: ['Chat', 'InputGuide', 'Read'] + * ============================================================================ */ + +export const CountChatInputGuideTotalQuerySchema = z.object({ + appId: z.string().meta({ example: '68ad85a7463006c963799a05', description: '应用 ID' }) +}); +export type CountChatInputGuideTotalQueryType = z.infer; + +export const CountChatInputGuideTotalResponseSchema = z.object({ + total: z.number().int().nonnegative().meta({ example: 10, description: '总数' }) +}); +export type CountChatInputGuideTotalResponseType = z.infer< + typeof CountChatInputGuideTotalResponseSchema +>; + +/* ============================================================================ + * API: 创建对话输入引导 + * Route: POST /api/core/chat/inputGuide/create + * Method: POST + * Description: 批量创建对话输入引导文本 + * Tags: ['Chat', 'InputGuide', 'Write'] + * ============================================================================ */ + +export const CreateChatInputGuideBodySchema = z.object({ + appId: z.string().meta({ example: '68ad85a7463006c963799a05', description: '应用 ID' }), + textList: z + .array(z.string()) + .min(1) + .meta({ example: ['如何开始使用?', '有哪些功能?'], description: '引导文本列表' }) +}); +export type CreateChatInputGuideBodyType = z.infer; + +export const CreateChatInputGuideResponseSchema = z.object({ + insertLength: z + .number() + .int() + .nonnegative() + .meta({ example: 2, description: '实际插入成功的数量' }) +}); +export type CreateChatInputGuideResponseType = z.infer; + +/* ============================================================================ + * API: 删除对话输入引导 + * Route: DELETE /api/core/chat/inputGuide/delete + * Method: DELETE + * Description: 批量删除指定的对话输入引导 + * Tags: ['Chat', 'InputGuide', 'Delete'] + * ============================================================================ */ + +export const DeleteChatInputGuideBodySchema = z.object({ + appId: z.string().meta({ example: '68ad85a7463006c963799a05', description: '应用 ID' }), + dataIdList: z + .array(z.string()) + .min(1) + .meta({ + example: ['68ad85a7463006c963799a05', '68ad85a7463006c963799a06'], + description: '要删除的引导 ID 列表' + }) +}); +export type DeleteChatInputGuideBodyType = z.infer; + +export const DeleteChatInputGuideResponseSchema = z.object({}); +export type DeleteChatInputGuideResponseType = z.infer; + +/* ============================================================================ + * API: 删除应用所有对话输入引导 + * Route: DELETE /api/core/chat/inputGuide/deleteAll + * Method: DELETE + * Description: 删除指定应用的所有对话输入引导 + * Tags: ['Chat', 'InputGuide', 'Delete'] + * ============================================================================ */ + +export const DeleteAllChatInputGuideBodySchema = z.object({ + appId: z.string().meta({ example: '68ad85a7463006c963799a05', description: '应用 ID' }) +}); +export type DeleteAllChatInputGuideBodyType = z.infer; + +export const DeleteAllChatInputGuideResponseSchema = z.object({}); +export type DeleteAllChatInputGuideResponseType = z.infer< + typeof DeleteAllChatInputGuideResponseSchema +>; + +/* ============================================================================ + * API: 查询对话输入引导(公开接口) + * Route: POST /api/core/chat/inputGuide/query + * Method: POST + * Description: 根据搜索词查询对话输入引导,支持分享链接和团队 Token 鉴权 + * Tags: ['Chat', 'InputGuide', 'Read'] + * ============================================================================ */ + +export const QueryChatInputGuideBodySchema = OutLinkChatAuthSchema.extend({ + appId: z.string().meta({ example: '68ad85a7463006c963799a05', description: '应用 ID' }), + searchKey: z.string().meta({ example: '如何使用', description: '搜索关键词' }) +}); +export type QueryChatInputGuideBodyType = z.infer; + +export const QueryChatInputGuideResponseSchema = z.array( + z.string().meta({ example: '如何开始使用?', description: '引导文本' }) +); +export type QueryChatInputGuideResponseType = z.infer; + +/* ============================================================================ + * API: 更新对话输入引导 + * Route: PUT /api/core/chat/inputGuide/update + * Method: PUT + * Description: 更新指定的对话输入引导文本 + * Tags: ['Chat', 'InputGuide', 'Write'] + * ============================================================================ */ + +export const UpdateChatInputGuideBodySchema = z.object({ + appId: z.string().meta({ example: '68ad85a7463006c963799a05', description: '应用 ID' }), + dataId: z.string().meta({ example: '68ad85a7463006c963799a05', description: '要更新的引导 ID' }), + text: z.string().min(1).meta({ example: '如何开始使用?', description: '新的引导文本' }) +}); +export type UpdateChatInputGuideBodyType = z.infer; + +export const UpdateChatInputGuideResponseSchema = z.object({}); +export type UpdateChatInputGuideResponseType = z.infer; diff --git a/packages/global/openapi/core/chat/inputGuide/index.ts b/packages/global/openapi/core/chat/inputGuide/index.ts index 78adc93933..be2b6f0346 100644 --- a/packages/global/openapi/core/chat/inputGuide/index.ts +++ b/packages/global/openapi/core/chat/inputGuide/index.ts @@ -1,6 +1,20 @@ import type { OpenAPIPath } from '../../../type'; import { TagsMap } from '../../../tag'; -import { ChatInputGuideListBodySchema, ChatInputGuideListResponseSchema } from './api'; +import { + ChatInputGuideListBodySchema, + ChatInputGuideListResponseSchema, + CountChatInputGuideTotalResponseSchema, + CreateChatInputGuideBodySchema, + CreateChatInputGuideResponseSchema, + DeleteChatInputGuideBodySchema, + DeleteChatInputGuideResponseSchema, + DeleteAllChatInputGuideBodySchema, + DeleteAllChatInputGuideResponseSchema, + QueryChatInputGuideBodySchema, + QueryChatInputGuideResponseSchema, + UpdateChatInputGuideBodySchema, + UpdateChatInputGuideResponseSchema +} from './api'; export const ChatInputGuidePath: OpenAPIPath = { '/core/chat/inputGuide/list': { @@ -26,5 +40,150 @@ export const ChatInputGuidePath: OpenAPIPath = { } } } + }, + '/core/chat/inputGuide/countTotal': { + get: { + summary: '统计对话输入引导总数', + description: '获取指定应用的对话输入引导总数', + tags: [TagsMap.chatInputGuide], + parameters: [ + { + in: 'query', + name: 'appId', + schema: { type: 'string', example: '68ad85a7463006c963799a05', description: '应用 ID' }, + required: true + } + ], + responses: { + 200: { + description: '成功返回总数', + content: { + 'application/json': { + schema: CountChatInputGuideTotalResponseSchema + } + } + } + } + } + }, + '/core/chat/inputGuide/create': { + post: { + summary: '创建对话输入引导', + description: '批量创建对话输入引导文本', + tags: [TagsMap.chatInputGuide], + requestBody: { + content: { + 'application/json': { + schema: CreateChatInputGuideBodySchema + } + } + }, + responses: { + 200: { + description: '成功返回插入数量', + content: { + 'application/json': { + schema: CreateChatInputGuideResponseSchema + } + } + } + } + } + }, + '/core/chat/inputGuide/delete': { + delete: { + summary: '删除对话输入引导', + description: '批量删除指定的对话输入引导', + tags: [TagsMap.chatInputGuide], + requestBody: { + content: { + 'application/json': { + schema: DeleteChatInputGuideBodySchema + } + } + }, + responses: { + 200: { + description: '删除成功', + content: { + 'application/json': { + schema: DeleteChatInputGuideResponseSchema + } + } + } + } + } + }, + '/core/chat/inputGuide/deleteAll': { + delete: { + summary: '删除应用所有对话输入引导', + description: '删除指定应用的所有对话输入引导', + tags: [TagsMap.chatInputGuide], + requestBody: { + content: { + 'application/json': { + schema: DeleteAllChatInputGuideBodySchema + } + } + }, + responses: { + 200: { + description: '删除成功', + content: { + 'application/json': { + schema: DeleteAllChatInputGuideResponseSchema + } + } + } + } + } + }, + '/core/chat/inputGuide/query': { + post: { + summary: '查询对话输入引导(公开接口)', + description: '根据搜索词查询对话输入引导,支持分享链接和团队 Token 鉴权', + tags: [TagsMap.chatInputGuide], + requestBody: { + content: { + 'application/json': { + schema: QueryChatInputGuideBodySchema + } + } + }, + responses: { + 200: { + description: '成功返回引导文本列表', + content: { + 'application/json': { + schema: QueryChatInputGuideResponseSchema + } + } + } + } + } + }, + '/core/chat/inputGuide/update': { + put: { + summary: '更新对话输入引导', + description: '更新指定的对话输入引导文本', + tags: [TagsMap.chatInputGuide], + requestBody: { + content: { + 'application/json': { + schema: UpdateChatInputGuideBodySchema + } + } + }, + responses: { + 200: { + description: '更新成功', + content: { + 'application/json': { + schema: UpdateChatInputGuideResponseSchema + } + } + } + } + } } }; diff --git a/packages/global/openapi/core/chat/outLink/api.ts b/packages/global/openapi/core/chat/outLink/api.ts new file mode 100644 index 0000000000..ca352a9a2f --- /dev/null +++ b/packages/global/openapi/core/chat/outLink/api.ts @@ -0,0 +1,9 @@ +import z from 'zod'; + +// ============= Init OutLink Chat ============= +export const InitOutLinkChatQuerySchema = z.object({ + chatId: z.string().optional().describe('会话ID'), + shareId: z.string().describe('分享链接ID'), + outLinkUid: z.string().describe('外链用户ID') +}); +export type InitOutLinkChatQueryType = z.infer; diff --git a/packages/global/openapi/core/chat/outLink/index.ts b/packages/global/openapi/core/chat/outLink/index.ts new file mode 100644 index 0000000000..7452e39e37 --- /dev/null +++ b/packages/global/openapi/core/chat/outLink/index.ts @@ -0,0 +1,21 @@ +import type { OpenAPIPath } from '../../../type'; +import { TagsMap } from '../../../tag'; +import { InitOutLinkChatQuerySchema } from './api'; + +export const OutLinkChatPath: OpenAPIPath = { + '/core/chat/outLink/init': { + get: { + summary: '初始化外链会话', + description: '通过分享链接初始化会话,获取应用配置和历史会话信息', + tags: [TagsMap.chatPage], + requestParams: { + query: InitOutLinkChatQuerySchema + }, + responses: { + 200: { + description: '成功返回会话初始化信息' + } + } + } + } +}; diff --git a/packages/global/openapi/core/chat/quote/api.ts b/packages/global/openapi/core/chat/quote/api.ts deleted file mode 100644 index 23881eca62..0000000000 --- a/packages/global/openapi/core/chat/quote/api.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { z } from 'zod'; -import { ObjectIdSchema } from '../../../../common/type/mongo'; -import { OutLinkChatAuthSchema } from '../../../../support/permission/chat'; -import { DatasetCiteItemSchema } from '../../../../core/dataset/type'; - -/* ============================================================================ - * API: 获取对话引用数据 - * Route: POST /api/core/chat/quote/getQuote - * Method: POST - * Description: 获取指定对话消息的数据集引用列表 - * Tags: ['Chat', 'Quote', 'Read'] - * ============================================================================ */ - -export const GetQuoteBodySchema = OutLinkChatAuthSchema.extend({ - appId: ObjectIdSchema.describe('应用 ID'), - chatId: z.string().min(1).max(256).meta({ - example: 'chat_abc123', - description: '对话 ID' - }), - chatItemDataId: z.string().min(1).max(256).meta({ - example: 'item_abc123', - description: '对话消息 dataId' - }), - datasetDataIdList: z - .array(z.string().min(1).max(256)) - .max(200) - .meta({ - example: ['68ad85a7463006c963799a05'], - description: '数据集数据 ID 列表' - }), - collectionIdList: z - .array(z.string().min(1).max(256)) - .max(200) - .meta({ - example: ['68ad85a7463006c963799a06'], - description: '集合 ID 列表' - }) -}); -export type GetQuoteBodyType = z.infer; - -export const GetQuoteResponseSchema = z.array(DatasetCiteItemSchema); -export type GetQuoteResponseType = z.infer; - -/* ============================================================================ - * API: 获取集合分页引用数据 - * Route: POST /api/core/chat/quote/getCollectionQuote - * Method: POST - * Description: 以链式分页方式获取指定集合的引用数据,支持前后翻页 - * Tags: ['Chat', 'Quote', 'Read'] - * ============================================================================ */ - -export const GetCollectionQuoteBodySchema = OutLinkChatAuthSchema.extend({ - appId: ObjectIdSchema.describe('应用 ID'), - chatId: z.string().min(1).max(256).describe('对话 ID'), - chatItemDataId: z.string().min(1).max(256).describe('对话消息 dataId'), - collectionId: ObjectIdSchema.describe('集合 ID'), - pageSize: z.number().int().min(1).max(30).default(15).describe('每页条数,范围 [1, 30]'), - anchor: z.number().optional().describe('当前锚点 chunkIndex'), - initialId: z.string().optional().describe('初始定位数据 ID'), - nextId: z.string().optional().describe('向后翻页的游标 ID'), - prevId: z.string().optional().describe('向前翻页的游标 ID') -}); -export type GetCollectionQuoteBodyType = z.infer; - -export const GetCollectionQuoteResSchema = z.object({ - list: z.array( - DatasetCiteItemSchema.extend({ - id: z.string().describe('数据 ID(alias _id)'), - anchor: z.number().optional().describe('chunk 序号(alias index)') - }) - ), - hasMorePrev: z.boolean().describe('是否还有更多前置数据'), - hasMoreNext: z.boolean().describe('是否还有更多后置数据') -}); -export type GetCollectionQuoteResType = z.infer; diff --git a/packages/global/openapi/core/chat/quote/index.ts b/packages/global/openapi/core/chat/quote/index.ts deleted file mode 100644 index 59a2b4e8b4..0000000000 --- a/packages/global/openapi/core/chat/quote/index.ts +++ /dev/null @@ -1,59 +0,0 @@ -import type { OpenAPIPath } from '../../../type'; -import { TagsMap } from '../../../tag'; -import { - GetQuoteBodySchema, - GetQuoteResponseSchema, - GetCollectionQuoteBodySchema, - GetCollectionQuoteResSchema -} from './api'; - -export const ChatQuotePath: OpenAPIPath = { - '/core/chat/quote/getQuote': { - post: { - summary: '获取对话引用数据', - description: '获取指定对话消息的数据集引用列表,需要对话访问权限', - tags: [TagsMap.chatPage], - requestBody: { - content: { - 'application/json': { - schema: GetQuoteBodySchema - } - } - }, - responses: { - 200: { - description: '成功返回引用数据列表', - content: { - 'application/json': { - schema: GetQuoteResponseSchema - } - } - } - } - } - }, - '/core/chat/quote/getCollectionQuote': { - post: { - summary: '获取集合分页引用数据', - description: '以链式分页方式获取指定集合的引用数据,支持前后翻页,需要对话访问权限', - tags: [TagsMap.chatPage], - requestBody: { - content: { - 'application/json': { - schema: GetCollectionQuoteBodySchema - } - } - }, - responses: { - 200: { - description: '成功返回分页引用数据', - content: { - 'application/json': { - schema: GetCollectionQuoteResSchema - } - } - } - } - } - } -}; diff --git a/packages/global/openapi/core/chat/record/api.ts b/packages/global/openapi/core/chat/record/api.ts new file mode 100644 index 0000000000..370dcc317f --- /dev/null +++ b/packages/global/openapi/core/chat/record/api.ts @@ -0,0 +1,176 @@ +import z from 'zod'; +import { OutLinkChatAuthSchema } from '../../../../support/permission/chat'; +import { ObjectIdSchema } from '../../../../common/type/mongo'; +import { DatasetCiteItemSchema } from '../../../../core/dataset/type'; +import { LinkedListResponseSchema, LinkedPaginationSchema, PaginationSchema } from '../../../api'; +import { ChatItemMiniSchema } from '../../../../core/chat/type'; +import { AppTTSConfigTypeSchema } from '../../../../core/app/type'; +import { GetChatTypeEnum } from '../../../../core/chat/constants'; + +/* ============================================================================ + * API: 获取对话响应详细数据 + * Route: GET /api/core/chat/record/getResData + * Method: GET + * Description: 根据 dataId 获取对话中某条 AI 回复的详细响应数据 + * ============================================================================ */ + +export const GetResDataQuerySchema = OutLinkChatAuthSchema.extend({ + appId: z.string().describe('应用ID'), + chatId: z.string().optional().describe('会话ID'), + dataId: z.string().describe('对话数据ID') +}); +export type GetResDataQueryType = z.infer; + +/* ============================================================================ + * API: 删除对话记录 + * Route: DELETE /api/core/chat/record/delete + * Method: DELETE + * Description: 软删除指定的对话消息记录(设置 deleteTime) + * ============================================================================ */ + +export const DeleteChatRecordBodySchema = OutLinkChatAuthSchema.extend({ + appId: ObjectIdSchema.meta({ example: '68ad85a7463006c963799a05', description: '应用 ID' }), + chatId: z.string().meta({ example: 'chat123', description: '会话 ID' }), + contentId: z.string().optional().meta({ + example: 'content123', + description: '要删除的消息 ID' + }), + delFile: z.coerce.boolean().optional().meta({ + example: false, + description: '是否同时删除关联文件' + }) +}); +export type DeleteChatRecordBodyType = z.infer; + +export const DeleteChatRecordResponseSchema = z.object({}); +export type DeleteChatRecordResponseType = z.infer; + +/* ============================================================================ + * API: 获取对话引用数据 + * Route: POST /api/core/chat/quote/getQuote + * Method: POST + * Description: 获取指定对话消息的数据集引用列表 + * ============================================================================ */ + +export const GetQuoteBodySchema = OutLinkChatAuthSchema.extend({ + appId: ObjectIdSchema.describe('应用 ID'), + chatId: z.string().min(1).max(256).meta({ + example: 'chat_abc123', + description: '对话 ID' + }), + chatItemDataId: z.string().min(1).max(256).meta({ + example: 'item_abc123', + description: '对话消息 dataId' + }), + datasetDataIdList: z + .array(z.string().min(1).max(256)) + .max(200) + .meta({ + example: ['68ad85a7463006c963799a05'], + description: '数据集数据 ID 列表' + }), + collectionIdList: z + .array(z.string().min(1).max(256)) + .max(200) + .meta({ + example: ['68ad85a7463006c963799a06'], + description: '集合 ID 列表' + }) +}); +export type GetQuoteBodyType = z.infer; + +export const GetQuoteResponseSchema = z.array(DatasetCiteItemSchema); +export type GetQuoteResponseType = z.infer; + +/* ============================================================================ + * API: 获取集合分页引用数据 + * Route: POST /api/core/chat/quote/getCollectionQuote + * Method: POST + * Description: 以链式分页方式获取指定集合的引用数据,支持前后翻页 + * ============================================================================ */ + +export const GetCollectionQuoteBodySchema = OutLinkChatAuthSchema.extend({ + appId: ObjectIdSchema.describe('应用 ID'), + chatId: z.string().min(1).max(256).describe('对话 ID'), + chatItemDataId: z.string().min(1).max(256).describe('对话消息 dataId'), + collectionId: ObjectIdSchema.describe('集合 ID'), + pageSize: z.number().int().min(1).max(30).default(15).describe('每页条数,范围 [1, 30]'), + anchor: z.number().optional().describe('当前锚点 chunkIndex'), + initialId: z.string().optional().describe('初始定位数据 ID'), + nextId: z.string().optional().describe('向后翻页的游标 ID'), + prevId: z.string().optional().describe('向前翻页的游标 ID') +}); +export type GetCollectionQuoteBodyType = z.infer; + +export const GetCollectionQuoteResSchema = z.object({ + list: z.array( + DatasetCiteItemSchema.extend({ + id: z.string().describe('数据 ID(alias _id)'), + anchor: z.number().optional().describe('chunk 序号(alias index)') + }) + ), + hasMorePrev: z.boolean().describe('是否还有更多前置数据'), + hasMoreNext: z.boolean().describe('是否还有更多后置数据') +}); +export type GetCollectionQuoteResType = z.infer; + +/* ============================================================================ + * API: 分页获取对话记录 + * Route: POST /api/core/chat/record/getPaginationRecords + * Method: POST + * Description: 分页获取指定应用和会话的对话记录,支持多种鉴权模式 + * ============================================================================ */ + +const GetRecordPropsSchema = z.object({ + appId: ObjectIdSchema.meta({ example: '68ad85a7463006c963799a05', description: '应用 ID' }), + chatId: z.string().optional().meta({ example: 'chat123', description: '会话 ID' }), + loadCustomFeedbacks: z.boolean().optional().meta({ + example: false, + description: '是否加载自定义反馈' + }), + type: z + .enum(GetChatTypeEnum) + .optional() + .meta({ example: 'normal', description: '获取类型,影响数据过滤规则' }), + includeDeleted: z.boolean().optional().meta({ + example: false, + description: '是否包含已删除的记录' + }) +}); +export const GetPaginationRecordsBodySchema = PaginationSchema.extend( + OutLinkChatAuthSchema.shape +).extend(GetRecordPropsSchema.shape); +export type GetPaginationRecordsBodyType = z.infer; + +export const GetPaginationRecordsResponseSchema = z.object({ + list: z.array(z.any()).meta({ description: '对话记录列表' }), + total: z.number().int().nonnegative().meta({ example: 10, description: '总数' }) +}); +export type GetPaginationRecordsResponseType = z.infer; + +/* ============================================================================ + * API: 获取对话记录(v2) + * Route: POST /api/core/chat/record/getRecordsV2 + * Method: POST + * Description: 获取对话记录(v2) + * ============================================================================ */ +export const GetRecordsV2BodySchema = LinkedPaginationSchema(GetRecordPropsSchema.shape); +export type GetRecordsV2BodyType = z.infer; +export const GetRecordsV2ResponseSchema = LinkedListResponseSchema(ChatItemMiniSchema).extend({ + total: z.int() +}); +export type GetRecordsV2ResponseType = z.infer; + +/* ============================================================================ + * API: 获取语音合成 + * Route: POST /api/core/chat/record/getSpeech + * Method: POST + * Description: 将文本转换为语音,返回二进制音频数据流 + * ============================================================================ */ + +export const GetChatSpeechBodySchema = OutLinkChatAuthSchema.extend({ + appId: z.string().meta({ example: '68ad85a7463006c963799a05', description: '应用 ID' }), + ttsConfig: AppTTSConfigTypeSchema.meta({ description: 'TTS 配置' }), + input: z.string().meta({ example: '你好,世界', description: '要转换的文本内容' }) +}); +export type GetChatSpeechBodyType = z.infer; diff --git a/packages/global/openapi/core/chat/record/index.ts b/packages/global/openapi/core/chat/record/index.ts new file mode 100644 index 0000000000..84eb4b20c9 --- /dev/null +++ b/packages/global/openapi/core/chat/record/index.ts @@ -0,0 +1,176 @@ +import type { OpenAPIPath } from '../../../type'; +import { TagsMap } from '../../../tag'; +import { + GetResDataQuerySchema, + DeleteChatRecordBodySchema, + DeleteChatRecordResponseSchema, + GetQuoteBodySchema, + GetQuoteResponseSchema, + GetCollectionQuoteBodySchema, + GetCollectionQuoteResSchema, + GetPaginationRecordsBodySchema, + GetPaginationRecordsResponseSchema, + GetRecordsV2BodySchema, + GetRecordsV2ResponseSchema, + GetChatSpeechBodySchema +} from './api'; + +export const ChatRecordPath: OpenAPIPath = { + '/core/chat/record/getPaginationRecords': { + post: { + summary: '分页获取对话记录', + description: '分页获取指定应用和会话的对话记录,支持多种鉴权模式', + tags: [TagsMap.chatRecord], + requestBody: { + content: { + 'application/json': { + schema: GetPaginationRecordsBodySchema + } + } + }, + responses: { + 200: { + description: '成功返回对话记录', + content: { + 'application/json': { + schema: GetPaginationRecordsResponseSchema + } + } + } + } + } + }, + '/core/chat/record/getRecords_v2': { + post: { + summary: '根据锚点获取对话记录', + description: '根据锚点获取指定应用和会话的对话记录,支持多种鉴权模式', + tags: [TagsMap.chatRecord], + requestBody: { + content: { + 'application/json': { + schema: GetRecordsV2BodySchema + } + } + }, + responses: { + 200: { + description: '成功返回对话记录', + content: { + 'application/json': { + schema: GetRecordsV2ResponseSchema + } + } + } + } + } + }, + + '/core/chat/record/getResData': { + get: { + summary: '获取对话响应详细数据', + description: '根据 dataId 获取对话中某条 AI 回复的详细响应数据', + tags: [TagsMap.chatRecord], + requestParams: { + query: GetResDataQuerySchema + }, + responses: { + 200: { + description: '成功返回响应数据' + } + } + } + }, + + '/core/chat/record/getQuote': { + post: { + summary: '获取对话引用数据', + description: '获取指定对话消息的数据集引用列表,需要对话访问权限', + tags: [TagsMap.chatRecord], + requestBody: { + content: { + 'application/json': { + schema: GetQuoteBodySchema + } + } + }, + responses: { + 200: { + description: '成功返回引用数据列表', + content: { + 'application/json': { + schema: GetQuoteResponseSchema + } + } + } + } + } + }, + '/core/chat/record/getCollectionQuote': { + post: { + summary: '获取集合分页引用数据', + description: '以链式分页方式获取指定集合的引用数据,支持前后翻页,需要对话访问权限', + tags: [TagsMap.chatRecord], + requestBody: { + content: { + 'application/json': { + schema: GetCollectionQuoteBodySchema + } + } + }, + responses: { + 200: { + description: '成功返回分页引用数据', + content: { + 'application/json': { + schema: GetCollectionQuoteResSchema + } + } + } + } + } + }, + + '/core/chat/record/delete': { + delete: { + summary: '删除对话记录', + description: '软删除指定的对话消息记录(设置 deleteTime)', + tags: [TagsMap.chatRecord], + requestBody: { + content: { + 'application/json': { + schema: DeleteChatRecordBodySchema + } + } + }, + responses: { + 200: { + description: '删除成功', + content: { + 'application/json': { + schema: DeleteChatRecordResponseSchema + } + } + } + } + } + }, + '/core/chat/record/getSpeech': { + post: { + summary: '获取语音合成', + description: '将文本转换为语音,返回二进制音频数据流', + tags: [TagsMap.chatRecord], + requestBody: { + content: { + 'application/json': { + schema: GetChatSpeechBodySchema + } + } + }, + responses: { + 200: { + description: '成功返回二进制音频数据流' + } + } + } + } +}; diff --git a/packages/global/openapi/core/dataset/api.ts b/packages/global/openapi/core/dataset/api.ts new file mode 100644 index 0000000000..a86e5339b5 --- /dev/null +++ b/packages/global/openapi/core/dataset/api.ts @@ -0,0 +1,413 @@ +import { z } from 'zod'; +import { DatasetSearchModeEnum, DatasetTypeEnum } from '../../../core/dataset/constants'; +import { ApiDatasetServerSchema } from '../../../core/dataset/apiDataset/type'; +import { ObjectIdSchema } from '../../../common/type/mongo'; +import { ParentIdSchema } from '../../../common/parentFolder/type'; +import { EmbeddingModelItemSchema } from '../../../core/ai/model.schema'; +import { + ChunkSettingsSchema, + DatasetItemSchema, + DatasetListItemSchema, + SearchDataResponseItemSchema +} from '../../../core/dataset/type'; + +/* ============================================================================ + * API: 创建知识库 + * Route: POST /api/core/dataset/create + * ============================================================================ */ + +// 入参 Schema +export const CreateDatasetBodySchema = z.object({ + parentId: ParentIdSchema.meta({ + example: '68ad85a7463006c963799a05', + description: '父级文件夹 ID,不传则创建在根目录' + }), + type: z.enum(DatasetTypeEnum).meta({ + example: DatasetTypeEnum.dataset, + description: '知识库类型' + }), + name: z.string().meta({ + example: '我的知识库', + description: '知识库名称' + }), + intro: z.string().meta({ + example: '这是一个用于存储产品文档的知识库', + description: '知识库简介' + }), + avatar: z.string().meta({ + example: '/imgs/dataset/avatar.png', + description: '知识库头像' + }), + vectorModel: z.string().optional().meta({ + example: 'text-embedding-3-small', + description: '向量模型名称,不传则使用默认向量模型' + }), + agentModel: z.string().optional().meta({ + example: 'gpt-4o-mini', + description: '知识库 Agent 模型名称,不传则使用默认模型' + }), + vlmModel: z.string().optional().meta({ + example: 'gpt-4o', + description: '视觉语言模型名称' + }), + apiDatasetServer: ApiDatasetServerSchema.optional().meta({ + description: '第三方知识库服务器配置(API/飞书/语雀)' + }) +}); + +export type CreateDatasetBody = z.infer; + +// 出参 Schema +export const CreateDatasetResponseSchema = ObjectIdSchema.meta({ + example: '68ad85a7463006c963799a05', + description: '新创建的知识库 ID' +}); + +export type CreateDatasetResponse = z.infer; + +/* ============================================================================ + * API: 创建知识库并上传文件 + * Route: POST /api/core/dataset/createWithFiles + * ============================================================================ */ + +// 入参 Schema +export const CreateDatasetWithFilesBodySchema = z.object({ + datasetParams: z + .object({ + name: z.string().meta({ + example: '我的知识库', + description: '知识库名称' + }), + avatar: z.string().meta({ + example: '/imgs/dataset/avatar.png', + description: '知识库头像' + }), + parentId: ParentIdSchema.meta({ + example: '68ad85a7463006c963799a05', + description: '父级文件夹 ID' + }), + vectorModel: z.string().optional().meta({ + example: 'text-embedding-3-small', + description: '向量模型名称,不传则使用默认向量模型' + }), + agentModel: z.string().optional().meta({ + example: 'gpt-4o-mini', + description: 'Agent 模型名称,不传则使用默认模型' + }), + vlmModel: z.string().optional().meta({ + example: 'gpt-4o', + description: '视觉语言模型名称' + }) + }) + .meta({ description: '知识库参数' }), + files: z + .array( + z.object({ + fileId: z.string().meta({ + example: 'temp/abc123.pdf', + description: '临时文件 ID,必须以 temp/ 开头' + }), + name: z.string().meta({ + example: '产品文档.pdf', + description: '文件名称' + }) + }) + ) + .meta({ description: '待上传的文件列表' }) +}); + +export type CreateDatasetWithFilesBody = z.infer; + +// 出参 Schema +export const CreateDatasetWithFilesResponseSchema = z.object({ + datasetId: ObjectIdSchema.meta({ + example: '68ad85a7463006c963799a05', + description: '新创建的知识库 ID' + }), + name: z.string().meta({ + example: '我的知识库', + description: '知识库名称' + }), + avatar: z.string().meta({ + example: '/imgs/dataset/avatar.png', + description: '知识库头像' + }), + vectorModel: EmbeddingModelItemSchema.meta({ + description: '向量模型信息' + }) +}); + +export type CreateDatasetWithFilesResponse = z.infer; + +/* ============================================================================ + * API: 删除知识库 + * Route: DELETE /api/core/dataset/delete + * ============================================================================ */ +export const DeleteDatasetQuerySchema = z.object({ + id: ObjectIdSchema.meta({ + example: '68ad85a7463006c963799a05', + description: '知识库 ID' + }) +}); +export type DeleteDatasetQuery = z.infer; + +/* ============================================================================ + * API: 获取知识库详情 + * Route: GET /api/core/dataset/detail + * ============================================================================ */ +export const GetDatasetDetailQuerySchema = z.object({ + id: ObjectIdSchema.meta({ + example: '68ad85a7463006c963799a05', + description: '知识库 ID' + }) +}); +export type GetDatasetDetailQuery = z.infer; + +// 出参复用 DatasetItemSchema +export const GetDatasetDetailResponseSchema = DatasetItemSchema; +export type GetDatasetDetailResponse = z.infer; + +/* ============================================================================ + * API: 获取知识库列表 + * Route: POST /api/core/dataset/list + * ============================================================================ */ +export const GetDatasetListBodySchema = z.object({ + parentId: ParentIdSchema.meta({ + example: '68ad85a7463006c963799a05', + description: '父级文件夹 ID,null 或不传表示根目录' + }), + type: z.enum(DatasetTypeEnum).optional().meta({ + example: DatasetTypeEnum.dataset, + description: '知识库类型筛选' + }), + searchKey: z.string().optional().meta({ + example: '产品文档', + description: '搜索关键词,按名称和简介模糊匹配' + }) +}); +export type GetDatasetListBody = z.infer; + +// 出参复用 DatasetListItemSchema +export const GetDatasetListResponseSchema = z.array(DatasetListItemSchema); +export type GetDatasetListResponse = z.infer; + +/* ============================================================================ + * API: 获取知识库路径 + * Route: GET /api/core/dataset/paths + * ============================================================================ */ +export const GetDatasetPathsQuerySchema = z.object({ + sourceId: z.string().optional().meta({ + example: '68ad85a7463006c963799a05', + description: '知识库 ID' + }), + type: z.enum(['current', 'parent']).meta({ + example: 'current', + description: 'current: 包含自身路径; parent: 仅返回父级路径' + }) +}); +export type GetDatasetPathsQuery = z.infer; + +export const DatasetPathItemSchema = z.object({ + parentId: ParentIdSchema.meta({ + example: '68ad85a7463006c963799a05', + description: '节点 ID' + }), + parentName: z.string().meta({ + example: '产品文档', + description: '节点名称' + }) +}); +export const GetDatasetPathsResponseSchema = z.array(DatasetPathItemSchema); +export type GetDatasetPathsResponse = z.infer; + +/* ============================================================================ + * API: 更新知识库 + * Route: PUT /api/core/dataset/update + * ============================================================================ */ +export const UpdateDatasetBodySchema = z.object({ + id: ObjectIdSchema.meta({ + example: '68ad85a7463006c963799a05', + description: '知识库 ID' + }), + parentId: ParentIdSchema.meta({ + example: '68ad85a7463006c963799a05', + description: '父级文件夹 ID,传 null 表示移动到根目录' + }), + name: z.string().optional().meta({ + example: '我的知识库', + description: '知识库名称' + }), + avatar: z.string().optional().meta({ + example: '/imgs/dataset/avatar.png', + description: '知识库头像' + }), + intro: z.string().optional().meta({ + example: '这是一个用于存储产品文档的知识库', + description: '知识库简介' + }), + agentModel: z.string().optional().meta({ + example: 'gpt-4o-mini', + description: '知识库 Agent 模型名称' + }), + vlmModel: z.string().optional().meta({ + example: 'gpt-4o', + description: '视觉语言模型名称' + }), + websiteConfig: z + .object({ + url: z.string().meta({ description: '网站 URL' }), + selector: z.string().meta({ description: '网站选择器' }) + }) + .optional() + .meta({ + description: '网站知识库配置' + }), + externalReadUrl: z.string().optional().meta({ + description: '外部读取 URL' + }), + apiDatasetServer: ApiDatasetServerSchema.optional().meta({ + description: '第三方知识库服务器配置(API/飞书/语雀)' + }), + autoSync: z.boolean().optional().meta({ + description: '是否自动同步' + }), + chunkSettings: ChunkSettingsSchema.optional().meta({ + description: '分块配置' + }) +}); +export type UpdateDatasetBody = z.infer; + +/* ============================================================================ + * API: 恢复知识库继承权限 + * Route: PUT /api/core/dataset/resumeInheritPermission + * ============================================================================ */ +export const ResumeDatasetInheritPermissionBodySchema = z.object({ + datasetId: ObjectIdSchema.meta({ + example: '68ad85a7463006c963799a05', + description: '知识库 ID' + }) +}); +export type ResumeDatasetInheritPermissionBody = z.infer< + typeof ResumeDatasetInheritPermissionBodySchema +>; + +/* ============================================================================ + * API: 创建知识库文件夹 + * Route: POST /api/core/dataset/folder/create + * ============================================================================ */ +export const CreateDatasetFolderBodySchema = z.object({ + parentId: ParentIdSchema.meta({ + example: '68ad85a7463006c963799a05', + description: '父级文件夹 ID,不传则创建在根目录' + }), + name: z.string().meta({ + example: '我的文件夹', + description: '文件夹名称' + }), + intro: z.string().meta({ + example: '存放产品相关知识库', + description: '文件夹简介' + }) +}); +export type CreateDatasetFolderBody = z.infer; + +/* ============================================================================ + * API: 搜索测试 + * Route: POST /api/core/dataset/searchTest + * ============================================================================ */ +export const SearchDatasetTestBodySchema = z.object({ + datasetId: ObjectIdSchema.meta({ + example: '68ad85a7463006c963799a05', + description: '知识库 ID' + }), + text: z.string().meta({ + example: 'FastGPT 是什么', + description: '搜索文本' + }), + similarity: z.number().optional().meta({ + example: 0.3, + description: '最低相似度阈值' + }), + limit: z.number().optional().meta({ + example: 5000, + description: '最大返回 token 数' + }), + searchMode: z.enum(DatasetSearchModeEnum).optional().meta({ + example: DatasetSearchModeEnum.mixedRecall, + description: '搜索模式' + }), + embeddingWeight: z.number().optional().meta({ + example: 1, + description: '向量搜索权重' + }), + usingReRank: z.boolean().optional().meta({ + description: '是否使用重排序' + }), + rerankModel: z.string().optional().meta({ + description: '重排序模型名称' + }), + rerankWeight: z.number().optional().meta({ + description: '重排序权重' + }), + datasetSearchUsingExtensionQuery: z.boolean().optional().meta({ + description: '是否使用问题扩展' + }), + datasetSearchExtensionModel: z.string().optional().meta({ + description: '问题扩展模型' + }), + datasetSearchExtensionBg: z.string().optional().meta({ + description: '问题扩展背景描述' + }), + datasetDeepSearch: z.boolean().optional().meta({ + description: '是否启用深度搜索' + }), + datasetDeepSearchModel: z.string().optional().meta({ + description: '深度搜索模型' + }), + datasetDeepSearchMaxTimes: z.number().optional().meta({ + description: '深度搜索最大轮次' + }), + datasetDeepSearchBg: z.string().optional().meta({ + description: '深度搜索背景描述' + }) +}); +export type SearchDatasetTestBody = z.infer; + +export const SearchDatasetTestResponseSchema = z.object({ + list: z.array(SearchDataResponseItemSchema).meta({ + description: '搜索结果列表' + }), + duration: z.string().meta({ + example: '0.523s', + description: '搜索耗时' + }), + limit: z.number().meta({ + description: '实际使用的最大 token 数' + }), + searchMode: z.enum(DatasetSearchModeEnum).meta({ + description: '实际使用的搜索模式' + }), + usingReRank: z.boolean().meta({ + description: '是否使用了重排序' + }), + similarity: z.number().meta({ + description: '实际使用的相似度阈值' + }), + queryExtensionModel: z.string().optional().meta({ + description: '问题扩展使用的模型' + }) +}); +export type SearchDatasetTestResponse = z.infer; + +/* ============================================================================ + * API: 导出知识库全部数据 + * Route: GET /api/core/dataset/exportAll + * Description: 流式输出 CSV 文件 + * ============================================================================ */ +export const ExportDatasetQuerySchema = z.object({ + datasetId: ObjectIdSchema.meta({ + example: '68ad85a7463006c963799a05', + description: '知识库 ID' + }) +}); +export type ExportDatasetQuery = z.infer; diff --git a/packages/global/openapi/core/dataset/collection/api.ts b/packages/global/openapi/core/dataset/collection/api.ts index ce01a537af..44e0b2f003 100644 --- a/packages/global/openapi/core/dataset/collection/api.ts +++ b/packages/global/openapi/core/dataset/collection/api.ts @@ -1,7 +1,33 @@ +import { ParentIdSchema } from '../../../../common/parentFolder/type'; import { ObjectIdSchema } from '../../../../common/type/mongo'; import { OutLinkChatAuthSchema } from '../../../../support/permission/chat'; import z from 'zod'; +// ============= Scroll Collections ============= +export const ScrollCollectionsBodySchema = z.object({ + datasetId: z.string(), + parentId: z.string().nullable().optional().default(null), + searchText: z.string().optional().default(''), + selectFolder: z.boolean().optional().default(false), + filterTags: z.array(z.string()).optional().default([]), + simple: z.boolean().optional().default(false) +}); +export type ScrollCollectionsBodyType = z.infer; + +// ============= Update Collection ============= +export const UpdateDatasetCollectionBodySchema = z.object({ + id: ObjectIdSchema.optional().describe('集合ID,与 datasetId+externalFileId 二选一'), + parentId: ParentIdSchema.describe('父级目录ID'), + name: z.string().optional().describe('集合名称'), + tags: z.array(z.string()).optional().describe('标签列表(标签名称,非ID)'), + forbid: z.boolean().optional().describe('是否禁用'), + createTime: z.coerce.date().optional().describe('创建时间'), + datasetId: z.string().optional().describe('数据集ID,配合 externalFileId 使用'), + externalFileId: z.string().optional().describe('外部文件ID,配合 datasetId 使用') +}); +export type UpdateDatasetCollectionBodyType = z.infer; + +// ============= Export Collection ============= // Schema 1: Basic collection export with authentication const BasicExportSchema = z .object({ diff --git a/packages/global/openapi/core/dataset/collection/index.ts b/packages/global/openapi/core/dataset/collection/index.ts index 9c83369b69..d30d7693e7 100644 --- a/packages/global/openapi/core/dataset/collection/index.ts +++ b/packages/global/openapi/core/dataset/collection/index.ts @@ -1,8 +1,50 @@ import type { OpenAPIPath } from '../../../type'; import { TagsMap } from '../../../tag'; -import { ExportCollectionBodySchema } from './api'; +import { + ExportCollectionBodySchema, + ScrollCollectionsBodySchema, + UpdateDatasetCollectionBodySchema +} from './api'; export const DatasetCollectionPath: OpenAPIPath = { + '/core/dataset/collection/scrollList': { + post: { + summary: '获取数据集集合列表(滚动分页)', + description: '获取数据集集合列表(滚动分页)', + tags: [TagsMap.datasetCollection], + requestBody: { + content: { + 'application/json': { + schema: ScrollCollectionsBodySchema + } + } + }, + responses: { + 200: { + description: '成功返回集合列表' + } + } + } + }, + '/core/dataset/collection/update': { + post: { + summary: '更新数据集集合信息', + description: '更新数据集集合信息,支持通过集合ID或数据集ID+外部文件ID定位集合', + tags: [TagsMap.datasetCollection], + requestBody: { + content: { + 'application/json': { + schema: UpdateDatasetCollectionBodySchema + } + } + }, + responses: { + 200: { + description: '成功更新集合信息' + } + } + } + }, '/core/dataset/collection/export': { post: { summary: '下载集合的所有数据块', diff --git a/packages/global/openapi/core/dataset/index.ts b/packages/global/openapi/core/dataset/index.ts index f7873cf912..7b8f41da5e 100644 --- a/packages/global/openapi/core/dataset/index.ts +++ b/packages/global/openapi/core/dataset/index.ts @@ -1,8 +1,219 @@ import type { OpenAPIPath } from '../../type'; +import { TagsMap } from '../../tag'; import { DatasetDataPath } from './data'; import { DatasetCollectionPath } from './collection'; +import { + CreateDatasetBodySchema, + CreateDatasetWithFilesBodySchema, + DeleteDatasetQuerySchema, + GetDatasetDetailQuerySchema, + GetDatasetListBodySchema, + GetDatasetPathsQuerySchema, + UpdateDatasetBodySchema, + ResumeDatasetInheritPermissionBodySchema, + CreateDatasetFolderBodySchema, + SearchDatasetTestBodySchema, + ExportDatasetQuerySchema +} from './api'; export const DatasetPath: OpenAPIPath = { - ...DatasetDataPath, - ...DatasetCollectionPath + '/core/dataset/create': { + post: { + summary: '创建知识库', + description: '创建新的知识库,支持多种类型(普通知识库、文件夹、网站知识库等)', + tags: [TagsMap.datasetCommon], + requestBody: { + content: { + 'application/json': { + schema: CreateDatasetBodySchema + } + } + }, + responses: { + 200: { + description: '成功返回新创建的知识库 ID' + } + } + } + }, + '/core/dataset/createWithFiles': { + post: { + summary: '创建知识库并上传文件', + description: '一步完成知识库创建和文件上传,自动创建集合并开始数据处理', + tags: [TagsMap.datasetCommon], + requestBody: { + content: { + 'application/json': { + schema: CreateDatasetWithFilesBodySchema + } + } + }, + responses: { + 200: { + description: '成功返回知识库信息和向量模型配置' + } + } + } + }, + '/core/dataset/folder/create': { + post: { + summary: '创建知识库文件夹', + description: '创建知识库文件夹,用于组织和管理知识库', + tags: [TagsMap.datasetCommon], + requestBody: { + content: { + 'application/json': { + schema: CreateDatasetFolderBodySchema + } + } + }, + responses: { + 200: { + description: '成功创建文件夹' + } + } + } + }, + '/core/dataset/list': { + post: { + summary: '获取知识库列表', + description: '获取当前用户有权限访问的知识库列表,支持按类型和关键词筛选', + tags: [TagsMap.datasetCommon], + requestBody: { + content: { + 'application/json': { + schema: GetDatasetListBodySchema + } + } + }, + responses: { + 200: { + description: '成功返回知识库列表' + } + } + } + }, + '/core/dataset/paths': { + get: { + summary: '获取知识库路径', + description: '获取知识库的父级路径链,用于面包屑导航', + tags: [TagsMap.datasetCommon], + requestParams: { + query: GetDatasetPathsQuerySchema + }, + responses: { + 200: { + description: '成功返回路径列表' + } + } + } + }, + '/core/dataset/detail': { + get: { + summary: '获取知识库详情', + description: '获取知识库详细信息,包括模型配置、权限和同步状态', + tags: [TagsMap.datasetCommon], + requestParams: { + query: GetDatasetDetailQuerySchema + }, + responses: { + 200: { + description: '成功返回知识库详情' + } + } + } + }, + + '/core/dataset/delete': { + delete: { + summary: '删除知识库', + description: '删除知识库及其所有子知识库,需要所有者权限', + tags: [TagsMap.datasetCommon], + requestParams: { + query: DeleteDatasetQuerySchema + }, + responses: { + 200: { + description: '成功删除知识库' + } + } + } + }, + '/core/dataset/update': { + put: { + summary: '更新知识库', + description: '更新知识库信息、配置或移动知识库到其他目录', + tags: [TagsMap.datasetCommon], + requestBody: { + content: { + 'application/json': { + schema: UpdateDatasetBodySchema + } + } + }, + responses: { + 200: { + description: '成功更新知识库' + } + } + } + }, + '/core/dataset/resumeInheritPermission': { + put: { + summary: '恢复知识库继承权限', + description: '恢复知识库的继承权限,使其权限与父级保持一致', + tags: [TagsMap.datasetCommon], + requestBody: { + content: { + 'application/json': { + schema: ResumeDatasetInheritPermissionBodySchema + } + } + }, + responses: { + 200: { + description: '成功恢复继承权限' + } + } + } + }, + + '/core/dataset/searchTest': { + post: { + summary: '搜索测试', + description: '对知识库执行搜索测试,支持多种搜索模式、重排序和问题扩展', + tags: [TagsMap.datasetCommon], + requestBody: { + content: { + 'application/json': { + schema: SearchDatasetTestBodySchema + } + } + }, + responses: { + 200: { + description: '成功返回搜索结果列表及耗时信息' + } + } + } + }, + + '/core/dataset/exportAll': { + get: { + summary: '导出知识库全部数据', + description: '以流式 CSV 格式导出知识库及其所有子知识库的数据', + tags: [TagsMap.datasetCommon], + requestParams: { + query: ExportDatasetQuerySchema + }, + responses: { + 200: { + description: '流式返回 CSV 文件' + } + } + } + }, + + ...DatasetCollectionPath, + ...DatasetDataPath }; diff --git a/packages/global/openapi/index.ts b/packages/global/openapi/index.ts index 245e162516..1f0bc6b8e9 100644 --- a/packages/global/openapi/index.ts +++ b/packages/global/openapi/index.ts @@ -39,16 +39,22 @@ export const openAPIDocument = createDocument({ tags: [TagsMap.aiSkill, TagsMap.sandbox] }, { - name: '对话', - tags: [TagsMap.chatSetting, TagsMap.chatPage] + name: '对话模块配置', + tags: [TagsMap.chatSetting, TagsMap.chatPage, TagsMap.chatInputGuide] }, { - name: '对话管理', - tags: [TagsMap.chatHistory, TagsMap.chatController, TagsMap.chatFeedback] + name: '对话模块使用', + tags: [ + TagsMap.chatHistory, + TagsMap.chatFeedback, + TagsMap.chatFile, + TagsMap.chatRecord, + TagsMap.chatController + ] }, { name: '知识库', - tags: [TagsMap.datasetCollection] + tags: [TagsMap.datasetCommon, TagsMap.datasetCollection] }, { name: '插件系统', diff --git a/packages/global/openapi/support/index.ts b/packages/global/openapi/support/index.ts index bb2e5b39fb..fa9e09bd4e 100644 --- a/packages/global/openapi/support/index.ts +++ b/packages/global/openapi/support/index.ts @@ -3,10 +3,12 @@ import type { OpenAPIPath } from '../type'; import { WalletPath } from './wallet'; import { ApiKeyPath } from './openapi'; import { CustomDomainPath } from './customDomain'; +import { OutLinkPath } from './outLink'; export const SupportPath: OpenAPIPath = { ...UserPath, ...WalletPath, ...ApiKeyPath, - ...CustomDomainPath + ...CustomDomainPath, + ...OutLinkPath }; diff --git a/packages/global/openapi/support/outLink/api.ts b/packages/global/openapi/support/outLink/api.ts new file mode 100644 index 0000000000..c142c5dc04 --- /dev/null +++ b/packages/global/openapi/support/outLink/api.ts @@ -0,0 +1,10 @@ +import z from 'zod'; +import { PublishChannelEnum } from '../../../support/outLink/constant'; +import { ObjectIdSchema } from '../../../common/type/mongo'; + +// ============= OutLink List ============= +export const OutLinkListQuerySchema = z.object({ + appId: ObjectIdSchema.describe('应用ID'), + type: z.enum(PublishChannelEnum).describe('发布渠道类型') +}); +export type OutLinkListQueryType = z.infer; diff --git a/packages/global/openapi/support/outLink/index.ts b/packages/global/openapi/support/outLink/index.ts new file mode 100644 index 0000000000..5169547f4d --- /dev/null +++ b/packages/global/openapi/support/outLink/index.ts @@ -0,0 +1,21 @@ +import type { OpenAPIPath } from '../../type'; +import { TagsMap } from '../../tag'; +import { OutLinkListQuerySchema } from './api'; + +export const OutLinkPath: OpenAPIPath = { + '/support/outLink/list': { + get: { + summary: '获取应用的发布渠道列表', + description: '查询指定应用的所有 OutLink 发布渠道配置', + tags: [TagsMap.publishChannel], + requestParams: { + query: OutLinkListQuerySchema + }, + responses: { + 200: { + description: '成功返回发布渠道列表' + } + } + } + } +}; diff --git a/packages/global/openapi/tag.ts b/packages/global/openapi/tag.ts index 088bb8dca4..4ff201a57d 100644 --- a/packages/global/openapi/tag.ts +++ b/packages/global/openapi/tag.ts @@ -17,17 +17,23 @@ export const TagsMap = { appPer: '应用权限', mcpTools: 'MCP 工具管理', - // Chat - home + /* ===== Chat ===== */ chatPage: '对话页面通用', chatHistory: '历史记录管理', - chatController: '对话操作', chatFeedback: '对话反馈', + chatFile: '文件操作', + chatRecord: '对话记录管理', + chatController: '对话操作', chatSetting: '门户页配置', + // 辅助功能 chatInputGuide: '对话输入引导', // Dataset - datasetCollection: '集合', - datasetData: '数据', + datasetCommon: '知识库管理', + datasetCollection: '集合管理', + datasetCollectionController: '集合操作', + datasetData: '数据管理', + datasetTraining: '训练管理', // Plugin pluginToolTag: '工具标签', diff --git a/packages/global/support/outLink/api.ts b/packages/global/support/outLink/api.ts index 12870bf9b5..f7f909994c 100644 --- a/packages/global/support/outLink/api.ts +++ b/packages/global/support/outLink/api.ts @@ -1,6 +1,6 @@ import { z } from 'zod'; import type { HistoryItemType } from '../../core/chat/type'; -import type { OutLinkSchema, PlaygroundVisibilityConfigType } from './type'; +import type { OutLinkSchemaType, PlaygroundVisibilityConfigType } from './type'; import { PlaygroundVisibilityConfigSchema } from './type'; export type AuthOutLinkInitProps = { @@ -8,7 +8,7 @@ export type AuthOutLinkInitProps = { tokenUrl?: string; }; export type AuthOutLinkChatProps = { ip?: string | null; outLinkUid: string; question: string }; -export type AuthOutLinkLimitProps = AuthOutLinkChatProps & { outLink: OutLinkSchema }; +export type AuthOutLinkLimitProps = AuthOutLinkChatProps & { outLink: OutLinkSchemaType }; export type AuthOutLinkResponse = { uid: string; }; diff --git a/packages/global/support/outLink/type.ts b/packages/global/support/outLink/type.ts index 693c83ec4b..4cf54ac62b 100644 --- a/packages/global/support/outLink/type.ts +++ b/packages/global/support/outLink/type.ts @@ -58,7 +58,7 @@ export type OutlinkAppType = | WechatAppType | undefined; -export type OutLinkSchema = { +export type OutLinkSchemaType = { _id: string; shareId: string; teamId: string; @@ -108,16 +108,16 @@ export type OutLinkSchema = { export type OutLinkEditType = { _id?: string; name: string; - showCite?: OutLinkSchema['showCite']; - showRunningStatus?: OutLinkSchema['showRunningStatus']; - showSkillReferences?: OutLinkSchema['showSkillReferences']; - showFullText?: OutLinkSchema['showFullText']; - canDownloadSource?: OutLinkSchema['canDownloadSource']; + showCite?: OutLinkSchemaType['showCite']; + showRunningStatus?: OutLinkSchemaType['showRunningStatus']; + showSkillReferences?: OutLinkSchemaType['showSkillReferences']; + showFullText?: OutLinkSchemaType['showFullText']; + canDownloadSource?: OutLinkSchemaType['canDownloadSource']; // response when request immediateResponse?: string; // response when error or other situation defaultResponse?: string; - limit?: OutLinkSchema['limit']; + limit?: OutLinkSchemaType['limit']; // config for specific platform app?: T; diff --git a/packages/global/support/permission/controller.ts b/packages/global/support/permission/controller.ts index e4921305ea..e4cf51700b 100644 --- a/packages/global/support/permission/controller.ts +++ b/packages/global/support/permission/controller.ts @@ -13,6 +13,7 @@ import { OwnerPermissionVal, OwnerRoleVal } from './constant'; +import z from 'zod'; export type PerConstructPros = { role?: RoleValueType; @@ -135,3 +136,8 @@ export class Permission { this.updatePermissionCallback?.(); } } + +// 仅用于 TypeScript 类型推导,运行时不做实例验证 +export const PermissionSchema = z + .custom(() => true) + .meta({ description: '权限对象(Permission 类实例)' }); diff --git a/packages/service/common/api/type.ts b/packages/service/common/api/type.ts index 04f9912e25..7d924896e9 100644 --- a/packages/service/common/api/type.ts +++ b/packages/service/common/api/type.ts @@ -1,8 +1,3 @@ -import type { - ApiDatasetDetailResponse, - FeishuServer, - YuqueServer -} from '@fastgpt/global/core/dataset/apiDataset/type'; import type { DeepRagSearchProps, SearchDatasetDataResponse diff --git a/packages/service/common/string/tiktoken/index.ts b/packages/service/common/string/tiktoken/index.ts index 99d93f4779..f3e7ebb403 100644 --- a/packages/service/common/string/tiktoken/index.ts +++ b/packages/service/common/string/tiktoken/index.ts @@ -5,7 +5,7 @@ import { type ChatCompletionTool } from '@fastgpt/global/core/ai/type'; import { chats2GPTMessages } from '@fastgpt/global/core/chat/adapt'; -import { type ChatItemType } from '@fastgpt/global/core/chat/type'; +import { type ChatItemMiniType } from '@fastgpt/global/core/chat/type'; import { WorkerNameEnum, getWorkerController } from '../../../worker/utils'; import type { ChatCompletionRequestMessageRoleEnum } from '@fastgpt/global/core/ai/constants'; import { getLogger, LogCategories } from '../../logger'; @@ -45,7 +45,7 @@ export const countGptMessagesTokens = async ( } }; -export const countMessagesTokens = (messages: ChatItemType[]) => { +export const countMessagesTokens = (messages: ChatItemMiniType[]) => { const adaptMessages = chats2GPTMessages({ messages, reserveId: true }); return countGptMessagesTokens(adaptMessages); diff --git a/packages/service/common/system/utils.ts b/packages/service/common/system/utils.ts index 9931129587..37ec03b001 100644 --- a/packages/service/common/system/utils.ts +++ b/packages/service/common/system/utils.ts @@ -9,6 +9,8 @@ const SERVICE_LOCAL_HOST = : `${process.env.HOSTNAME || 'localhost'}:${SERVICE_LOCAL_PORT}`; export const isInternalAddress = async (url: string): Promise => { + if (isDevEnv) return false; + const isInternalIPv6 = (ip: string): boolean => { // 移除 IPv6 地址中的方括号(如果有) const cleanIp = ip.replace(/^\[|\]$/g, ''); diff --git a/packages/service/core/ai/functions/queryExtension.ts b/packages/service/core/ai/functions/queryExtension.ts index e7d09166e2..7b22ffb5c6 100644 --- a/packages/service/core/ai/functions/queryExtension.ts +++ b/packages/service/core/ai/functions/queryExtension.ts @@ -1,5 +1,5 @@ import { replaceVariable } from '@fastgpt/global/common/string/tools'; -import { type ChatItemType } from '@fastgpt/global/core/chat/type'; +import { type ChatItemMiniType } from '@fastgpt/global/core/chat/type'; import { chats2GPTMessages } from '@fastgpt/global/core/chat/adapt'; import { getLLMModel } from '../model'; import { filterGPTMessageByMaxContext } from '../llm/utils'; @@ -130,7 +130,7 @@ export const queryExtension = async ({ }: { chatBg?: string; query: string; - histories: ChatItemType[]; + histories: ChatItemMiniType[]; llmModel: string; embeddingModel: string; generateCount?: number; diff --git a/packages/service/core/chat/chatItemSchema.ts b/packages/service/core/chat/chatItemSchema.ts index 72ea592166..baddf81515 100644 --- a/packages/service/core/chat/chatItemSchema.ts +++ b/packages/service/core/chat/chatItemSchema.ts @@ -1,6 +1,6 @@ import { connectionMongo, getMongoModel } from '../../common/mongo'; const { Schema } = connectionMongo; -import { type ChatItemSchemaType } from '@fastgpt/global/core/chat/type'; +import { type ChatItemDBSchemaType } from '@fastgpt/global/core/chat/type'; import { ChatRoleMap } from '@fastgpt/global/core/chat/constants'; import { getNanoid } from '@fastgpt/global/common/string/tools'; import { @@ -103,7 +103,7 @@ ChatItemSchema.index({ appId: 1, chatId: 1, _id: -1 }); // Query by role (AI/Human), get latest chat item, permission check ChatItemSchema.index({ appId: 1, chatId: 1, obj: 1, _id: -1 }); -export const MongoChatItem = getMongoModel( +export const MongoChatItem = getMongoModel( ChatItemCollectionName, ChatItemSchema ); diff --git a/packages/service/core/chat/controller.ts b/packages/service/core/chat/controller.ts index 7b32338477..8aa665d453 100644 --- a/packages/service/core/chat/controller.ts +++ b/packages/service/core/chat/controller.ts @@ -1,4 +1,4 @@ -import type { ChatHistoryItemResType, ChatItemType } from '@fastgpt/global/core/chat/type'; +import type { ChatHistoryItemResType, ChatItemMiniType } from '@fastgpt/global/core/chat/type'; import { MongoChatItem } from './chatItemSchema'; import { MongoChat } from './chatSchema'; import { DispatchNodeResponseKeyEnum } from '@fastgpt/global/core/workflow/runtime/constants'; @@ -35,7 +35,7 @@ export async function getChatItems({ prevId?: string; nextId?: string; }): Promise<{ - histories: ChatItemType[]; + histories: ChatItemMiniType[]; total: number; hasMorePrev: boolean; hasMoreNext: boolean; diff --git a/packages/service/core/chat/utils.ts b/packages/service/core/chat/utils.ts index 393e76bac9..d3badd0b2d 100644 --- a/packages/service/core/chat/utils.ts +++ b/packages/service/core/chat/utils.ts @@ -1,5 +1,5 @@ import { ChatRoleEnum } from '@fastgpt/global/core/chat/constants'; -import type { ChatItemType } from '@fastgpt/global/core/chat/type'; +import type { ChatItemMiniType } from '@fastgpt/global/core/chat/type'; import { getS3ChatSource } from '../../common/s3/sources/chat'; import type { FlowNodeInputItemType } from '@fastgpt/global/core/workflow/type/io'; import { FlowNodeInputTypeEnum } from '@fastgpt/global/core/workflow/node/constant'; @@ -8,10 +8,10 @@ import { VariableInputEnum } from '@fastgpt/global/core/workflow/constants'; import { clone, cloneDeep } from 'lodash'; export const addPreviewUrlToChatItems = async ( - histories: ChatItemType[], + histories: ChatItemMiniType[], type: 'chatFlow' | 'workflowTool' ) => { - async function addToChatflow(item: ChatItemType) { + async function addToChatflow(item: ChatItemMiniType) { for await (const value of item.value) { if ('file' in value && value.file && value.file.key) { const { url } = await s3ChatSource.createGetChatFileURL({ @@ -23,7 +23,7 @@ export const addPreviewUrlToChatItems = async ( } } - async function addToWorkflowTool(item: ChatItemType) { + async function addToWorkflowTool(item: ChatItemMiniType) { if (item.obj !== ChatRoleEnum.Human || !Array.isArray(item.value)) return; for (let j = 0; j < item.value.length; j++) { diff --git a/packages/service/core/dataset/apiDataset/custom/api.ts b/packages/service/core/dataset/apiDataset/custom/api.ts index 9b400e7ee1..aa00ed16eb 100644 --- a/packages/service/core/dataset/apiDataset/custom/api.ts +++ b/packages/service/core/dataset/apiDataset/custom/api.ts @@ -1,8 +1,8 @@ import type { - ApiFileReadContentResponse, - APIFileReadResponse, + ApiFileReadContentResponseType, + APIFileReadResponseType, ApiDatasetDetailResponse, - APIFileServer + APIFileServerType } from '@fastgpt/global/core/dataset/apiDataset/type'; import { type Method } from 'axios'; import { createProxyAxios } from '../../../../common/api/axios'; @@ -29,7 +29,7 @@ type APIFileListResponse = { hasChild?: boolean; }; -export const useApiDatasetRequest = ({ apiServer }: { apiServer: APIFileServer }) => { +export const useApiDatasetRequest = ({ apiServer }: { apiServer: APIFileServerType }) => { const logger = getLogger(LogCategories.MODULE.DATASET.API_DATASET); const instance = createProxyAxios({ baseURL: apiServer.baseUrl, @@ -177,7 +177,7 @@ export const useApiDatasetRequest = ({ apiServer }: { apiServer: APIFileServer } apiFileId: string; customPdfParse?: boolean; datasetId: string; - }): Promise => { + }): Promise => { const data = await request< { title?: string; @@ -239,7 +239,11 @@ export const useApiDatasetRequest = ({ apiServer }: { apiServer: APIFileServer } }; const getFilePreviewUrl = async ({ apiFileId }: { apiFileId: string }) => { - const { url } = await request(`/v1/file/read`, { id: apiFileId }, 'GET'); + const { url } = await request( + `/v1/file/read`, + { id: apiFileId }, + 'GET' + ); if (!url || typeof url !== 'string') { return Promise.reject('Invalid response url'); diff --git a/packages/service/core/dataset/apiDataset/feishuDataset/api.ts b/packages/service/core/dataset/apiDataset/feishuDataset/api.ts index 7d74d36720..8cc9909e9d 100644 --- a/packages/service/core/dataset/apiDataset/feishuDataset/api.ts +++ b/packages/service/core/dataset/apiDataset/feishuDataset/api.ts @@ -1,8 +1,8 @@ import type { APIFileItemType, - ApiFileReadContentResponse, + ApiFileReadContentResponseType, ApiDatasetDetailResponse, - FeishuServer + FeishuServerType } from '@fastgpt/global/core/dataset/apiDataset/type'; import { type ParentIdType } from '@fastgpt/global/common/parentFolder/type'; import { type Method } from 'axios'; @@ -33,7 +33,7 @@ type FeishuFileListResponse = { const feishuBaseUrl = process.env.FEISHU_BASE_URL || 'https://open.feishu.cn'; const logger = getLogger(LogCategories.MODULE.DATASET.API_DATASET); -export const useFeishuDatasetRequest = ({ feishuServer }: { feishuServer: FeishuServer }) => { +export const useFeishuDatasetRequest = ({ feishuServer }: { feishuServer: FeishuServerType }) => { const instance = createProxyAxios({ baseURL: feishuBaseUrl, timeout: 60000 @@ -150,7 +150,7 @@ export const useFeishuDatasetRequest = ({ feishuServer }: { feishuServer: Feishu apiFileId }: { apiFileId: string; - }): Promise => { + }): Promise => { const [{ content }, { document }] = await Promise.all([ request<{ content: string }>( `/open-apis/docx/v1/documents/${apiFileId}/raw_content`, diff --git a/packages/service/core/dataset/apiDataset/yuqueDataset/api.ts b/packages/service/core/dataset/apiDataset/yuqueDataset/api.ts index b251e7c68f..d1c0382886 100644 --- a/packages/service/core/dataset/apiDataset/yuqueDataset/api.ts +++ b/packages/service/core/dataset/apiDataset/yuqueDataset/api.ts @@ -1,7 +1,7 @@ import type { APIFileItemType, - ApiFileReadContentResponse, - YuqueServer, + ApiFileReadContentResponseType, + YuqueServerType, ApiDatasetDetailResponse } from '@fastgpt/global/core/dataset/apiDataset/type'; import { type Method } from 'axios'; @@ -42,7 +42,7 @@ type YuqueTocListResponse = { const yuqueBaseUrl = process.env.YUQUE_DATASET_BASE_URL || 'https://www.yuque.com'; -export const useYuqueDatasetRequest = ({ yuqueServer }: { yuqueServer: YuqueServer }) => { +export const useYuqueDatasetRequest = ({ yuqueServer }: { yuqueServer: YuqueServerType }) => { const logger = getLogger(LogCategories.MODULE.DATASET.API_DATASET); const instance = createProxyAxios({ baseURL: yuqueBaseUrl, @@ -202,7 +202,7 @@ export const useYuqueDatasetRequest = ({ yuqueServer }: { yuqueServer: YuqueServ apiFileId }: { apiFileId: string; - }): Promise => { + }): Promise => { if (typeof apiFileId !== 'string') return Promise.reject('Invalid file id'); const [parentId, fileId] = apiFileId.split(/-(.*?)-(.*)/); diff --git a/packages/service/core/dataset/search/controller.ts b/packages/service/core/dataset/search/controller.ts index 40c62d31b3..45a5eb8170 100644 --- a/packages/service/core/dataset/search/controller.ts +++ b/packages/service/core/dataset/search/controller.ts @@ -28,7 +28,7 @@ import { MongoDatasetCollectionTags } from '../tag/schema'; import { computeFilterIntersection } from './utils'; import { readFromSecondary } from '../../../common/mongo/utils'; import { MongoDatasetDataText } from '../data/dataTextSchema'; -import { type ChatItemType } from '@fastgpt/global/core/chat/type'; +import { type ChatItemMiniType } from '@fastgpt/global/core/chat/type'; import type { NodeInputKeyEnum } from '@fastgpt/global/core/workflow/constants'; import { datasetSearchQueryExtension } from './utils'; import type { RerankModelItemType } from '@fastgpt/global/core/ai/model.schema'; @@ -41,7 +41,7 @@ import { getLogger, LogCategories } from '../../../common/logger'; const logger = getLogger(LogCategories.MODULE.DATASET.DATA); export type SearchDatasetDataProps = { - histories: ChatItemType[]; + histories: ChatItemMiniType[]; teamId: string; uid?: string; tmbId?: string; diff --git a/packages/service/core/dataset/search/utils.ts b/packages/service/core/dataset/search/utils.ts index a8c185c582..efef1599b5 100644 --- a/packages/service/core/dataset/search/utils.ts +++ b/packages/service/core/dataset/search/utils.ts @@ -1,5 +1,5 @@ import { queryExtension } from '../../ai/functions/queryExtension'; -import { type ChatItemType } from '@fastgpt/global/core/chat/type'; +import { type ChatItemMiniType } from '@fastgpt/global/core/chat/type'; import { hashStr } from '@fastgpt/global/common/string/tools'; import { getLogger, LogCategories } from '../../../common/logger'; @@ -28,7 +28,7 @@ export const datasetSearchQueryExtension = async ({ llmModel?: string; embeddingModel?: string; extensionBg?: string; - histories?: ChatItemType[]; + histories?: ChatItemMiniType[]; }) => { const filterSamQuery = (queries: string[]) => { const set = new Set(); diff --git a/packages/service/core/workflow/dispatch/abandoned/runApp.ts b/packages/service/core/workflow/dispatch/abandoned/runApp.ts index e97e0b1840..76fce73e00 100644 --- a/packages/service/core/workflow/dispatch/abandoned/runApp.ts +++ b/packages/service/core/workflow/dispatch/abandoned/runApp.ts @@ -1,5 +1,5 @@ /* Abandoned */ -import type { ChatItemType } from '@fastgpt/global/core/chat/type'; +import type { ChatItemMiniType } from '@fastgpt/global/core/chat/type'; import type { ModuleDispatchProps } from '@fastgpt/global/core/workflow/runtime/type'; import { type SelectAppItemType } from '@fastgpt/global/core/workflow/template/system/abandoned/runApp/type'; import { runWorkflow } from '../index'; @@ -21,12 +21,12 @@ import { ReadPermissionVal } from '@fastgpt/global/support/permission/constant'; type Props = ModuleDispatchProps<{ [NodeInputKeyEnum.userChatInput]: string; - [NodeInputKeyEnum.history]?: ChatItemType[] | number; + [NodeInputKeyEnum.history]?: ChatItemMiniType[] | number; app: SelectAppItemType; }>; type Response = DispatchNodeResultType<{ [NodeOutputKeyEnum.answerText]: string; - [NodeOutputKeyEnum.history]: ChatItemType[]; + [NodeOutputKeyEnum.history]: ChatItemMiniType[]; }>; export const dispatchAppRequest = async (props: Props): Promise => { diff --git a/packages/service/core/workflow/dispatch/ai/agent/index.ts b/packages/service/core/workflow/dispatch/ai/agent/index.ts index 61f4a92d3c..8c1b3cc82b 100644 --- a/packages/service/core/workflow/dispatch/ai/agent/index.ts +++ b/packages/service/core/workflow/dispatch/ai/agent/index.ts @@ -11,7 +11,7 @@ import { getNodeErrResponse, getHistories } from '../../utils'; import type { AIChatItemValueItemType, ChatHistoryItemResType, - ChatItemType + ChatItemMiniType } from '@fastgpt/global/core/chat/type'; import { ChatRoleEnum } from '@fastgpt/global/core/chat/constants'; import { @@ -47,7 +47,7 @@ import { getLogger, LogCategories } from '../../../../../common/logger'; import { env } from '../../../../../env'; export type DispatchAgentModuleProps = ModuleDispatchProps<{ - [NodeInputKeyEnum.history]?: ChatItemType[]; + [NodeInputKeyEnum.history]?: ChatItemMiniType[]; [NodeInputKeyEnum.userChatInput]: string; [NodeInputKeyEnum.aiChatVision]?: boolean; diff --git a/packages/service/core/workflow/dispatch/ai/agent/sub/dataset/index.ts b/packages/service/core/workflow/dispatch/ai/agent/sub/dataset/index.ts index 5de3149441..931e439380 100644 --- a/packages/service/core/workflow/dispatch/ai/agent/sub/dataset/index.ts +++ b/packages/service/core/workflow/dispatch/ai/agent/sub/dataset/index.ts @@ -28,7 +28,7 @@ type DatasetSearchParams = { datasets: SelectedDatasetType[]; similarity: number; maxTokens: number; - searchMode: `${DatasetSearchModeEnum}`; + searchMode: DatasetSearchModeEnum; embeddingWeight?: number; usingReRank: boolean; rerankModel?: string; diff --git a/packages/service/core/workflow/dispatch/ai/agent/sub/file/utils.ts b/packages/service/core/workflow/dispatch/ai/agent/sub/file/utils.ts index 660b76f56f..0619b9c15f 100644 --- a/packages/service/core/workflow/dispatch/ai/agent/sub/file/utils.ts +++ b/packages/service/core/workflow/dispatch/ai/agent/sub/file/utils.ts @@ -3,7 +3,7 @@ import { SubAppIds } from '@fastgpt/global/core/workflow/node/agent/constants'; import { parseUrlToFileType } from '../../../../../utils/context'; import { getLogger, LogCategories } from '../../../../../../../common/logger'; import { getHistoryFileLinks } from '../../../../tools/readFiles'; -import type { ChatItemType } from '@fastgpt/global/core/chat/type'; +import type { ChatItemMiniType } from '@fastgpt/global/core/chat/type'; import { ChatFileTypeEnum } from '@fastgpt/global/core/chat/constants'; import z from 'zod'; @@ -41,7 +41,7 @@ export const formatFileInput = ({ fileUrls?: string[]; requestOrigin?: string; maxFiles: number; - histories: ChatItemType[]; + histories: ChatItemMiniType[]; useSkill: boolean; }): { filesMap: Record; diff --git a/packages/service/core/workflow/dispatch/ai/chat.ts b/packages/service/core/workflow/dispatch/ai/chat.ts index a7f80ed7eb..163533616b 100644 --- a/packages/service/core/workflow/dispatch/ai/chat.ts +++ b/packages/service/core/workflow/dispatch/ai/chat.ts @@ -1,5 +1,5 @@ import { filterGPTMessageByMaxContext } from '../../../ai/llm/utils'; -import type { ChatItemType, UserChatItemFileItemType } from '@fastgpt/global/core/chat/type'; +import type { ChatItemMiniType, UserChatItemFileItemType } from '@fastgpt/global/core/chat/type'; import { ChatRoleEnum } from '@fastgpt/global/core/chat/constants'; import { SseResponseEventEnum } from '@fastgpt/global/core/workflow/runtime/constants'; import { textAdaptGptResponse } from '@fastgpt/global/core/workflow/runtime/utils'; @@ -44,7 +44,7 @@ import { formatModelChars2Points } from '../../../../support/wallet/usage/utils' export type ChatProps = ModuleDispatchProps< AIChatNodeProps & { [NodeInputKeyEnum.userChatInput]?: string; - [NodeInputKeyEnum.history]?: ChatItemType[] | number; + [NodeInputKeyEnum.history]?: ChatItemMiniType[] | number; [NodeInputKeyEnum.aiChatDatasetQuote]?: SearchDataResponseItemType[]; } >; @@ -52,7 +52,7 @@ export type ChatResponse = DispatchNodeResultType< { [NodeOutputKeyEnum.answerText]: string; [NodeOutputKeyEnum.reasoningText]?: string; - [NodeOutputKeyEnum.history]: ChatItemType[]; + [NodeOutputKeyEnum.history]: ChatItemMiniType[]; }, { [NodeOutputKeyEnum.errorText]: string; @@ -339,7 +339,7 @@ async function getMultiInput({ usageId, runningUserInfo }: { - histories: ChatItemType[]; + histories: ChatItemMiniType[]; inputFiles: UserChatItemFileItemType[]; fileLinks?: string[]; stringQuoteText?: string; // file quote @@ -421,7 +421,7 @@ async function getChatMessages({ version?: string; useDatasetQuote: boolean; - histories: ChatItemType[]; + histories: ChatItemMiniType[]; systemPrompt: string; userChatInput: string; @@ -465,7 +465,7 @@ async function getChatMessages({ .filter(Boolean) .join('\n\n===---===---===\n\n'); - const messages: ChatItemType[] = [ + const messages: ChatItemMiniType[] = [ ...getSystemPrompt_ChatItemType(concatenateSystemPrompt), ...histories, { diff --git a/packages/service/core/workflow/dispatch/ai/classifyQuestion.ts b/packages/service/core/workflow/dispatch/ai/classifyQuestion.ts index ab029334e9..2141151fba 100644 --- a/packages/service/core/workflow/dispatch/ai/classifyQuestion.ts +++ b/packages/service/core/workflow/dispatch/ai/classifyQuestion.ts @@ -1,5 +1,5 @@ import { chats2GPTMessages } from '@fastgpt/global/core/chat/adapt'; -import type { ChatItemType } from '@fastgpt/global/core/chat/type'; +import type { ChatItemMiniType } from '@fastgpt/global/core/chat/type'; import { ChatRoleEnum } from '@fastgpt/global/core/chat/constants'; import type { ClassifyQuestionAgentItemType } from '@fastgpt/global/core/workflow/template/system/classifyQuestion/type'; import type { NodeInputKeyEnum } from '@fastgpt/global/core/workflow/constants'; @@ -21,7 +21,7 @@ const logger = getLogger(LogCategories.MODULE.WORKFLOW.AI); type Props = ModuleDispatchProps<{ [NodeInputKeyEnum.aiModel]: string; [NodeInputKeyEnum.aiSystemPrompt]?: string; - [NodeInputKeyEnum.history]?: ChatItemType[] | number; + [NodeInputKeyEnum.history]?: ChatItemMiniType[] | number; [NodeInputKeyEnum.userChatInput]: string; [NodeInputKeyEnum.agents]: ClassifyQuestionAgentItemType[]; }>; @@ -110,7 +110,7 @@ const completions = async ({ lastMemory, params: { agents, systemPrompt = '', userChatInput } }: ActionProps) => { - const messages: ChatItemType[] = [ + const messages: ChatItemMiniType[] = [ { obj: ChatRoleEnum.System, value: [ diff --git a/packages/service/core/workflow/dispatch/ai/extract.ts b/packages/service/core/workflow/dispatch/ai/extract.ts index b8b430b9de..66ceb8c943 100644 --- a/packages/service/core/workflow/dispatch/ai/extract.ts +++ b/packages/service/core/workflow/dispatch/ai/extract.ts @@ -1,6 +1,6 @@ import { chats2GPTMessages } from '@fastgpt/global/core/chat/adapt'; import { filterGPTMessageByMaxContext } from '../../../ai/llm/utils'; -import type { ChatItemType } from '@fastgpt/global/core/chat/type'; +import type { ChatItemMiniType } from '@fastgpt/global/core/chat/type'; import { ChatRoleEnum } from '@fastgpt/global/core/chat/constants'; import type { ContextExtractAgentItemType } from '@fastgpt/global/core/workflow/template/system/contextExtract/type'; import type { NodeInputKeyEnum } from '@fastgpt/global/core/workflow/constants'; @@ -34,7 +34,7 @@ import { createLLMResponse } from '../../../ai/llm/request'; import type { JsonSchemaPropertiesItemType } from '@fastgpt/global/core/app/jsonschema'; type Props = ModuleDispatchProps<{ - [NodeInputKeyEnum.history]?: ChatItemType[]; + [NodeInputKeyEnum.history]?: ChatItemMiniType[]; [NodeInputKeyEnum.contextExtractInput]: string; [NodeInputKeyEnum.extractKeys]: ContextExtractAgentItemType[]; [NodeInputKeyEnum.description]: string; @@ -188,7 +188,7 @@ const toolChoice = async (props: ActionProps) => { lastMemory } = props; - const messages: ChatItemType[] = [ + const messages: ChatItemMiniType[] = [ { obj: ChatRoleEnum.System, value: [ @@ -293,7 +293,7 @@ const completions = async (props: ActionProps) => { params: { content, description } } = props; - const messages: ChatItemType[] = [ + const messages: ChatItemMiniType[] = [ { obj: ChatRoleEnum.System, value: [ diff --git a/packages/service/core/workflow/dispatch/ai/tool/index.ts b/packages/service/core/workflow/dispatch/ai/tool/index.ts index c14f2a1553..3af8581afb 100644 --- a/packages/service/core/workflow/dispatch/ai/tool/index.ts +++ b/packages/service/core/workflow/dispatch/ai/tool/index.ts @@ -11,7 +11,7 @@ import { runToolCall } from './toolCall'; import { type DispatchToolModuleProps, type ToolNodeItemType } from './type'; import type { UserChatItemFileItemType, - ChatItemType, + ChatItemMiniType, UserChatItemValueItemType } from '@fastgpt/global/core/chat/type'; import { ChatRoleEnum } from '@fastgpt/global/core/chat/constants'; @@ -152,8 +152,8 @@ export const dispatchRunTools = async (props: DispatchToolModuleProps): Promise< .filter(Boolean) .join('\n\n===---===---===\n\n'); - const messages: ChatItemType[] = (() => { - const value: ChatItemType[] = [ + const messages: ChatItemMiniType[] = (() => { + const value: ChatItemMiniType[] = [ ...getSystemPrompt_ChatItemType(concatenateSystemPrompt), // Add file input prompt to histories ...chatHistories.map((item) => { @@ -319,7 +319,7 @@ const getMultiInput = async ({ uId }: { runningUserInfo: ChatDispatchProps['runningUserInfo']; - histories: ChatItemType[]; + histories: ChatItemMiniType[]; fileLinks?: string[]; requestOrigin?: string; maxFiles: number; diff --git a/packages/service/core/workflow/dispatch/ai/tool/type.ts b/packages/service/core/workflow/dispatch/ai/tool/type.ts index 724778301b..c02b0be1ad 100644 --- a/packages/service/core/workflow/dispatch/ai/tool/type.ts +++ b/packages/service/core/workflow/dispatch/ai/tool/type.ts @@ -6,13 +6,13 @@ import type { NodeInputKeyEnum } from '@fastgpt/global/core/workflow/constants'; import type { ModuleDispatchProps } from '@fastgpt/global/core/workflow/runtime/type'; import type { RuntimeNodeItemType } from '@fastgpt/global/core/workflow/runtime/type'; import type { DispatchFlowResponse } from '../../type'; -import type { AIChatItemValueItemType, ChatItemType } from '@fastgpt/global/core/chat/type'; +import type { AIChatItemValueItemType, ChatItemMiniType } from '@fastgpt/global/core/chat/type'; import type { ToolCallChildrenInteractive } from '@fastgpt/global/core/workflow/template/system/interactive/type'; import type { LLMModelItemType } from '@fastgpt/global/core/ai/model.schema'; import type { JSONSchemaInputType } from '@fastgpt/global/core/app/jsonschema'; export type DispatchToolModuleProps = ModuleDispatchProps<{ - [NodeInputKeyEnum.history]?: ChatItemType[]; + [NodeInputKeyEnum.history]?: ChatItemMiniType[]; [NodeInputKeyEnum.userChatInput]: string; [NodeInputKeyEnum.fileUrlList]?: string[]; diff --git a/packages/service/core/workflow/dispatch/child/runApp.ts b/packages/service/core/workflow/dispatch/child/runApp.ts index 42dbec2edf..53024b3e3c 100644 --- a/packages/service/core/workflow/dispatch/child/runApp.ts +++ b/packages/service/core/workflow/dispatch/child/runApp.ts @@ -1,4 +1,4 @@ -import type { ChatItemType } from '@fastgpt/global/core/chat/type'; +import type { ChatItemMiniType } from '@fastgpt/global/core/chat/type'; import type { ModuleDispatchProps } from '@fastgpt/global/core/workflow/runtime/type'; import { runWorkflow } from '../index'; import { ChatRoleEnum } from '@fastgpt/global/core/chat/constants'; @@ -25,14 +25,14 @@ import { getRunningUserInfoByTmbId } from '../../../../support/user/team/utils'; type Props = ModuleDispatchProps<{ [NodeInputKeyEnum.userChatInput]: string; - [NodeInputKeyEnum.history]?: ChatItemType[] | number; + [NodeInputKeyEnum.history]?: ChatItemMiniType[] | number; [NodeInputKeyEnum.fileUrlList]?: string[]; [NodeInputKeyEnum.forbidStream]?: boolean; [NodeInputKeyEnum.fileUrlList]?: string[]; }>; type Response = DispatchNodeResultType<{ [NodeOutputKeyEnum.answerText]: string; - [NodeOutputKeyEnum.history]: ChatItemType[]; + [NodeOutputKeyEnum.history]: ChatItemMiniType[]; }>; export const dispatchRunAppNode = async (props: Props): Promise => { diff --git a/packages/service/core/workflow/dispatch/dataset/search.ts b/packages/service/core/workflow/dispatch/dataset/search.ts index 0b35b4b36d..45f3c79784 100644 --- a/packages/service/core/workflow/dispatch/dataset/search.ts +++ b/packages/service/core/workflow/dispatch/dataset/search.ts @@ -23,7 +23,7 @@ type DatasetSearchProps = ModuleDispatchProps<{ [NodeInputKeyEnum.datasetSimilarity]: number; [NodeInputKeyEnum.datasetMaxTokens]: number; [NodeInputKeyEnum.userChatInput]?: string; - [NodeInputKeyEnum.datasetSearchMode]: `${DatasetSearchModeEnum}`; + [NodeInputKeyEnum.datasetSearchMode]: DatasetSearchModeEnum; [NodeInputKeyEnum.datasetSearchEmbeddingWeight]?: number; [NodeInputKeyEnum.datasetSearchUsingReRank]: boolean; diff --git a/packages/service/core/workflow/dispatch/tools/queryExternsion.ts b/packages/service/core/workflow/dispatch/tools/queryExternsion.ts index 309435a8ab..d8207cff1f 100644 --- a/packages/service/core/workflow/dispatch/tools/queryExternsion.ts +++ b/packages/service/core/workflow/dispatch/tools/queryExternsion.ts @@ -1,4 +1,4 @@ -import type { ChatItemType } from '@fastgpt/global/core/chat/type'; +import type { ChatItemMiniType } from '@fastgpt/global/core/chat/type'; import type { ModuleDispatchProps } from '@fastgpt/global/core/workflow/runtime/type'; import type { NodeInputKeyEnum } from '@fastgpt/global/core/workflow/constants'; import { NodeOutputKeyEnum } from '@fastgpt/global/core/workflow/constants'; @@ -13,7 +13,7 @@ import { type DispatchNodeResultType } from '@fastgpt/global/core/workflow/runti type Props = ModuleDispatchProps<{ [NodeInputKeyEnum.aiModel]: string; [NodeInputKeyEnum.aiSystemPrompt]?: string; - [NodeInputKeyEnum.history]?: ChatItemType[] | number; + [NodeInputKeyEnum.history]?: ChatItemMiniType[] | number; [NodeInputKeyEnum.userChatInput]: string; }>; type Response = DispatchNodeResultType<{ diff --git a/packages/service/core/workflow/dispatch/tools/readFiles.ts b/packages/service/core/workflow/dispatch/tools/readFiles.ts index 6f9f0710cc..db6034a884 100644 --- a/packages/service/core/workflow/dispatch/tools/readFiles.ts +++ b/packages/service/core/workflow/dispatch/tools/readFiles.ts @@ -10,7 +10,7 @@ import { detectFileEncoding } from '@fastgpt/global/common/file/tools'; import { parseUrlToFileType } from '../../utils/context'; import { readFileContentByBuffer } from '../../../../common/file/read/utils'; import { ChatRoleEnum } from '@fastgpt/global/core/chat/constants'; -import { type ChatItemType } from '@fastgpt/global/core/chat/type'; +import { type ChatItemMiniType } from '@fastgpt/global/core/chat/type'; import { addDays } from 'date-fns'; import { getNodeErrResponse } from '../utils'; import { isInternalAddress, PRIVATE_URL_TEXT } from '../../../../common/system/utils'; @@ -105,7 +105,7 @@ export const dispatchReadFiles = async (props: Props): Promise => { } }; -export const getHistoryFileLinks = (histories: ChatItemType[]) => { +export const getHistoryFileLinks = (histories: ChatItemMiniType[]) => { return histories .filter((item) => { if (item.obj === ChatRoleEnum.Human) { diff --git a/packages/service/core/workflow/dispatch/utils.ts b/packages/service/core/workflow/dispatch/utils.ts index 2ae78da5ef..4440e62b24 100644 --- a/packages/service/core/workflow/dispatch/utils.ts +++ b/packages/service/core/workflow/dispatch/utils.ts @@ -1,7 +1,7 @@ import path from 'path'; import { getErrText } from '@fastgpt/global/common/error/utils'; import { ChatRoleEnum } from '@fastgpt/global/core/chat/constants'; -import type { ChatItemType } from '@fastgpt/global/core/chat/type'; +import type { ChatItemMiniType } from '@fastgpt/global/core/chat/type'; import { NodeOutputKeyEnum, VariableInputEnum } from '@fastgpt/global/core/workflow/constants'; import type { VariableItemType } from '@fastgpt/global/core/app/type'; import { encryptSecret } from '../../../common/secret/aes256gcm'; @@ -228,7 +228,10 @@ export const filterToolNodeIdByEdges = ({ .map((edge) => edge.target); }; -export const getHistories = (history?: ChatItemType[] | number, histories: ChatItemType[] = []) => { +export const getHistories = ( + history?: ChatItemMiniType[] | number, + histories: ChatItemMiniType[] = [] +) => { if (!history) return []; // Select reference history if (Array.isArray(history)) return history; diff --git a/packages/service/env.ts b/packages/service/env.ts index 526de1c59d..b3a5591b81 100644 --- a/packages/service/env.ts +++ b/packages/service/env.ts @@ -57,6 +57,9 @@ export const env = createEnv({ APP_FOLDER_MAX_AMOUNT: z.coerce.number().int().positive().default(1000), DATASET_FOLDER_MAX_AMOUNT: z.coerce.number().int().positive().default(1000), + // ===== Security ===== + CHECK_INTERNAL_IP: BoolSchema.default(false).meta({ description: '是否启用内网 IP 检查' }), + // Beta features // Whether the Skill feature is enabled (frontend entries + backend runtime) SHOW_SKILL: BoolSchema.default(false) diff --git a/packages/service/support/outLink/runtime/auth.ts b/packages/service/support/outLink/runtime/auth.ts index 476a924fe6..298cf1dc3b 100644 --- a/packages/service/support/outLink/runtime/auth.ts +++ b/packages/service/support/outLink/runtime/auth.ts @@ -6,7 +6,7 @@ import type { } from '@fastgpt/global/support/outLink/api'; import { axios } from '../../../common/api/axios'; import { OutLinkErrEnum } from '@fastgpt/global/common/error/code/outLink'; -import type { OutLinkSchema } from '@fastgpt/global/support/outLink/type'; +import type { OutLinkSchemaType } from '@fastgpt/global/support/outLink/type'; import { addMinutes } from 'date-fns'; import { S3_KEY_PATH_INVALID_CHARS } from '../../../common/s3/constants'; import { UserError } from '@fastgpt/global/common/error/utils'; @@ -49,7 +49,7 @@ export const authOutLinkInit = async ({ return { uid }; }; -const authIpLimit = async ({ ip, outLink }: { ip: string; outLink: OutLinkSchema }) => { +const authIpLimit = async ({ ip, outLink }: { ip: string; outLink: OutLinkSchemaType }) => { if (!outLink.limit || !outLink.limit.QPM) { return; } diff --git a/packages/service/support/outLink/runtime/utils.ts b/packages/service/support/outLink/runtime/utils.ts index 4b99dca11f..b509e5ec08 100644 --- a/packages/service/support/outLink/runtime/utils.ts +++ b/packages/service/support/outLink/runtime/utils.ts @@ -7,7 +7,7 @@ import { storeEdges2RuntimeEdges, storeNodes2RuntimeNodes } from '@fastgpt/global/core/workflow/runtime/utils'; -import type { OutlinkAppType, OutLinkSchema } from '@fastgpt/global/support/outLink/type'; +import type { OutlinkAppType, OutLinkSchemaType } from '@fastgpt/global/support/outLink/type'; import { getAppLatestVersion } from '../../../core/app/version/controller'; import { MongoApp } from '../../../core/app/schema'; import { getChatItems } from '../../../core/chat/controller'; @@ -69,7 +69,7 @@ export const resetChat = ({ appId, chatId }: { appId: string; chatId: string }) }; export type outLinkInvokeChatProps = { - outLinkConfig: OutLinkSchema; + outLinkConfig: OutLinkSchemaType; chatId: string; // specific chat query: UserChatItemValueItemType[]; res?: NextApiResponse; diff --git a/packages/service/support/outLink/schema.ts b/packages/service/support/outLink/schema.ts index 11a02a9f43..27f807dd52 100644 --- a/packages/service/support/outLink/schema.ts +++ b/packages/service/support/outLink/schema.ts @@ -1,6 +1,6 @@ import { connectionMongo, getMongoModel } from '../../common/mongo'; const { Schema } = connectionMongo; -import { type OutLinkSchema as SchemaType } from '@fastgpt/global/support/outLink/type'; +import { type OutLinkSchemaType } from '@fastgpt/global/support/outLink/type'; import { TeamCollectionName, TeamMemberCollectionName @@ -111,16 +111,12 @@ OutLinkSchema.virtual('associatedApp', { const logger = getLogger(LogCategories.INFRA.MONGO); -try { - OutLinkSchema.index({ shareId: -1 }); - OutLinkSchema.index({ teamId: 1, tmbId: 1, appId: 1 }); - // Wechat polling recovery: find online channels on startup - OutLinkSchema.index( - { type: 1, 'app.status': 1 }, - { partialFilterExpression: { type: 'wechat', 'app.status': 'online' } } - ); -} catch (error) { - logger.error('Failed to build outlink indexes', { error }); -} +OutLinkSchema.index({ shareId: -1 }); +OutLinkSchema.index({ teamId: 1, tmbId: 1, appId: 1 }); +// Wechat polling recovery: find online channels on startup +OutLinkSchema.index( + { type: 1, 'app.status': 1 }, + { partialFilterExpression: { type: 'wechat', 'app.status': 'online' } } +); -export const MongoOutLink = getMongoModel('outlinks', OutLinkSchema); +export const MongoOutLink = getMongoModel('outlinks', OutLinkSchema); diff --git a/packages/service/support/outLink/wechat/mq.ts b/packages/service/support/outLink/wechat/mq.ts index 3c62cdd1e8..6ee6f5bc2b 100644 --- a/packages/service/support/outLink/wechat/mq.ts +++ b/packages/service/support/outLink/wechat/mq.ts @@ -2,7 +2,7 @@ import { getWorker, getQueue, QueueNames, type Job } from '../../../common/bullm import { getLogger, LogCategories } from '../../../common/logger'; import { ILinkClient } from './ilinkClient'; import type { WechatPollJobData } from './type'; -import type { OutLinkSchema, WechatAppType } from '@fastgpt/global/support/outLink/type'; +import type { OutLinkSchemaType, WechatAppType } from '@fastgpt/global/support/outLink/type'; import { MongoOutLink } from '../../../support/outLink/schema'; import { outlinkInvokeChat } from '../../../support/outLink/runtime/utils'; import { setRedisCache, getRedisCache } from '../../../common/redis/cache'; @@ -23,7 +23,7 @@ async function processWechatPollJob(job: Job): Promise // 1. 获取渠道配置 const outLink = (await MongoOutLink.findOne({ shareId - }).lean()) as unknown as OutLinkSchema; + }).lean()) as unknown as OutLinkSchemaType; if (!outLink || !outLink.app) { logger.warn('OutLink not found, stop polling', { shareId }); return; @@ -111,7 +111,7 @@ async function processWechatPollJob(job: Job): Promise /* ============ 处理单个用户分组 ============ */ async function processUserGroup( - outLink: OutLinkSchema, + outLink: OutLinkSchemaType, group: ParsedMessageGroup ): Promise { const app = outLink.app; diff --git a/packages/service/support/permission/publish/authLink.ts b/packages/service/support/permission/publish/authLink.ts index 479c3babbc..0e06889900 100644 --- a/packages/service/support/permission/publish/authLink.ts +++ b/packages/service/support/permission/publish/authLink.ts @@ -1,5 +1,5 @@ import { type AppDetailType } from '@fastgpt/global/core/app/type'; -import { type OutlinkAppType, type OutLinkSchema } from '@fastgpt/global/support/outLink/type'; +import { type OutlinkAppType, type OutLinkSchemaType } from '@fastgpt/global/support/outLink/type'; import { MongoOutLink } from '../../outLink/schema'; import { OutLinkErrEnum } from '@fastgpt/global/common/error/code/outLink'; import { OwnerPermissionVal } from '@fastgpt/global/support/permission/constant'; @@ -17,7 +17,7 @@ export async function authOutLinkCrud({ }): Promise< AuthResponseType & { app: AppDetailType; - outLink: OutLinkSchema; + outLink: OutLinkSchemaType; } > { const result = await parseHeaderCert(props); @@ -62,7 +62,7 @@ export async function authOutLinkValid({ if (!shareId) { return Promise.reject(OutLinkErrEnum.linkUnInvalid); } - const outLinkConfig = await MongoOutLink.findOne({ shareId }).lean>(); + const outLinkConfig = await MongoOutLink.findOne({ shareId }).lean>(); if (!outLinkConfig) { return Promise.reject(OutLinkErrEnum.linkUnInvalid); diff --git a/packages/web/common/fetch/type.ts b/packages/web/common/fetch/type.ts deleted file mode 100644 index 237acbc6a3..0000000000 --- a/packages/web/common/fetch/type.ts +++ /dev/null @@ -1,16 +0,0 @@ -import type { PaginationProps, PaginationResponseType } from '@fastgpt/global/openapi/api'; -export type { PaginationProps, PaginationResponseType as PaginationResponse }; - -export type LinkedPaginationProps = T & { - pageSize: number; - anchor?: A; - initialId?: string; - nextId?: string; - prevId?: string; -}; - -export type LinkedListResponse = { - list: Array; - hasMorePrev: boolean; - hasMoreNext: boolean; -}; diff --git a/packages/web/hooks/useLinkedScroll.tsx b/packages/web/hooks/useLinkedScroll.tsx index de5c9b8efd..ea15a78df7 100644 --- a/packages/web/hooks/useLinkedScroll.tsx +++ b/packages/web/hooks/useLinkedScroll.tsx @@ -1,5 +1,5 @@ import { useCallback, useEffect, useRef, useState, type ReactNode } from 'react'; -import { type LinkedListResponse, type LinkedPaginationProps } from '../common/fetch/type'; +import { type LinkedListResponse, type LinkedPaginationProps } from '@fastgpt/global/openapi/api'; import { Box, type BoxProps } from '@chakra-ui/react'; import { useTranslation } from 'next-i18next'; import { useScroll, useMemoizedFn, useDebounceEffect, useLatest } from 'ahooks'; diff --git a/packages/web/hooks/usePagination.tsx b/packages/web/hooks/usePagination.tsx index 545a271536..23b4404447 100644 --- a/packages/web/hooks/usePagination.tsx +++ b/packages/web/hooks/usePagination.tsx @@ -23,7 +23,7 @@ import { useThrottleEffect } from 'ahooks'; -import { type PaginationProps, type PaginationResponse } from '../common/fetch/type'; +import { type PaginationProps, type PaginationResponse } from '@fastgpt/global/openapi/api'; import MyMenu from '../components/common/MyMenu'; import { useSystem } from './useSystem'; import { useRouter } from 'next/router'; diff --git a/projects/app/src/components/core/chat/ChatContainer/ChatBox/Provider.tsx b/projects/app/src/components/core/chat/ChatContainer/ChatBox/Provider.tsx index 9e1e5e7329..65548b5e8c 100644 --- a/projects/app/src/components/core/chat/ChatContainer/ChatBox/Provider.tsx +++ b/projects/app/src/components/core/chat/ChatContainer/ChatBox/Provider.tsx @@ -18,8 +18,7 @@ import { defaultWhisperConfig } from '@fastgpt/global/core/app/constants'; import { createContext, useContextSelector } from 'use-context-selector'; -import { VariableInputEnum } from '@fastgpt/global/core/workflow/constants'; -import { getChatResData } from '@/web/core/chat/api'; +import { getChatResData } from '@/web/core/chat/record/api'; import { ChatItemContext } from '@/web/core/chat/context/chatItemContext'; import { ChatRecordContext } from '@/web/core/chat/context/chatRecordContext'; import { useCreation } from 'ahooks'; diff --git a/projects/app/src/components/core/chat/ChatContainer/ChatBox/components/QuoteList.tsx b/projects/app/src/components/core/chat/ChatContainer/ChatBox/components/QuoteList.tsx index 92ce94f0f0..101955f670 100644 --- a/projects/app/src/components/core/chat/ChatContainer/ChatBox/components/QuoteList.tsx +++ b/projects/app/src/components/core/chat/ChatContainer/ChatBox/components/QuoteList.tsx @@ -8,7 +8,7 @@ import { WorkflowRuntimeContext } from '../../context/workflowRuntimeContext'; import { ChatItemContext } from '@/web/core/chat/context/chatItemContext'; import { useRequest } from '@fastgpt/web/hooks/useRequest'; import { useChatStore } from '@/web/core/chat/context/useChatStore'; -import { getQuoteDataList } from '@/web/core/chat/api'; +import { getQuoteDataList } from '@/web/core/chat/record/api'; const QuoteList = React.memo(function QuoteList({ chatItemDataId = '', diff --git a/projects/app/src/components/core/chat/ChatContainer/ChatBox/hooks/useChatBox.tsx b/projects/app/src/components/core/chat/ChatContainer/ChatBox/hooks/useChatBox.tsx index dd33cab9d9..8e13052abd 100644 --- a/projects/app/src/components/core/chat/ChatContainer/ChatBox/hooks/useChatBox.tsx +++ b/projects/app/src/components/core/chat/ChatContainer/ChatBox/hooks/useChatBox.tsx @@ -1,5 +1,5 @@ import { type ExportChatType } from '@/types/chat'; -import { type ChatItemType } from '@fastgpt/global/core/chat/type'; +import { type ChatItemMiniType } from '@fastgpt/global/core/chat/type'; import { useCallback } from 'react'; import { htmlTemplate } from '@/web/core/chat/constants'; import { fileDownload } from '@/web/common/file/utils'; @@ -7,7 +7,7 @@ import { useTranslation } from 'next-i18next'; export const useChatBox = () => { const { t } = useTranslation(); const onExportChat = useCallback( - ({ type, history }: { type: ExportChatType; history: ChatItemType[] }) => { + ({ type, history }: { type: ExportChatType; history: ChatItemMiniType[] }) => { const getHistoryHtml = () => { const historyDom = document.getElementById('history'); if (!historyDom) return; diff --git a/projects/app/src/components/core/chat/ChatContainer/ChatBox/index.tsx b/projects/app/src/components/core/chat/ChatContainer/ChatBox/index.tsx index f7e9ac2a21..0160745911 100644 --- a/projects/app/src/components/core/chat/ChatContainer/ChatBox/index.tsx +++ b/projects/app/src/components/core/chat/ChatContainer/ChatBox/index.tsx @@ -14,7 +14,7 @@ import type { import type { ChatSiteItemType } from './type'; import { useToast } from '@fastgpt/web/hooks/useToast'; import { getErrText } from '@fastgpt/global/common/error/utils'; -import { Box, Button, Checkbox, Flex } from '@chakra-ui/react'; +import { Box, Checkbox, Flex } from '@chakra-ui/react'; import { EventNameEnum, eventBus } from '@/web/common/utils/eventbus'; import { chats2GPTMessages } from '@fastgpt/global/core/chat/adapt'; import { useForm } from 'react-hook-form'; @@ -26,7 +26,7 @@ import { updateChatUserFeedback, updateFeedbackReadStatus } from '@/web/core/chat/feedback/api'; -import { delChatRecordById } from '@/web/core/chat/api'; +import { delChatRecordById } from '@/web/core/chat/record/api'; import type { AdminMarkType } from './components/SelectMarkCollection'; import MyTooltip from '@fastgpt/web/components/common/MyTooltip'; import { postQuestionGuide } from '@/web/core/ai/api'; diff --git a/projects/app/src/global/core/api/datasetReq.ts b/projects/app/src/global/core/api/datasetReq.ts index f8b71561b2..264a3801db 100644 --- a/projects/app/src/global/core/api/datasetReq.ts +++ b/projects/app/src/global/core/api/datasetReq.ts @@ -1,5 +1,5 @@ import { DatasetCollectionTypeEnum, DatasetTypeEnum } from '@fastgpt/global/core/dataset/constants'; -import type { PaginationProps } from '@fastgpt/web/common/fetch/type'; +import type { PaginationProps } from '@fastgpt/global/openapi/api'; import type { ParentIdType } from '@fastgpt/global/common/parentFolder/type'; /* ===== dataset ===== */ diff --git a/projects/app/src/global/core/chat/api.ts b/projects/app/src/global/core/chat/api.ts index efc7194d0c..3ed3f12c95 100644 --- a/projects/app/src/global/core/chat/api.ts +++ b/projects/app/src/global/core/chat/api.ts @@ -1,61 +1,7 @@ -import type { AppChatConfigType, AppTTSConfigType } from '@fastgpt/global/core/app/type'; -import type { AdminFbkType } from '@fastgpt/global/core/chat/type'; -import type { OutLinkChatAuthProps } from '@fastgpt/global/support/permission/chat'; -import type { AppTypeEnum } from '@fastgpt/global/core/app/constants'; -import type { GetChatTypeEnum } from '@/global/core/chat/constants'; -import type { ChatSourceEnum } from '@fastgpt/global/core/chat/constants'; -import type { FlowNodeInputItemType } from '@fastgpt/global/core/workflow/type/io'; - -export type GetChatSpeechProps = OutLinkChatAuthProps & { - appId: string; - ttsConfig: AppTTSConfigType; - input: string; - shareId?: string; -}; - /* ---------- chat ----------- */ - -export type GetChatRecordsProps = OutLinkChatAuthProps & { - appId: string; - chatId?: string; - loadCustomFeedbacks?: boolean; - type?: `${GetChatTypeEnum}`; - includeDeleted?: boolean; -}; - -export type InitOutLinkChatProps = { - chatId?: string; - shareId: string; - outLinkUid: string; -}; export type InitTeamChatProps = { teamId: string; appId: string; chatId?: string; teamToken: string; }; -export type InitChatResponse = { - chatId?: string; - appId: string; - userAvatar?: string; - title?: string; - variables?: Record; - app: { - chatConfig?: AppChatConfigType; - chatModels?: string[]; - name: string; - avatar: string; - intro: string; - canUse?: boolean; - type: `${AppTypeEnum}`; - pluginInputs: FlowNodeInputItemType[]; - }; -}; - -/* -------- chat item ---------- */ -export type DeleteChatItemProps = OutLinkChatAuthProps & { - appId: string; - chatId: string; - contentId?: string; - delFile?: boolean; -}; diff --git a/projects/app/src/global/core/chat/constants.ts b/projects/app/src/global/core/chat/constants.ts index dfca254b80..68c60ddebd 100644 --- a/projects/app/src/global/core/chat/constants.ts +++ b/projects/app/src/global/core/chat/constants.ts @@ -1,7 +1,7 @@ import { AppTypeEnum } from '@fastgpt/global/core/app/constants'; -import { type InitChatResponse } from './api'; +import type { InitChatResponseType } from '@fastgpt/global/openapi/core/chat/controler/api'; -export const defaultChatData: InitChatResponse = { +export const defaultChatData: InitChatResponseType = { chatId: '', appId: '', app: { @@ -15,10 +15,3 @@ export const defaultChatData: InitChatResponse = { title: '', variables: {} }; - -export enum GetChatTypeEnum { - normal = 'normal', - outLink = 'outLink', - team = 'team', - home = 'home' -} diff --git a/projects/app/src/global/core/chat/utils.ts b/projects/app/src/global/core/chat/utils.ts index 539510f700..17208f77c6 100644 --- a/projects/app/src/global/core/chat/utils.ts +++ b/projects/app/src/global/core/chat/utils.ts @@ -1,7 +1,7 @@ import { ChatRoleEnum } from '@fastgpt/global/core/chat/constants'; import type { ChatHistoryItemResType, - ChatItemType, + ChatItemMiniType, ToolCiteLinksType, ErrorTextItemType } from '@fastgpt/global/core/chat/type'; @@ -14,9 +14,9 @@ export const isLLMNode = (item: ChatHistoryItemResType) => item.moduleType === FlowNodeTypeEnum.chatNode || item.moduleType === FlowNodeTypeEnum.toolCall; export function transformPreviewHistories( - histories: ChatItemType[], + histories: ChatItemMiniType[], responseDetail: boolean -): ChatItemType[] { +): ChatItemMiniType[] { return histories.map((item) => { return { ...addStatisticalDataToHistoryItem(item), @@ -47,7 +47,7 @@ const extractCitationIdsFromText = (text: string): string[] => { return Array.from(new Set(ids)); }; -export function addStatisticalDataToHistoryItem(historyItem: ChatItemType) { +export function addStatisticalDataToHistoryItem(historyItem: ChatItemMiniType) { if (historyItem.obj !== ChatRoleEnum.AI) return historyItem; if (historyItem.totalQuoteList !== undefined || historyItem.toolCiteLinks !== undefined) return historyItem; diff --git a/projects/app/src/global/core/dataset/api.ts b/projects/app/src/global/core/dataset/api.ts index fe1cc1c5bd..0ea7c91b9c 100644 --- a/projects/app/src/global/core/dataset/api.ts +++ b/projects/app/src/global/core/dataset/api.ts @@ -2,38 +2,10 @@ import type { PushDatasetDataChunkProps, PushDatasetDataResponse } from '@fastgpt/global/core/dataset/api'; -import type { - APIFileServer, - FeishuServer, - YuqueServer -} from '@fastgpt/global/core/dataset/apiDataset/type'; -import type { - DatasetSearchModeEnum, - DatasetTypeEnum -} from '@fastgpt/global/core/dataset/constants'; -import { - DatasetSourceReadTypeEnum, - ImportDataSourceEnum, - TrainingModeEnum -} from '@fastgpt/global/core/dataset/constants'; -import type { SearchDataResponseItemType } from '@fastgpt/global/core/dataset/type'; +import type { DatasetTypeEnum } from '@fastgpt/global/core/dataset/constants'; import type { ApiDatasetServerType } from '@fastgpt/global/core/dataset/apiDataset/type'; -import { DatasetDataIndexItemType } from '@fastgpt/global/core/dataset/type'; -import type { NodeInputKeyEnum } from '@fastgpt/global/core/workflow/constants'; -import { PermissionValueType } from '@fastgpt/global/support/permission/type'; /* ================= dataset ===================== */ -export type CreateDatasetParams = { - parentId?: string; - type: DatasetTypeEnum; - name: string; - intro: string; - avatar: string; - vectorModel?: string; - agentModel?: string; - vlmModel?: string; - apiDatasetServer?: ApiDatasetServerType; -}; export type RebuildEmbeddingProps = { datasetId: string; @@ -50,38 +22,3 @@ export type CreateCollectionResponse = Promise<{ export type InsertOneDatasetDataProps = PushDatasetDataChunkProps & { collectionId: string; }; - -/* -------------- search ---------------- */ -export type SearchTestProps = { - datasetId: string; - text: string; - [NodeInputKeyEnum.datasetSimilarity]?: number; - [NodeInputKeyEnum.datasetMaxTokens]?: number; - - [NodeInputKeyEnum.datasetSearchMode]?: `${DatasetSearchModeEnum}`; - [NodeInputKeyEnum.datasetSearchEmbeddingWeight]?: number; - - [NodeInputKeyEnum.datasetSearchUsingReRank]?: boolean; - [NodeInputKeyEnum.datasetSearchRerankModel]?: string; - [NodeInputKeyEnum.datasetSearchRerankWeight]?: number; - - [NodeInputKeyEnum.datasetSearchUsingExtensionQuery]?: boolean; - [NodeInputKeyEnum.datasetSearchExtensionModel]?: string; - [NodeInputKeyEnum.datasetSearchExtensionBg]?: string; - - [NodeInputKeyEnum.datasetDeepSearch]?: boolean; - [NodeInputKeyEnum.datasetDeepSearchModel]?: string; - [NodeInputKeyEnum.datasetDeepSearchMaxTimes]?: number; - [NodeInputKeyEnum.datasetDeepSearchBg]?: string; -}; -export type SearchTestResponse = { - list: SearchDataResponseItemType[]; - duration: string; - limit: number; - searchMode: `${DatasetSearchModeEnum}`; - usingReRank: boolean; - similarity: number; - queryExtensionModel?: string; -}; - -/* =========== training =========== */ diff --git a/projects/app/src/global/core/dataset/type.ts b/projects/app/src/global/core/dataset/type.ts index e460e5295c..13e328a0f9 100644 --- a/projects/app/src/global/core/dataset/type.ts +++ b/projects/app/src/global/core/dataset/type.ts @@ -1,44 +1,44 @@ -import { ParentTreePathItemType } from '@fastgpt/global/common/parentFolder/type'; -import type { DatasetCollectionSchemaType } from '@fastgpt/global/core/dataset/type'; -import { DatasetDataSchemaType, DatasetTagType } from '@fastgpt/global/core/dataset/type'; -import type { DatasetPermission } from '@fastgpt/global/support/permission/dataset/controller'; - -/* ================= dataset ===================== */ +import { ObjectIdSchema } from '@fastgpt/global/common/type/mongo'; +import { DatasetCollectionSchema } from '@fastgpt/global/core/dataset/type'; +import { PermissionSchema } from '@fastgpt/global/support/permission/controller'; +import { DatasetPermission } from '@fastgpt/global/support/permission/dataset/controller'; +import z from 'zod'; /* ================= collection ===================== */ -export type DatasetCollectionsListItemType = { - _id: string; - parentId?: DatasetCollectionSchemaType['parentId']; - tmbId: DatasetCollectionSchemaType['tmbId']; - name: DatasetCollectionSchemaType['name']; - type: DatasetCollectionSchemaType['type']; - createTime: DatasetCollectionSchemaType['createTime']; - updateTime: DatasetCollectionSchemaType['updateTime']; - forbid?: DatasetCollectionSchemaType['forbid']; - trainingType?: DatasetCollectionSchemaType['trainingType']; - tags?: string[]; +export const DatasetCollectionsListItemSchema = z.object({ + _id: ObjectIdSchema.meta({ description: '集合 ID' }), + parentId: DatasetCollectionSchema.shape.parentId, + tmbId: DatasetCollectionSchema.shape.tmbId, + name: DatasetCollectionSchema.shape.name, + type: DatasetCollectionSchema.shape.type, + createTime: DatasetCollectionSchema.shape.createTime, + updateTime: DatasetCollectionSchema.shape.updateTime, + forbid: DatasetCollectionSchema.shape.forbid, + trainingType: DatasetCollectionSchema.shape.trainingType, + tags: z.array(z.string()).optional().meta({ description: '标签' }), - externalFileId?: string; + externalFileId: z.string().optional().meta({ description: '外部文件 ID' }), - fileId?: string; - rawLink?: string; - permission: DatasetPermission; - - dataAmount: number; - trainingAmount: number; - hasError?: boolean; -}; + fileId: z.string().optional().meta({ description: '文件 ID' }), + rawLink: z.string().optional().meta({ description: '原始链接' }), + permission: PermissionSchema, + dataAmount: z.number().meta({ description: '数据数量' }), + trainingAmount: z.number().meta({ description: '训练数量' }), + hasError: z.boolean().optional().meta({ description: '是否错误' }) +}); +export type DatasetCollectionsListItemType = z.infer; /* ================= data ===================== */ -export type DatasetDataListItemType = { - _id: string; - datasetId: string; - collectionId: string; - q?: string; - a?: string; - imageId?: string; - imageSize?: number; - imagePreviewUrl?: string; //image preview url - chunkIndex?: number; - updated?: boolean; -}; +export const DatasetDataListItemSchema = z.object({ + _id: ObjectIdSchema.meta({ description: '数据 ID' }), + datasetId: ObjectIdSchema.meta({ description: '数据集 ID' }), + collectionId: ObjectIdSchema.meta({ description: '集合 ID' }), + q: z.string().optional().meta({ description: '问题' }), + a: z.string().optional().meta({ description: '答案' }), + imageId: z.string().optional().meta({ description: '图片 ID' }), + imageSize: z.number().optional().meta({ description: '图片大小' }), + imagePreviewUrl: z.string().optional().meta({ description: '图片预览 URL' }), + chunkIndex: z.number().optional().meta({ description: '块索引' }), + updated: z.boolean().optional().meta({ description: '是否更新' }) +}); +export type DatasetDataListItemType = z.infer; diff --git a/projects/app/src/global/core/workflow/api.ts b/projects/app/src/global/core/workflow/api.ts index 63f123bc12..39e7d61fa3 100644 --- a/projects/app/src/global/core/workflow/api.ts +++ b/projects/app/src/global/core/workflow/api.ts @@ -2,7 +2,7 @@ import type { AppSchemaType } from '@fastgpt/global/core/app/type'; import type { ChatHistoryItemResType, UserChatItemValueItemType, - ChatItemType + ChatItemMiniType } from '@fastgpt/global/core/chat/type'; import type { RuntimeNodeItemType } from '@fastgpt/global/core/workflow/runtime/type'; import type { WorkflowInteractiveResponseType } from '@fastgpt/global/core/workflow/template/system/interactive/type'; @@ -20,7 +20,7 @@ export type PostWorkflowDebugProps = { variables: Record; appId: string; query?: UserChatItemValueItemType[]; - history?: ChatItemType[]; + history?: ChatItemMiniType[]; chatConfig?: AppSchemaType['chatConfig']; usageId?: string; }; diff --git a/projects/app/src/pageComponents/account/team/GroupManage/GroupManageMember.tsx b/projects/app/src/pageComponents/account/team/GroupManage/GroupManageMember.tsx index 9505d2dbab..987faabadf 100644 --- a/projects/app/src/pageComponents/account/team/GroupManage/GroupManageMember.tsx +++ b/projects/app/src/pageComponents/account/team/GroupManage/GroupManageMember.tsx @@ -16,7 +16,7 @@ import SearchInput from '@fastgpt/web/components/common/Input/SearchInput'; import { type MemberGroupListItemType } from '@fastgpt/global/support/permission/memberGroup/type'; import { getTeamMembers } from '@/web/support/user/team/api'; import { type TeamMemberItemType } from '@fastgpt/global/support/user/team/type'; -import { type PaginationResponse } from '@fastgpt/web/common/fetch/type'; +import { type PaginationResponse } from '@fastgpt/global/openapi/api'; import { useScrollPagination } from '@fastgpt/web/hooks/useScrollPagination'; import _ from 'lodash'; import MemberItemCard from '@/components/support/permission/MemberManager/MemberItemCard'; diff --git a/projects/app/src/pageComponents/account/team/GroupManage/GroupTransferOwnerModal.tsx b/projects/app/src/pageComponents/account/team/GroupManage/GroupTransferOwnerModal.tsx index 1e13e329d3..950af55200 100644 --- a/projects/app/src/pageComponents/account/team/GroupManage/GroupTransferOwnerModal.tsx +++ b/projects/app/src/pageComponents/account/team/GroupManage/GroupTransferOwnerModal.tsx @@ -22,7 +22,7 @@ import { type MemberGroupListItemType } from '@fastgpt/global/support/permission import { GetSearchUserGroupOrg } from '@/web/support/user/api'; import { type Omit } from '@fastgpt/web/components/common/DndDrag'; import { getTeamMembers } from '@/web/support/user/team/api'; -import { type PaginationResponse } from '@fastgpt/web/common/fetch/type'; +import { type PaginationResponse } from '@fastgpt/global/openapi/api'; import { useScrollPagination } from '@fastgpt/web/hooks/useScrollPagination'; import _ from 'lodash'; diff --git a/projects/app/src/pageComponents/account/team/MemberTable.tsx b/projects/app/src/pageComponents/account/team/MemberTable.tsx index 7c3a1d3931..62c1fcea30 100644 --- a/projects/app/src/pageComponents/account/team/MemberTable.tsx +++ b/projects/app/src/pageComponents/account/team/MemberTable.tsx @@ -44,7 +44,7 @@ import { type TeamMemberItemType } from '@fastgpt/global/support/user/team/type' import { useToast } from '@fastgpt/web/hooks/useToast'; import MyBox from '@fastgpt/web/components/common/MyBox'; import { useScrollPagination } from '@fastgpt/web/hooks/useScrollPagination'; -import { type PaginationResponse } from '@fastgpt/web/common/fetch/type'; +import { type PaginationResponse } from '@fastgpt/global/openapi/api'; import _ from 'lodash'; import MySelect from '@fastgpt/web/components/common/MySelect'; import { useEditTitle } from '@/web/common/hooks/useEditTitle'; diff --git a/projects/app/src/pageComponents/account/team/TransferOwnershipModal.tsx b/projects/app/src/pageComponents/account/team/TransferOwnershipModal.tsx index 61cb647b80..d61d32213d 100644 --- a/projects/app/src/pageComponents/account/team/TransferOwnershipModal.tsx +++ b/projects/app/src/pageComponents/account/team/TransferOwnershipModal.tsx @@ -19,7 +19,7 @@ import MyModal from '@fastgpt/web/components/common/MyModal'; import { useRequest } from '@fastgpt/web/hooks/useRequest'; import { getTeamMembers, putTransferTeamOwnership } from '@/web/support/user/team/api'; import { useScrollPagination } from '@fastgpt/web/hooks/useScrollPagination'; -import { type PaginationResponse } from '@fastgpt/web/common/fetch/type'; +import { type PaginationResponse } from '@fastgpt/global/openapi/api'; import { useUserStore } from '@/web/support/user/useUserStore'; import { useContextSelector } from 'use-context-selector'; import { TeamContext } from './context'; diff --git a/projects/app/src/pageComponents/app/detail/Logs/DetailLogsModal.tsx b/projects/app/src/pageComponents/app/detail/Logs/DetailLogsModal.tsx index 4d9039c918..e2810f6a42 100644 --- a/projects/app/src/pageComponents/app/detail/Logs/DetailLogsModal.tsx +++ b/projects/app/src/pageComponents/app/detail/Logs/DetailLogsModal.tsx @@ -11,7 +11,7 @@ import LightRowTabs from '@fastgpt/web/components/common/Tabs/LightRowTabs'; import { PluginRunBoxTabEnum } from '@/components/core/chat/ChatContainer/PluginRunBox/constants'; import { useSystem } from '@fastgpt/web/hooks/useSystem'; import { PcHeader } from '@/pageComponents/chat/ChatHeader'; -import { GetChatTypeEnum } from '@/global/core/chat/constants'; +import { GetChatTypeEnum } from '@fastgpt/global/core/chat/constants'; import ChatItemContextProvider, { ChatItemContext } from '@/web/core/chat/context/chatItemContext'; import ChatRecordContextProvider, { ChatRecordContext diff --git a/projects/app/src/pageComponents/app/detail/Publish/Link/SelectUsingWayModal.tsx b/projects/app/src/pageComponents/app/detail/Publish/Link/SelectUsingWayModal.tsx index 4d9eb2b806..d079a6f973 100644 --- a/projects/app/src/pageComponents/app/detail/Publish/Link/SelectUsingWayModal.tsx +++ b/projects/app/src/pageComponents/app/detail/Publish/Link/SelectUsingWayModal.tsx @@ -1,4 +1,4 @@ -import { type OutLinkSchema } from '@fastgpt/global/support/outLink/type'; +import { type OutLinkSchemaType } from '@fastgpt/global/support/outLink/type'; import React, { useCallback, useState } from 'react'; import MyModal from '@fastgpt/web/components/common/MyModal'; import { useTranslation } from 'next-i18next'; @@ -19,7 +19,13 @@ enum UsingWayEnum { script = 'script' } -const SelectUsingWayModal = ({ share, onClose }: { share: OutLinkSchema; onClose: () => void }) => { +const SelectUsingWayModal = ({ + share, + onClose +}: { + share: OutLinkSchemaType; + onClose: () => void; +}) => { const { t } = useTranslation(); const theme = useTheme(); const { copyData } = useCopyData(); diff --git a/projects/app/src/pageComponents/app/detail/Publish/Link/index.tsx b/projects/app/src/pageComponents/app/detail/Publish/Link/index.tsx index f9b6d31c1a..af02aecb6b 100644 --- a/projects/app/src/pageComponents/app/detail/Publish/Link/index.tsx +++ b/projects/app/src/pageComponents/app/detail/Publish/Link/index.tsx @@ -31,7 +31,7 @@ import { formatTimeToChatTime } from '@fastgpt/global/common/string/time'; import { useCopyData } from '@fastgpt/web/hooks/useCopyData'; import { useForm } from 'react-hook-form'; import { defaultOutLinkForm } from '@/web/core/app/constants'; -import type { OutLinkEditType, OutLinkSchema } from '@fastgpt/global/support/outLink/type'; +import type { OutLinkEditType, OutLinkSchemaType } from '@fastgpt/global/support/outLink/type'; import { PublishChannelEnum } from '@fastgpt/global/support/outLink/constant'; import { useTranslation } from 'next-i18next'; import { useToast } from '@fastgpt/web/hooks/useToast'; @@ -56,7 +56,7 @@ const Share = ({ appId }: { appId: string; type: PublishChannelEnum }) => { const { feConfigs } = useSystemStore(); const { copyData } = useCopyData(); const [editLinkData, setEditLinkData] = useState(); - const [selectedLinkData, setSelectedLinkData] = useState(); + const [selectedLinkData, setSelectedLinkData] = useState(); const { toast } = useToast(); const { ConfirmModal, openConfirm } = useConfirm({ content: t('common:support.outlink.Delete link tip'), @@ -155,7 +155,7 @@ const Share = ({ appId }: { appId: string; type: PublishChannelEnum }) => {