diff --git a/.claude/design/web/common/hook/tableMultiple/index.md b/.claude/design/web/common/hook/tableMultiple/index.md index e1a557353..99fc303f9 100644 --- a/.claude/design/web/common/hook/tableMultiple/index.md +++ b/.claude/design/web/common/hook/tableMultiple/index.md @@ -12,4 +12,4 @@ 1. 选中的值存储在 hook 里,便于判断是否触发底部悬浮层 2. 悬浮层外层 Box 在 hook 里,child 由调用组件实现 -3. FastGPT/packages/web/hooks/useTableMultipleSelect.tsx 在这个文件下实现 \ No newline at end of file +3. FastGPT/packages/web/hooks/useTableMultipleSelect.tsx 在这个文件下实现 diff --git a/document/content/docs/introduction/guide/plugins/meta.json b/document/content/docs/introduction/guide/plugins/meta.json index 9cda406e9..774b87483 100644 --- a/document/content/docs/introduction/guide/plugins/meta.json +++ b/document/content/docs/introduction/guide/plugins/meta.json @@ -1,5 +1,14 @@ { "title": "系统插件", "description": "介绍如何使用和提交系统插件,以及各插件的填写说明", - "pages": ["dev_system_tool","how_to_submit_system_plugin","searxng_plugin_guide","google_search_plugin_guide","bing_search_plugin","doc2x_plugin_guide"] -} \ No newline at end of file + "pages": [ + "dev_system_tool", + "how_to_submit_system_plugin", + "upload_system_tool", + "searxng_plugin_guide", + "google_search_plugin_guide", + "bing_search_plugin", + "doc2x_plugin_guide", + "deepseek_plugin_guide" + ] +} diff --git a/document/content/docs/introduction/guide/plugins/upload_system_tool.mdx b/document/content/docs/introduction/guide/plugins/upload_system_tool.mdx new file mode 100644 index 000000000..b5dc82156 --- /dev/null +++ b/document/content/docs/introduction/guide/plugins/upload_system_tool.mdx @@ -0,0 +1,123 @@ +--- +title: 如何在线上传系统工具 +description: FastGPT 系统工具在线上传指南 +--- + +> 从 FastGPT 4.14.0 版本开始,系统管理员可以通过 Web 界面直接上传和更新系统工具,无需重新部署服务 + +## 权限要求 + +⚠️ **重要提示**:只有 **root 用户** 才能使用在线上传系统工具功能。 + +- 确保您已使用 `root` 账户登录 FastGPT +- 普通用户无法看到"导入/更新"按钮和删除功能 + +## 支持的文件格式 + +- **文件类型**:`.js` 文件 +- **文件大小**:最大 10MB +- **文件数量**:每次只能上传一个文件 + +## 上传步骤 + +### 1. 进入系统工具页面 + +1. 登录 FastGPT 管理后台 +2. 导航到:**工作台** → **系统工具** +3. 确认页面右上角显示"导入/更新"按钮(只有 root 用户可见) + +![](/imgs/plugins/entry.png) + +### 2. 准备工具文件 + +在上传之前,请确保您的 `.js` 文件是从 fastgpt-plugin 项目中通过 `bun run build` 命令打包后的 dist/tools/built-in 文件夹下得到的 + +![](/imgs/plugins/file.png) + +### 3. 执行上传 + +1. 点击 **"导入/更新"** 按钮 +2. 在弹出的对话框中,点击文件选择区域 +3. 选择您准备好的 `.js` 工具文件 +4. 确认文件信息无误后,点击 **"确认导入"** + +### 4. 上传过程 + +- 上传成功后会显示成功提示 +- 页面自动刷新,新工具会出现在工具列表中 + +## 功能特点 + +### 工具管理 + +- **查看工具**:所有用户都可以查看已安装的系统工具 +- **上传工具**:仅 root 用户可以上传新工具或更新现有工具 +- **删除工具**:仅 root 用户可以删除已上传的工具 + +### 工具类型识别 + +系统会根据工具的配置自动识别工具类型: + +- 🔧 **工具 (tools)** +- 🔍 **搜索 (search)** +- 🎨 **多模态 (multimodal)** +- 💬 **通讯 (communication)** +- 📦 **其他 (other)** + +## 常见问题 + +### Q: 上传失败,提示"文件内容存在错误" + +**可能原因:** +- fastgpt-plugin 项目不是最新的,导致打包的 `.js` 文件缺少正确的内容 +- 工具配置格式不正确 + +**解决方案:** +1. 拉取最新的 fastgpt-plugin 项目重新进行 `bun run build` 获得打包后的 `.js` 文件 +2. 检查本地插件运行是否成功 + +### Q: 无法看到"导入/更新"按钮 + +**原因:** 当前用户不是 root 用户 + +**解决方案:** 使用 root 账户重新登录 + +### Q: 文件上传超时 + +**可能原因:** +- 文件过大(超过 10MB) +- 网络连接不稳定 + +**解决方案:** +1. 确认文件大小在限制范围内 +2. 检查网络连接 +3. 尝试重新上传 + +## 最佳实践 + +### 上传前检查 + +1. **代码测试**:在本地环境测试工具功能 +2. **格式验证**:确保符合 FastGPT 工具规范 +3. **文件大小**:保持文件在合理大小范围内 + +### 版本管理 + +- 建议为工具添加版本号注释 +- 更新工具时,先备份原有版本 +- 记录更新日志和功能变更 + +### 安全考虑 + +- 仅上传来源可信的工具文件 +- 避免包含敏感信息或凭据 +- 定期审查已安装的工具 + +### 存储方式 + +- 工具文件存储在 MinIO 中 +- 工具元数据保存在 MongoDB 中 + +--- + +通过在线上传功能,您可以快速部署和管理系统工具,提高 FastGPT 的扩展性和灵活性。如遇到问题,请参考上述常见问题或联系技术支持。 diff --git a/document/content/docs/toc.mdx b/document/content/docs/toc.mdx index 48f073af6..21538ea8d 100644 --- a/document/content/docs/toc.mdx +++ b/document/content/docs/toc.mdx @@ -90,6 +90,7 @@ description: FastGPT 文档目录 - [/docs/introduction/guide/plugins/doc2x_plugin_guide](/docs/introduction/guide/plugins/doc2x_plugin_guide) - [/docs/introduction/guide/plugins/google_search_plugin_guide](/docs/introduction/guide/plugins/google_search_plugin_guide) - [/docs/introduction/guide/plugins/searxng_plugin_guide](/docs/introduction/guide/plugins/searxng_plugin_guide) +- [/docs/introduction/guide/plugins/upload_system_tool](/docs/introduction/guide/plugins/upload_system_tool) - [/docs/introduction/guide/team_permissions/invitation_link](/docs/introduction/guide/team_permissions/invitation_link) - [/docs/introduction/guide/team_permissions/team_roles_permissions](/docs/introduction/guide/team_permissions/team_roles_permissions) - [/docs/introduction/index](/docs/introduction/index) @@ -105,7 +106,7 @@ description: FastGPT 文档目录 - [/docs/upgrading/4-12/4122](/docs/upgrading/4-12/4122) - [/docs/upgrading/4-12/4123](/docs/upgrading/4-12/4123) - [/docs/upgrading/4-12/4124](/docs/upgrading/4-12/4124) -- [/docs/upgrading/4-12/4125](/docs/upgrading/4-12/4125) +- [/docs/upgrading/4-13/4130](/docs/upgrading/4-13/4130) - [/docs/upgrading/4-8/40](/docs/upgrading/4-8/40) - [/docs/upgrading/4-8/41](/docs/upgrading/4-8/41) - [/docs/upgrading/4-8/42](/docs/upgrading/4-8/42) diff --git a/document/content/docs/upgrading/4-12/4125.mdx b/document/content/docs/upgrading/4-12/4125.mdx deleted file mode 100644 index 883913ba7..000000000 --- a/document/content/docs/upgrading/4-12/4125.mdx +++ /dev/null @@ -1,26 +0,0 @@ ---- -title: 'V4.12.5(进行中)' -description: 'FastGPT V4.12.5 更新说明' ---- - -## 🚀 新增内容 - - -## ⚙️ 优化 - -1. 系统工具增加对应 author 名字显示。同时使用安全的 I18n 翻译。 - -## 🐛 修复 - -1. debug 模式下,全局变量未传递。 -2. debug 模式下,前方节点参数无法传递至后方节点。 -3. 调试模式下,开启“自动执行”,会跳过外部变量的填写。 -4. 自动语音回复未生效。 -5. 节点复制,报错捕获配置丢失。 -6. “猜你想问”的自定义提示词,保存时,上一次的值会被置空。 -7. 配置了二级路由的情况下,知识库检索出来的图片地址拼接异常。 -8. Prompt 编辑器,键盘输入时会清除掉 Markdown 标记。 - -## 🔨 插件更新 - -1. 新增火山引擎融合信息搜索工具。 diff --git a/document/content/docs/upgrading/4-12/meta.json b/document/content/docs/upgrading/4-12/meta.json index 1d468c07c..30c62716d 100644 --- a/document/content/docs/upgrading/4-12/meta.json +++ b/document/content/docs/upgrading/4-12/meta.json @@ -1,5 +1,5 @@ { "title": "4.12.x", "description": "", - "pages": ["4125", "4124", "4123", "4122", "4121", "4120"] + "pages": ["4124", "4123", "4122", "4121", "4120"] } diff --git a/document/content/docs/upgrading/4-13/4130.mdx b/document/content/docs/upgrading/4-13/4130.mdx new file mode 100644 index 000000000..24ff33150 --- /dev/null +++ b/document/content/docs/upgrading/4-13/4130.mdx @@ -0,0 +1,83 @@ +--- +title: 'V4.13.0(进行中)' +description: 'FastGPT V4.13.0 更新说明' +--- + +## 更新指南 + +### 1. 更新镜像: + +- 更新 FastGPT 镜像tag: v4.13.0 +- 更新 FastGPT 商业版镜像tag: v4.13.0 +- 更新 fastgpt-plugin 镜像 tag: v0.2.0 +- mcp_server 无需更新 +- Sandbox 无需更新 +- AIProxy 无需更新 + +### 2. 更新环境变量 + +1. 更新 `fastgpt-plugin` 环境变量名字,并新增`S3_PLUGIN_BUCKET`、`MONGODB_URI`、`REDIS_URL`值。 + +``` + +S3_EXTERNAL_BASE_URL=https://xxx.com # S3 外网地址 +S3_ENDPOINT=localhost +S3_PORT=9000 +S3_USE_SSL=false +S3_ACCESS_KEY=minioadmin +S3_SECRET_KEY=minioadmin +S3_TOOL_BUCKET=fastgpt-tool # 系统工具,创建的临时文件,存储的桶,要求公开读私有写。 +S3_PLUGIN_BUCKET=fastgpt-plugin # 系统插件热安装文件的桶,私有读写。 +RETENTION_DAYS=15 # 系统工具临时文件保存天数 +MONGODB_URI=mongodb://myusername:mypassword@mongo:27017/fastgpt?authSource=admin # MongoDB 链接参数 +REDIS_URL=redis://default:mypassword@redis:6379 # Redis 链接参数 +``` + +2. 增加`fastgpt`和`fastgpt-pro(商业版)` s3 相关环境变量。 + +``` +# S3 外网地址 +S3_EXTERNAL_BASE_URL= +S3_ENDPOINT=localhost +S3_PORT=9000 +S3_USE_SSL=false +S3_ACCESS_KEY=minioadmin +S3_SECRET_KEY=minioadmin +S3_PLUGIN_BUCKET=fastgpt-plugin # 系统插件热安装文件的桶,私有读写。 +``` + +## 🚀 新增内容 + +1. 应用新增 HTTP 工具集类型,取代原 HTTP 插件。 +2. 支持系统管理员通过文件形式快速安装系统工具。 +3. 团队管理员支持分配模型权限。 +4. 代码运行节点支持 AI 辅助生成。 +5. 知识库文件解析支持配置最大并发数。(开源版通过 config.json 文件中`systemEnv.datasetParseMaxProcess`属性配置,商业版通过 admin 后台配置。) + +## ⚙️ 优化 + +1. 系统工具增加对应 author 名字显示。同时使用安全的 I18n 翻译。 +2. 计量计费账单推送和合并逻辑。 +3. 对话记录中,节点详情单独分表存储。 +4. 删除 chat_items 中无效的 dataId 索引。 +5. 工作流UI性能优化,减少 UI 重绘。 +6. 对话中,知识库引用鉴权采用整个对话框鉴权,而不是单条记录。 +7. 工作流动态输入输出变量交互优化。 + +## 🐛 修复 + +1. debug 模式下,全局变量未传递。 +2. debug 模式下,前方节点参数无法传递至后方节点。 +3. 调试模式下,开启“自动执行”,会跳过外部变量的填写。 +4. 自动语音回复未生效。 +5. 节点复制,报错捕获配置丢失。 +6. “猜你想问”的自定义提示词,保存时,上一次的值会被置空。 +7. 配置了二级路由的情况下,知识库检索出来的图片地址拼接异常。 +8. Prompt 编辑器,键盘输入时会清除掉 Markdown 标记。 +9. 知识库集合页面,有训练数据时候无法自动刷新页面。 +10. 工作流快速添加节点弹窗,工具箱页面二次打开时为空。 +11. PPTX 文件解析顺序错误。 + +## 🔨 插件更新 + +1. 新增火山引擎融合信息搜索工具。 diff --git a/document/content/docs/upgrading/4-13/meta.json b/document/content/docs/upgrading/4-13/meta.json new file mode 100644 index 000000000..6ed99a30c --- /dev/null +++ b/document/content/docs/upgrading/4-13/meta.json @@ -0,0 +1,5 @@ +{ + "title": "4.13.x", + "description": "", + "pages": ["4130"] +} diff --git a/document/content/docs/upgrading/meta.json b/document/content/docs/upgrading/meta.json index 8427f6eaa..89e4c054a 100644 --- a/document/content/docs/upgrading/meta.json +++ b/document/content/docs/upgrading/meta.json @@ -4,6 +4,8 @@ "description": "FastGPT 版本更新介绍及升级操作", "pages": [ "index", + "---4.13.x---", + "...4-13", "---4.12.x---", "...4-12", "---4.11.x---", diff --git a/document/data/doc-last-modified.json b/document/data/doc-last-modified.json index 947298e8f..4f906560f 100644 --- a/document/data/doc-last-modified.json +++ b/document/data/doc-last-modified.json @@ -8,7 +8,7 @@ "document/content/docs/faq/other.mdx": "2025-08-04T22:07:52+08:00", "document/content/docs/faq/points_consumption.mdx": "2025-08-02T19:38:37+08:00", "document/content/docs/introduction/cloud.mdx": "2025-08-02T19:38:37+08:00", - "document/content/docs/introduction/commercial.mdx": "2025-08-04T22:07:52+08:00", + "document/content/docs/introduction/commercial.mdx": "2025-09-21T23:09:46+08:00", "document/content/docs/introduction/development/community.mdx": "2025-08-02T19:38:37+08:00", "document/content/docs/introduction/development/configuration.mdx": "2025-08-05T23:20:39+08:00", "document/content/docs/introduction/development/custom-models/bge-rerank.mdx": "2025-07-23T21:35:03+08:00", @@ -87,6 +87,7 @@ "document/content/docs/introduction/guide/plugins/doc2x_plugin_guide.mdx": "2025-07-23T21:35:03+08:00", "document/content/docs/introduction/guide/plugins/google_search_plugin_guide.mdx": "2025-07-23T21:35:03+08:00", "document/content/docs/introduction/guide/plugins/searxng_plugin_guide.mdx": "2025-07-23T21:35:03+08:00", + "document/content/docs/introduction/guide/plugins/upload_system_tool.mdx": "2025-09-20T19:49:21+08:00", "document/content/docs/introduction/guide/team_permissions/invitation_link.mdx": "2025-07-23T21:35:03+08:00", "document/content/docs/introduction/guide/team_permissions/team_roles_permissions.mdx": "2025-07-23T21:35:03+08:00", "document/content/docs/introduction/index.en.mdx": "2025-07-23T21:35:03+08:00", @@ -99,7 +100,7 @@ "document/content/docs/protocol/terms.en.mdx": "2025-08-03T22:37:45+08:00", "document/content/docs/protocol/terms.mdx": "2025-08-03T22:37:45+08:00", "document/content/docs/toc.en.mdx": "2025-08-04T13:42:36+08:00", - "document/content/docs/toc.mdx": "2025-09-17T22:29:56+08:00", + "document/content/docs/toc.mdx": "2025-09-23T14:19:37+08:00", "document/content/docs/upgrading/4-10/4100.mdx": "2025-08-02T19:38:37+08:00", "document/content/docs/upgrading/4-10/4101.mdx": "2025-09-08T20:07:20+08:00", "document/content/docs/upgrading/4-11/4110.mdx": "2025-08-05T23:20:39+08:00", @@ -109,7 +110,7 @@ "document/content/docs/upgrading/4-12/4122.mdx": "2025-09-07T14:41:48+08:00", "document/content/docs/upgrading/4-12/4123.mdx": "2025-09-07T20:55:14+08:00", "document/content/docs/upgrading/4-12/4124.mdx": "2025-09-17T22:29:56+08:00", - "document/content/docs/upgrading/4-12/4125.mdx": "2025-09-18T16:15:12+08:00", + "document/content/docs/upgrading/4-13/4130.mdx": "2025-09-24T21:54:28+08:00", "document/content/docs/upgrading/4-8/40.mdx": "2025-08-02T19:38:37+08:00", "document/content/docs/upgrading/4-8/41.mdx": "2025-08-02T19:38:37+08:00", "document/content/docs/upgrading/4-8/42.mdx": "2025-08-02T19:38:37+08:00", diff --git a/document/public/imgs/plugins/entry.png b/document/public/imgs/plugins/entry.png new file mode 100644 index 000000000..aaadd6fad Binary files /dev/null and b/document/public/imgs/plugins/entry.png differ diff --git a/document/public/imgs/plugins/file.png b/document/public/imgs/plugins/file.png new file mode 100644 index 000000000..8843823d1 Binary files /dev/null and b/document/public/imgs/plugins/file.png differ diff --git a/packages/global/common/system/types/index.d.ts b/packages/global/common/system/types/index.d.ts index fa5bc8a7f..4fd196276 100644 --- a/packages/global/common/system/types/index.d.ts +++ b/packages/global/common/system/types/index.d.ts @@ -130,10 +130,12 @@ export type FastGPTFeConfigsType = { export type SystemEnvType = { openapiPrefix?: string; + tokenWorkers: number; // token count max worker + + datasetParseMaxProcess: number; vectorMaxProcess: number; qaMaxProcess: number; vlmMaxProcess: number; - tokenWorkers: number; // token count max worker hnswEfSearch: number; hnswMaxScanTuples: number; diff --git a/packages/global/core/app/constants.ts b/packages/global/core/app/constants.ts index 92216c8f6..e44d1fedf 100644 --- a/packages/global/core/app/constants.ts +++ b/packages/global/core/app/constants.ts @@ -11,10 +11,13 @@ export enum AppTypeEnum { simple = 'simple', workflow = 'advanced', plugin = 'plugin', - httpPlugin = 'httpPlugin', - toolSet = 'toolSet', + toolSet = 'toolSet', // 'mcp' + httpToolSet = 'httpToolSet', tool = 'tool', - hidden = 'hidden' + hidden = 'hidden', + + // deprecated + httpPlugin = 'httpPlugin' } export const AppFolderTypeList = [AppTypeEnum.folder, AppTypeEnum.httpPlugin]; diff --git a/packages/global/core/app/httpPlugin/utils.ts b/packages/global/core/app/httpPlugin/utils.ts deleted file mode 100644 index d69967ed3..000000000 --- a/packages/global/core/app/httpPlugin/utils.ts +++ /dev/null @@ -1,390 +0,0 @@ -import { getNanoid } from '../../../common/string/tools'; -import { type OpenApiJsonSchema } from './type'; -import yaml from 'js-yaml'; -import type { OpenAPIV3 } from 'openapi-types'; -import { type FlowNodeInputItemType, type FlowNodeOutputItemType } from '../../workflow/type/io'; -import { FlowNodeInputTypeEnum, FlowNodeOutputTypeEnum } from '../../workflow/node/constant'; -import { WorkflowIOValueTypeEnum } from '../../workflow/constants'; -import { PluginInputModule } from '../../workflow/template/system/pluginInput'; -import { PluginOutputModule } from '../../workflow/template/system/pluginOutput'; -import { HttpNode468 } from '../../workflow/template/system/http468'; -import { type HttpParamAndHeaderItemType } from '../../workflow/type/io'; -import { type StoreNodeItemType } from '../../workflow/type/node'; -import { HttpImgUrl } from '../../../common/file/image/constants'; -import SwaggerParser from '@apidevtools/swagger-parser'; -import { getHandleId } from '../../workflow/utils'; -import { type CreateHttpPluginChildrenPros } from '../controller'; -import { AppTypeEnum } from '../constants'; -import type { StoreEdgeItemType } from '../../workflow/type/edge'; - -export const str2OpenApiSchema = async (yamlStr = ''): Promise => { - try { - const data = (() => { - try { - return JSON.parse(yamlStr); - } catch (jsonError) { - return yaml.load(yamlStr, { schema: yaml.FAILSAFE_SCHEMA }); - } - })(); - const jsonSchema = (await SwaggerParser.parse(data)) as OpenAPIV3.Document; - - const serverPath = jsonSchema.servers?.[0].url || ''; - const pathData = Object.keys(jsonSchema.paths) - .map((path) => { - const methodData: any = data.paths[path]; - return Object.keys(methodData) - .filter((method) => - ['get', 'post', 'put', 'delete', 'patch'].includes(method.toLocaleLowerCase()) - ) - .map((method) => { - const methodInfo = methodData[method]; - if (methodInfo.deprecated) return; - const result = { - path, - method, - name: methodInfo.operationId || path, - description: methodInfo.description || methodInfo.summary, - params: methodInfo.parameters, - request: methodInfo?.requestBody, - response: methodInfo.responses - }; - return result; - }); - }) - .flat() - .filter(Boolean) as OpenApiJsonSchema['pathData']; - return { pathData, serverPath }; - } catch (err) { - throw new Error('Invalid Schema'); - } -}; - -export const getType = (schema: { type: string; items?: { type: string } }) => { - const typeMap: { [key: string]: WorkflowIOValueTypeEnum } = { - string: WorkflowIOValueTypeEnum.arrayString, - number: WorkflowIOValueTypeEnum.arrayNumber, - integer: WorkflowIOValueTypeEnum.arrayNumber, - boolean: WorkflowIOValueTypeEnum.arrayBoolean, - object: WorkflowIOValueTypeEnum.arrayObject - }; - - if (schema?.type === 'integer') { - return WorkflowIOValueTypeEnum.number; - } - - if (schema?.type === 'array' && schema?.items) { - const itemType = typeMap[schema.items.type]; - if (itemType) { - return itemType; - } - } - - return schema?.type as WorkflowIOValueTypeEnum; -}; - -export const httpApiSchema2Plugins = async ({ - parentId, - apiSchemaStr = '', - customHeader = '' -}: { - parentId: string; - apiSchemaStr?: string; - customHeader?: string; -}): Promise => { - const jsonSchema = await str2OpenApiSchema(apiSchemaStr); - - const baseUrl = jsonSchema.serverPath; - - return jsonSchema.pathData.map((item) => { - const pluginOutputId = getNanoid(); - const httpId = getNanoid(); - const pluginInputId = getNanoid(); - const inputIdMap = new Map(); - - const pluginOutputKey = 'result'; - - const properties = item.request?.content?.['application/json']?.schema?.properties; - const propsKeys = properties ? Object.keys(properties) : []; - - const pluginInputs: FlowNodeInputItemType[] = [ - ...(item.params?.map((param: any) => { - return { - key: param.name, - valueType: getType(param.schema), - label: param.name, - renderTypeList: [FlowNodeInputTypeEnum.reference], - required: param.required, - description: param.description, - toolDescription: param.description, - canEdit: true - }; - }) || []), - ...(propsKeys?.map((key) => { - const prop = properties[key]; - return { - key, - valueType: getType(prop), - label: key, - renderTypeList: [FlowNodeInputTypeEnum.reference], - required: false, - description: prop.description, - toolDescription: prop.description, - canEdit: true - }; - }) || []) - ]; - - const pluginOutputs: FlowNodeOutputItemType[] = [ - ...(item.params?.map((param: any) => { - const id = getNanoid(); - inputIdMap.set(param.name, id); - return { - id, - key: param.name, - valueType: getType(param.schema), - label: param.name, - type: FlowNodeOutputTypeEnum.source - }; - }) || []), - ...(propsKeys?.map((key) => { - const id = getNanoid(); - inputIdMap.set(key, id); - return { - id, - key, - valueType: getType(properties[key]), - label: key, - type: FlowNodeOutputTypeEnum.source, - edit: true - }; - }) || []) - ]; - - const httpInputs: FlowNodeInputItemType[] = [ - ...(item.params?.map((param: any) => { - return { - key: param.name, - valueType: getType(param.schema), - label: param.name, - renderTypeList: [FlowNodeInputTypeEnum.reference], - canEdit: true, - value: [pluginInputId, inputIdMap.get(param.name)] - }; - }) || []), - ...(propsKeys?.map((key) => { - return { - key, - valueType: getType(properties[key]), - label: key, - renderTypeList: [FlowNodeInputTypeEnum.reference], - canEdit: true, - value: [pluginInputId, inputIdMap.get(key)] - }; - }) || []) - ]; - - /* http node setting */ - const httpNodeParams: HttpParamAndHeaderItemType[] = []; - const httpNodeHeaders: HttpParamAndHeaderItemType[] = []; - let httpNodeBody = '{}'; - const requestUrl = `${baseUrl}${item.path}`; - - if (item.params && item.params.length > 0) { - for (const param of item.params) { - if (param.in === 'header') { - httpNodeHeaders.push({ - key: param.name, - type: getType(param.schema) || WorkflowIOValueTypeEnum.string, - value: `{{${param.name}}}` - }); - } else if (param.in === 'body') { - httpNodeBody = JSON.stringify( - { ...JSON.parse(httpNodeBody), [param.name]: `{{${param.name}}}` }, - null, - 2 - ); - } else if (param.in === 'query') { - httpNodeParams.push({ - key: param.name, - type: getType(param.schema) || WorkflowIOValueTypeEnum.string, - value: `{{${param.name}}}` - }); - } - } - } - if (item.request) { - const properties = item.request?.content?.['application/json']?.schema?.properties || {}; - const keys = Object.keys(properties); - if (keys.length > 0) { - httpNodeBody = JSON.stringify( - keys.reduce((acc: any, key) => { - acc[key] = `{{${key}}}`; - return acc; - }, {}), - null, - 2 - ); - } - } - if (customHeader) { - const headersObj = (() => { - try { - return JSON.parse(customHeader) as Record; - } catch (err) { - return {}; - } - })(); - for (const key in headersObj) { - httpNodeHeaders.push({ - key, - type: WorkflowIOValueTypeEnum.string, - // @ts-ignore - value: headersObj[key] - }); - } - } - - /* Combine complete modules */ - const nodes: StoreNodeItemType[] = [ - { - nodeId: pluginInputId, - name: PluginInputModule.name, - intro: PluginInputModule.intro, - avatar: PluginInputModule.avatar, - flowNodeType: PluginInputModule.flowNodeType, - showStatus: PluginInputModule.showStatus, - position: { - x: 473.55206291900333, - y: -145.65080850146154 - }, - version: PluginInputModule.version, - inputs: pluginInputs, - outputs: pluginOutputs - }, - { - nodeId: pluginOutputId, - name: PluginOutputModule.name, - intro: PluginOutputModule.intro, - avatar: PluginOutputModule.avatar, - flowNodeType: PluginOutputModule.flowNodeType, - showStatus: PluginOutputModule.showStatus, - position: { - x: 1847.5956872650024, - y: 5.114324648101558 - }, - version: PluginOutputModule.version, - inputs: [ - { - key: pluginOutputKey, - valueType: WorkflowIOValueTypeEnum.string, - label: pluginOutputKey, - renderTypeList: [FlowNodeInputTypeEnum.reference], - required: false, - description: '', - canEdit: true, - value: [httpId, 'httpRawResponse'] - } - ], - outputs: [ - { - id: pluginOutputId, - key: pluginOutputKey, - valueType: WorkflowIOValueTypeEnum.string, - label: pluginOutputKey, - type: FlowNodeOutputTypeEnum.static - } - ] - }, - { - nodeId: httpId, - name: HttpNode468.name, - intro: HttpNode468.intro, - avatar: HttpNode468.avatar, - flowNodeType: HttpNode468.flowNodeType, - showStatus: true, - position: { - x: 1188.947986995841, - y: -473.52694296182904 - }, - version: HttpNode468.version, - inputs: [ - HttpNode468.inputs[0], - ...httpInputs, - { - key: 'system_httpMethod', - renderTypeList: [FlowNodeInputTypeEnum.custom], - valueType: WorkflowIOValueTypeEnum.string, - label: '', - value: item.method.toUpperCase(), - required: true - }, - { - key: 'system_httpReqUrl', - renderTypeList: [FlowNodeInputTypeEnum.hidden], - valueType: WorkflowIOValueTypeEnum.string, - label: '', - description: 'core.module.input.description.Http Request Url', - placeholder: 'https://api.ai.com/getInventory', - required: false, - value: requestUrl - }, - { - key: 'system_httpHeader', - renderTypeList: [FlowNodeInputTypeEnum.custom], - valueType: WorkflowIOValueTypeEnum.any, - value: httpNodeHeaders, - label: '', - description: 'core.module.input.description.Http Request Header', - placeholder: 'core.module.input.description.Http Request Header', - required: false - }, - { - key: 'system_httpParams', - renderTypeList: [FlowNodeInputTypeEnum.hidden], - valueType: WorkflowIOValueTypeEnum.any, - value: httpNodeParams, - label: '', - required: false - }, - { - key: 'system_httpJsonBody', - renderTypeList: [FlowNodeInputTypeEnum.hidden], - valueType: WorkflowIOValueTypeEnum.any, - value: httpNodeBody, - label: '', - required: false - } - ], - outputs: HttpNode468.outputs - } - ]; - - const edges: StoreEdgeItemType[] = [ - { - source: pluginInputId, - target: httpId, - sourceHandle: getHandleId(pluginInputId, 'source', 'right'), - targetHandle: getHandleId(httpId, 'target', 'left') - }, - { - source: httpId, - target: pluginOutputId, - sourceHandle: getHandleId(httpId, 'source', 'right'), - targetHandle: getHandleId(pluginOutputId, 'target', 'left') - } - ]; - - return { - name: item.name, - avatar: HttpImgUrl, - intro: item.description, - parentId, - type: AppTypeEnum.plugin, - modules: nodes, - edges, - pluginData: { - pluginUniId: item.name - } - }; - }); -}; diff --git a/packages/global/core/app/httpPlugin/type.d.ts b/packages/global/core/app/httpTools/type.d.ts similarity index 88% rename from packages/global/core/app/httpPlugin/type.d.ts rename to packages/global/core/app/httpTools/type.d.ts index f8494334e..be3dd4eb9 100644 --- a/packages/global/core/app/httpPlugin/type.d.ts +++ b/packages/global/core/app/httpTools/type.d.ts @@ -1,4 +1,4 @@ -export type PathDataType = { +type PathDataType = { name: string; description: string; method: string; diff --git a/packages/global/core/app/httpTools/utils.ts b/packages/global/core/app/httpTools/utils.ts new file mode 100644 index 000000000..329e5ce7b --- /dev/null +++ b/packages/global/core/app/httpTools/utils.ts @@ -0,0 +1,174 @@ +import { getNanoid } from '../../../common/string/tools'; +import type { PathDataType } from './type'; +import { type RuntimeNodeItemType } from '../../workflow/runtime/type'; +import { FlowNodeOutputTypeEnum, FlowNodeTypeEnum } from '../../workflow/node/constant'; +import { type HttpToolConfigType } from '../type'; +import { PluginSourceEnum } from '../plugin/constants'; +import { jsonSchema2NodeInput, jsonSchema2NodeOutput } from '../jsonschema'; +import { type StoreSecretValueType } from '../../../common/secret/type'; +import { type JsonSchemaPropertiesItemType } from '../jsonschema'; +import { NodeOutputKeyEnum, WorkflowIOValueTypeEnum } from '../../workflow/constants'; +import { i18nT } from '../../../../web/i18n/utils'; + +export const getHTTPToolSetRuntimeNode = ({ + name, + avatar, + baseUrl = '', + customHeaders = '', + apiSchemaStr = '', + toolList = [], + headerSecret +}: { + name?: string; + avatar?: string; + baseUrl?: string; + customHeaders?: string; + apiSchemaStr?: string; + toolList?: HttpToolConfigType[]; + headerSecret?: StoreSecretValueType; +}): RuntimeNodeItemType => { + return { + nodeId: getNanoid(16), + flowNodeType: FlowNodeTypeEnum.toolSet, + avatar, + intro: 'HTTP Tools', + toolConfig: { + httpToolSet: { + baseUrl, + toolList, + headerSecret, + customHeaders, + apiSchemaStr, + toolId: '' + } + }, + inputs: [], + outputs: [], + name: name || '', + version: '' + }; +}; + +export const getHTTPToolRuntimeNode = ({ + tool, + nodeId, + avatar = 'core/app/type/httpToolsFill', + parentId +}: { + tool: Omit; + nodeId?: string; + avatar?: string; + parentId: string; +}): RuntimeNodeItemType => { + return { + nodeId: nodeId || getNanoid(16), + flowNodeType: FlowNodeTypeEnum.tool, + avatar, + intro: tool.description, + toolConfig: { + httpTool: { + toolId: `${PluginSourceEnum.http}-${parentId}/${tool.name}` + } + }, + inputs: jsonSchema2NodeInput(tool.inputSchema), + outputs: [ + ...jsonSchema2NodeOutput(tool.outputSchema), + { + id: NodeOutputKeyEnum.rawResponse, + key: NodeOutputKeyEnum.rawResponse, + required: true, + label: i18nT('workflow:raw_response'), + description: i18nT('workflow:tool_raw_response_description'), + valueType: WorkflowIOValueTypeEnum.any, + type: FlowNodeOutputTypeEnum.static + } + ], + name: tool.name, + version: '' + }; +}; + +export const pathData2ToolList = async ( + pathData: PathDataType[] +): Promise => { + try { + return pathData.map((pathItem) => { + const inputProperties: Record = {}; + const inputRequired: string[] = []; + const outputProperties: Record = {}; + const outputRequired: string[] = []; + + if (pathItem.params && Array.isArray(pathItem.params)) { + pathItem.params.forEach((param) => { + if (param.name && param.schema) { + inputProperties[param.name] = { + type: param.schema.type || 'any', + description: param.description || '' + }; + + if (param.required) { + inputRequired.push(param.name); + } + } + }); + } + if (pathItem.request?.content?.['application/json']?.schema) { + const requestSchema = pathItem.request.content['application/json'].schema; + + if (requestSchema.properties) { + Object.entries(requestSchema.properties).forEach(([key, value]: [string, any]) => { + inputProperties[key] = { + type: value.type || 'any', + description: value.description || '' + }; + }); + } + + if (requestSchema.required && Array.isArray(requestSchema.required)) { + inputRequired.push(...requestSchema.required); + } + } + + const responseToProcess = + pathItem.response?.['200'] || + pathItem.response?.['201'] || + pathItem.response?.['202'] || + pathItem.response?.default; + + if (responseToProcess?.content?.['application/json']?.schema) { + const responseSchema = responseToProcess.content['application/json'].schema; + if (responseSchema.properties) { + Object.entries(responseSchema.properties).forEach(([key, value]: [string, any]) => { + outputProperties[key] = { + type: value.type || 'any', + description: value.description || '' + }; + }); + } + if (responseSchema.required && Array.isArray(responseSchema.required)) { + outputRequired.push(...responseSchema.required); + } + } + + return { + name: pathItem.name, + description: pathItem.description || pathItem.name, + path: pathItem.path, + method: pathItem.method?.toLowerCase(), + inputSchema: { + type: 'object', + properties: inputProperties, + required: inputRequired + }, + outputSchema: { + type: 'object', + properties: outputProperties, + required: outputRequired + } + }; + }); + } catch (error) { + console.error('Error converting API schema to tool list:', error); + return []; + } +}; diff --git a/packages/global/core/app/jsonschema.ts b/packages/global/core/app/jsonschema.ts index ac28f7367..18836a3f8 100644 --- a/packages/global/core/app/jsonschema.ts +++ b/packages/global/core/app/jsonschema.ts @@ -1,10 +1,15 @@ import { WorkflowIOValueTypeEnum } from '../workflow/constants'; -import { FlowNodeInputTypeEnum } from '../workflow/node/constant'; -import type { FlowNodeInputItemType } from '../workflow/type/io'; +import { FlowNodeInputTypeEnum, FlowNodeOutputTypeEnum } from '../workflow/node/constant'; +import type { FlowNodeInputItemType, FlowNodeOutputItemType } from '../workflow/type/io'; +import SwaggerParser from '@apidevtools/swagger-parser'; +import yaml from 'js-yaml'; +import type { OpenAPIV3 } from 'openapi-types'; +import type { OpenApiJsonSchema } from './httpTools/type'; type SchemaInputValueType = 'string' | 'number' | 'integer' | 'boolean' | 'array' | 'object'; export type JsonSchemaPropertiesItemType = { description?: string; + 'x-tool-description'?: string; type: SchemaInputValueType; enum?: string[]; minimum?: number; @@ -16,6 +21,11 @@ export type JSONSchemaInputType = { properties?: Record; required?: string[]; }; +export type JSONSchemaOutputType = { + type: SchemaInputValueType; + properties?: Record; + required?: string[]; +}; export const getNodeInputTypeFromSchemaInputType = ({ type, @@ -79,8 +89,119 @@ export const jsonSchema2NodeInput = (jsonSchema: JSONSchemaInputType): FlowNodeI label: key, valueType: getNodeInputTypeFromSchemaInputType({ type: value.type, arrayItems: value.items }), description: value.description, - toolDescription: value.description || key, + toolDescription: value['x-tool-description'] ?? value.description ?? key, required: jsonSchema?.required?.includes(key), ...getNodeInputRenderTypeFromSchemaInputType(value) })); }; +export const jsonSchema2NodeOutput = ( + jsonSchema: JSONSchemaOutputType +): FlowNodeOutputItemType[] => { + return Object.entries(jsonSchema?.properties || {}).map(([key, value]) => ({ + id: key, + key, + label: key, + required: jsonSchema?.required?.includes(key), + type: FlowNodeOutputTypeEnum.static, + valueType: getNodeInputTypeFromSchemaInputType({ type: value.type, arrayItems: value.items }), + description: value.description, + toolDescription: value['x-tool-description'] ?? value.description ?? key + })); +}; +export const str2OpenApiSchema = async (yamlStr = ''): Promise => { + try { + const data = (() => { + try { + return JSON.parse(yamlStr); + } catch (jsonError) { + return yaml.load(yamlStr, { schema: yaml.FAILSAFE_SCHEMA }); + } + })(); + const jsonSchema = (await SwaggerParser.dereference(data)) as OpenAPIV3.Document; + + const serverPath = (() => { + if (jsonSchema.servers && jsonSchema.servers.length > 0) { + return jsonSchema.servers[0].url || ''; + } + if (data.host || data.basePath) { + const scheme = data.schemes && data.schemes.length > 0 ? data.schemes[0] : 'https'; + const host = data.host || ''; + const basePath = data.basePath || ''; + return `${scheme}://${host}${basePath}`; + } + return ''; + })(); + + const pathData = Object.keys(jsonSchema.paths) + .map((path) => { + const methodData: any = jsonSchema.paths[path]; + return Object.keys(methodData) + .filter((method) => + ['get', 'post', 'put', 'delete', 'patch'].includes(method.toLocaleLowerCase()) + ) + .map((method) => { + const methodInfo = methodData[method]; + if (methodInfo.deprecated) return; + + const requestBody = (() => { + if (methodInfo?.requestBody) { + return methodInfo.requestBody; + } + if (methodInfo.parameters) { + const bodyParam = methodInfo.parameters.find( + (param: OpenAPIV3.ParameterObject) => param.in === 'body' + ); + if (bodyParam) { + return { + content: { + 'application/json': { + schema: bodyParam.schema + } + } + }; + } + } + return undefined; + })(); + + const result = { + path, + method, + name: methodInfo.operationId || path, + description: methodInfo.description || methodInfo.summary, + params: methodInfo.parameters, + request: requestBody, + response: methodInfo.responses + }; + return result; + }); + }) + .flat() + .filter(Boolean) as OpenApiJsonSchema['pathData']; + return { pathData, serverPath }; + } catch (err) { + throw new Error('Invalid Schema'); + } +}; +export const getSchemaValueType = (schema: { type: string; items?: { type: string } }) => { + const typeMap: { [key: string]: WorkflowIOValueTypeEnum } = { + string: WorkflowIOValueTypeEnum.arrayString, + number: WorkflowIOValueTypeEnum.arrayNumber, + integer: WorkflowIOValueTypeEnum.arrayNumber, + boolean: WorkflowIOValueTypeEnum.arrayBoolean, + object: WorkflowIOValueTypeEnum.arrayObject + }; + + if (schema?.type === 'integer') { + return WorkflowIOValueTypeEnum.number; + } + + if (schema?.type === 'array' && schema?.items) { + const itemType = typeMap[schema.items.type]; + if (itemType) { + return itemType; + } + } + + return schema?.type as WorkflowIOValueTypeEnum; +}; diff --git a/packages/global/core/app/mcpTools/utils.ts b/packages/global/core/app/mcpTools/utils.ts index 38e24c311..9d387081e 100644 --- a/packages/global/core/app/mcpTools/utils.ts +++ b/packages/global/core/app/mcpTools/utils.ts @@ -1,14 +1,6 @@ -import { - NodeInputKeyEnum, - NodeOutputKeyEnum, - WorkflowIOValueTypeEnum -} from '../../workflow/constants'; +import { NodeOutputKeyEnum, WorkflowIOValueTypeEnum } from '../../workflow/constants'; import { i18nT } from '../../../../web/i18n/utils'; -import { - FlowNodeInputTypeEnum, - FlowNodeOutputTypeEnum, - FlowNodeTypeEnum -} from '../../workflow/node/constant'; +import { FlowNodeOutputTypeEnum, FlowNodeTypeEnum } from '../../workflow/node/constant'; import { type McpToolConfigType } from '../type'; import { type RuntimeNodeItemType } from '../../workflow/runtime/type'; import { type StoreSecretValueType } from '../../../common/secret/type'; diff --git a/packages/global/core/app/plugin/constants.ts b/packages/global/core/app/plugin/constants.ts index 56f44a0db..3dc350f9f 100644 --- a/packages/global/core/app/plugin/constants.ts +++ b/packages/global/core/app/plugin/constants.ts @@ -3,6 +3,7 @@ export enum PluginSourceEnum { systemTool = 'systemTool', // FastGPT-plugin tools, pure code. commercial = 'commercial', // configured in Pro, with associatedPluginId. Specially, commercial-dalle3 is a systemTool mcp = 'mcp', // mcp + http = 'http', // http // @deprecated community = 'community' // this is deprecated, will be replaced by systemTool } diff --git a/packages/global/core/app/plugin/type.d.ts b/packages/global/core/app/plugin/type.d.ts index 807e15a4d..1849beef7 100644 --- a/packages/global/core/app/plugin/type.d.ts +++ b/packages/global/core/app/plugin/type.d.ts @@ -56,6 +56,9 @@ export type SystemPluginTemplateItemType = WorkflowTemplateType & { inputList?: FlowNodeInputItemType['inputList']; inputListVal?: Record; hasSystemSecret?: boolean; + + // Plugin source type + toolSource?: 'uploaded' | 'built-in'; }; export type SystemPluginTemplateListItemType = Omit< diff --git a/packages/global/core/app/plugin/utils.ts b/packages/global/core/app/plugin/utils.ts index 8b6088639..a7301eff2 100644 --- a/packages/global/core/app/plugin/utils.ts +++ b/packages/global/core/app/plugin/utils.ts @@ -60,5 +60,11 @@ export function splitCombinePluginId(id: string) { pluginId }; } + if (source === 'http') { + return { + source: PluginSourceEnum.http, + pluginId + }; + } return { source, pluginId: id }; } diff --git a/packages/global/core/app/type.d.ts b/packages/global/core/app/type.d.ts index f0fb3666a..017dff7d7 100644 --- a/packages/global/core/app/type.d.ts +++ b/packages/global/core/app/type.d.ts @@ -15,7 +15,7 @@ import type { ParentIdType } from '../../common/parentFolder/type'; import { FlowNodeInputTypeEnum } from '../../core/workflow/node/constant'; import type { WorkflowTemplateBasicType } from '@fastgpt/global/core/workflow/type'; import type { SourceMemberType } from '../../support/user/type'; -import type { JSONSchemaInputType } from './jsonschema'; +import type { JSONSchemaInputType, JSONSchemaOutputType } from './jsonschema'; export type AppSchema = { _id: string; @@ -120,6 +120,15 @@ export type McpToolConfigType = { inputSchema: JSONSchemaInputType; }; +export type HttpToolConfigType = { + name: string; + description: string; + inputSchema: JSONSchemaInputType; + outputSchema: JSONSchemaOutputType; + path: string; + method: string; +}; + /* app chat config type */ export type AppChatConfigType = { welcomeText?: string; diff --git a/packages/global/core/chat/type.d.ts b/packages/global/core/chat/type.d.ts index df10bd3fd..3a69f7d91 100644 --- a/packages/global/core/chat/type.d.ts +++ b/packages/global/core/chat/type.d.ts @@ -20,6 +20,7 @@ import type { WorkflowInteractiveResponseType } from '../workflow/template/syste import type { FlowNodeInputItemType } from '../workflow/type/io'; import type { FlowNodeTemplateType } from '../workflow/type/node.d'; +/* --------- chat ---------- */ export type ChatSchemaType = { _id: string; chatId: string; @@ -49,6 +50,7 @@ export type ChatWithAppSchema = Omit & { appId: AppSchema; }; +/* --------- chat item ---------- */ export type UserChatItemValueItemType = { type: ChatItemValueTypeEnum.text | ChatItemValueTypeEnum.file; text?: { @@ -65,6 +67,7 @@ export type UserChatItemType = { value: UserChatItemValueItemType[]; hideInUI?: boolean; }; + export type SystemChatItemValueItemType = { type: ChatItemValueTypeEnum.text; text?: { @@ -92,7 +95,6 @@ export type AIChatItemValueItemType = { tools?: ToolModuleResponseItemType[]; interactive?: WorkflowInteractiveResponseType; }; - export type AIChatItemType = { obj: ChatRoleEnum.AI; value: AIChatItemValueItemType[]; @@ -101,14 +103,22 @@ export type AIChatItemType = { userBadFeedback?: string; customFeedbacks?: string[]; adminFeedback?: AdminFbkType; + + durationSeconds?: number; + errorMsg?: string; + citeCollectionIds?: string[]; + + // @deprecated 不再存储在 chatItemSchema 里,分别存储到 chatItemResponseSchema [DispatchNodeResponseKeyEnum.nodeResponse]?: ChatHistoryItemResType[]; }; + export type ChatItemValueItemType = | UserChatItemValueItemType | SystemChatItemValueItemType | AIChatItemValueItemType; +export type ChatItemMergeType = UserChatItemType | SystemChatItemType | AIChatItemType; -export type ChatItemSchema = (UserChatItemType | SystemChatItemType | AIChatItemType) & { +export type ChatItemSchema = ChatItemMergeType & { dataId: string; chatId: string; userId: string; @@ -116,8 +126,6 @@ export type ChatItemSchema = (UserChatItemType | SystemChatItemType | AIChatItem tmbId: string; appId: string; time: Date; - durationSeconds?: number; - errorMsg?: string; }; export type AdminFbkType = { @@ -128,7 +136,6 @@ export type AdminFbkType = { a?: string; }; -/* --------- chat item ---------- */ export type ResponseTagItemType = { totalQuoteList?: SearchDataResponseItemType[]; llmModuleAccount?: number; @@ -136,12 +143,12 @@ export type ResponseTagItemType = { toolCiteLinks?: ToolCiteLinksType[]; }; -export type ChatItemType = (UserChatItemType | SystemChatItemType | AIChatItemType) & { +export type ChatItemType = ChatItemMergeType & { dataId?: string; } & ResponseTagItemType; // Frontend type -export type ChatSiteItemType = (UserChatItemType | SystemChatItemType | AIChatItemType) & { +export type ChatSiteItemType = ChatItemMergeType & { _id?: string; dataId: string; status: `${ChatStatusEnum}`; @@ -153,6 +160,16 @@ export type ChatSiteItemType = (UserChatItemType | SystemChatItemType | AIChatIt errorMsg?: string; } & ChatBoxInputType & ResponseTagItemType; + +/* --------- chat item response ---------- */ +export type ChatItemResponseSchemaType = { + teamId: string; + appId: string; + chatId: string; + chatItemDataId: string; + data: ChatHistoryItemResType; +}; + /* --------- team chat --------- */ export type ChatAppListSchema = { apps: AppType[]; diff --git a/packages/global/core/chat/utils.ts b/packages/global/core/chat/utils.ts index 56de24735..a3c4efd05 100644 --- a/packages/global/core/chat/utils.ts +++ b/packages/global/core/chat/utils.ts @@ -85,10 +85,10 @@ export const getHistoryPreview = ( // Filter workflow public response export const filterPublicNodeResponseData = ({ - flowResponses = [], + nodeRespones = [], responseDetail = false }: { - flowResponses?: ChatHistoryItemResType[]; + nodeRespones?: ChatHistoryItemResType[]; responseDetail?: boolean; }) => { const publicNodeMap: Record = { @@ -98,19 +98,28 @@ export const filterPublicNodeResponseData = ({ [FlowNodeTypeEnum.pluginOutput]: true }; - const filedList = responseDetail - ? ['quoteList', 'moduleType', 'pluginOutput', 'runningTime'] - : ['moduleType', 'pluginOutput', 'runningTime']; + const filedMap: Record = responseDetail + ? { + quoteList: true, + moduleType: true, + pluginOutput: true, + runningTime: true + } + : { + moduleType: true, + pluginOutput: true, + runningTime: true + }; - return flowResponses + return nodeRespones .filter((item) => publicNodeMap[item.moduleType]) .map((item) => { const obj: DispatchNodeResponseType = {}; for (let key in item) { if (key === 'toolDetail' || key === 'pluginDetail') { // @ts-ignore - obj[key] = filterPublicNodeResponseData({ flowResponses: item[key], responseDetail }); - } else if (filedList.includes(key)) { + obj[key] = filterPublicNodeResponseData({ nodeRespones: item[key], responseDetail }); + } else if (filedMap[key]) { // @ts-ignore obj[key] = item[key]; } @@ -211,38 +220,32 @@ export const mergeChatResponseData = ( return item; }); - let lastResponse: ChatHistoryItemResType | undefined = undefined; - let hasMerged = false; + const result: ChatHistoryItemResType[] = []; + const mergeMap = new Map(); // mergeSignId -> result index - const firstPassResult = responseWithMergedPlugins.reduce( - (acc, curr) => { - if ( - lastResponse && - lastResponse.mergeSignId && - curr.mergeSignId === lastResponse.mergeSignId - ) { - const concatResponse: ChatHistoryItemResType = { - ...curr, - runningTime: +((lastResponse.runningTime || 0) + (curr.runningTime || 0)).toFixed(2), - totalPoints: (lastResponse.totalPoints || 0) + (curr.totalPoints || 0), - childTotalPoints: (lastResponse.childTotalPoints || 0) + (curr.childTotalPoints || 0), - toolDetail: [...(lastResponse.toolDetail || []), ...(curr.toolDetail || [])], - loopDetail: [...(lastResponse.loopDetail || []), ...(curr.loopDetail || [])], - pluginDetail: [...(lastResponse.pluginDetail || []), ...(curr.pluginDetail || [])] - }; - hasMerged = true; - return [...acc.slice(0, -1), concatResponse]; - } else { - lastResponse = curr; - return [...acc, curr]; + for (const item of responseWithMergedPlugins) { + if (item.mergeSignId && mergeMap.has(item.mergeSignId)) { + // Merge with existing item + const existingIndex = mergeMap.get(item.mergeSignId)!; + const existing = result[existingIndex]; + + result[existingIndex] = { + ...item, + runningTime: +((existing.runningTime || 0) + (item.runningTime || 0)).toFixed(2), + totalPoints: (existing.totalPoints || 0) + (item.totalPoints || 0), + childTotalPoints: (existing.childTotalPoints || 0) + (item.childTotalPoints || 0), + toolDetail: [...(existing.toolDetail || []), ...(item.toolDetail || [])], + loopDetail: [...(existing.loopDetail || []), ...(item.loopDetail || [])], + pluginDetail: [...(existing.pluginDetail || []), ...(item.pluginDetail || [])] + }; + } else { + // Add new item + result.push(item); + if (item.mergeSignId) { + mergeMap.set(item.mergeSignId, result.length - 1); } - }, - [] - ); - - if (hasMerged && firstPassResult.length > 1) { - return mergeChatResponseData(firstPassResult); + } } - return firstPassResult; + return result; }; diff --git a/packages/global/core/workflow/runtime/type.d.ts b/packages/global/core/workflow/runtime/type.d.ts index 6eca00de6..31abaee7f 100644 --- a/packages/global/core/workflow/runtime/type.d.ts +++ b/packages/global/core/workflow/runtime/type.d.ts @@ -48,6 +48,7 @@ export type ChatDispatchProps = { id: string; // May be the id of the system plug-in (cannot be used directly to look up the table) teamId: string; tmbId: string; // App tmbId + name: string; isChildApp?: boolean; }; runningUserInfo: { @@ -78,6 +79,7 @@ export type ChatDispatchProps = { responseAllData?: boolean; responseDetail?: boolean; + usageId?: string; }; export type ModuleDispatchProps = ChatDispatchProps & { diff --git a/packages/global/core/workflow/template/system/http468.ts b/packages/global/core/workflow/template/system/http468.ts index 5c1412fdb..9e85e907b 100644 --- a/packages/global/core/workflow/template/system/http468.ts +++ b/packages/global/core/workflow/template/system/http468.ts @@ -36,7 +36,8 @@ export const HttpNode468: FlowNodeTemplateType = { selectValueTypeList: Object.values(WorkflowIOValueTypeEnum), showDescription: false, showDefaultValue: true - } + }, + deprecated: true }, { key: NodeInputKeyEnum.httpMethod, diff --git a/packages/global/core/workflow/template/system/interactive/type.d.ts b/packages/global/core/workflow/template/system/interactive/type.d.ts index 571ca1042..9fe0586ce 100644 --- a/packages/global/core/workflow/template/system/interactive/type.d.ts +++ b/packages/global/core/workflow/template/system/interactive/type.d.ts @@ -14,6 +14,8 @@ type InteractiveBasicType = { memoryMessages: ChatCompletionMessageParam[]; // 这轮工具中,产生的新的 messages toolCallId: string; // 记录对应 tool 的id,用于后续交互节点可以替换掉 tool 的 response }; + + usageId?: string; }; type InteractiveNodeType = { diff --git a/packages/global/core/workflow/type/node.d.ts b/packages/global/core/workflow/type/node.d.ts index 69a83353c..33656022d 100644 --- a/packages/global/core/workflow/type/node.d.ts +++ b/packages/global/core/workflow/type/node.d.ts @@ -19,7 +19,12 @@ import { ChatNodeUsageType } from '../../../support/wallet/bill/type'; import { RuntimeNodeItemType } from '../runtime/type'; import { RuntimeEdgeItemType, StoreEdgeItemType } from './edge'; import { NextApiResponse } from 'next'; -import type { AppDetailType, AppSchema, McpToolConfigType } from '../../app/type'; +import type { + AppDetailType, + AppSchema, + McpToolConfigType, + HttpToolConfigType +} from '../../app/type'; import type { ParentIdType } from 'common/parentFolder/type'; import { AppTypeEnum } from '../../app/constants'; import type { WorkflowInteractiveResponseType } from '../template/system/interactive/type'; @@ -46,6 +51,17 @@ export type NodeToolConfigType = { description: string; }[]; }; + httpToolSet?: { + toolId: string; + baseUrl: string; + toolList: HttpToolConfigType[]; + apiSchemaStr: string; + customHeaders: string; + headerSecret?: StoreSecretValueType; + }; + httpTool?: { + toolId: string; + }; }; export type FlowNodeCommonType = { @@ -142,6 +158,7 @@ export type NodeTemplateListItemType = { instructions?: string; // 使用说明 courseUrl?: string; // 教程链接 sourceMember?: SourceMember; + toolSource?: 'uploaded' | 'built-in'; // Plugin source type }; export type NodeTemplateListType = { diff --git a/packages/global/core/workflow/utils.ts b/packages/global/core/workflow/utils.ts index c5bd93296..b469c6da5 100644 --- a/packages/global/core/workflow/utils.ts +++ b/packages/global/core/workflow/utils.ts @@ -27,7 +27,8 @@ import type { ChatInputGuideConfigType, AppChatConfigType, AppAutoExecuteConfigType, - AppQGConfigType + AppQGConfigType, + AppSchema } from '../app/type'; import { type EditorVariablePickerType } from '../../../web/components/common/Textarea/PromptEditor/type'; import { @@ -441,3 +442,24 @@ export const getPluginRunUserQuery = ({ }) }; }; + +export const removeUnauthModels = async ({ + modules, + allowedModels = new Set() +}: { + modules: AppSchema['modules']; + allowedModels?: Set; +}) => { + if (modules) { + modules.forEach((module) => { + module.inputs.forEach((input) => { + if (input.key === 'model') { + if (!allowedModels.has(input.value)) { + input.value = undefined; + } + } + }); + }); + } + return modules; +}; diff --git a/packages/global/package.json b/packages/global/package.json index 75e16b2f6..9fdb0952c 100644 --- a/packages/global/package.json +++ b/packages/global/package.json @@ -2,7 +2,7 @@ "name": "@fastgpt/global", "version": "1.0.0", "dependencies": { - "@fastgpt-sdk/plugin": "^0.1.16", + "@fastgpt-sdk/plugin": "^0.1.19", "@apidevtools/swagger-parser": "^10.1.0", "@bany/curl-to-json": "^1.2.8", "axios": "^1.12.1", diff --git a/packages/global/support/permission/constant.ts b/packages/global/support/permission/constant.ts index de254f89b..d8c173bb8 100644 --- a/packages/global/support/permission/constant.ts +++ b/packages/global/support/permission/constant.ts @@ -49,7 +49,8 @@ export const PermissionTypeMap = { export enum PerResourceTypeEnum { team = 'team', app = 'app', - dataset = 'dataset' + dataset = 'dataset', + model = 'model' } /* new permission */ diff --git a/packages/global/support/permission/model/constant.ts b/packages/global/support/permission/model/constant.ts new file mode 100644 index 000000000..e4aeaf9ec --- /dev/null +++ b/packages/global/support/permission/model/constant.ts @@ -0,0 +1,11 @@ +import { + NullRoleVal, + CommonPerKeyEnum, + CommonRoleList, + CommonPerList, + CommonRoleKeyEnum +} from '../constant'; + +export const ModelDefaultRole = NullRoleVal; +export const ModelReadPerVal = CommonPerList[CommonPerKeyEnum.read]; +export const ModelReadRolVal = CommonRoleList[CommonRoleKeyEnum.read]; diff --git a/packages/global/support/permission/model/controller.ts b/packages/global/support/permission/model/controller.ts new file mode 100644 index 000000000..36cc347e1 --- /dev/null +++ b/packages/global/support/permission/model/controller.ts @@ -0,0 +1,3 @@ +import { Permission } from '../controller'; + +export class ModelPermission extends Permission {} diff --git a/packages/global/support/permission/type.ts b/packages/global/support/permission/type.ts index c1752210a..7a5e9ace6 100644 --- a/packages/global/support/permission/type.ts +++ b/packages/global/support/permission/type.ts @@ -1,8 +1,8 @@ import type { UserModelSchema } from '../user/type'; -import type { RequireOnlyOne } from '../../common/type/utils'; import type { TeamMemberSchema } from '../user/team/type'; import type { CommonRoleKeyEnum } from './constant'; import { type CommonPerKeyEnum, type PerResourceTypeEnum } from './constant'; +import type { CollaboratorIdType } from './collaborator'; // PermissionValueType, the type of permission's value is a number, which is a bit field actually. // It is spired by the permission system in Linux. @@ -63,11 +63,8 @@ export type ResourcePermissionType = { resourceType: ResourceType; permission: PermissionValueType; resourceId: string; -} & RequireOnlyOne<{ - tmbId: string; - groupId: string; - orgId: string; -}>; + resourceName: string; +} & CollaboratorIdType; export type ResourcePerWithTmbWithUser = Omit & { tmbId: TeamMemberSchema & { user: UserModelSchema }; diff --git a/packages/global/support/wallet/usage/api.d.ts b/packages/global/support/wallet/usage/api.d.ts index 72325c8e9..ffb1494e7 100644 --- a/packages/global/support/wallet/usage/api.d.ts +++ b/packages/global/support/wallet/usage/api.d.ts @@ -1,5 +1,5 @@ -import type { UsageSourceEnum } from './constants'; -import type { UsageListItemCountType, UsageListItemType } from './type'; +import type { UsageItemTypeEnum, UsageSourceEnum } from './constants'; +import type { UsageItemCountType, UsageItemType, UsageListItemType, UsageSchemaType } from './type'; export type CreateTrainingUsageProps = { name: string; @@ -22,21 +22,15 @@ export type GetUsageDashboardResponseItem = { totalPoints: number; }; -export type ConcatUsageProps = UsageListItemCountType & { +export type CreateUsageProps = Omit; +export type ConcatUsageProps = { teamId: string; - tmbId: string; - billId?: string; + usageId: string; totalPoints: number; - listIndex?: number; -}; - -export type CreateUsageProps = { + itemType: UsageItemTypeEnum; +} & UsageItemCountType; +export type PushUsageItemsProps = { teamId: string; - tmbId: string; - appName: string; - appId?: string; - pluginId?: string; - totalPoints: number; - source: `${UsageSourceEnum}`; - list: UsageListItemType[]; + usageId: string; + list: UsageItemType[]; }; diff --git a/packages/global/support/wallet/usage/constants.ts b/packages/global/support/wallet/usage/constants.ts index ac5581238..88122bc9f 100644 --- a/packages/global/support/wallet/usage/constants.ts +++ b/packages/global/support/wallet/usage/constants.ts @@ -14,7 +14,8 @@ export enum UsageSourceEnum { pdfParse = 'pdfParse', mcp = 'mcp', evaluation = 'evaluation', - optimize_prompt = 'optimize_prompt' + optimize_prompt = 'optimize_prompt', + code_copilot = 'code_copilot' } export const UsageSourceMap = { @@ -59,5 +60,20 @@ export const UsageSourceMap = { }, [UsageSourceEnum.optimize_prompt]: { label: i18nT('common:support.wallet.usage.Optimize Prompt') + }, + [UsageSourceEnum.code_copilot]: { + label: i18nT('common:support.wallet.usage.Code Copilot') } }; + +export enum UsageItemTypeEnum { + training_vector = 1, + training_qa = 2, + training_autoIndex = 3, + training_imageIndex = 4, + training_paragraph = 5, + training_imageParse = 6, + + evaluation_generateAnswer = 7, + evaluation_answerAccuracy = 8 +} diff --git a/packages/global/support/wallet/usage/type.d.ts b/packages/global/support/wallet/usage/type.d.ts index 56f4c59a8..5a9cd0199 100644 --- a/packages/global/support/wallet/usage/type.d.ts +++ b/packages/global/support/wallet/usage/type.d.ts @@ -1,8 +1,33 @@ import type { SourceMemberType } from '../../../support/user/type'; -import type { CreateUsageProps } from './api'; -import { UsageSourceEnum } from './constants'; +import type { UsageItemTypeEnum, UsageSourceEnum } from './constants'; -export type UsageListItemCountType = { +export type UsageSchemaType = { + _id: string; + time: Date; + + teamId: string; + tmbId: string; + appName: string; + appId?: string; + pluginId?: string; + totalPoints: number; + source: `${UsageSourceEnum}`; + + // @deprecated + list?: UsageItemType[]; +}; +export type UsageItemSchemaType = { + _id: string; + teamId: string; + usageId: string; + name: string; + amount: number; + time: Date; + itemType?: UsageItemTypeEnum; // Use in usage concat +} & UsageItemCountType; + +export type UsageItemCountType = { + model?: string; inputTokens?: number; outputTokens?: number; charsLength?: number; @@ -14,24 +39,18 @@ export type UsageListItemCountType = { tokens?: number; }; -export type UsageListItemType = UsageListItemCountType & { +export type UsageItemType = UsageItemCountType & { moduleName: string; amount: number; - model?: string; - count?: number; + itemType?: UsageItemTypeEnum; }; -export type UsageSchemaType = CreateUsageProps & { - _id: string; - time: Date; -}; - -export type UsageItemType = { +export type UsageListItemType = { id: string; time: Date; appName: string; source: UsageSchemaType['source']; totalPoints: number; - list: UsageSchemaType['list']; + list: Omit[]; sourceMember: SourceMemberType; }; diff --git a/packages/service/common/api/type.d.ts b/packages/service/common/api/type.d.ts index 09f596e00..04f9912e2 100644 --- a/packages/service/common/api/type.d.ts +++ b/packages/service/common/api/type.d.ts @@ -8,7 +8,11 @@ import type { SearchDatasetDataResponse } from '../../core/dataset/search/controller'; import type { AuthOpenApiLimitProps } from '../../support/openapi/auth'; -import type { CreateUsageProps, ConcatUsageProps } from '@fastgpt/global/support/wallet/usage/api'; +import type { + CreateUsageProps, + ConcatUsageProps, + PushUsageItemsProps +} from '@fastgpt/global/support/wallet/usage/api'; declare global { var textCensorHandler: (params: { text: string }) => Promise<{ code: number; message?: string }>; @@ -16,4 +20,5 @@ declare global { var authOpenApiHandler: (data: AuthOpenApiLimitProps) => Promise; var createUsageHandler: (data: CreateUsageProps) => any; var concatUsageHandler: (data: ConcatUsageProps) => any; + var pushUsageItemsHandler: (data: PushUsageItemsProps) => any; } diff --git a/packages/service/common/cache/index.ts b/packages/service/common/cache/index.ts new file mode 100644 index 000000000..546e649e5 --- /dev/null +++ b/packages/service/common/cache/index.ts @@ -0,0 +1,60 @@ +import './init'; +import { getGlobalRedisConnection } from '../../common/redis'; +import type { SystemCacheKeyEnum } from './type'; +import { randomUUID } from 'node:crypto'; +import { initCache } from './init'; + +const cachePrefix = `VERSION_KEY:`; + +/** + * + * @param key SystemCacheKeyEnum + * @param id string (teamId, tmbId, etc), if '*' is used, all keys will be refreshed + */ +export const refreshVersionKey = async (key: `${SystemCacheKeyEnum}`, id?: string | '*') => { + const redis = getGlobalRedisConnection(); + if (!global.systemCache) initCache(); + + const val = randomUUID(); + const versionKey = id ? `${cachePrefix}${key}:${id}` : `${cachePrefix}${key}`; + if (id === '*') { + const pattern = `${cachePrefix}${key}:*`; + const keys = await redis.keys(pattern); + if (keys.length > 0) { + await redis.del(keys); + } + } else { + await redis.set(versionKey, val); + } +}; + +export const getVersionKey = async (key: `${SystemCacheKeyEnum}`, id?: string) => { + const redis = getGlobalRedisConnection(); + if (!global.systemCache) initCache(); + + const versionKey = id ? `${cachePrefix}${key}:${id}` : `${cachePrefix}${key}`; + const val = await redis.get(versionKey); + if (val) return val; + + // if there is no val set to the key, init a new val. + const initVal = randomUUID(); + await redis.set(versionKey, initVal); + return initVal; +}; + +export const getCachedData = async (key: T, id?: string) => { + if (!global.systemCache) initCache(); + + const versionKey = await getVersionKey(key, id); + const isDisableCache = process.env.DISABLE_CACHE === 'true'; + + // 命中缓存 + if (global.systemCache[key].versionKey === versionKey && !isDisableCache) { + return global.systemCache[key].data; + } + + const refreshedData = await global.systemCache[key].refreshFunc(); + global.systemCache[key].data = refreshedData; + global.systemCache[key].versionKey = versionKey; + return global.systemCache[key].data; +}; diff --git a/packages/service/common/cache/init.ts b/packages/service/common/cache/init.ts new file mode 100644 index 000000000..a8a2696ac --- /dev/null +++ b/packages/service/common/cache/init.ts @@ -0,0 +1,17 @@ +import { SystemCacheKeyEnum } from './type'; +import { refreshSystemTools } from '../../core/app/plugin/controller'; + +export const initCache = () => { + global.systemCache = { + [SystemCacheKeyEnum.systemTool]: { + versionKey: '', + data: [], + refreshFunc: refreshSystemTools + }, + [SystemCacheKeyEnum.modelPermission]: { + versionKey: '', + data: null, + refreshFunc: () => Promise.resolve(null) + } + }; +}; diff --git a/packages/service/common/cache/type.ts b/packages/service/common/cache/type.ts new file mode 100644 index 000000000..6fbcfe42a --- /dev/null +++ b/packages/service/common/cache/type.ts @@ -0,0 +1,23 @@ +import type { SystemPluginTemplateItemType } from '@fastgpt/global/core/app/plugin/type'; + +export enum SystemCacheKeyEnum { + systemTool = 'systemTool', + modelPermission = 'modelPermission' +} + +export type SystemCacheDataType = { + [SystemCacheKeyEnum.systemTool]: SystemPluginTemplateItemType[]; + [SystemCacheKeyEnum.modelPermission]: null; +}; + +type SystemCacheType = { + [K in SystemCacheKeyEnum]: { + versionKey: string; + data: SystemCacheDataType[K]; + refreshFunc: () => Promise; + }; +}; + +declare global { + var systemCache: SystemCacheType; +} diff --git a/packages/service/common/file/gridfs/controller.ts b/packages/service/common/file/gridfs/controller.ts index afa99571e..d1e707816 100644 --- a/packages/service/common/file/gridfs/controller.ts +++ b/packages/service/common/file/gridfs/controller.ts @@ -196,7 +196,8 @@ export const readFileContentFromMongo = async ({ bucketName, fileId, customPdfParse = false, - getFormatText + getFormatText, + usageId }: { teamId: string; tmbId: string; @@ -204,6 +205,7 @@ export const readFileContentFromMongo = async ({ fileId: string; customPdfParse?: boolean; getFormatText?: boolean; // 数据类型都尽可能转化成 markdown 格式 + usageId?: string; }): Promise<{ rawText: string; filename: string; @@ -237,6 +239,7 @@ export const readFileContentFromMongo = async ({ // Get raw text const { rawText } = await readRawContentByFileBuffer({ customPdfParse, + usageId, getFormatText, extension, teamId, diff --git a/packages/service/common/file/read/utils.ts b/packages/service/common/file/read/utils.ts index 461da09dc..3cc8e26e8 100644 --- a/packages/service/common/file/read/utils.ts +++ b/packages/service/common/file/read/utils.ts @@ -47,6 +47,7 @@ export const readRawContentByFileBuffer = async ({ encoding, metadata, customPdfParse = false, + usageId, getFormatText = true }: { teamId: string; @@ -58,6 +59,7 @@ export const readRawContentByFileBuffer = async ({ metadata?: Record; customPdfParse?: boolean; + usageId?: string; getFormatText?: boolean; }): Promise<{ rawText: string; @@ -104,7 +106,8 @@ export const readRawContentByFileBuffer = async ({ createPdfParseUsage({ teamId, tmbId, - pages: response.pages + pages: response.pages, + usageId }); return { @@ -123,7 +126,8 @@ export const readRawContentByFileBuffer = async ({ createPdfParseUsage({ teamId, tmbId, - pages + pages, + usageId }); return { diff --git a/packages/service/common/s3/config.ts b/packages/service/common/s3/config.ts new file mode 100644 index 000000000..501d61be4 --- /dev/null +++ b/packages/service/common/s3/config.ts @@ -0,0 +1,10 @@ +import type { S3ServiceConfig } from './type'; + +export const defualtS3Config: Omit = { + endPoint: process.env.S3_ENDPOINT || 'localhost', + port: process.env.S3_PORT ? parseInt(process.env.S3_PORT) : 9000, + useSSL: process.env.S3_USE_SSL === 'true', + accessKey: process.env.S3_ACCESS_KEY || 'minioadmin', + secretKey: process.env.S3_SECRET_KEY || 'minioadmin', + externalBaseURL: process.env.S3_EXTERNAL_BASE_URL +}; diff --git a/packages/service/common/s3/const.ts b/packages/service/common/s3/const.ts new file mode 100644 index 000000000..c91043dc1 --- /dev/null +++ b/packages/service/common/s3/const.ts @@ -0,0 +1,20 @@ +export const mimeMap: Record = { + '.jpg': 'image/jpeg', + '.jpeg': 'image/jpeg', + '.png': 'image/png', + '.gif': 'image/gif', + '.webp': 'image/webp', + '.svg': 'image/svg+xml', + '.pdf': 'application/pdf', + '.txt': 'text/plain', + '.json': 'application/json', + '.csv': 'text/csv', + '.zip': 'application/zip', + '.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + '.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + '.pptx': 'application/vnd.openxmlformats-officedocument.presentationml.presentation', + '.doc': 'application/msword', + '.xls': 'application/vnd.ms-excel', + '.ppt': 'application/vnd.ms-powerpoint', + '.js': 'application/javascript' +}; diff --git a/packages/service/common/s3/controller.ts b/packages/service/common/s3/controller.ts new file mode 100644 index 000000000..faaae7cdc --- /dev/null +++ b/packages/service/common/s3/controller.ts @@ -0,0 +1,168 @@ +import { Client } from 'minio'; +import { + type FileMetadataType, + type PresignedUrlInput as UploadPresignedURLProps, + type UploadPresignedURLResponse, + type S3ServiceConfig +} from './type'; +import { defualtS3Config } from './config'; +import { randomBytes } from 'crypto'; +import { HttpProxyAgent } from 'http-proxy-agent'; +import { HttpsProxyAgent } from 'https-proxy-agent'; +import { extname } from 'path'; +import { addLog } from '../../common/system/log'; +import { getErrText } from '@fastgpt/global/common/error/utils'; +import { mimeMap } from './const'; + +export class S3Service { + private client: Client; + private config: S3ServiceConfig; + private initialized: boolean = false; + initFunction?: () => Promise; + + constructor(config?: Partial) { + this.config = { ...defualtS3Config, ...config } as S3ServiceConfig; + + this.client = new Client({ + endPoint: this.config.endPoint, + port: this.config.port, + useSSL: this.config.useSSL, + accessKey: this.config.accessKey, + secretKey: this.config.secretKey, + transportAgent: process.env.HTTP_PROXY + ? new HttpProxyAgent(process.env.HTTP_PROXY) + : process.env.HTTPS_PROXY + ? new HttpsProxyAgent(process.env.HTTPS_PROXY) + : undefined + }); + + this.initFunction = config?.initFunction; + } + + public async init() { + if (!this.initialized) { + if (!(await this.client.bucketExists(this.config.bucket))) { + addLog.debug(`Creating bucket: ${this.config.bucket}`); + await this.client.makeBucket(this.config.bucket); + } + + await this.initFunction?.(); + this.initialized = true; + } + } + + private generateFileId(): string { + return randomBytes(16).toString('hex'); + } + + private generateAccessUrl(filename: string): string { + const protocol = this.config.useSSL ? 'https' : 'http'; + const port = + this.config.port && this.config.port !== (this.config.useSSL ? 443 : 80) + ? `:${this.config.port}` + : ''; + + const externalBaseURL = this.config.externalBaseURL; + return externalBaseURL + ? `${externalBaseURL}/${this.config.bucket}/${encodeURIComponent(filename)}` + : `${protocol}://${this.config.endPoint}${port}/${this.config.bucket}/${encodeURIComponent(filename)}`; + } + + uploadFile = async (fileBuffer: Buffer, originalFilename: string): Promise => { + await this.init(); + const inferContentType = (filename: string) => { + const ext = extname(filename).toLowerCase(); + return mimeMap[ext] || 'application/octet-stream'; + }; + + if (this.config.maxFileSize && fileBuffer.length > this.config.maxFileSize) { + return Promise.reject( + `File size ${fileBuffer.length} exceeds limit ${this.config.maxFileSize}` + ); + } + + const fileId = this.generateFileId(); + const objectName = `${fileId}-${originalFilename}`; + const uploadTime = new Date(); + + const contentType = inferContentType(originalFilename); + await this.client.putObject(this.config.bucket, objectName, fileBuffer, fileBuffer.length, { + 'Content-Type': contentType, + 'Content-Disposition': `attachment; filename="${encodeURIComponent(originalFilename)}"`, + 'x-amz-meta-original-filename': encodeURIComponent(originalFilename), + 'x-amz-meta-upload-time': uploadTime.toISOString() + }); + + const metadata: FileMetadataType = { + fileId, + originalFilename, + contentType, + size: fileBuffer.length, + uploadTime, + accessUrl: this.generateAccessUrl(objectName) + }; + + return metadata; + }; + + generateUploadPresignedURL = async ({ + filepath, + contentType, + metadata, + filename + }: UploadPresignedURLProps): Promise => { + await this.init(); + const objectName = `${filepath}/${filename}`; + + try { + const policy = this.client.newPostPolicy(); + + policy.setBucket(this.config.bucket); + policy.setKey(objectName); + if (contentType) { + policy.setContentType(contentType); + } + if (this.config.maxFileSize) { + policy.setContentLengthRange(1, this.config.maxFileSize); + } + policy.setExpires(new Date(Date.now() + 10 * 60 * 1000)); // 10 mins + + policy.setUserMetaData({ + 'original-filename': encodeURIComponent(filename), + 'upload-time': new Date().toISOString(), + ...metadata + }); + + const { postURL, formData } = await this.client.presignedPostPolicy(policy); + + const response: UploadPresignedURLResponse = { + objectName, + uploadUrl: postURL, + formData + }; + + return response; + } catch (error) { + addLog.error('Failed to generate Upload Presigned URL', error); + return Promise.reject(`Failed to generate Upload Presigned URL: ${getErrText(error)}`); + } + }; + + generateDownloadUrl = (objectName: string): string => { + const pathParts = objectName.split('/'); + const encodedParts = pathParts.map((part) => encodeURIComponent(part)); + const encodedObjectName = encodedParts.join('/'); + return `${this.config.bucket}/${encodedObjectName}`; + }; + + getFile = async (objectName: string): Promise => { + const stat = await this.client.statObject(this.config.bucket, objectName); + + if (stat.size > 0) { + const accessUrl = this.generateDownloadUrl(objectName); + return accessUrl; + } + + return Promise.reject(`File ${objectName} not found`); + }; +} diff --git a/packages/service/common/s3/index.ts b/packages/service/common/s3/index.ts new file mode 100644 index 000000000..761bd5640 --- /dev/null +++ b/packages/service/common/s3/index.ts @@ -0,0 +1,16 @@ +import { S3Service } from './controller'; + +export const PluginS3Service = (() => { + if (!global.pluginS3Service) { + global.pluginS3Service = new S3Service({ + bucket: process.env.S3_PLUGIN_BUCKET || 'fastgpt-plugin', + maxFileSize: 50 * 1024 * 1024 // 50MB + }); + } + + return global.pluginS3Service; +})(); + +declare global { + var pluginS3Service: S3Service; +} diff --git a/packages/service/common/s3/type.ts b/packages/service/common/s3/type.ts new file mode 100644 index 000000000..a48053049 --- /dev/null +++ b/packages/service/common/s3/type.ts @@ -0,0 +1,49 @@ +import type { ClientOptions } from 'minio'; + +export type S3ServiceConfig = { + bucket: string; + externalBaseURL?: string; + /** + * Unit: Byte + */ + maxFileSize?: number; + /** + * for executing some init function for the s3 service + */ + initFunction?: () => Promise; +} & ClientOptions; + +export type FileMetadataType = { + fileId: string; + originalFilename: string; + contentType: string; + size: number; + uploadTime: Date; + accessUrl: string; +}; + +export type PresignedUrlInput = { + filepath: string; + filename: string; + contentType?: string; + metadata?: Record; +}; + +export type UploadPresignedURLResponse = { + objectName: string; + uploadUrl: string; + formData: Record; +}; + +export type FileUploadInput = { + buffer: Buffer; + filename: string; +}; + +export enum PluginTypeEnum { + tool = 'tool' +} + +export const PluginFilePath = { + [PluginTypeEnum.tool]: 'plugin/tools' +}; diff --git a/packages/service/common/system/constants.ts b/packages/service/common/system/constants.ts index facf9ab12..313e9031c 100644 --- a/packages/service/common/system/constants.ts +++ b/packages/service/common/system/constants.ts @@ -2,3 +2,7 @@ export const FastGPTProUrl = process.env.PRO_URL ? `${process.env.PRO_URL}/api` export const FastGPTPluginUrl = process.env.PLUGIN_BASE_URL ? `${process.env.PLUGIN_BASE_URL}` : ''; // @ts-ignore export const isFastGPTProService = () => !!global.systemConfig; + +export const isProVersion = () => { + return !!global.feConfigs?.isPlus; +}; diff --git a/packages/service/common/vectorDB/controller.ts b/packages/service/common/vectorDB/controller.ts index a2658b97e..dc59726dc 100644 --- a/packages/service/common/vectorDB/controller.ts +++ b/packages/service/common/vectorDB/controller.ts @@ -2,12 +2,8 @@ import { PgVectorCtrl } from './pg'; import { ObVectorCtrl } from './oceanbase'; import { getVectorsByText } from '../../core/ai/embedding'; -import type { - EmbeddingRecallCtrlProps} from './controller.d'; -import { - type DelDatasetVectorCtrlProps, - type InsertVectorProps -} from './controller.d'; +import type { EmbeddingRecallCtrlProps } from './controller.d'; +import { type DelDatasetVectorCtrlProps, type InsertVectorProps } from './controller.d'; import { type EmbeddingModelItemType } from '@fastgpt/global/core/ai/model.d'; import { MILVUS_ADDRESS, PG_ADDRESS, OCEANBASE_ADDRESS } from './constants'; import { MilvusCtrl } from './milvus'; diff --git a/packages/service/core/ai/config/utils.ts b/packages/service/core/ai/config/utils.ts index 743b1a521..3869b264d 100644 --- a/packages/service/core/ai/config/utils.ts +++ b/packages/service/core/ai/config/utils.ts @@ -19,6 +19,8 @@ import { delay } from '@fastgpt/global/common/system/utils'; import { pluginClient } from '../../../thirdProvider/fastgptPlugin'; import { setCron } from '../../../common/system/cron'; import { preloadModelProviders } from '../../../core/app/provider/controller'; +import { refreshVersionKey } from '../../../common/cache'; +import { SystemCacheKeyEnum } from '../../../common/cache/type'; export const loadSystemModels = async (init = false, language = 'en') => { const pushModel = (model: SystemModelItemType) => { @@ -253,6 +255,7 @@ export const updatedReloadSystemModel = async () => { await loadSystemModels(true); // 2. 更新缓存(仅主节点触发) await updateFastGPTConfigBuffer(); + await refreshVersionKey(SystemCacheKeyEnum.modelPermission, '*'); // 3. 延迟1秒,等待其他节点刷新 await delay(1000); }; diff --git a/packages/service/core/ai/llm/request.ts b/packages/service/core/ai/llm/request.ts index 41b84ced2..1a45ac6be 100644 --- a/packages/service/core/ai/llm/request.ts +++ b/packages/service/core/ai/llm/request.ts @@ -142,8 +142,8 @@ export const createLLMResponse = async ( // Usage count const inputTokens = - usage?.prompt_tokens ?? (await countGptMessagesTokens(requestBody.messages, requestBody.tools)); - const outputTokens = usage?.completion_tokens ?? (await countGptMessagesTokens(assistantMessage)); + usage?.prompt_tokens || (await countGptMessagesTokens(requestBody.messages, requestBody.tools)); + const outputTokens = usage?.completion_tokens || (await countGptMessagesTokens(assistantMessage)); return { isStreamResponse, @@ -645,4 +645,4 @@ const createChatCompletion = async ({ } return Promise.reject(error); } -}; \ No newline at end of file +}; diff --git a/packages/service/core/app/controller.ts b/packages/service/core/app/controller.ts index 5cf05f1fb..4821b344d 100644 --- a/packages/service/core/app/controller.ts +++ b/packages/service/core/app/controller.ts @@ -23,6 +23,7 @@ import { PerResourceTypeEnum } from '@fastgpt/global/support/permission/constant import { removeImageByPath } from '../../common/file/image/controller'; import { mongoSessionRun } from '../../common/mongo/sessionRun'; import { MongoAppLogKeys } from './logs/logkeysSchema'; +import { MongoChatItemResponse } from '../chat/chatItemResponseSchema'; export const beforeUpdateAppFormat = ({ nodes }: { nodes?: StoreNodeItemType[] }) => { if (!nodes) return; @@ -159,6 +160,9 @@ export const onDelOneApp = async ({ // Delete chats await deleteChatFiles({ appId }); + await MongoChatItemResponse.deleteMany({ + appId + }); await MongoChatItem.deleteMany({ appId }); diff --git a/packages/service/core/app/evaluation/evalSchema.ts b/packages/service/core/app/evaluation/evalSchema.ts index a8678ebda..e5a78a013 100644 --- a/packages/service/core/app/evaluation/evalSchema.ts +++ b/packages/service/core/app/evaluation/evalSchema.ts @@ -5,7 +5,7 @@ import { import { connectionMongo, getMongoModel } from '../../../common/mongo'; import { AppCollectionName } from '../schema'; import type { EvaluationSchemaType } from '@fastgpt/global/core/app/evaluation/type'; -import { UsageCollectionName } from '../../../support/wallet/usage/schema'; +import { UsageCollectionName } from '../../../support/wallet/usage/constants'; const { Schema } = connectionMongo; export const EvaluationCollectionName = 'eval'; diff --git a/packages/service/core/app/http.ts b/packages/service/core/app/http.ts new file mode 100644 index 000000000..698695441 --- /dev/null +++ b/packages/service/core/app/http.ts @@ -0,0 +1,58 @@ +import { type StoreSecretValueType } from '@fastgpt/global/common/secret/type'; +import { getSecretValue } from '../../common/secret/utils'; +import axios from 'axios'; +import { getErrText } from '@fastgpt/global/common/error/utils'; +import type { RequireOnlyOne } from '@fastgpt/global/common/type/utils'; + +export type RunHTTPToolParams = { + baseUrl: string; + toolPath: string; + method: string; + params: Record; + headerSecret?: StoreSecretValueType; + customHeaders?: Record; +}; + +export type RunHTTPToolResult = RequireOnlyOne<{ + data?: any; + errorMsg?: string; +}>; + +export async function runHTTPTool({ + baseUrl, + toolPath, + method = 'POST', + params, + headerSecret, + customHeaders +}: RunHTTPToolParams): Promise { + try { + const headers = { + 'Content-Type': 'application/json', + ...(customHeaders || {}), + ...(headerSecret ? getSecretValue({ storeSecret: headerSecret }) : {}) + }; + + const { data } = await axios({ + method: method.toUpperCase(), + baseURL: baseUrl.startsWith('https://') ? baseUrl : `https://${baseUrl}`, + url: toolPath, + headers, + data: params, + params, + timeout: 300000, + httpsAgent: new (require('https').Agent)({ + rejectUnauthorized: false + }) + }); + + return { + data + }; + } catch (error: any) { + console.log(error); + return { + errorMsg: getErrText(error) + }; + } +} diff --git a/packages/service/core/app/plugin/controller.ts b/packages/service/core/app/plugin/controller.ts index 387985927..d76fa9689 100644 --- a/packages/service/core/app/plugin/controller.ts +++ b/packages/service/core/app/plugin/controller.ts @@ -43,10 +43,13 @@ import { isProduction } from '@fastgpt/global/common/system/constants'; import { Output_Template_Error_Message } from '@fastgpt/global/core/workflow/template/output'; import { splitCombinePluginId } from '@fastgpt/global/core/app/plugin/utils'; import { getMCPToolRuntimeNode } from '@fastgpt/global/core/app/mcpTools/utils'; +import { getHTTPToolRuntimeNode } from '@fastgpt/global/core/app/httpTools/utils'; import { AppTypeEnum } from '@fastgpt/global/core/app/constants'; import { getMCPChildren } from '../mcp'; import { cloneDeep } from 'lodash'; import { UserError } from '@fastgpt/global/common/error/utils'; +import { getCachedData } from '../../../common/cache'; +import { SystemCacheKeyEnum } from '../../../common/cache/type'; type ChildAppType = SystemPluginTemplateItemType & { teamId?: string; @@ -56,6 +59,8 @@ type ChildAppType = SystemPluginTemplateItemType & { isLatestVersion?: boolean; // Auto computed }; +export const getSystemTools = () => getCachedData(SystemCacheKeyEnum.systemTool); + export const getSystemPluginByIdAndVersionId = async ( pluginId: string, versionId?: string @@ -190,11 +195,11 @@ export async function getChildAppPreviewNode({ mcpToolSet: { toolId: pluginId, toolList: children, - url: '' + url: '', + headerSecret: {} } }; } - return { id: String(item._id), teamId: String(item.teamId), @@ -260,6 +265,45 @@ export async function getChildAppPreviewNode({ isLatestVersion: true }; } + // http tool + else if (source === PluginSourceEnum.http) { + const [parentId, toolName] = pluginId.split('/'); + const item = await MongoApp.findById(parentId).lean(); + if (!item) return Promise.reject(PluginErrEnum.unExist); + + const version = await getAppVersionById({ appId: parentId, versionId, app: item }); + const toolConfig = version.nodes[0].toolConfig?.httpToolSet; + const tool = await (async () => { + if (toolConfig?.toolList) { + return toolConfig.toolList.find((item) => item.name === toolName); + } + return undefined; + })(); + if (!tool) return Promise.reject(PluginErrEnum.unExist); + return { + avatar: item.avatar, + id: appId, + name: tool.name, + templateType: FlowNodeTemplateTypeEnum.tools, + workflow: { + nodes: [ + getHTTPToolRuntimeNode({ + tool: { + description: tool.description, + inputSchema: tool.inputSchema, + outputSchema: tool.outputSchema, + name: `${item.name}/${tool.name}` + }, + avatar: item.avatar, + parentId: item._id + }) + ], + edges: [] + }, + version: '', + isLatestVersion: true + }; + } // 1. System Tools // 2. System Plugins configured in Pro (has associatedPluginId) else { @@ -503,97 +547,63 @@ const dbPluginFormat = (item: SystemPluginConfigSchemaType): SystemPluginTemplat }; /* FastsGPT-Pluign api: */ -function getCachedSystemPlugins() { - if (!global.systemToolsCache) { - global.systemToolsCache = { - expires: 0, - data: [] as SystemPluginTemplateItemType[] +export const refreshSystemTools = async (): Promise => { + const tools = await APIGetSystemToolList(); + // 从数据库里加载插件配置进行替换 + const systemToolsArray = await MongoSystemPlugin.find({}).lean(); + const systemTools = new Map(systemToolsArray.map((plugin) => [plugin.pluginId, plugin])); + + const formatTools = tools.map((item) => { + const dbPluginConfig = systemTools.get(item.id); + const isFolder = tools.some((tool) => tool.parentId === item.id); + + const versionList = (item.versionList as SystemPluginTemplateItemType['versionList']) || []; + + return { + id: item.id, + parentId: item.parentId, + isFolder, + name: item.name, + avatar: item.avatar, + intro: item.description, + toolDescription: item.toolDescription, + author: item.author, + courseUrl: item.courseUrl, + instructions: dbPluginConfig?.customConfig?.userGuide, + weight: item.weight, + toolSource: item.toolSource || 'built-in', + workflow: { + nodes: [], + edges: [] + }, + versionList, + templateType: item.templateType, + showStatus: true, + isActive: dbPluginConfig?.isActive ?? item.isActive ?? true, + inputList: item?.secretInputConfig, + hasSystemSecret: !!dbPluginConfig?.inputListVal, + + originCost: dbPluginConfig?.originCost ?? 0, + currentCost: dbPluginConfig?.currentCost ?? 0, + systemKeyCost: dbPluginConfig?.systemKeyCost ?? 0, + hasTokenFee: dbPluginConfig?.hasTokenFee ?? false, + pluginOrder: dbPluginConfig?.pluginOrder }; - } - return global.systemToolsCache; -} - -const cleanSystemPluginCache = () => { - global.systemToolsCache = undefined; -}; - -export const refetchSystemPlugins = () => { - const changeStream = MongoSystemPlugin.watch(); - - changeStream.on('change', () => { - try { - cleanSystemPluginCache(); - } catch (error) {} }); -}; -export const getSystemTools = async (): Promise => { - if (getCachedSystemPlugins().expires > Date.now() && isProduction) { - return getCachedSystemPlugins().data; - } else { - const tools = await APIGetSystemToolList(); + const dbPlugins = systemToolsArray + .filter((item) => item.customConfig?.associatedPluginId) + .map((item) => dbPluginFormat(item)); - // 从数据库里加载插件配置进行替换 - const systemToolsArray = await MongoSystemPlugin.find({}).lean(); - const systemTools = new Map(systemToolsArray.map((plugin) => [plugin.pluginId, plugin])); + const concatTools = [...formatTools, ...dbPlugins]; + concatTools.sort((a, b) => (a.pluginOrder ?? 0) - (b.pluginOrder ?? 0)); - const formatTools = tools.map((item) => { - const dbPluginConfig = systemTools.get(item.id); - const isFolder = tools.some((tool) => tool.parentId === item.id); + global.systemToolsTypeCache = {}; + concatTools.forEach((item) => { + global.systemToolsTypeCache[item.templateType] = 1; + }); - const versionList = (item.versionList as SystemPluginTemplateItemType['versionList']) || []; - - return { - id: item.id, - parentId: item.parentId, - isFolder, - name: item.name, - avatar: item.avatar, - intro: item.description, - toolDescription: item.toolDescription, - author: item.author, - courseUrl: item.courseUrl, - instructions: dbPluginConfig?.customConfig?.userGuide, - weight: item.weight, - workflow: { - nodes: [], - edges: [] - }, - versionList, - templateType: item.templateType, - showStatus: true, - isActive: dbPluginConfig?.isActive ?? item.isActive ?? true, - inputList: item?.secretInputConfig, - hasSystemSecret: !!dbPluginConfig?.inputListVal, - - originCost: dbPluginConfig?.originCost ?? 0, - currentCost: dbPluginConfig?.currentCost ?? 0, - systemKeyCost: dbPluginConfig?.systemKeyCost ?? 0, - hasTokenFee: dbPluginConfig?.hasTokenFee ?? false, - pluginOrder: dbPluginConfig?.pluginOrder - }; - }); - - // TODO: Check the app exists - const dbPlugins = systemToolsArray - .filter((item) => item.customConfig?.associatedPluginId) - .map((item) => dbPluginFormat(item)); - - const concatTools = [...formatTools, ...dbPlugins]; - concatTools.sort((a, b) => (a.pluginOrder ?? 0) - (b.pluginOrder ?? 0)); - - global.systemToolsCache = { - expires: Date.now() + 30 * 60 * 1000, // 30 minutes - data: concatTools - }; - - global.systemToolsTypeCache = {}; - concatTools.forEach((item) => { - global.systemToolsTypeCache[item.templateType] = 1; - }); - - return concatTools; - } + return concatTools; }; export const getSystemToolById = async (id: string): Promise => { @@ -613,11 +623,5 @@ export const getSystemToolById = async (id: string): Promise; } diff --git a/packages/service/core/app/templates/register.ts b/packages/service/core/app/templates/register.ts index 3b3df55b7..23daf81d1 100644 --- a/packages/service/core/app/templates/register.ts +++ b/packages/service/core/app/templates/register.ts @@ -35,7 +35,7 @@ const getAppTemplates = async () => { const res = [ ...communityTemplateConfig, - ...dbTemplates.filter((t) => !isCommunityTemplate(t.templateId)) + ...dbTemplates.filter((t) => isCommercialTemaplte(t.templateId)) ].sort((a, b) => (a.order ?? 0) - (b.order ?? 0)); return res; @@ -60,8 +60,8 @@ export const getAppTemplatesAndLoadThem = async (refresh = false) => { } }; -export const isCommunityTemplate = (templateId: string) => { - return templateId.startsWith(PluginSourceEnum.community); +export const isCommercialTemaplte = (templateId: string) => { + return templateId.startsWith(PluginSourceEnum.commercial); }; declare global { diff --git a/packages/service/core/app/tool/api.ts b/packages/service/core/app/tool/api.ts index db76aa5f8..5890515bf 100644 --- a/packages/service/core/app/tool/api.ts +++ b/packages/service/core/app/tool/api.ts @@ -1,7 +1,7 @@ import type { I18nStringStrictType, ToolTypeEnum } from '@fastgpt/global/sdk/fastgpt-plugin'; import { RunToolWithStream } from '@fastgpt/global/sdk/fastgpt-plugin'; import { PluginSourceEnum } from '@fastgpt/global/core/app/plugin/constants'; -import { pluginClient, BASE_URL, TOKEN } from '../../../thirdProvider/fastgptPlugin'; +import { pluginClient, PLUGIN_BASE_URL, PLUGIN_TOKEN } from '../../../thirdProvider/fastgptPlugin'; import { addLog } from '../../../common/system/log'; import { retryFn } from '@fastgpt/global/common/system/utils'; @@ -26,8 +26,8 @@ export async function APIGetSystemToolList() { } const runToolInstance = new RunToolWithStream({ - baseUrl: BASE_URL, - token: TOKEN + baseUrl: PLUGIN_BASE_URL, + token: PLUGIN_TOKEN }); export const APIRunSystemTool = runToolInstance.run.bind(runToolInstance); diff --git a/packages/service/core/chat/chatItemResponseSchema.ts b/packages/service/core/chat/chatItemResponseSchema.ts new file mode 100644 index 000000000..31b2c2258 --- /dev/null +++ b/packages/service/core/chat/chatItemResponseSchema.ts @@ -0,0 +1,47 @@ +import { connectionMongo, getMongoModel } from '../../common/mongo'; +const { Schema } = connectionMongo; +import type { ChatItemResponseSchemaType } from '@fastgpt/global/core/chat/type'; +import { TeamCollectionName } from '@fastgpt/global/support/user/team/constant'; +import { AppCollectionName } from '../app/schema'; +import { ChatItemResponseCollectionName } from './constants'; + +const ChatItemResponseSchema = new Schema({ + teamId: { + type: Schema.Types.ObjectId, + ref: TeamCollectionName, + required: true + }, + appId: { + type: Schema.Types.ObjectId, + ref: AppCollectionName, + required: true + }, + chatId: { + type: String, + require: true + }, + chatItemDataId: { + type: String, + require: true + }, + data: { + type: Object, + default: {} + }, + + time: { + type: Date, + default: () => new Date() + } +}); + +// Get response/Delete +ChatItemResponseSchema.index({ appId: 1, chatId: 1, chatItemDataId: 1 }); + +// Clear expired response +ChatItemResponseSchema.index({ teamId: 1, time: -1 }); + +export const MongoChatItemResponse = getMongoModel( + ChatItemResponseCollectionName, + ChatItemResponseSchema +); diff --git a/packages/service/core/chat/chatItemSchema.ts b/packages/service/core/chat/chatItemSchema.ts index c192d81b1..770d3637f 100644 --- a/packages/service/core/chat/chatItemSchema.ts +++ b/packages/service/core/chat/chatItemSchema.ts @@ -10,8 +10,7 @@ import { import { AppCollectionName } from '../app/schema'; import { userCollectionName } from '../../support/user/schema'; import { DispatchNodeResponseKeyEnum } from '@fastgpt/global/core/workflow/runtime/constants'; - -export const ChatItemCollectionName = 'chatitems'; +import { ChatItemCollectionName } from './constants'; const ChatItemSchema = new Schema({ teamId: { @@ -35,7 +34,7 @@ const ChatItemSchema = new Schema({ dataId: { type: String, require: true, - default: () => getNanoid(22) + default: () => getNanoid(24) }, appId: { type: Schema.Types.ObjectId, @@ -61,6 +60,8 @@ const ChatItemSchema = new Schema({ type: Array, default: [] }, + + // Field memory memories: Object, errorMsg: String, userGoodFeedback: String, @@ -75,29 +76,25 @@ const ChatItemSchema = new Schema({ a: String } }, - [DispatchNodeResponseKeyEnum.nodeResponse]: { - type: Array, - default: [] - }, - durationSeconds: Number + durationSeconds: Number, + citeCollectionIds: [String], + + // @deprecated + [DispatchNodeResponseKeyEnum.nodeResponse]: Array }); -try { - ChatItemSchema.index({ dataId: 1 }); - /* delete by app; - delete by chat id; - get chat list; - get chat logs; - close custom feedback; - */ - ChatItemSchema.index({ appId: 1, chatId: 1, dataId: 1 }); - // timer, clear history - ChatItemSchema.index({ teamId: 1, time: -1 }); +/* + delete by app; + delete by chat id; + get chat list; + get chat logs; + close custom feedback; +*/ +ChatItemSchema.index({ appId: 1, chatId: 1, dataId: 1 }); +// timer, clear history +ChatItemSchema.index({ teamId: 1, time: -1 }); - // Admin charts - ChatItemSchema.index({ obj: 1, time: -1 }, { partialFilterExpression: { obj: 'Human' } }); -} catch (error) { - console.log(error); -} +// Admin charts +ChatItemSchema.index({ obj: 1, time: -1 }, { partialFilterExpression: { obj: 'Human' } }); export const MongoChatItem = getMongoModel(ChatItemCollectionName, ChatItemSchema); diff --git a/packages/service/core/chat/chatSchema.ts b/packages/service/core/chat/chatSchema.ts index a8bdf2e0e..8ca948960 100644 --- a/packages/service/core/chat/chatSchema.ts +++ b/packages/service/core/chat/chatSchema.ts @@ -7,8 +7,7 @@ import { TeamMemberCollectionName } from '@fastgpt/global/support/user/team/constant'; import { AppCollectionName } from '../app/schema'; - -export const chatCollectionName = 'chat'; +import { chatCollectionName } from './constants'; const ChatSchema = new Schema({ chatId: { diff --git a/packages/service/core/chat/constants.ts b/packages/service/core/chat/constants.ts new file mode 100644 index 000000000..c3b6a6444 --- /dev/null +++ b/packages/service/core/chat/constants.ts @@ -0,0 +1,3 @@ +export const chatCollectionName = 'chats'; +export const ChatItemResponseCollectionName = 'chat_item_responses'; +export const ChatItemCollectionName = 'chatitems'; diff --git a/packages/service/core/chat/controller.ts b/packages/service/core/chat/controller.ts index b6113869a..c854e4c11 100644 --- a/packages/service/core/chat/controller.ts +++ b/packages/service/core/chat/controller.ts @@ -1,10 +1,13 @@ -import type { ChatItemType } from '@fastgpt/global/core/chat/type'; +import type { ChatHistoryItemResType, ChatItemType } from '@fastgpt/global/core/chat/type'; import { MongoChatItem } from './chatItemSchema'; import { addLog } from '../../common/system/log'; import { delFileByFileIdList, getGFSCollection } from '../../common/file/gridfs/controller'; import { BucketNameEnum } from '@fastgpt/global/common/file/constants'; import { MongoChat } from './chatSchema'; import { UserError } from '@fastgpt/global/common/error/utils'; +import { DispatchNodeResponseKeyEnum } from '@fastgpt/global/core/workflow/runtime/constants'; +import { ChatRoleEnum } from '@fastgpt/global/core/chat/constants'; +import { MongoChatItemResponse } from './chatItemResponseSchema'; export async function getChatItems({ appId, @@ -23,12 +26,44 @@ export async function getChatItems({ return { histories: [], total: 0 }; } + // Extend dataId + field = `dataId ${field}`; + const [histories, total] = await Promise.all([ - MongoChatItem.find({ chatId, appId }, field).sort({ _id: -1 }).skip(offset).limit(limit).lean(), - MongoChatItem.countDocuments({ chatId, appId }) + MongoChatItem.find({ appId, chatId }, field).sort({ _id: -1 }).skip(offset).limit(limit).lean(), + MongoChatItem.countDocuments({ appId, chatId }) ]); histories.reverse(); + // Add node responses field + if (field.includes(DispatchNodeResponseKeyEnum.nodeResponse)) { + const chatItemDataIds = histories + .filter((item) => item.obj === ChatRoleEnum.AI && !item.responseData?.length) + .map((item) => item.dataId); + + const chatItemResponsesMap = await MongoChatItemResponse.find( + { appId, chatId, chatItemDataId: { $in: chatItemDataIds } }, + { chatItemDataId: 1, data: 1 } + ) + .lean() + .then((res) => { + const map = new Map(); + res.forEach((item) => { + const val = map.get(item.chatItemDataId) || []; + val.push(item.data); + map.set(item.chatItemDataId, val); + }); + return map; + }); + + histories.forEach((item) => { + const val = chatItemResponsesMap.get(String(item.dataId)); + if (item.obj === ChatRoleEnum.AI && val) { + item.responseData = val; + } + }); + } + return { histories, total }; } diff --git a/packages/service/core/chat/pushChatLog.ts b/packages/service/core/chat/pushChatLog.ts index 86677c55f..afc14ab3f 100644 --- a/packages/service/core/chat/pushChatLog.ts +++ b/packages/service/core/chat/pushChatLog.ts @@ -2,11 +2,7 @@ import { addLog } from '../../common/system/log'; import { MongoChatItem } from './chatItemSchema'; import { MongoChat } from './chatSchema'; import axios from 'axios'; -import { - type AIChatItemType, - type ChatItemType, - type UserChatItemType -} from '@fastgpt/global/core/chat/type'; +import { type AIChatItemType, type UserChatItemType } from '@fastgpt/global/core/chat/type'; import { ChatItemValueTypeEnum } from '@fastgpt/global/core/chat/constants'; export type Metadata = { @@ -43,18 +39,6 @@ export const pushChatLog = ({ } }; -type ChatItem = ChatItemType & { - userGoodFeedback?: string; - userBadFeedback?: string; - chatId: string; - responseData: { - moduleType: string; - runningTime: number; //s - historyPreview: { obj: string; value: string }[]; - }[]; - time: Date; -}; - type ChatLog = { title: string; feedback: 'like' | 'dislike' | null; diff --git a/packages/service/core/chat/saveChat.ts b/packages/service/core/chat/saveChat.ts index 09e466ade..f6ae19003 100644 --- a/packages/service/core/chat/saveChat.ts +++ b/packages/service/core/chat/saveChat.ts @@ -12,10 +12,11 @@ import { type AppChatConfigType } from '@fastgpt/global/core/app/type'; import { mergeChatResponseData } from '@fastgpt/global/core/chat/utils'; import { pushChatLog } from './pushChatLog'; import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant'; -import { DispatchNodeResponseKeyEnum } from '@fastgpt/global/core/workflow/runtime/constants'; import { extractDeepestInteractive } from '@fastgpt/global/core/workflow/runtime/utils'; import { MongoAppChatLog } from '../app/logs/chatLogsSchema'; import { writePrimary } from '../../common/mongo/utils'; +import { MongoChatItemResponse } from './chatItemResponseSchema'; +import { chatValue2RuntimePrompt } from '@fastgpt/global/core/chat/adapt'; type Props = { chatId: string; @@ -31,12 +32,59 @@ type Props = { sourceName?: string; shareId?: string; outLinkUid?: string; - content: [UserChatItemType & { dataId?: string }, AIChatItemType & { dataId?: string }]; + userContent: UserChatItemType & { dataId?: string }; + aiContent: AIChatItemType & { dataId?: string }; metadata?: Record; durationSeconds: number; //s errorMsg?: string; }; +const formatAiContent = ({ + aiContent, + durationSeconds, + errorMsg +}: { + aiContent: AIChatItemType & { dataId?: string }; + durationSeconds: number; + errorMsg?: string; +}) => { + const { responseData, ...aiResponse } = aiContent; + + const citeCollectionIds = new Set(); + + const nodeResponses = responseData?.map((responseItem) => { + if (responseItem.moduleType === FlowNodeTypeEnum.datasetSearchNode && responseItem.quoteList) { + return { + ...responseItem, + quoteList: responseItem.quoteList.map((quote) => { + citeCollectionIds.add(quote.collectionId); + return { + id: quote.id, + chunkIndex: quote.chunkIndex, + datasetId: quote.datasetId, + collectionId: quote.collectionId, + sourceId: quote.sourceId, + sourceName: quote.sourceName, + score: quote.score + }; + }) + }; + } + return responseItem; + }); + + return { + aiResponse: { + ...aiResponse, + durationSeconds, + errorMsg, + citeCollectionIds: Array.from(citeCollectionIds) + }, + nodeResponses, + citeCollectionIds + }; +}; + export async function saveChat({ chatId, appId, @@ -51,7 +99,8 @@ export async function saveChat({ sourceName, shareId, outLinkUid, - content, + userContent, + aiContent, durationSeconds, errorMsg, metadata = {} @@ -81,42 +130,15 @@ export async function saveChat({ )?.inputs; // Format save chat content: Remove quote q/a - const processedContent = content.map((item) => { - if (item.obj === ChatRoleEnum.AI) { - const nodeResponse = item[DispatchNodeResponseKeyEnum.nodeResponse]?.map((responseItem) => { - if ( - responseItem.moduleType === FlowNodeTypeEnum.datasetSearchNode && - responseItem.quoteList - ) { - return { - ...responseItem, - quoteList: responseItem.quoteList.map((quote: any) => ({ - id: quote.id, - chunkIndex: quote.chunkIndex, - datasetId: quote.datasetId, - collectionId: quote.collectionId, - sourceId: quote.sourceId, - sourceName: quote.sourceName, - score: quote.score, - tokens: quote.tokens - })) - }; - } - return responseItem; - }); - - return { - ...item, - [DispatchNodeResponseKeyEnum.nodeResponse]: nodeResponse, - durationSeconds, - errorMsg - }; - } - return item; + const { aiResponse, nodeResponses } = formatAiContent({ + aiContent, + durationSeconds, + errorMsg }); + const processedContent = [userContent, aiResponse]; await mongoSessionRun(async (session) => { - const [{ _id: chatItemIdHuman }, { _id: chatItemIdAi }] = await MongoChatItem.create( + const [{ _id: chatItemIdHuman }, { _id: chatItemIdAi, dataId }] = await MongoChatItem.create( processedContent.map((item) => ({ chatId, teamId, @@ -127,6 +149,20 @@ export async function saveChat({ { session, ordered: true, ...writePrimary } ); + // Create chat item respones + if (nodeResponses) { + await MongoChatItemResponse.create( + nodeResponses.map((item) => ({ + teamId, + appId, + chatId, + chatItemDataId: dataId, + data: item + })), + { session, ordered: true, ...writePrimary } + ); + } + await MongoChat.updateOne( { appId, @@ -166,18 +202,15 @@ export async function saveChat({ }); }); + // Create chat data log try { const userId = String(outLinkUid || tmbId); const now = new Date(); const fifteenMinutesAgo = new Date(now.getTime() - 15 * 60 * 1000); - const aiResponse = processedContent.find((item) => item.obj === ChatRoleEnum.AI); - const errorCount = aiResponse?.responseData?.some((item) => item.errorText) ? 1 : 0; + const errorCount = nodeResponses?.some((item) => item.errorText) ? 1 : 0; const totalPoints = - aiResponse?.responseData?.reduce( - (sum: number, item: any) => sum + (item.totalPoints || 0), - 0 - ) || 0; + nodeResponses?.reduce((sum: number, item: any) => sum + (item.totalPoints || 0), 0) || 0; const hasHistoryChat = await MongoAppChatLog.exists({ teamId, @@ -242,20 +275,16 @@ export async function saveChat({ } export const updateInteractiveChat = async ({ + teamId, chatId, + appId, - userInteractiveVal, - aiResponse, - newVariables, - durationSeconds -}: { - chatId: string; - appId: string; - userInteractiveVal: string; - aiResponse: AIChatItemType & { dataId?: string }; - newVariables?: Record; - durationSeconds: number; -}) => { + userContent, + aiContent, + variables, + durationSeconds, + errorMsg +}: Props) => { if (!chatId) return; const chatItem = await MongoChatItem.findOne({ appId, chatId, obj: ChatRoleEnum.AI }).sort({ @@ -276,17 +305,23 @@ export const updateInteractiveChat = async ({ } const parsedUserInteractiveVal = (() => { + const { text: userInteractiveVal } = chatValue2RuntimePrompt(userContent.value); try { return JSON.parse(userInteractiveVal); } catch (err) { return userInteractiveVal; } })(); + const { aiResponse, nodeResponses } = formatAiContent({ + aiContent, + durationSeconds, + errorMsg + }); let finalInteractive = extractDeepestInteractive(interactiveValue.interactive); if (finalInteractive.type === 'userSelect') { - finalInteractive.params.userSelectedVal = userInteractiveVal; + finalInteractive.params.userSelectedVal = parsedUserInteractiveVal; } else if ( finalInteractive.type === 'userInput' && typeof parsedUserInteractiveVal === 'object' @@ -308,16 +343,14 @@ export const updateInteractiveChat = async ({ ? [...chatItem.customFeedbacks, ...aiResponse.customFeedbacks] : aiResponse.customFeedbacks; } - - if (aiResponse.responseData) { - chatItem.responseData = chatItem.responseData - ? mergeChatResponseData([...chatItem.responseData, ...aiResponse.responseData]) - : aiResponse.responseData; - } - if (aiResponse.value) { chatItem.value = chatItem.value ? [...chatItem.value, ...aiResponse.value] : aiResponse.value; } + if (aiResponse.citeCollectionIds) { + chatItem.citeCollectionIds = chatItem.citeCollectionIds + ? [...chatItem.citeCollectionIds, ...aiResponse.citeCollectionIds] + : aiResponse.citeCollectionIds; + } chatItem.durationSeconds = chatItem.durationSeconds ? +(chatItem.durationSeconds + durationSeconds).toFixed(2) @@ -332,7 +365,7 @@ export const updateInteractiveChat = async ({ }, { $set: { - variables: newVariables, + variables, updateTime: new Date() } }, @@ -340,5 +373,36 @@ export const updateInteractiveChat = async ({ session } ); + + // Create chat item respones + if (nodeResponses) { + // Merge + const lastResponse = await MongoChatItemResponse.findOneAndDelete({ + appId, + chatId, + chatItemDataId: chatItem.dataId + }) + .sort({ + _id: -1 + }) + .lean() + .session(session); + + const newResponses = lastResponse?.data + ? // @ts-ignore + mergeChatResponseData([lastResponse?.data, ...nodeResponses]) + : nodeResponses; + + await MongoChatItemResponse.create( + newResponses.map((item) => ({ + teamId, + appId, + chatId, + chatItemDataId: chatItem.dataId, + data: item + })), + { session, ordered: true, ...writePrimary } + ); + } }); }; diff --git a/packages/service/core/dataset/collection/controller.ts b/packages/service/core/dataset/collection/controller.ts index 3e0231005..2ff967309 100644 --- a/packages/service/core/dataset/collection/controller.ts +++ b/packages/service/core/dataset/collection/controller.ts @@ -1,8 +1,4 @@ -import { - DatasetCollectionTypeEnum, - DatasetCollectionDataProcessModeEnum, - DatasetTypeEnum -} from '@fastgpt/global/core/dataset/constants'; +import { DatasetCollectionDataProcessModeEnum } from '@fastgpt/global/core/dataset/constants'; import type { CreateDatasetCollectionParams } from '@fastgpt/global/core/dataset/api.d'; import { MongoDatasetCollection } from './schema'; import type { @@ -25,9 +21,7 @@ import { createTrainingUsage } from '../../../support/wallet/usage/controller'; import { UsageSourceEnum } from '@fastgpt/global/support/wallet/usage/constants'; import { getLLMModel, getEmbeddingModel, getVlmModel } from '../../ai/model'; import { pushDataListToTrainingQueue, pushDatasetToParseQueue } from '../training/controller'; -import { MongoImage } from '../../../common/file/image/schema'; import { hashStr } from '@fastgpt/global/common/string/tools'; -import { addDays } from 'date-fns'; import { MongoDatasetDataText } from '../data/dataTextSchema'; import { retryFn } from '@fastgpt/global/common/system/utils'; import { getTrainingModeByCollection } from './utils'; @@ -184,9 +178,9 @@ export const createCollectionAndInsertData = async ({ }); // 4. create training bill - const traingBillId = await (async () => { + const traingUsageId = await (async () => { if (billId) return billId; - const { billId: newBillId } = await createTrainingUsage({ + const { usageId: newUsageId } = await createTrainingUsage({ teamId, tmbId, appName: formatCreateCollectionParams.name, @@ -196,7 +190,7 @@ export const createCollectionAndInsertData = async ({ vllmModel: getVlmModel(dataset.vlmModel)?.name, session }); - return newBillId; + return newUsageId; })(); // 5. insert to training queue @@ -212,7 +206,7 @@ export const createCollectionAndInsertData = async ({ vlmModel: dataset.vlmModel, indexSize, mode: trainingMode, - billId: traingBillId, + billId: traingUsageId, data: chunks.map((item, index) => ({ ...item, indexes: item.indexes?.map((text) => ({ @@ -229,7 +223,7 @@ export const createCollectionAndInsertData = async ({ tmbId, datasetId: dataset._id, collectionId, - billId: traingBillId, + billId: traingUsageId, session }); return { diff --git a/packages/service/core/dataset/read.ts b/packages/service/core/dataset/read.ts index 694aefcec..b17efe205 100644 --- a/packages/service/core/dataset/read.ts +++ b/packages/service/core/dataset/read.ts @@ -156,7 +156,8 @@ export const readDatasetSourceRawText = async ({ externalFileId, apiDatasetServer, customPdfParse, - getFormatText + getFormatText, + usageId }: { teamId: string; tmbId: string; @@ -168,6 +169,7 @@ export const readDatasetSourceRawText = async ({ selector?: string; // link selector externalFileId?: string; // external file dataset apiDatasetServer?: ApiDatasetServerType; // api dataset + usageId?: string; }): Promise<{ title?: string; rawText: string; @@ -178,8 +180,9 @@ export const readDatasetSourceRawText = async ({ tmbId, bucketName: BucketNameEnum.dataset, fileId: sourceId, + getFormatText, customPdfParse, - getFormatText + usageId }); return { title: filename, diff --git a/packages/service/core/workflow/dispatch/abandoned/runApp.ts b/packages/service/core/workflow/dispatch/abandoned/runApp.ts index d97229957..4f97a4af8 100644 --- a/packages/service/core/workflow/dispatch/abandoned/runApp.ts +++ b/packages/service/core/workflow/dispatch/abandoned/runApp.ts @@ -63,6 +63,7 @@ export const dispatchAppRequest = async (props: Props): Promise => { ...props, runningAppInfo: { id: String(appData._id), + name: appData.name, teamId: String(appData.teamId), tmbId: String(appData.tmbId) }, diff --git a/packages/service/core/workflow/dispatch/ai/agent/index.ts b/packages/service/core/workflow/dispatch/ai/agent/index.ts index da334a04b..55e553b8f 100644 --- a/packages/service/core/workflow/dispatch/ai/agent/index.ts +++ b/packages/service/core/workflow/dispatch/ai/agent/index.ts @@ -48,6 +48,7 @@ export const dispatchRunTools = async (props: DispatchToolModuleProps): Promise< lastInteractive, runningUserInfo, externalProvider, + usageId, params: { model, systemPrompt, @@ -117,7 +118,8 @@ export const dispatchRunTools = async (props: DispatchToolModuleProps): Promise< customPdfParse: chatConfig?.fileSelectConfig?.customPdfParse, fileLinks, inputFiles: globalFiles, - hasReadFilesTool + hasReadFilesTool, + usageId }); const concatenateSystemPrompt = [ @@ -273,7 +275,8 @@ const getMultiInput = async ({ maxFiles, customPdfParse, inputFiles, - hasReadFilesTool + hasReadFilesTool, + usageId }: { runningUserInfo: ChatDispatchProps['runningUserInfo']; histories: ChatItemType[]; @@ -283,6 +286,7 @@ const getMultiInput = async ({ customPdfParse?: boolean; inputFiles: UserChatItemValueItemType['file'][]; hasReadFilesTool: boolean; + usageId?: string; }) => { // Not file quote if (!fileLinks || hasReadFilesTool) { @@ -309,6 +313,7 @@ const getMultiInput = async ({ requestOrigin, maxFiles, customPdfParse, + usageId, teamId: runningUserInfo.teamId, tmbId: runningUserInfo.tmbId }); diff --git a/packages/service/core/workflow/dispatch/ai/chat.ts b/packages/service/core/workflow/dispatch/ai/chat.ts index d7275ab03..c6cf2d37f 100644 --- a/packages/service/core/workflow/dispatch/ai/chat.ts +++ b/packages/service/core/workflow/dispatch/ai/chat.ts @@ -74,6 +74,7 @@ export const dispatchChatCompletion = async (props: ChatProps): Promise @@ -359,9 +363,10 @@ async function getMultiInput({ urls, requestOrigin, maxFiles, - customPdfParse, teamId: runningUserInfo.teamId, - tmbId: runningUserInfo.tmbId + tmbId: runningUserInfo.tmbId, + customPdfParse, + usageId }); return { diff --git a/packages/service/core/workflow/dispatch/child/runApp.ts b/packages/service/core/workflow/dispatch/child/runApp.ts index 878e3702f..6502e1981 100644 --- a/packages/service/core/workflow/dispatch/child/runApp.ts +++ b/packages/service/core/workflow/dispatch/child/runApp.ts @@ -20,7 +20,7 @@ import { authAppByTmbId } from '../../../../support/permission/app/auth'; import { ReadPermissionVal } from '@fastgpt/global/support/permission/constant'; import { getAppVersionById } from '../../../app/version/controller'; import { parseUrlToFileType } from '@fastgpt/global/common/file/tools'; -import { getUserChatInfoAndAuthTeamPoints } from '../../../../support/permission/auth/team'; +import { getUserChatInfo } from '../../../../support/user/team/utils'; import { getRunningUserInfoByTmbId } from '../../../../support/user/team/utils'; type Props = ModuleDispatchProps<{ @@ -99,7 +99,7 @@ export const dispatchRunAppNode = async (props: Props): Promise => { // Rewrite children app variables const systemVariables = filterSystemVariables(variables); - const { externalProvider } = await getUserChatInfoAndAuthTeamPoints(appData.tmbId); + const { externalProvider } = await getUserChatInfo(appData.tmbId); const childrenRunVariables = { ...systemVariables, ...childrenAppVariables, @@ -144,6 +144,7 @@ export const dispatchRunAppNode = async (props: Props): Promise => { : {}), runningAppInfo: { id: String(appData._id), + name: appData.name, teamId: String(appData.teamId), tmbId: String(appData.tmbId), isChildApp: true diff --git a/packages/service/core/workflow/dispatch/child/runTool.ts b/packages/service/core/workflow/dispatch/child/runTool.ts index 5b3136b6a..6b03fe0d4 100644 --- a/packages/service/core/workflow/dispatch/child/runTool.ts +++ b/packages/service/core/workflow/dispatch/child/runTool.ts @@ -10,6 +10,7 @@ import { NodeInputKeyEnum } from '@fastgpt/global/core/workflow/constants'; import { MCPClient } from '../../../app/mcp'; import { getSecretValue } from '../../../../common/secret/utils'; import type { McpToolDataType } from '@fastgpt/global/core/app/mcpTools/type'; +import type { HttpToolConfigType } from '@fastgpt/global/core/app/type'; import { APIRunSystemTool } from '../../../app/tool/api'; import { MongoSystemPlugin } from '../../../app/plugin/systemPluginSchema'; import { SystemToolInputTypeEnum } from '@fastgpt/global/core/app/systemTool/constants'; @@ -20,6 +21,7 @@ import { pushTrack } from '../../../../common/middle/tracks/utils'; import { getNodeErrResponse } from '../utils'; import { splitCombinePluginId } from '@fastgpt/global/core/app/plugin/utils'; import { getAppVersionById } from '../../../../core/app/version/controller'; +import { runHTTPTool } from '../../../app/http'; type SystemInputConfigType = { type: SystemToolInputTypeEnum; @@ -34,7 +36,7 @@ type RunToolProps = ModuleDispatchProps<{ type RunToolResponse = DispatchNodeResultType< { - [NodeOutputKeyEnum.rawResponse]?: any; // MCP Tool + [NodeOutputKeyEnum.rawResponse]?: any; [key: string]: any; }, Record @@ -214,6 +216,60 @@ export const dispatchRunTool = async (props: RunToolProps): Promise tool.name === toolName); + if (!httpTool) { + throw new Error(`HTTP tool ${toolName} not found`); + } + + const { data, errorMsg } = await runHTTPTool({ + baseUrl: baseUrl, + toolPath: httpTool.path, + method: httpTool.method, + params, + headerSecret, + customHeaders: customHeaders + ? typeof customHeaders === 'string' + ? JSON.parse(customHeaders) + : customHeaders + : undefined + }); + + if (errorMsg) { + if (catchError) { + return { + error: { [NodeOutputKeyEnum.errorText]: errorMsg }, + [DispatchNodeResponseKeyEnum.nodeResponse]: { + toolRes: errorMsg, + moduleLogo: avatar + }, + [DispatchNodeResponseKeyEnum.toolResponses]: errorMsg + }; + } + throw new Error(errorMsg); + } + + return { + data: { [NodeOutputKeyEnum.rawResponse]: data, ...(typeof data === 'object' ? data : {}) }, + [DispatchNodeResponseKeyEnum.nodeResponse]: { + toolRes: data, + moduleLogo: avatar + }, + [DispatchNodeResponseKeyEnum.toolResponses]: data + }; } else { // mcp tool (old version compatible) const { toolData, system_toolData, ...restParams } = params; diff --git a/packages/service/core/workflow/dispatch/dataset/search.ts b/packages/service/core/workflow/dispatch/dataset/search.ts index 07d657d6d..0708c5f18 100644 --- a/packages/service/core/workflow/dispatch/dataset/search.ts +++ b/packages/service/core/workflow/dispatch/dataset/search.ts @@ -15,7 +15,6 @@ import { type ChatNodeUsageType } from '@fastgpt/global/support/wallet/bill/type import { MongoDataset } from '../../../dataset/schema'; import { i18nT } from '../../../../../web/i18n/utils'; import { filterDatasetsByTmbId } from '../../../dataset/utils'; -import { ModelTypeEnum } from '@fastgpt/global/core/ai/model'; import { getDatasetSearchToolResponsePrompt } from '../../../../../global/core/ai/prompt/dataset'; import { getNodeErrResponse } from '../utils'; diff --git a/packages/service/core/workflow/dispatch/index.ts b/packages/service/core/workflow/dispatch/index.ts index 73097fc6d..eed55c500 100644 --- a/packages/service/core/workflow/dispatch/index.ts +++ b/packages/service/core/workflow/dispatch/index.ts @@ -47,8 +47,13 @@ import { rewriteRuntimeWorkFlow, removeSystemVariable } from './utils'; import { getHandleId } from '@fastgpt/global/core/workflow/utils'; import { callbackMap } from './constants'; import { anyValueDecrypt } from '../../../common/secret/utils'; +import { getUserChatInfo } from '../../../support/user/team/utils'; +import { checkTeamAIPoints } from '../../../support/permission/teamLimit'; +import type { UsageSourceEnum } from '@fastgpt/global/support/wallet/usage/constants'; +import { createChatUsageRecord, pushChatItemUsage } from '../../../support/wallet/usage/controller'; +import type { RequireOnlyOne } from '@fastgpt/global/common/type/utils'; -type Props = Omit & { +type Props = Omit & { runtimeNodes: RuntimeNodeItemType[]; runtimeEdges: RuntimeEdgeItemType[]; defaultSkipNodeQueue?: WorkflowDebugResponse['skipNodeQueue']; @@ -61,8 +66,38 @@ type NodeResponseCompleteType = Omit & { }; // Run workflow -export async function dispatchWorkFlow(data: Props): Promise { - const { res, stream, externalProvider } = data; +type WorkflowUsageProps = RequireOnlyOne<{ + usageSource: UsageSourceEnum; + concatUsage: (points: number) => any; + usageId: string; +}>; +export async function dispatchWorkFlow({ + usageSource, + usageId, + concatUsage, + ...data +}: Props & WorkflowUsageProps): Promise { + const { res, stream, runningUserInfo, runningAppInfo, lastInteractive } = data; + + await checkTeamAIPoints(runningUserInfo.teamId); + const [{ timezone, externalProvider }, newUsageId] = await Promise.all([ + getUserChatInfo(runningUserInfo.tmbId), + (() => { + if (lastInteractive?.usageId) { + return lastInteractive.usageId; + } + if (usageSource) { + return createChatUsageRecord({ + appName: runningAppInfo.name, + appId: runningAppInfo.id, + teamId: runningUserInfo.teamId, + tmbId: runningUserInfo.tmbId, + source: usageSource + }); + } + return usageId; + })() + ]); let streamCheckTimer: NodeJS.Timeout | null = null; @@ -96,15 +131,22 @@ export async function dispatchWorkFlow(data: Props): Promise { if (streamCheckTimer) { clearInterval(streamCheckTimer); @@ -116,6 +158,7 @@ type RunWorkflowProps = ChatDispatchProps & { runtimeNodes: RuntimeNodeItemType[]; runtimeEdges: RuntimeEdgeItemType[]; defaultSkipNodeQueue?: WorkflowDebugResponse['skipNodeQueue']; + concatUsage?: (points: number) => any; }; export const runWorkflow = async (data: RunWorkflowProps): Promise => { let { @@ -129,7 +172,10 @@ export const runWorkflow = async (data: RunWorkflowProps): Promise sum + (item.totalPoints || 0), 0)); + } + this.chatNodeUsages = this.chatNodeUsages.concat(nodeDispatchUsages); } @@ -827,7 +884,8 @@ export const runWorkflow = async (data: RunWorkflowProps): Promise { +}: Props & { + timezone: string; +}): SystemVariablesType => { // Get global variables(Label -> key; Key -> key) const variablesConfig = chatConfig?.variables || []; diff --git a/packages/service/core/workflow/dispatch/plugin/run.ts b/packages/service/core/workflow/dispatch/plugin/run.ts index 275600609..fa2602d0e 100644 --- a/packages/service/core/workflow/dispatch/plugin/run.ts +++ b/packages/service/core/workflow/dispatch/plugin/run.ts @@ -21,7 +21,7 @@ import { getPluginRunUserQuery } from '@fastgpt/global/core/workflow/utils'; import type { NodeInputKeyEnum, NodeOutputKeyEnum } from '@fastgpt/global/core/workflow/constants'; import { getChildAppRuntimeById } from '../../../app/plugin/controller'; import { runWorkflow } from '../index'; -import { getUserChatInfoAndAuthTeamPoints } from '../../../../support/permission/auth/team'; +import { getUserChatInfo } from '../../../../support/user/team/utils'; import { dispatchRunTool } from '../child/runTool'; import type { PluginRuntimeType } from '@fastgpt/global/core/app/plugin/type'; @@ -111,7 +111,7 @@ export const dispatchRunPlugin = async (props: RunPluginProps): Promise => { histories, chatConfig, node: { version }, - params: { fileUrlList = [] } + params: { fileUrlList = [] }, + usageId } = props; const maxFiles = chatConfig?.fileSelectConfig?.maxFiles || 20; const customPdfParse = chatConfig?.fileSelectConfig?.customPdfParse || false; @@ -68,7 +69,8 @@ export const dispatchReadFiles = async (props: Props): Promise => { maxFiles, teamId, tmbId, - customPdfParse + customPdfParse, + usageId }); return { @@ -119,7 +121,8 @@ export const getFileContentFromLinks = async ({ maxFiles, teamId, tmbId, - customPdfParse + customPdfParse, + usageId }: { urls: string[]; requestOrigin?: string; @@ -127,6 +130,7 @@ export const getFileContentFromLinks = async ({ teamId: string; tmbId: string; customPdfParse?: boolean; + usageId?: string; }) => { const parseUrlList = urls // Remove invalid urls @@ -225,7 +229,8 @@ export const getFileContentFromLinks = async ({ buffer, encoding, customPdfParse, - getFormatText: true + getFormatText: true, + usageId }); // Add to buffer diff --git a/packages/service/core/workflow/dispatch/utils.ts b/packages/service/core/workflow/dispatch/utils.ts index 70fdcf7a6..3200561d4 100644 --- a/packages/service/core/workflow/dispatch/utils.ts +++ b/packages/service/core/workflow/dispatch/utils.ts @@ -18,11 +18,13 @@ import { import { getNanoid } from '@fastgpt/global/common/string/tools'; import { type SearchDataResponseItemType } from '@fastgpt/global/core/dataset/type'; import { getMCPToolRuntimeNode } from '@fastgpt/global/core/app/mcpTools/utils'; +import { getHTTPToolRuntimeNode } from '@fastgpt/global/core/app/httpTools/utils'; import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant'; import { MongoApp } from '../../../core/app/schema'; import { getMCPChildren } from '../../../core/app/mcp'; import { getSystemToolRunTimeNodeFromSystemToolset } from '../utils'; import type { localeType } from '@fastgpt/global/common/i18n/type'; +import type { HttpToolConfigType } from '@fastgpt/global/core/app/type'; export const getWorkflowResponseWrite = ({ res, @@ -197,7 +199,8 @@ export const rewriteRuntimeWorkFlow = async ({ for (const toolSetNode of toolSetNodes) { nodeIdsToRemove.add(toolSetNode.nodeId); const systemToolId = toolSetNode.toolConfig?.systemToolSet?.toolId; - const mcpToolsetVal = toolSetNode.toolConfig?.mcpToolSet ?? toolSetNode.inputs[0].value; + const mcpToolsetVal = toolSetNode.toolConfig?.mcpToolSet ?? toolSetNode.inputs?.[0]?.value; + const httpToolsetVal = toolSetNode.toolConfig?.httpToolSet; const incomingEdges = edges.filter((edge) => edge.target === toolSetNode.nodeId); const pushEdges = (nodeId: string) => { @@ -243,6 +246,22 @@ export const rewriteRuntimeWorkFlow = async ({ }); pushEdges(newToolNode.nodeId); }); + } else if (httpToolsetVal) { + const parentId = toolSetNode.pluginId || ''; + httpToolsetVal.toolList.forEach((tool: HttpToolConfigType, index: number) => { + const newToolNode = getHTTPToolRuntimeNode({ + tool: { + ...tool, + name: `${toolSetNode.name}/${tool.name}` + }, + nodeId: `${parentId}${index}`, + avatar: toolSetNode.avatar, + parentId + }); + + nodes.push(newToolNode); + pushEdges(newToolNode.nodeId); + }); } } diff --git a/packages/service/package.json b/packages/service/package.json index bb146d44b..eb367ff8e 100644 --- a/packages/service/package.json +++ b/packages/service/package.json @@ -27,6 +27,8 @@ "encoding": "^0.1.13", "file-type": "^19.0.0", "form-data": "^4.0.4", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.6", "iconv-lite": "^0.6.3", "ioredis": "^5.6.0", "joplin-turndown-plugin-gfm": "^1.0.12", @@ -35,6 +37,7 @@ "jsonwebtoken": "^9.0.2", "lodash": "^4.17.21", "mammoth": "^1.6.0", + "minio": "^8.0.5", "mongoose": "^8.10.1", "multer": "2.0.2", "mysql2": "^3.11.3", diff --git a/packages/service/support/permission/auth/team.ts b/packages/service/support/permission/auth/team.ts deleted file mode 100644 index adfe5b40a..000000000 --- a/packages/service/support/permission/auth/team.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { MongoTeamMember } from '../../user/team/teamMemberSchema'; -import { checkTeamAIPoints } from '../teamLimit'; -import { type UserModelSchema } from '@fastgpt/global/support/user/type'; -import { type TeamSchema } from '@fastgpt/global/support/user/team/type'; -import { TeamErrEnum } from '@fastgpt/global/common/error/code/team'; - -export async function getUserChatInfoAndAuthTeamPoints(tmbId: string) { - const tmb = await MongoTeamMember.findById(tmbId, 'userId teamId') - .populate<{ user: UserModelSchema; team: TeamSchema }>([ - { - path: 'user', - select: 'timezone' - }, - { - path: 'team', - select: 'openaiAccount externalWorkflowVariables' - } - ]) - .lean(); - - if (!tmb) return Promise.reject(TeamErrEnum.notUser); - - await checkTeamAIPoints(tmb.team._id); - - return { - timezone: tmb.user.timezone, - externalProvider: { - openaiAccount: tmb.team.openaiAccount, - externalWorkflowVariables: tmb.team.externalWorkflowVariables - } - }; -} diff --git a/packages/service/support/permission/model/controller.ts b/packages/service/support/permission/model/controller.ts new file mode 100644 index 000000000..c7eb0cf4f --- /dev/null +++ b/packages/service/support/permission/model/controller.ts @@ -0,0 +1,46 @@ +import { PerResourceTypeEnum } from '@fastgpt/global/support/permission/constant'; +import { getGroupsByTmbId } from '../memberGroup/controllers'; +import { getOrgsByTmbId } from '../org/controllers'; +import { MongoResourcePermission } from '../schema'; +import { getCollaboratorId } from '@fastgpt/global/support/permission/utils'; +import { isProVersion } from '../../../common/system/constants'; + +export const getMyModels = async ({ + teamId, + tmbId, + isTeamOwner +}: { + teamId: string; + tmbId: string; + isTeamOwner: boolean; +}) => { + if (isTeamOwner || !isProVersion()) { + return global.systemModelList.map((m) => m.model); + } + const [groups, orgs] = await Promise.all([ + getGroupsByTmbId({ + teamId, + tmbId + }), + getOrgsByTmbId({ + teamId, + tmbId + }) + ]); + + const myIdSet = new Set([tmbId, ...groups.map((g) => g._id), ...orgs.map((o) => o._id)]); + + const rps = await MongoResourcePermission.find({ + teamId, + resourceType: PerResourceTypeEnum.model + }).lean(); + + const permissionConfiguredModelSet = new Set(rps.map((rp) => rp.resourceName)); + const unconfiguredModels = global.systemModelList.filter( + (model) => !permissionConfiguredModelSet.has(model.model) + ); + + const myModels = rps.filter((rp) => myIdSet.has(getCollaboratorId(rp))); + + return [...unconfiguredModels.map((m) => m.model), ...myModels.map((m) => m.resourceName)]; +}; diff --git a/packages/service/support/permission/schema.ts b/packages/service/support/permission/schema.ts index 56c5e602d..1eebfceb2 100644 --- a/packages/service/support/permission/schema.ts +++ b/packages/service/support/permission/schema.ts @@ -41,14 +41,20 @@ export const ResourcePermissionSchema = new Schema({ type: Number, required: true }, + /** * Optional. Only be set when the resource is *inherited* from the parent resource. * For recording the self permission. When cancel the inheritance, it will overwrite the permission property and set to `unset`. */ - // Resource ID: App or DataSet or any other resource type. - // It is null if the resourceType is team. resourceId: { type: Schema.Types.ObjectId + }, + + /** + * Optional, For some resources, which do not have resourceId, the resourceName is required. + */ + resourceName: { + type: String } }); @@ -72,6 +78,7 @@ ResourcePermissionSchema.virtual('org', { }); try { + // Indexes for resourceId-based resources ResourcePermissionSchema.index( { resourceType: 1, @@ -84,6 +91,9 @@ try { partialFilterExpression: { groupId: { $exists: true + }, + resourceId: { + $exists: true } } } @@ -101,6 +111,9 @@ try { partialFilterExpression: { orgId: { $exists: true + }, + resourceId: { + $exists: true } } } @@ -118,17 +131,106 @@ try { partialFilterExpression: { tmbId: { $exists: true + }, + resourceId: { + $exists: true } } } ); - // Delete tmb permission - ResourcePermissionSchema.index({ - resourceType: 1, - teamId: 1, - resourceId: 1 - }); + // General index for resourceId-based resources + ResourcePermissionSchema.index( + { + resourceType: 1, + teamId: 1, + resourceId: 1 + }, + { + partialFilterExpression: { + resourceId: { + $exists: true + } + } + } + ); + + // Indexes for resourceName-based resources + ResourcePermissionSchema.index( + { + resourceType: 1, + teamId: 1, + resourceName: 1, + groupId: 1 + }, + { + unique: true, + partialFilterExpression: { + groupId: { + $exists: true + }, + resourceName: { + $exists: true + } + } + } + ); + + ResourcePermissionSchema.index( + { + resourceType: 1, + teamId: 1, + resourceName: 1, + orgId: 1 + }, + { + unique: true, + partialFilterExpression: { + orgId: { + $exists: true + }, + resourceName: { + $exists: true + } + } + } + ); + + ResourcePermissionSchema.index( + { + resourceType: 1, + teamId: 1, + resourceName: 1, + tmbId: 1 + }, + { + unique: true, + partialFilterExpression: { + tmbId: { + $exists: true + }, + resourceName: { + $exists: true + } + } + } + ); + + // General index for resourceName-based resources + ResourcePermissionSchema.index( + { + resourceType: 1, + teamId: 1, + resourceName: 1 + }, + { + partialFilterExpression: { + resourceName: { + $exists: true + } + } + } + ); } catch (error) { console.log(error); } diff --git a/packages/service/support/permission/teamLimit.ts b/packages/service/support/permission/teamLimit.ts index 145d0d90c..3ae9287b1 100644 --- a/packages/service/support/permission/teamLimit.ts +++ b/packages/service/support/permission/teamLimit.ts @@ -46,7 +46,13 @@ export const checkTeamAppLimit = async (teamId: string, amount = 1) => { MongoApp.countDocuments({ teamId, type: { - $in: [AppTypeEnum.simple, AppTypeEnum.workflow, AppTypeEnum.plugin, AppTypeEnum.toolSet] + $in: [ + AppTypeEnum.simple, + AppTypeEnum.workflow, + AppTypeEnum.plugin, + AppTypeEnum.toolSet, + AppTypeEnum.httpToolSet + ] } }) ]); diff --git a/packages/service/support/user/audit/util.ts b/packages/service/support/user/audit/util.ts index 9e04d2a45..cefeb9fb7 100644 --- a/packages/service/support/user/audit/util.ts +++ b/packages/service/support/user/audit/util.ts @@ -16,6 +16,7 @@ export function getI18nAppType(type: AppTypeEnum): string { if (type === AppTypeEnum.workflow) return i18nT('account_team:type.Workflow bot'); if (type === AppTypeEnum.plugin) return i18nT('account_team:type.Plugin'); if (type === AppTypeEnum.httpPlugin) return i18nT('account_team:type.Http plugin'); + if (type === AppTypeEnum.httpToolSet) return i18nT('account_team:type.Http tool set'); if (type === AppTypeEnum.toolSet) return i18nT('account_team:type.Tool set'); if (type === AppTypeEnum.tool) return i18nT('account_team:type.Tool'); return i18nT('common:UnKnow'); diff --git a/packages/service/support/user/team/utils.ts b/packages/service/support/user/team/utils.ts index eccdffac4..c6745a80f 100644 --- a/packages/service/support/user/team/utils.ts +++ b/packages/service/support/user/team/utils.ts @@ -33,3 +33,28 @@ export async function getRunningUserInfoByTmbId(tmbId: string) { return Promise.reject(TeamErrEnum.notUser); } + +export async function getUserChatInfo(tmbId: string) { + const tmb = await MongoTeamMember.findById(tmbId, 'userId teamId') + .populate<{ user: UserModelSchema; team: TeamSchema }>([ + { + path: 'user', + select: 'timezone' + }, + { + path: 'team', + select: 'openaiAccount externalWorkflowVariables' + } + ]) + .lean(); + + if (!tmb) return Promise.reject(TeamErrEnum.notUser); + + return { + timezone: tmb.user.timezone, + externalProvider: { + openaiAccount: tmb.team.openaiAccount, + externalWorkflowVariables: tmb.team.externalWorkflowVariables + } + }; +} diff --git a/packages/service/support/wallet/usage/constants.ts b/packages/service/support/wallet/usage/constants.ts new file mode 100644 index 000000000..2f9d9bd65 --- /dev/null +++ b/packages/service/support/wallet/usage/constants.ts @@ -0,0 +1,2 @@ +export const UsageCollectionName = 'usages'; +export const UsageItemCollectionName = 'usage_items'; diff --git a/packages/service/support/wallet/usage/controller.ts b/packages/service/support/wallet/usage/controller.ts index 1fa22ba3d..7b1392820 100644 --- a/packages/service/support/wallet/usage/controller.ts +++ b/packages/service/support/wallet/usage/controller.ts @@ -1,19 +1,21 @@ -import { UsageSourceEnum } from '@fastgpt/global/support/wallet/usage/constants'; +import { UsageItemTypeEnum, UsageSourceEnum } from '@fastgpt/global/support/wallet/usage/constants'; import { MongoUsage } from './schema'; import { type ClientSession } from '../../../common/mongo'; import { addLog } from '../../../common/system/log'; import { type ChatNodeUsageType } from '@fastgpt/global/support/wallet/bill/type'; -import { - type ConcatUsageProps, - type CreateUsageProps +import type { + PushUsageItemsProps, + ConcatUsageProps, + CreateUsageProps } from '@fastgpt/global/support/wallet/usage/api'; import { i18nT } from '../../../../web/i18n/utils'; import { formatModelChars2Points } from './utils'; -import { ModelTypeEnum } from '@fastgpt/global/core/ai/model'; +import { mongoSessionRun } from '../../../common/mongo/sessionRun'; +import { MongoUsageItem } from './usageItemSchema'; export async function createUsage(data: CreateUsageProps) { try { - await global.createUsageHandler(data); + return await global.createUsageHandler(data); } catch (error) { addLog.error('createUsage error', error); } @@ -25,15 +27,94 @@ export async function concatUsage(data: ConcatUsageProps) { addLog.error('concatUsage error', error); } } +export async function pushUsageItems(data: PushUsageItemsProps) { + try { + await global.pushUsageItemsHandler(data); + } catch (error) { + addLog.error('pushUsageItems error', error); + } +} -export const createChatUsage = ({ +export const createPdfParseUsage = async ({ + teamId, + tmbId, + pages, + usageId +}: { + teamId: string; + tmbId: string; + pages: number; + usageId?: string; +}) => { + const unitPrice = global.systemEnv?.customPdfParse?.price || 0; + const totalPoints = pages * unitPrice; + + if (usageId) { + pushUsageItems({ + teamId, + usageId, + list: [{ moduleName: i18nT('account_usage:pdf_enhanced_parse'), amount: totalPoints, pages }] + }); + } else { + createUsage({ + teamId, + tmbId, + appName: i18nT('account_usage:pdf_enhanced_parse'), + totalPoints, + source: UsageSourceEnum.pdfParse, + list: [ + { + moduleName: i18nT('account_usage:pdf_enhanced_parse'), + amount: totalPoints, + pages + } + ] + }); + } +}; +export const pushLLMTrainingUsage = async ({ + teamId, + model, + inputTokens, + outputTokens, + usageId, + type +}: { + teamId: string; + model: string; + inputTokens: number; + outputTokens: number; + usageId: string; + type: UsageItemTypeEnum; +}) => { + // Compute points + const { totalPoints } = formatModelChars2Points({ + model, + inputTokens, + outputTokens + }); + + concatUsage({ + usageId, + teamId, + itemType: type, + totalPoints, + inputTokens, + outputTokens + }); + + return { totalPoints }; +}; + +/* Create usage, and return usageId */ +// Chat +export const createChatUsageRecord = async ({ appName, appId, pluginId, teamId, tmbId, - source, - flowUsages + source }: { appName: string; appId?: string; @@ -41,42 +122,46 @@ export const createChatUsage = ({ teamId: string; tmbId: string; source: UsageSourceEnum; - flowUsages: ChatNodeUsageType[]; }) => { - const totalPoints = flowUsages.reduce((sum, item) => sum + (item.totalPoints || 0), 0); - - createUsage({ + const [{ _id: usageId }] = await MongoUsage.create( + [ + { + teamId, + tmbId, + appId, + pluginId, + appName, + source, + totalPoints: 0 + } + ], + { ordered: true } + ); + return String(usageId); +}; +export const pushChatItemUsage = ({ + teamId, + usageId, + nodeUsages +}: { + teamId: string; + usageId: string; + nodeUsages: ChatNodeUsageType[]; +}) => { + pushUsageItems({ teamId, - tmbId, - appName, - appId, - pluginId, - totalPoints, - source, - list: flowUsages.map((item) => ({ + usageId, + list: nodeUsages.map((item) => ({ moduleName: item.moduleName, - amount: item.totalPoints || 0, + amount: item.totalPoints, model: item.model, inputTokens: item.inputTokens, outputTokens: item.outputTokens })) }); - addLog.debug(`Create chat usage`, { - source, - teamId, - totalPoints - }); - return { totalPoints }; }; -export type DatasetTrainingMode = 'paragraph' | 'qa' | 'autoIndex' | 'imageIndex' | 'imageParse'; -export const datasetTrainingUsageIndexMap: Record = { - paragraph: 1, - qa: 2, - autoIndex: 3, - imageIndex: 4, - imageParse: 5 -}; +// Dataset training export const createTrainingUsage = async ({ teamId, tmbId, @@ -91,189 +176,161 @@ export const createTrainingUsage = async ({ tmbId: string; appName: string; billSource: UsageSourceEnum; - vectorModel?: string; + + vectorModel: string; agentModel?: string; vllmModel?: string; session?: ClientSession; }) => { - const [{ _id }] = await MongoUsage.create( - [ + const create = async (session: ClientSession) => { + const [result] = await MongoUsage.create( + [ + { + teamId, + tmbId, + source: billSource, + appName, + totalPoints: 0 + } + ], + { session, ordered: true } + ); + await MongoUsageItem.create( + [ + { + teamId, + usageId: result._id, + itemType: UsageItemTypeEnum.training_vector, + name: i18nT('account_usage:embedding_index'), + model: vectorModel, + amount: 0, + inputTokens: 0 + }, + ...(agentModel + ? [ + { + teamId, + usageId: result._id, + itemType: UsageItemTypeEnum.training_paragraph, + name: i18nT('account_usage:llm_paragraph'), + model: agentModel, + amount: 0, + inputTokens: 0, + outputTokens: 0 + }, + { + teamId, + usageId: result._id, + itemType: UsageItemTypeEnum.training_qa, + name: i18nT('account_usage:qa'), + model: agentModel, + amount: 0, + inputTokens: 0, + outputTokens: 0 + }, + { + teamId, + usageId: result._id, + itemType: UsageItemTypeEnum.training_autoIndex, + name: i18nT('account_usage:auto_index'), + model: agentModel, + amount: 0, + inputTokens: 0, + outputTokens: 0 + } + ] + : []), + ...(vllmModel + ? [ + { + teamId, + usageId: result._id, + itemType: UsageItemTypeEnum.training_imageIndex, + name: i18nT('account_usage:image_index'), + model: vllmModel, + amount: 0, + inputTokens: 0, + outputTokens: 0 + }, + { + teamId, + usageId: result._id, + itemType: UsageItemTypeEnum.training_imageParse, + name: i18nT('account_usage:image_parse'), + model: vllmModel, + amount: 0, + inputTokens: 0, + outputTokens: 0 + } + ] + : []) + ], { - teamId, - tmbId, - appName, - source: billSource, - totalPoints: 0, - list: [ - ...(vectorModel - ? [ - { - moduleName: i18nT('account_usage:embedding_index'), - model: vectorModel, - amount: 0, - inputTokens: 0, - outputTokens: 0 - } - ] - : []), - ...(agentModel - ? [ - { - moduleName: i18nT('account_usage:llm_paragraph'), - model: agentModel, - amount: 0, - inputTokens: 0, - outputTokens: 0 - }, - { - moduleName: i18nT('account_usage:qa'), - model: agentModel, - amount: 0, - inputTokens: 0, - outputTokens: 0 - }, - { - moduleName: i18nT('account_usage:auto_index'), - model: agentModel, - amount: 0, - inputTokens: 0, - outputTokens: 0 - } - ] - : []), - ...(vllmModel - ? [ - { - moduleName: i18nT('account_usage:image_index'), - model: vllmModel, - amount: 0, - inputTokens: 0, - outputTokens: 0 - }, - { - moduleName: i18nT('account_usage:image_parse'), - model: vllmModel, - amount: 0, - inputTokens: 0, - outputTokens: 0 - } - ] - : []) - ] + session, + ordered: true } - ], - { session, ordered: true } - ); + ); - return { billId: String(_id) }; -}; - -export const createPdfParseUsage = async ({ - teamId, - tmbId, - pages -}: { - teamId: string; - tmbId: string; - pages: number; -}) => { - const unitPrice = global.systemEnv?.customPdfParse?.price || 0; - const totalPoints = pages * unitPrice; - - createUsage({ - teamId, - tmbId, - appName: i18nT('account_usage:pdf_enhanced_parse'), - totalPoints, - source: UsageSourceEnum.pdfParse, - list: [ - { - moduleName: i18nT('account_usage:pdf_enhanced_parse'), - amount: totalPoints, - pages - } - ] - }); -}; - -export const pushLLMTrainingUsage = async ({ - teamId, - tmbId, - model, - inputTokens, - outputTokens, - billId, - mode -}: { - teamId: string; - tmbId: string; - model: string; - inputTokens: number; - outputTokens: number; - billId: string; - mode: DatasetTrainingMode; -}) => { - const index = datasetTrainingUsageIndexMap[mode]; - - // Compute points - const { totalPoints } = formatModelChars2Points({ - model, - inputTokens, - outputTokens - }); - - concatUsage({ - billId, - teamId, - tmbId, - totalPoints, - inputTokens, - outputTokens, - listIndex: index - }); - - return { totalPoints }; + return { usageId: String(result._id) }; + }; + if (session) return create(session); + return mongoSessionRun(create); }; +// Evaluation export const createEvaluationUsage = async ({ teamId, tmbId, appName, - model, - session + model }: { teamId: string; tmbId: string; appName: string; model: string; - session?: ClientSession; }) => { - const [{ _id: usageId }] = await MongoUsage.create( - [ + const { usageId } = await mongoSessionRun(async (session) => { + const [{ _id: usageId }] = await MongoUsage.create( + [ + { + teamId, + tmbId, + appName, + source: UsageSourceEnum.evaluation, + totalPoints: 0 + } + ], + { session, ordered: true } + ); + await MongoUsageItem.create( + [ + { + teamId, + usageId, + itemType: UsageItemTypeEnum.evaluation_generateAnswer, + name: i18nT('account_usage:generate_answer'), + amount: 0, + count: 0 + }, + { + teamId, + usageId, + itemType: UsageItemTypeEnum.evaluation_answerAccuracy, + name: i18nT('account_usage:answer_accuracy'), + amount: 0, + inputTokens: 0, + outputTokens: 0, + model + } + ], { - teamId, - tmbId, - appName, - source: UsageSourceEnum.evaluation, - totalPoints: 0, - list: [ - { - moduleName: i18nT('account_usage:generate_answer'), - amount: 0, - count: 0 - }, - { - moduleName: i18nT('account_usage:answer_accuracy'), - amount: 0, - inputTokens: 0, - outputTokens: 0, - model - } - ] + session, + ordered: true } - ], - { session, ordered: true } - ); + ); + + return { usageId: String(usageId) }; + }); return { usageId }; }; diff --git a/packages/service/support/wallet/usage/schema.ts b/packages/service/support/wallet/usage/schema.ts index 16ae19047..db644447f 100644 --- a/packages/service/support/wallet/usage/schema.ts +++ b/packages/service/support/wallet/usage/schema.ts @@ -1,4 +1,4 @@ -import { connectionMongo, getMongoModel, type Model } from '../../../common/mongo'; +import { connectionMongo, getMongoModel } from '../../../common/mongo'; const { Schema } = connectionMongo; import { type UsageSchemaType } from '@fastgpt/global/support/wallet/usage/type'; import { UsageSourceEnum } from '@fastgpt/global/support/wallet/usage/constants'; @@ -6,8 +6,7 @@ import { TeamCollectionName, TeamMemberCollectionName } from '@fastgpt/global/support/user/team/constant'; - -export const UsageCollectionName = 'usages'; +import { UsageCollectionName, UsageItemCollectionName } from './constants'; const UsageSchema = new Schema({ teamId: { @@ -30,6 +29,11 @@ const UsageSchema = new Schema({ type: String, default: '' }, + totalPoints: { + // total points + type: Number, + required: true + }, appId: { type: Schema.Types.ObjectId, ref: 'apps', @@ -44,26 +48,21 @@ const UsageSchema = new Schema({ type: Date, default: () => new Date() }, - totalPoints: { - // total points - type: Number, - required: true - }, - // total: { - // // total points - // type: Number, - // required: true - // }, + + // @description It will not be used again in the future. list: { - type: Array, - default: [] + type: Array } }); +UsageSchema.virtual('usageItems', { + ref: UsageItemCollectionName, + localField: '_id', + foreignField: 'usageId' +}); + try { UsageSchema.index({ teamId: 1, tmbId: 1, source: 1, time: 1, appName: 1, _id: -1 }); - // timer task. clear dead team - // UsageSchema.index({ teamId: 1, time: -1 }); UsageSchema.index({ time: 1 }, { expireAfterSeconds: 360 * 24 * 60 * 60 }); } catch (error) { diff --git a/packages/service/support/wallet/usage/type.d.ts b/packages/service/support/wallet/usage/type.d.ts index 947b1041a..80e82629f 100644 --- a/packages/service/support/wallet/usage/type.d.ts +++ b/packages/service/support/wallet/usage/type.d.ts @@ -1,16 +1,6 @@ -export type ConcatBillQueueItemType = { - billId: string; // usageId - listIndex?: number; - totalPoints: number; - - // Model usage - inputTokens?: number; - outputTokens?: number; - // Times - count?: number; -}; +import type { ConcatUsageProps } from '@fastgpt/global/support/wallet/usage/api'; declare global { var reduceAiPointsQueue: { teamId: string; totalPoints: number }[]; - var concatBillQueue: ConcatBillQueueItemType[]; + var concatBillQueue: ConcatUsageProps[]; } diff --git a/packages/service/support/wallet/usage/usageItemSchema.ts b/packages/service/support/wallet/usage/usageItemSchema.ts new file mode 100644 index 000000000..b9d018814 --- /dev/null +++ b/packages/service/support/wallet/usage/usageItemSchema.ts @@ -0,0 +1,53 @@ +import { connectionMongo, getMongoModel } from '../../../common/mongo'; +const { Schema } = connectionMongo; +import type { UsageItemSchemaType } from '@fastgpt/global/support/wallet/usage/type'; +import { UsageCollectionName, UsageItemCollectionName } from './constants'; +import { TeamCollectionName } from '@fastgpt/global/support/user/team/constant'; + +const UsageItemSchema = new Schema({ + teamId: { + type: Schema.Types.ObjectId, + ref: TeamCollectionName, + required: true + }, + usageId: { + type: Schema.Types.ObjectId, + ref: UsageCollectionName, + required: true + }, + name: { + // usage name + type: String, + required: true + }, + amount: { + type: Number, + default: 0 + }, + itemType: Number, + time: { + type: Date, + default: () => new Date() + }, + + // Params + inputTokens: Number, + outputTokens: Number, + charsLength: Number, + duration: Number, + pages: Number, + count: Number, + model: String +}); + +try { + UsageItemSchema.index({ usageId: 'hashed' }); + UsageItemSchema.index({ time: 1 }, { expireAfterSeconds: 360 * 24 * 60 * 60 }); +} catch (error) { + console.log(error); +} + +export const MongoUsageItem = getMongoModel( + UsageItemCollectionName, + UsageItemSchema +); diff --git a/packages/service/thirdProvider/fastgptPlugin/index.ts b/packages/service/thirdProvider/fastgptPlugin/index.ts index e720eaeb9..c38d9509f 100644 --- a/packages/service/thirdProvider/fastgptPlugin/index.ts +++ b/packages/service/thirdProvider/fastgptPlugin/index.ts @@ -1,9 +1,9 @@ import { createClient } from '@fastgpt/global/sdk/fastgpt-plugin'; -export const BASE_URL = process.env.PLUGIN_BASE_URL || ''; -export const TOKEN = process.env.PLUGIN_TOKEN || ''; +export const PLUGIN_BASE_URL = process.env.PLUGIN_BASE_URL || ''; +export const PLUGIN_TOKEN = process.env.PLUGIN_TOKEN || ''; export const pluginClient = createClient({ - baseUrl: BASE_URL, - token: TOKEN + baseUrl: PLUGIN_BASE_URL, + token: PLUGIN_TOKEN }); diff --git a/packages/service/worker/readFile/parseOffice.ts b/packages/service/worker/readFile/parseOffice.ts index d4de58f26..3b16dc770 100644 --- a/packages/service/worker/readFile/parseOffice.ts +++ b/packages/service/worker/readFile/parseOffice.ts @@ -43,9 +43,18 @@ const parsePowerPoint = async ({ return Promise.reject('解析 PPT 失败'); } + // Sort files by slide number to ensure correct order + const sortedFiles = files.sort((a, b) => { + const getSlideNumber = (path: string) => { + const match = path.match(/\d+/); + return match ? parseInt(match[0]) : 0; + }; + return getSlideNumber(a.path) - getSlideNumber(b.path); + }); + // Returning an array of all the xml contents read using fs.readFileSync const xmlContentArray = await Promise.all( - files.map(async (file) => { + sortedFiles.map(async (file) => { try { return await fs.promises.readFile(`${decompressPath}/${file.path}`, encoding); } catch (err) { diff --git a/packages/web/components/common/DateRangePicker/index.tsx b/packages/web/components/common/DateRangePicker/index.tsx index f21a4fa5b..335a76327 100644 --- a/packages/web/components/common/DateRangePicker/index.tsx +++ b/packages/web/components/common/DateRangePicker/index.tsx @@ -1,6 +1,6 @@ import React, { useState, useMemo, useRef, useEffect } from 'react'; import type { BoxProps } from '@chakra-ui/react'; -import { Box, Card, Flex, useOutsideClick, Button } from '@chakra-ui/react'; +import { Box, Card, Flex, useTheme, useOutsideClick, Button } from '@chakra-ui/react'; import { addDays, format } from 'date-fns'; import { DayPicker } from 'react-day-picker'; import 'react-day-picker/dist/style.css'; diff --git a/packages/web/components/common/Icon/constants.ts b/packages/web/components/common/Icon/constants.ts index 053042726..1dec87576 100644 --- a/packages/web/components/common/Icon/constants.ts +++ b/packages/web/components/common/Icon/constants.ts @@ -13,6 +13,7 @@ export const iconPaths = { close: () => import('./icons/close.svg'), closeSolid: () => import('./icons/closeSolid.svg'), code: () => import('./icons/code.svg'), + codeCopilot: () => import('./icons/codeCopilot.svg'), collectionLight: () => import('./icons/collectionLight.svg'), collectionSolid: () => import('./icons/collectionSolid.svg'), comment: () => import('./icons/comment.svg'), diff --git a/packages/web/components/common/Icon/icons/codeCopilot.svg b/packages/web/components/common/Icon/icons/codeCopilot.svg new file mode 100644 index 000000000..7c253362c --- /dev/null +++ b/packages/web/components/common/Icon/icons/codeCopilot.svg @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/packages/web/components/common/MyMenu/index.tsx b/packages/web/components/common/MyMenu/index.tsx index 220589d19..6be822b44 100644 --- a/packages/web/components/common/MyMenu/index.tsx +++ b/packages/web/components/common/MyMenu/index.tsx @@ -225,7 +225,7 @@ const MyMenu = ({ autoSelect={false} direction={'ltr'} isLazy - lazyBehavior={'keepMounted'} + lazyBehavior={'unmount'} placement={placement} computePositionOnMount > diff --git a/packages/web/components/common/MyPopover/PopoverConfirm.tsx b/packages/web/components/common/MyPopover/PopoverConfirm.tsx index 1e4f4e434..a4b9eea8d 100644 --- a/packages/web/components/common/MyPopover/PopoverConfirm.tsx +++ b/packages/web/components/common/MyPopover/PopoverConfirm.tsx @@ -73,7 +73,7 @@ const PopoverConfirm = ({ openDelay={100} closeDelay={100} isLazy - lazyBehavior="keepMounted" + lazyBehavior="unmount" arrowSize={10} strategy={'fixed'} computePositionOnMount={true} diff --git a/packages/web/components/common/MyPopover/index.tsx b/packages/web/components/common/MyPopover/index.tsx index 24ed31a9c..5a9d07950 100644 --- a/packages/web/components/common/MyPopover/index.tsx +++ b/packages/web/components/common/MyPopover/index.tsx @@ -60,7 +60,7 @@ const MyPopover = ({ openDelay={100} closeDelay={100} isLazy - lazyBehavior="keepMounted" + lazyBehavior="unmount" autoFocus={false} > {Trigger} diff --git a/packages/web/components/common/MySelect/MultipleSelect.tsx b/packages/web/components/common/MySelect/MultipleSelect.tsx index c95ee045c..4ba33c691 100644 --- a/packages/web/components/common/MySelect/MultipleSelect.tsx +++ b/packages/web/components/common/MySelect/MultipleSelect.tsx @@ -1,7 +1,6 @@ import type { FlexProps } from '@chakra-ui/react'; import { Box, - Button, type ButtonProps, Checkbox, Flex, diff --git a/packages/web/components/common/Textarea/PromptEditor/Editor.tsx b/packages/web/components/common/Textarea/PromptEditor/Editor.tsx index f11a8ff34..daaebedc4 100644 --- a/packages/web/components/common/Textarea/PromptEditor/Editor.tsx +++ b/packages/web/components/common/Textarea/PromptEditor/Editor.tsx @@ -6,6 +6,7 @@ * */ +import type { CSSProperties } from 'react'; import { useMemo, useState, useTransition } from 'react'; import { LexicalComposer } from '@lexical/react/LexicalComposer'; import { PlainTextPlugin } from '@lexical/react/LexicalPlainTextPlugin'; @@ -40,18 +41,17 @@ import { useDeepCompareEffect } from 'ahooks'; import VariablePickerPlugin from './plugins/VariablePickerPlugin'; import MarkdownPlugin from './plugins/MarkdownPlugin'; import MyIcon from '../../Icon'; -import TabToSpacesPlugin from './plugins/TabToSpacesPlugin'; import ListExitPlugin from './plugins/ListExitPlugin'; +import KeyDownPlugin from './plugins/KeyDownPlugin'; -const Placeholder = ({ children }: { children: React.ReactNode }) => ( +const Placeholder = ({ children, padding }: { children: React.ReactNode; padding: string }) => ( @@ -78,12 +78,14 @@ export type EditorProps = { maxH?: number; maxLength?: number; placeholder?: string; + placeholderPadding?: string; isInvalid?: boolean; - + onKeyDown?: (e: React.KeyboardEvent) => void; ExtensionPopover?: ((e: { onChangeText: (text: string) => void; iconButtonStyle: Record; }) => React.ReactNode)[]; + boxStyle?: CSSProperties; }; export default function Editor({ @@ -100,10 +102,12 @@ export default function Editor({ onBlur, value, placeholder = '', + placeholderPadding = '12px 14px', bg = 'white', isInvalid, - - ExtensionPopover + onKeyDown, + ExtensionPopover, + boxStyle }: EditorProps & FormPropsType & { onOpenModal?: () => void; @@ -180,13 +184,14 @@ export default function Editor({ className={`${isInvalid ? styles.contentEditable_invalid : styles.contentEditable} ${styles.richText}`} style={{ minHeight: `${minH}px`, - maxHeight: `${maxH}px` + maxHeight: `${maxH}px`, + ...boxStyle }} onFocus={() => setFocus(true)} onBlur={() => setFocus(false)} /> } - placeholder={{placeholder}} + placeholder={{placeholder}} ErrorBoundary={LexicalErrorBoundary} /> ) : ( @@ -196,11 +201,12 @@ export default function Editor({ className={isInvalid ? styles.contentEditable_invalid : styles.contentEditable} style={{ minHeight: `${minH}px`, - maxHeight: `${maxH}px` + maxHeight: `${maxH}px`, + ...boxStyle }} /> } - placeholder={{placeholder}} + placeholder={{placeholder}} ErrorBoundary={LexicalErrorBoundary} /> )} @@ -210,6 +216,7 @@ export default function Editor({ + {variableLabels.length > 0 && ( diff --git a/packages/web/components/common/Textarea/PromptEditor/index.tsx b/packages/web/components/common/Textarea/PromptEditor/index.tsx index a968540ef..ae462e3df 100644 --- a/packages/web/components/common/Textarea/PromptEditor/index.tsx +++ b/packages/web/components/common/Textarea/PromptEditor/index.tsx @@ -13,6 +13,7 @@ const PromptEditor = ({ value, onChange, onBlur, + onKeyDown, title, isDisabled, ...props @@ -67,6 +68,7 @@ const PromptEditor = ({ onChange={onChangeInput} onChangeText={onChange} onBlur={onBlurInput} + onKeyDown={onKeyDown} /> {isDisabled && ( diff --git a/packages/web/components/common/Textarea/PromptEditor/plugins/KeyDownPlugin/index.tsx b/packages/web/components/common/Textarea/PromptEditor/plugins/KeyDownPlugin/index.tsx new file mode 100644 index 000000000..45aec718a --- /dev/null +++ b/packages/web/components/common/Textarea/PromptEditor/plugins/KeyDownPlugin/index.tsx @@ -0,0 +1,38 @@ +import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'; +import { KEY_DOWN_COMMAND, COMMAND_PRIORITY_HIGH } from 'lexical'; +import { useEffect } from 'react'; + +export default function KeyDownPlugin({ + onKeyDown +}: { + onKeyDown?: (e: React.KeyboardEvent) => void; +}) { + const [editor] = useLexicalComposerContext(); + + useEffect(() => { + if (!onKeyDown) return; + + return editor.registerCommand( + KEY_DOWN_COMMAND, + (event: KeyboardEvent) => { + const syntheticEvent = { + key: event.key, + shiftKey: event.shiftKey, + ctrlKey: event.ctrlKey, + metaKey: event.metaKey, + altKey: event.altKey, + preventDefault: () => event.preventDefault(), + stopPropagation: () => event.stopPropagation(), + nativeEvent: event + } as React.KeyboardEvent; + + onKeyDown(syntheticEvent); + + return event.defaultPrevented; + }, + COMMAND_PRIORITY_HIGH + ); + }, [editor, onKeyDown]); + + return null; +} diff --git a/packages/web/components/common/Textarea/PromptEditor/plugins/VariableLabelPlugin/index.tsx b/packages/web/components/common/Textarea/PromptEditor/plugins/VariableLabelPlugin/index.tsx index 56dc3b5af..55eaa7390 100644 --- a/packages/web/components/common/Textarea/PromptEditor/plugins/VariableLabelPlugin/index.tsx +++ b/packages/web/components/common/Textarea/PromptEditor/plugins/VariableLabelPlugin/index.tsx @@ -22,15 +22,22 @@ export default function VariableLabelPlugin({ throw new Error('VariableLabelPlugin: VariableLabelPlugin not registered on editor'); }, [editor]); - const createVariableLabelPlugin = useCallback((textNode: TextNode): VariableLabelNode => { - const [parentKey, childrenKey] = textNode.getTextContent().slice(3, -3).split('.'); - const currentVariable = variables.find( - (item) => item.parent.id === parentKey && item.key === childrenKey - ); - const variableLabel = `${currentVariable && t(currentVariable.parent?.label as any)}.${currentVariable?.label}`; - const nodeAvatar = currentVariable?.parent?.avatar || ''; - return $createVariableLabelNode(textNode.getTextContent(), variableLabel, nodeAvatar); - }, []); + const createVariableLabelPlugin = useCallback( + (textNode: TextNode): VariableLabelNode => { + const content = textNode.getTextContent().slice(3, -3); // Remove {{$ and $}} + const dotIndex = content.indexOf('.'); + const parentKey = content.slice(0, dotIndex); + const childrenKey = content.slice(dotIndex + 1); + + const currentVariable = variables.find( + (item) => item.parent.id === parentKey && item.key === childrenKey + ); + const variableLabel = `${currentVariable && t(currentVariable.parent?.label as any)}.${currentVariable?.label}`; + const nodeAvatar = currentVariable?.parent?.avatar || ''; + return $createVariableLabelNode(textNode.getTextContent(), variableLabel, nodeAvatar); + }, + [t] + ); const getVariableMatch = useCallback((text: string) => { const matches = REGEX.exec(text); diff --git a/packages/web/core/workflow/constants.ts b/packages/web/core/workflow/constants.ts index 59737bdee..bc1227bef 100644 --- a/packages/web/core/workflow/constants.ts +++ b/packages/web/core/workflow/constants.ts @@ -34,7 +34,7 @@ export const workflowSystemNodeTemplateList: { export const defaultGroup: SystemToolGroupSchemaType = { groupId: 'systemPlugin', groupAvatar: 'core/app/type/pluginLight', - groupName: i18nT('common:core.module.template.System Plugin'), + groupName: i18nT('app:core.module.template.System Tools'), groupOrder: 0, groupTypes: [] // from getPluginGroups }; diff --git a/packages/web/hooks/useMemoEnhance.ts b/packages/web/hooks/useMemoEnhance.ts new file mode 100644 index 000000000..e8403f030 --- /dev/null +++ b/packages/web/hooks/useMemoEnhance.ts @@ -0,0 +1,27 @@ +import { isEqual } from 'lodash'; +import { useRef } from 'react'; +import type { DependencyList } from 'react'; + +/** + * Enhanced memo hook that provides more stable references than useMemo + * Similar to ahooks useCreation, ensures factory function is only called when dependencies change + * @param factory - Function that returns the value to be memoized + * @param deps - Dependency array to determine when to re-execute factory + * @returns The memoized value + */ +export function useMemoEnhance(factory: () => T, deps: DependencyList): T { + const { current } = useRef({ + obj: undefined as undefined | T, + initialized: false, + deps: deps as DependencyList + }); + + // Check if this is the first render or if dependencies have changed + if (!current.initialized || !isEqual(current.deps, deps)) { + current.obj = factory(); + current.initialized = true; + current.deps = deps; + } + + return current.obj as T; +} diff --git a/packages/web/hooks/useTableMultipleSelect.tsx b/packages/web/hooks/useTableMultipleSelect.tsx index 84d2540e2..7d47a0d9d 100644 --- a/packages/web/hooks/useTableMultipleSelect.tsx +++ b/packages/web/hooks/useTableMultipleSelect.tsx @@ -63,10 +63,20 @@ export const useTableMultipleSelect = ({ ({ children, Controler, + activedStyles, ...props - }: { children: ReactNode; Controler: ReactNode } & FlexProps) => { - return ( - + }: { children?: ReactNode; activedStyles?: FlexProps; Controler: ReactNode } & FlexProps) => { + return hasSelections ? ( + {hasSelections && ( <> @@ -80,7 +90,7 @@ export const useTableMultipleSelect = ({ )} {children} - ); + ) : null; }, [hasSelections, isSelecteAll, selectAllTrigger, selectedCount, t] ); diff --git a/packages/web/i18n/en/account_model.json b/packages/web/i18n/en/account_model.json index 7e03ece02..631a9842c 100644 --- a/packages/web/i18n/en/account_model.json +++ b/packages/web/i18n/en/account_model.json @@ -89,5 +89,6 @@ "vlm_model": "Vlm", "vlm_model_tip": "Used to generate additional indexing of images in a document in the knowledge base", "volunme_of_failed_calls": "Error amount", - "waiting_test": "Waiting for testing" + "waiting_test": "Waiting for testing", + "model_permission_config_hint": "If no collaborators are added, all members are available by default" } diff --git a/packages/web/i18n/en/account_team.json b/packages/web/i18n/en/account_team.json index 36b54af9e..a79e7c89d 100644 --- a/packages/web/i18n/en/account_team.json +++ b/packages/web/i18n/en/account_team.json @@ -56,10 +56,10 @@ "create_invoice": "Issuing invoices", "create_org": "Create organization", "create_sub_org": "Create sub-organization", - "dataset.api_file": "API Import", + "dataset.api_file": "API Knowledge Base", "dataset.common_dataset": "Dataset", "dataset.external_file": "External File", - "dataset.feishu_dataset": "Feishu Spreadsheet", + "dataset.feishu_dataset": "Feishu Cloud Documentation", "dataset.folder_dataset": "Folder", "dataset.website_dataset": "Website Sync", "dataset.yuque_dataset": "Yuque Knowledge Base", diff --git a/packages/web/i18n/en/account_usage.json b/packages/web/i18n/en/account_usage.json index 6415a45bd..4c810e2c1 100644 --- a/packages/web/i18n/en/account_usage.json +++ b/packages/web/i18n/en/account_usage.json @@ -41,6 +41,8 @@ "points": "Points", "project_name": "Project name", "qa": "QA", + "rerank": "Rerank", + "search_test": "Search test", "select_member_and_source_first": "Please select members and types first", "share": "Share Link", "source": "source", diff --git a/packages/web/i18n/en/app.json b/packages/web/i18n/en/app.json index 8e682bfce..39bb925a8 100644 --- a/packages/web/i18n/en/app.json +++ b/packages/web/i18n/en/app.json @@ -2,6 +2,7 @@ "AutoOptimize": "Automatic optimization", "Click_to_delete_this_field": "Click to delete this field", "Filed_is_deprecated": "This field is deprecated", + "HTTP_tools_list_with_number": "Tool list: {{total}}", "Index": "Index", "MCP_tools_debug": "debug", "MCP_tools_detail": "check the details", @@ -22,10 +23,12 @@ "Optimizer_Reoptimize": "Re-optimize", "Optimizer_Replace": "replace", "Optimizer_Tooltip": "AI optimization prompt words", + "Params_config": "Params Config", "Role_setting": "Permission", "Run": "Execute", "Search_dataset": "Search dataset", "Selected": "Selected", + "Start_config": "Start configuration", "Team_Tags": "Team tags", "ai_point_price": "Billing", "ai_settings": "AI Configuration", @@ -42,6 +45,8 @@ "app.version_past": "Previously Published", "app.version_publish_tips": "This version will be saved to the team cloud, synchronized with the entire team, and update the app version on all release channels.", "app_detail": "Application Details", + "apply_code": "Apply", + "apply_code_failed": "Failed to apply code", "auto_execute": "Automatic execution", "auto_execute_default_prompt_placeholder": "Default questions sent when executing automatically", "auto_execute_tip": "After turning it on, the workflow will be automatically triggered when the user enters the conversation interface. \nExecution order: 1. Dialogue starter; 2. Global variables; 3. Automatic execution.", @@ -49,22 +54,28 @@ "chat_debug": "Chat Preview", "chat_logs": "Logs", "chat_logs_tips": "Logs will record the online, shared, and API (requires chatId) conversation records of this app.", - "common.day": "day", + "code_applied_successfully": "Code applied successfully", + "code_function_describe": "Describe your code function, enter \"/\" to select the variable", "config_ai_model_params": "Click to configure AI model related properties", "config_file_upload": "Click to Configure File Upload Rules", "config_question_guide": "Configuration guess you want to ask", "confirm_copy_app_tip": "The system will create an app with the same configuration for you, but permissions will not be copied. Please confirm!", "confirm_del_app_tip": "Are you sure you want to delete 【{{name}}】 and all of its chat history?", "confirm_delete_folder_tip": "Confirm to delete this folder? All apps and corresponding conversation records under it will be deleted. Please confirm!", + "copilot_config_message": "Current Node Configuration Information: \n Code Type: {{codeType}} \n Current Code: \\\\`\\\\`\\\\`{{codeType}} \n{{code}} \\\\`\\\\`\\\\` \n Input Parameters: {{inputs}} \n Output Parameters: {{outputs}}", + "copilot_confirm_message": "The original configuration has been received to understand the current code structure and input and output parameters. \nPlease explain your optimization requirements.", "copy_one_app": "Create Duplicate", "core.app.QG.Switch": "Enable guess what you want to ask", "core.dataset.import.Custom prompt": "Custom Prompt", + "core.module.template.System Tools": "System Tools", + "core.workflow.Copilot": "AI Generation", "create_by_curl": "By CURL", "create_by_template": "By template", "create_copy_success": "Duplicate Created Successfully", "create_empty_app": "Create Default App", "create_empty_plugin": "Create Default Plugin", "create_empty_workflow": "Create Default Workflow", + "create_http_toolset": "Create HTTP Toolset", "cron.every_day": "Run Daily", "cron.every_month": "Run Monthly", "cron.every_week": "Run Weekly", @@ -213,6 +224,8 @@ "publish_success": "Publish Successful", "question_guide_tip": "After the conversation, 3 guiding questions will be generated for you.", "reasoning_response": "Output thinking", + "reference_variable": "Reference variables", + "request_headers": "Request header", "response_format": "Response format", "save_team_app_log_keys": "Save as team configuration", "saved_success": "Saved successfully! \nTo use this version externally, click Save and Publish", @@ -252,6 +265,12 @@ "template_market": "Templates", "template_market_description": "Explore more features in the template market, with configuration tutorials and usage guides to help you understand and get started with various applications.", "template_market_empty_data": "No suitable templates found", + "test_all_passed": "All tests passed", + "test_code": "Test", + "test_code_incomplete": "The code parsing is incomplete and cannot be tested", + "test_execution_failed": "Test execution failed", + "test_failed_with_progress": "Test failed {{passed}}/{{total}}", + "this_tool_requires_no_input": "This tool does not require input", "time_granularity": "Time granularity", "time_range_limit": "Time range limit", "time_type": "Time Type", @@ -265,6 +284,7 @@ "tool_input_param_tip": "This plugin requires configuration of related information to run properly.", "tool_not_active": "This tool has not been activated yet", "tool_run_free": "This tool runs without points consumption", + "tool_tip": "When executed as a tool, is this field used as a tool response result?", "tool_type_tools": "tool", "tools_no_description": "This tool has not been introduced ~", "transition_to_workflow": "Convert to Workflow", @@ -283,7 +303,13 @@ "type.Create workflow bot": "Create Workflow", "type.Create workflow tip": "Build complex multi-turn dialogue AI applications through low-code methods, recommended for advanced users.", "type.Folder": "Folder", + "type.Http batch": "Bulk creation", + "type.Http batch tip": "Bulk creation tool with OpenAPI Schema", + "type.Http manual": "Create manually", + "type.Http manual tip": "Create a single tool by curl or manually filling in parameters", "type.Http plugin": "HTTP Plugin", + "type.Http plugin_deprecated": "The HTTP plug-in is deprecated. To configure Openapi Schema, use the HTTP tool set", + "type.Http tool set": "HTTP Tool Set", "type.Import from json": "Import JSON", "type.Import from json tip": "Create applications directly through JSON configuration files", "type.Import from json_error": "Failed to get workflow data, please check the URL or manually paste the JSON data", diff --git a/packages/web/i18n/en/common.json b/packages/web/i18n/en/common.json index df4a2bd8d..0a11bc546 100644 --- a/packages/web/i18n/en/common.json +++ b/packages/web/i18n/en/common.json @@ -416,7 +416,6 @@ "core.chat.response.module query": "Question/Search Term", "core.chat.response.module similarity": "Similarity", "core.chat.response.module temperature": "Temperature", - "core.chat.response.module time": "Run Time", "core.chat.response.plugin output": "Plugin Output Value", "core.chat.response.search using reRank": "Result Re-Rank", "core.chat.response.text output": "Text Output", @@ -619,6 +618,7 @@ "core.module.extract.Required Description": "Even if the field cannot be extracted, it will be returned using the default value", "core.module.extract.Target field": "Target Field", "core.module.http.Add props": "Add Parameter", + "core.module.http.Add_props_value": "Add parameter values", "core.module.http.AppId": "App ID", "core.module.http.ChatId": "Current Chat ID", "core.module.http.Current time": "Current Time", @@ -663,7 +663,6 @@ "core.module.template.AI support tool tip": "Models that support function calls can better use tool calls.", "core.module.template.Basic Node": "Basic", "core.module.template.Query extension": "Question Optimization", - "core.module.template.System Plugin": "System Plugin", "core.module.template.System input module": "System Input", "core.module.template.Team app": "Team", "core.module.template.UnKnow Module": "Unknown Module", @@ -984,10 +983,8 @@ "plugin.App": "Select App", "plugin.Currentapp": "Current App", "plugin.Description": "Description", - "plugin.Edit Http Plugin": "Edit HTTP Plugin", "plugin.Enter PAT": "Enter Personal Access Token (PAT)", "plugin.Get Plugin Module Detail Failed": "Failed to Retrieve Plugin Information", - "plugin.Import Plugin": "Import HTTP Plugin", "plugin.Import from URL": "Import from URL. https://xxxx", "plugin.Intro": "Plugin Introduction", "plugin.Invalid Env": "Invalid Laf Environment", @@ -1214,6 +1211,7 @@ "support.wallet.usage.App name": "App Name", "support.wallet.usage.Audio Speech": "Voice Playback", "support.wallet.usage.Bill Module": "Billing Module", + "support.wallet.usage.Code Copilot": "Code Copilot", "support.wallet.usage.Duration": "Duration (seconds)", "support.wallet.usage.Module name": "Module Name", "support.wallet.usage.Optimize Prompt": "Prompt word optimization", @@ -1225,6 +1223,7 @@ "support.wallet.usage.Total points": "AI Points Consumption", "support.wallet.usage.Usage Detail": "Usage Details", "support.wallet.usage.Whisper": "Voice Input", + "sure_delete_tool_cannot_undo": "Are you sure to delete the tool? \nThis operation cannot be withdrawn", "sync_link": "Sync Link", "sync_success": "Synced Successfully", "system.Concat us": "Contact Us", diff --git a/packages/web/i18n/en/file.json b/packages/web/i18n/en/file.json index d53d0a88c..172b7b0bd 100644 --- a/packages/web/i18n/en/file.json +++ b/packages/web/i18n/en/file.json @@ -13,12 +13,14 @@ "Only_support_uploading_one_image": "Only support uploading one image", "Please select the image to upload": "Please select the image to upload", "Please wait for all files to upload": "Please wait for all files to be uploaded to complete", + "common.upload_system_tools": "Upload system tools", "bucket_chat": "Conversation Files", "bucket_file": "Dataset Documents", "bucket_image": "picture", "click_to_view_raw_source": "Click to View Original Source", "common.Some images failed to process": "Some images failed to process", "common.dataset_data_input_image_support_format": "Support .jpg, .jpeg, .png, .gif, .webp formats", + "common.import_update": "Import/Update", "count.core.dataset.collection.Create Success": "{{count}} picture successfully imported", "delete_image": "Delete pictures", "file_name": "Filename", diff --git a/packages/web/i18n/zh-CN/account_model.json b/packages/web/i18n/zh-CN/account_model.json index 6d97ac6ef..52d1c3d99 100644 --- a/packages/web/i18n/zh-CN/account_model.json +++ b/packages/web/i18n/zh-CN/account_model.json @@ -89,5 +89,6 @@ "vlm_model": "图片理解模型", "vlm_model_tip": "用于知识库中对文档中的图片进行额外的索引生成", "volunme_of_failed_calls": "调用失败量", - "waiting_test": "等待测试" + "waiting_test": "等待测试", + "model_permission_config_hint": "若无协作者,默认全部成员可用" } diff --git a/packages/web/i18n/zh-CN/account_team.json b/packages/web/i18n/zh-CN/account_team.json index 67f3c37bd..e99dc78ff 100644 --- a/packages/web/i18n/zh-CN/account_team.json +++ b/packages/web/i18n/zh-CN/account_team.json @@ -57,10 +57,10 @@ "create_invoice": "开发票", "create_org": "创建部门", "create_sub_org": "创建子部门", - "dataset.api_file": "API导入", + "dataset.api_file": "API 知识库", "dataset.common_dataset": "知识库", "dataset.external_file": "外部文件", - "dataset.feishu_dataset": "飞书多维表格", + "dataset.feishu_dataset": "飞书云文档", "dataset.folder_dataset": "文件夹", "dataset.website_dataset": "网站同步", "dataset.yuque_dataset": "语雀知识库", @@ -249,6 +249,7 @@ "transfer_ownership": "转让所有者", "type.Folder": "文件夹", "type.Http plugin": "HTTP 插件", + "type.Http tool set": "HTTP 工具集", "type.Plugin": "插件", "type.Simple bot": "简易应用", "type.Tool": "工具", diff --git a/packages/web/i18n/zh-CN/account_usage.json b/packages/web/i18n/zh-CN/account_usage.json index 7ebb628f1..9dcebe0c9 100644 --- a/packages/web/i18n/zh-CN/account_usage.json +++ b/packages/web/i18n/zh-CN/account_usage.json @@ -43,6 +43,8 @@ "points": "积分", "project_name": "项目名", "qa": "问答对提取", + "rerank": "结果重排", + "search_test": "搜索测试", "select_member_and_source_first": "请先选中成员和类型", "share": "分享链接", "source": "来源", diff --git a/packages/web/i18n/zh-CN/app.json b/packages/web/i18n/zh-CN/app.json index cea7ed34d..d3e79c34e 100644 --- a/packages/web/i18n/zh-CN/app.json +++ b/packages/web/i18n/zh-CN/app.json @@ -2,6 +2,8 @@ "AutoOptimize": "自动优化", "Click_to_delete_this_field": "点击删除该字段", "Filed_is_deprecated": "该字段已弃用", + "HTTP_tools_detail": "查看详情", + "HTTP_tools_list_with_number": "工具列表: {{total}}", "Index": "索引", "MCP_tools_debug": "调试", "MCP_tools_detail": "查看详情", @@ -22,10 +24,12 @@ "Optimizer_Reoptimize": "重新优化", "Optimizer_Replace": "替换", "Optimizer_Tooltip": "AI 优化提示词", + "Params_config": "参数配置", "Role_setting": "权限设置", "Run": "运行", "Search_dataset": "搜索知识库", "Selected": "已选择", + "Start_config": "开始配置", "Team_Tags": "团队标签", "ai_point_price": "AI积分计费", "ai_settings": "AI 配置", @@ -42,6 +46,8 @@ "app.version_past": "发布过", "app.version_publish_tips": "该版本将被保存至团队云端,同步给整个团队,同时更新所有发布渠道的应用版本", "app_detail": "应用详情", + "apply_code": "应用", + "apply_code_failed": "应用代码失败", "auto_execute": "自动执行", "auto_execute_default_prompt_placeholder": "自动执行时,发送的默认问题", "auto_execute_tip": "开启后,用户进入对话界面将自动触发工作流。执行顺序:1、对话开场白;2、全局变量;3、自动执行。", @@ -49,22 +55,28 @@ "chat_debug": "调试预览", "chat_logs": "对话日志", "chat_logs_tips": "日志会记录该应用的在线、分享和 API(需填写 chatId)对话记录", - "common.day": "日", + "code_applied_successfully": "代码应用成功", + "code_function_describe": "描述你的代码功能,输入“/”可选择变量", "config_ai_model_params": "点击配置 AI 模型相关属性", "config_file_upload": "点击配置文件上传规则", "config_question_guide": "配置猜你想问", "confirm_copy_app_tip": "系统将为您创建一个相同配置应用,但权限不会进行复制,请确认!", "confirm_del_app_tip": "确认删除 【{{name}}】 及其所有聊天记录?", "confirm_delete_folder_tip": "确认删除该文件夹?将会删除它下面所有应用及对应的聊天记录,请确认!", + "copilot_config_message": "`当前节点配置信息: \n代码类型:{{codeType}} \n当前代码: \\`\\`\\`{{codeType}} \n{{code}} \\`\\`\\` \n输入参数: {{inputs}} \n输出参数: {{outputs}}`", + "copilot_confirm_message": "已接收到原始配置,了解当前代码结构和输入输出参数。请说明您的优化需求。", "copy_one_app": "创建副本", "core.app.QG.Switch": "启用猜你想问", "core.dataset.import.Custom prompt": "自定义提示词", + "core.module.template.System Tools": "系统工具", + "core.workflow.Copilot": "AI 生成", "create_by_curl": "从 CURL 创建", - "create_by_template": "从模板创建", + "create_by_template": "从模板选择", "create_copy_success": "创建副本成功", "create_empty_app": "创建空白应用", "create_empty_plugin": "创建空白插件", "create_empty_workflow": "创建空白工作流", + "create_http_toolset": "创建 HTTP 工具集", "cron.every_day": "每天执行", "cron.every_month": "每月执行", "cron.every_week": "每周执行", @@ -222,6 +234,8 @@ "publish_success": "发布成功", "question_guide_tip": "对话结束后,会为你生成 3 个引导性问题。", "reasoning_response": "输出思考", + "reference_variable": "引用变量", + "request_headers": "请求头", "response_format": "回复格式", "save_team_app_log_keys": "保存为团队配置", "saved_success": "保存成功!如需在外部使用该版本,请点击“保存并发布”", @@ -237,6 +251,7 @@ "stop_sign_placeholder": "多个序列号通过 | 隔开,例如:aaa|stop", "stream_response": "流输出", "stream_response_tip": "关闭该开关,可以强制模型使用非流模式,并且不会直接进行内容输出。可以在 AI 回复的输出中,获取本次模型输出的内容进行二次处理。", + "support.wallet.usage.Code Copilot": "代码助手", "sync_log_keys_popover_text": "当前字段配置仅对个人生效,是否需要保存至团队配置?", "sync_team_app_log_keys": "还原成团队配置", "system_secret": "系统密钥", @@ -261,6 +276,13 @@ "template_market": "模板市场", "template_market_description": "在模板市场探索更多玩法,配置教程与使用引导,带你理解并上手各种应用", "template_market_empty_data": "找不到合适的模板", + "test_all_passed": "测试全部通过", + "test_code": "测试", + "test_code_incomplete": "代码解析不完整,无法进行测试", + "test_execution_failed": "测试执行失败", + "test_failed_with_progress": "测试失败 {{passed}}/{{total}}", + "testing": "测试中", + "this_tool_requires_no_input": "该工具无需输入", "time_granularity": "时间粒度", "time_range_end": "结束时间", "time_range_limit": "时间范围限制", @@ -276,6 +298,7 @@ "tool_input_param_tip": "该插件正常运行需要配置相关信息", "tool_not_active": "该工具尚未激活", "tool_run_free": "该工具运行无积分消耗", + "tool_tip": "作为工具执行时,该字段是否作为工具响应结果", "tool_type_tools": "工具", "tools_no_description": "这个工具没有介绍~", "transition_to_workflow": "转成工作流", @@ -285,7 +308,8 @@ "tts_browser": "浏览器自带(免费)", "tts_close": "关闭", "type.All": "全部", - "type.Create http plugin tip": "通过 OpenAPI Schema 批量创建插件,兼容 GPTs 格式", + "type.Create http plugin tip": "通过 OpenAPI Schema 批量创建工具(兼容GPTs)、通过curl或手动创建工具", + "type.Create http toolset tip": "通过 OpenAPI Schema 批量创建工具(兼容GPTs)", "type.Create mcp tools tip": "通过输入 MCP 地址,自动解析并批量创建可调用的 MCP 工具", "type.Create one plugin tip": "可以自定义输入和输出的工作流,通常用于封装重复使用的工作流", "type.Create plugin bot": "创建插件", @@ -294,7 +318,13 @@ "type.Create workflow bot": "创建工作流", "type.Create workflow tip": "通过低代码的方式,构建逻辑复杂的多轮对话 AI 应用,推荐高级玩家使用", "type.Folder": "文件夹", + "type.Http batch": "批量创建", + "type.Http batch tip": "通过 OpenAPI Schema 批量创建工具", + "type.Http manual": "手动创建", + "type.Http manual tip": "通过 curl 或手动填写参数创建单个工具", "type.Http plugin": "HTTP 插件", + "type.Http plugin_deprecated": "HTTP 插件已废弃,如需配置 Openapi Schema,请使用 HTTP 工具集", + "type.Http tool set": "HTTP 工具集", "type.Import from json": "导入 JSON 配置", "type.Import from json tip": "通过 JSON 配置文件,直接创建应用", "type.Import from json_error": "获取工作流数据失败,请检查URL或手动粘贴JSON数据", diff --git a/packages/web/i18n/zh-CN/common.json b/packages/web/i18n/zh-CN/common.json index f0f867f23..9d5590285 100644 --- a/packages/web/i18n/zh-CN/common.json +++ b/packages/web/i18n/zh-CN/common.json @@ -416,7 +416,6 @@ "core.chat.response.module query": "问题/检索词", "core.chat.response.module similarity": "相似度", "core.chat.response.module temperature": "温度", - "core.chat.response.module time": "运行时长", "core.chat.response.plugin output": "插件输出值", "core.chat.response.search using reRank": "结果重排", "core.chat.response.text output": "文本输出", @@ -619,6 +618,7 @@ "core.module.extract.Required Description": "即使无法提取该字段,也会使用默认值进行返回", "core.module.extract.Target field": "目标字段", "core.module.http.Add props": "添加参数", + "core.module.http.Add_props_value": "添加参数值", "core.module.http.AppId": "应用 ID", "core.module.http.ChatId": "当前对话 ID", "core.module.http.Current time": "当前时间", @@ -663,7 +663,6 @@ "core.module.template.AI support tool tip": "支持函数调用的模型,可以更好的使用工具调用。", "core.module.template.Basic Node": "基础功能", "core.module.template.Query extension": "问题优化", - "core.module.template.System Plugin": "系统插件", "core.module.template.System input module": "系统输入", "core.module.template.Team app": "团队应用", "core.module.template.UnKnow Module": "未知模块", @@ -985,10 +984,8 @@ "plugin.App": "选择应用", "plugin.Currentapp": "当前应用", "plugin.Description": "描述", - "plugin.Edit Http Plugin": "编辑 HTTP 插件", "plugin.Enter PAT": "请输入访问凭证(PAT)", "plugin.Get Plugin Module Detail Failed": "获取插件信息异常", - "plugin.Import Plugin": "导入 HTTP 插件", "plugin.Import from URL": "从 URL 导入。https://xxxx", "plugin.Intro": "插件介绍", "plugin.Invalid Env": "laf 环境错误", @@ -1216,6 +1213,7 @@ "support.wallet.usage.App name": "应用名", "support.wallet.usage.Audio Speech": "语音播放", "support.wallet.usage.Bill Module": "扣费模块", + "support.wallet.usage.Code Copilot": "代码助手", "support.wallet.usage.Duration": "时长(秒)", "support.wallet.usage.Module name": "模块名", "support.wallet.usage.Optimize Prompt": "提示词优化", @@ -1227,6 +1225,7 @@ "support.wallet.usage.Total points": "AI 积分消耗", "support.wallet.usage.Usage Detail": "使用详情", "support.wallet.usage.Whisper": "语音输入", + "sure_delete_tool_cannot_undo": "是否确认删除该工具?该操作无法撤回", "sync_link": "同步链接", "sync_success": "同步成功", "system.Concat us": "联系我们", diff --git a/packages/web/i18n/zh-CN/file.json b/packages/web/i18n/zh-CN/file.json index bfd7df19c..02b6f7dcf 100644 --- a/packages/web/i18n/zh-CN/file.json +++ b/packages/web/i18n/zh-CN/file.json @@ -19,6 +19,8 @@ "click_to_view_raw_source": "点击查看来源", "common.Some images failed to process": "部分图片处理失败", "common.dataset_data_input_image_support_format": "支持 .jpg, .jpeg, .png, .gif, .webp 格式", + "common.import_update": "导入/更新", + "common.upload_system_tools": "上传系统工具", "count.core.dataset.collection.Create Success": "成功导入 {{count}} 张图片", "delete_image": "删除图片", "file_name": "文件名", diff --git a/packages/web/i18n/zh-Hant/account_model.json b/packages/web/i18n/zh-Hant/account_model.json index 04047acba..bd82dbef5 100644 --- a/packages/web/i18n/zh-Hant/account_model.json +++ b/packages/web/i18n/zh-Hant/account_model.json @@ -89,5 +89,6 @@ "vlm_model": "圖片理解模型", "vlm_model_tip": "用於知識庫中對文件中的圖片進行額外的索引生成", "volunme_of_failed_calls": "調用失敗量", - "waiting_test": "等待測試" + "waiting_test": "等待測試", + "model_permission_config_hint": "若無協作者,默認全部成員可用" } diff --git a/packages/web/i18n/zh-Hant/account_team.json b/packages/web/i18n/zh-Hant/account_team.json index b5268075b..a29ea1ae3 100644 --- a/packages/web/i18n/zh-Hant/account_team.json +++ b/packages/web/i18n/zh-Hant/account_team.json @@ -56,10 +56,10 @@ "create_invoice": "開發票", "create_org": "建立部門", "create_sub_org": "建立子部門", - "dataset.api_file": "API 匯入", + "dataset.api_file": "API 知識庫", "dataset.common_dataset": "知識庫", "dataset.external_file": "外部文件", - "dataset.feishu_dataset": "飛書多維表格", + "dataset.feishu_dataset": "飛書云文檔", "dataset.folder_dataset": "資料夾", "dataset.website_dataset": "網站同步", "dataset.yuque_dataset": "語雀知識庫", diff --git a/packages/web/i18n/zh-Hant/account_usage.json b/packages/web/i18n/zh-Hant/account_usage.json index 799080d8f..b786d8628 100644 --- a/packages/web/i18n/zh-Hant/account_usage.json +++ b/packages/web/i18n/zh-Hant/account_usage.json @@ -41,6 +41,8 @@ "points": "積分", "project_name": "專案名", "qa": "問答對提取", + "rerank": "結果重排", + "search_test": "搜索測試", "select_member_and_source_first": "請先選取成員和類型", "share": "分享連結", "source": "來源", diff --git a/packages/web/i18n/zh-Hant/app.json b/packages/web/i18n/zh-Hant/app.json index 25b9a1c8b..5267f65c8 100644 --- a/packages/web/i18n/zh-Hant/app.json +++ b/packages/web/i18n/zh-Hant/app.json @@ -2,6 +2,7 @@ "AutoOptimize": "自動優化", "Click_to_delete_this_field": "點擊刪除該字段", "Filed_is_deprecated": "該字段已棄用", + "HTTP_tools_list_with_number": "工具列表: {{total}}", "Index": "索引", "MCP_tools_debug": "偵錯", "MCP_tools_detail": "查看詳情", @@ -22,10 +23,12 @@ "Optimizer_Reoptimize": "重新優化", "Optimizer_Replace": "替換", "Optimizer_Tooltip": "AI 優化提示詞", + "Params_config": "參數配置", "Role_setting": "權限設定", "Run": "執行", "Search_dataset": "搜尋知識庫", "Selected": "已選擇", + "Start_config": "開始配置", "Team_Tags": "團隊標籤", "ai_point_price": "AI 積分計費", "ai_settings": "AI 設定", @@ -42,6 +45,8 @@ "app.version_past": "已發布過", "app.version_publish_tips": "此版本將儲存至團隊雲端,同步給整個團隊,同時更新所有發布通道的應用程式版本", "app_detail": "應用程式詳細資訊", + "apply_code": "應用", + "apply_code_failed": "應用代碼失敗", "auto_execute": "自動執行", "auto_execute_default_prompt_placeholder": "自動執行時,傳送的預設問題", "auto_execute_tip": "開啟後,使用者進入對話式介面將自動觸發工作流程。\n執行順序:1、對話開場白;2、全域變數;3、自動執行。", @@ -49,21 +54,28 @@ "chat_debug": "聊天預覽", "chat_logs": "對話紀錄", "chat_logs_tips": "紀錄會記錄此應用程式的線上、分享和 API(需填寫 chatId)對話紀錄", + "code_applied_successfully": "代碼應用成功", + "code_function_describe": "描述你的代碼功能,輸入“/”可選擇變量", "config_ai_model_params": "點選設定 AI 模型相關屬性", "config_file_upload": "點選設定檔案上傳規則", "config_question_guide": "設定猜你想問", "confirm_copy_app_tip": "系統將為您建立一個相同設定的應用程式,但權限不會複製,請確認!", "confirm_del_app_tip": "確認刪除【{{name}}】及其所有聊天紀錄?", "confirm_delete_folder_tip": "確認刪除這個資料夾?將會刪除它底下所有應用程式及對應的對話紀錄,請確認!", + "copilot_config_message": "當前節點配置信息: \n代碼類型:{{codeType}} \n當前代碼: \\\\`\\\\`\\\\`{{codeType}} \n{{code}} \\\\`\\\\`\\\\` \n輸入參數: {{inputs}} \n輸出參數: {{outputs}}", + "copilot_confirm_message": "已接收到原始配置,了解當前代碼結構和輸入輸出參數。\n請說明您的優化需求。", "copy_one_app": "建立副本", "core.app.QG.Switch": "啟用猜你想問", "core.dataset.import.Custom prompt": "自訂提示詞", + "core.module.template.System Tools": "系統工具", + "core.workflow.Copilot": "AI 生成", "create_by_curl": "從 CURL 建立", "create_by_template": "從範本建立", "create_copy_success": "建立副本成功", "create_empty_app": "建立空白應用程式", "create_empty_plugin": "建立空白外掛", "create_empty_workflow": "建立空白工作流程", + "create_http_toolset": "創建 HTTP 工具集", "cron.every_day": "每天執行", "cron.every_month": "每月執行", "cron.every_week": "每週執行", @@ -212,6 +224,8 @@ "publish_success": "發布成功", "question_guide_tip": "對話結束後,會為你產生 3 個引導性問題。", "reasoning_response": "輸出思考", + "reference_variable": "引用變量", + "request_headers": "請求頭", "response_format": "回覆格式", "save_team_app_log_keys": "保存為團隊配置", "saved_success": "儲存成功!\n如需在外部使用該版本,請點選“儲存並發布”", @@ -251,6 +265,12 @@ "template_market": "範本市集", "template_market_description": "在範本市集探索更多玩法,設定教學與使用指引,帶您理解並上手各種應用程式", "template_market_empty_data": "找不到合適的範本", + "test_all_passed": "測試全部通過", + "test_code": "測試", + "test_code_incomplete": "代碼解析不完整,無法進行測試", + "test_execution_failed": "測試執行失敗", + "test_failed_with_progress": "測試失敗 {{passed}}/{{total}}", + "this_tool_requires_no_input": "該工具無需輸入", "time_granularity": "時間粒度", "time_range_limit": "時間範圍限制", "time_type": "時間類型", @@ -264,6 +284,7 @@ "tool_input_param_tip": "這個外掛正常執行需要設定相關資訊", "tool_not_active": "該工具尚未激活", "tool_run_free": "該工具運行無積分消耗", + "tool_tip": "作為工具執行時,該字段是否作為工具響應結果", "tool_type_tools": "工具", "tools_no_description": "這個工具沒有介紹~", "transition_to_workflow": "轉換成工作流程", @@ -282,7 +303,13 @@ "type.Create workflow bot": "建立工作流程", "type.Create workflow tip": "透過低程式碼的方式,建立邏輯複雜的多輪對話 AI 應用程式,建議進階使用者使用", "type.Folder": "資料夾", + "type.Http batch": "批量創建", + "type.Http batch tip": "通過 OpenAPI Schema 批量創建工具", + "type.Http manual": "手動創建", + "type.Http manual tip": "通過 curl 或手動填寫參數創建單個工具", "type.Http plugin": "HTTP 外掛", + "type.Http plugin_deprecated": "HTTP 插件已廢棄,如需配置 Openapi Schema,請使用 HTTP 工具集", + "type.Http tool set": "HTTP 工具集", "type.Import from json": "匯入 JSON 設定", "type.Import from json tip": "透過 JSON 設定文件,直接建立應用", "type.Import from json_error": "獲取工作流數據失敗,請檢查URL或手動粘貼JSON數據", diff --git a/packages/web/i18n/zh-Hant/common.json b/packages/web/i18n/zh-Hant/common.json index ef9c3fba4..0a3ee744a 100644 --- a/packages/web/i18n/zh-Hant/common.json +++ b/packages/web/i18n/zh-Hant/common.json @@ -416,7 +416,6 @@ "core.chat.response.module query": "問題/搜尋詞", "core.chat.response.module similarity": "相似度", "core.chat.response.module temperature": "溫度", - "core.chat.response.module time": "執行時長", "core.chat.response.plugin output": "外掛程式輸出值", "core.chat.response.search using reRank": "結果重新排名", "core.chat.response.text output": "文字輸出", @@ -618,6 +617,7 @@ "core.module.extract.Required Description": "即使無法提取該欄位,也會使用預設值進行回傳", "core.module.extract.Target field": "目標欄位", "core.module.http.Add props": "新增參數", + "core.module.http.Add_props_value": "添加參數值", "core.module.http.AppId": "應用程式 ID", "core.module.http.ChatId": "目前對話 ID", "core.module.http.Current time": "目前時間", @@ -662,7 +662,6 @@ "core.module.template.AI support tool tip": "支援函式呼叫的模型可以更好地使用工具呼叫。", "core.module.template.Basic Node": "基本功能", "core.module.template.Query extension": "問題最佳化", - "core.module.template.System Plugin": "系統外掛", "core.module.template.System input module": "系統輸入模組", "core.module.template.Team app": "團隊應用程式", "core.module.template.UnKnow Module": "未知模組", @@ -983,10 +982,8 @@ "plugin.App": "選擇應用程式", "plugin.Currentapp": "目前應用程式", "plugin.Description": "描述", - "plugin.Edit Http Plugin": "編輯 HTTP 外掛程式", "plugin.Enter PAT": "請輸入個人存取權杖(PAT)", "plugin.Get Plugin Module Detail Failed": "取得外掛程式資訊失敗", - "plugin.Import Plugin": "匯入 HTTP 外掛程式", "plugin.Import from URL": "從網址匯入。https://xxxx", "plugin.Intro": "外掛程式介紹", "plugin.Invalid Env": "無效的 LAF 環境", @@ -1213,6 +1210,7 @@ "support.wallet.usage.App name": "應用程式名稱", "support.wallet.usage.Audio Speech": "語音播放", "support.wallet.usage.Bill Module": "計費模組", + "support.wallet.usage.Code Copilot": "代碼助手", "support.wallet.usage.Duration": "時長(秒)", "support.wallet.usage.Module name": "模組名稱", "support.wallet.usage.Optimize Prompt": "提示詞優化", @@ -1224,6 +1222,7 @@ "support.wallet.usage.Total points": "AI 點數消耗", "support.wallet.usage.Usage Detail": "使用詳細資訊", "support.wallet.usage.Whisper": "語音輸入", + "sure_delete_tool_cannot_undo": "是否確認刪除該工具?\n該操作無法撤回", "sync_link": "同步連結", "sync_success": "同步成功", "system.Concat us": "聯絡我們", diff --git a/packages/web/i18n/zh-Hant/file.json b/packages/web/i18n/zh-Hant/file.json index 8445cf42e..577e454cb 100644 --- a/packages/web/i18n/zh-Hant/file.json +++ b/packages/web/i18n/zh-Hant/file.json @@ -13,12 +13,14 @@ "Only_support_uploading_one_image": "僅支持上傳一張圖片", "Please select the image to upload": "請選擇要上傳的圖片", "Please wait for all files to upload": "請等待所有文件上傳完成", + "common.upload_system_tools": "上傳系統工具", "bucket_chat": "對話檔案", "bucket_file": "知識庫檔案", "bucket_image": "圖片", "click_to_view_raw_source": "點選檢視原始來源", "common.Some images failed to process": "部分圖片處理失敗", "common.dataset_data_input_image_support_format": "支持 .jpg, .jpeg, .png, .gif, .webp 格式", + "common.import_update": "導入/更新", "count.core.dataset.collection.Create Success": "成功導入 {{count}} 張圖片", "delete_image": "刪除圖片", "file_name": "檔案名稱", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cc3a31fa1..b014590f6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -72,8 +72,8 @@ importers: specifier: ^1.2.8 version: 1.2.8 '@fastgpt-sdk/plugin': - specifier: ^0.1.16 - version: 0.1.16(@types/node@20.14.0) + specifier: ^0.1.19 + version: 0.1.19(@types/node@20.14.0) axios: specifier: ^1.12.1 version: 1.12.1 @@ -198,6 +198,12 @@ importers: form-data: specifier: ^4.0.4 version: 4.0.4 + http-proxy-agent: + specifier: ^7.0.2 + version: 7.0.2 + https-proxy-agent: + specifier: ^7.0.6 + version: 7.0.6 iconv-lite: specifier: ^0.6.3 version: 0.6.3 @@ -222,6 +228,9 @@ importers: mammoth: specifier: ^1.6.0 version: 1.9.0 + minio: + specifier: ^8.0.5 + version: 8.0.5 mongoose: specifier: ^8.10.1 version: 8.12.1(socks@2.8.4) @@ -548,6 +557,9 @@ importers: mermaid: specifier: ^10.9.4 version: 10.9.4 + minio: + specifier: ^8.0.5 + version: 8.0.5 nanoid: specifier: ^5.1.3 version: 5.1.3 @@ -1982,8 +1994,8 @@ packages: resolution: {integrity: sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - '@fastgpt-sdk/plugin@0.1.16': - resolution: {integrity: sha512-HGoq0jy3YrY8RAJvn8b0u13uqOjjNe5OE4w2dySQc4dgUjHpsmMl9hfGzAhi9bpuUrtptpGohtf0ealNeAAfDQ==} + '@fastgpt-sdk/plugin@0.1.19': + resolution: {integrity: sha512-1UY7fcTy9Ve/SoH8dxeYOX+0uyvqVLwQDrQGpqRt8QbCdCsnH7ohqBbgZLITuq2UbgF4s9EN0BUNGtTEtEmNCw==} '@fastify/accept-negotiator@1.1.0': resolution: {integrity: sha512-OIHZrb2ImZ7XG85HXOONLcJWGosv7sIvM2ifAPQVhg9Lv7qdmMBNVaai4QTdyuaqbKM5eO6sLSQOYI7wEQeCJQ==} @@ -3847,6 +3859,9 @@ packages: '@zilliz/milvus2-sdk-node@2.4.10': resolution: {integrity: sha512-KeXRFePLGoAMFQRM2w+oyH0X+R1uaj+Pt1o0rAdgQfGTV9aGdEx2zOJAt3XPWKovbphvF6ANmCGw2bbk7alNxQ==} + '@zxing/text-encoding@0.9.0': + resolution: {integrity: sha512-U/4aVJ2mxI0aDNI8Uq0wEhMgY+u4CNtEb0om3+y3+niDAsoTCOB33UF0sxpzqzdqXLqmvc+vZyAt4O8pPdfkwA==} + abbrev@2.0.0: resolution: {integrity: sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} @@ -4190,6 +4205,9 @@ packages: bl@5.1.0: resolution: {integrity: sha512-tv1ZJHLfTDnXE6tMHv73YgSJaWR2AFuPwMntBe7XL/GBFHnT0CLnsHMogfk5+GzCDC5ZWarSCYaIGATZt9dNsQ==} + block-stream2@2.1.0: + resolution: {integrity: sha512-suhjmLI57Ewpmq00qaygS8UgEq2ly2PCItenIyhMqVjo4t4pGzqMvfgJuX8iWTeSDdfSSqS6j38fL4ToNL7Pfg==} + bluebird@3.4.7: resolution: {integrity: sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA==} @@ -4218,6 +4236,9 @@ packages: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} + browser-or-node@2.1.1: + resolution: {integrity: sha512-8CVjaLJGuSKMVTxJ2DpBl5XnlNDiT4cQFeuCJJrvJmts9YrTZDizTX7PjC2s6W4x+MBGZeEY6dGMrF04/6Hgqg==} + browserslist@4.24.4: resolution: {integrity: sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} @@ -4243,6 +4264,10 @@ packages: buffer-crc32@0.2.13: resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==} + buffer-crc32@1.0.0: + resolution: {integrity: sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==} + engines: {node: '>=8.0.0'} + buffer-equal-constant-time@1.0.1: resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==} @@ -4930,6 +4955,10 @@ packages: decode-named-character-reference@1.2.0: resolution: {integrity: sha512-c6fcElNV6ShtZXmsgNgFFV5tVX2PaV4g+MOAkb8eXHvn6sryJBrZa9r0zV6+dtTyoCKxtDy5tyQ5ZwQuidtd+Q==} + decode-uri-component@0.2.2: + resolution: {integrity: sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==} + engines: {node: '>=0.10'} + decompress-response@6.0.0: resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==} engines: {node: '>=10'} @@ -5532,6 +5561,10 @@ packages: fast-uri@3.0.6: resolution: {integrity: sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw==} + fast-xml-parser@4.5.3: + resolution: {integrity: sha512-RKihhV+SHsIUGXObeVy9AXiBbFwkVk7Syp8XgwN5U3JV416+Gwp/GO9i0JYKmikykgz/UHRrrV4ROuZEo/T0ig==} + hasBin: true + fastify-plugin@4.5.1: resolution: {integrity: sha512-stRHYGeuqpEZTL1Ef0Ovr2ltazUT9g844X5z/zEBFLG8RYlpDiOCIG+ATvYEp+/zmc7sN29mcIMp8gvYplYPIQ==} @@ -5595,6 +5628,10 @@ packages: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} engines: {node: '>=8'} + filter-obj@1.1.0: + resolution: {integrity: sha512-8rXg1ZnX7xzy2NGDVkBVaAy+lSlPNwad13BtgSlLuxfIslyt5Vg64U7tFcCt4WS1R0hvtnQybT/IyCkGZ3DpXQ==} + engines: {node: '>=0.10.0'} + finalhandler@1.3.1: resolution: {integrity: sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==} engines: {node: '>= 0.8'} @@ -6108,6 +6145,10 @@ packages: resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} engines: {node: '>= 0.10'} + ipaddr.js@2.2.0: + resolution: {integrity: sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA==} + engines: {node: '>= 10'} + is-absolute-url@4.0.1: resolution: {integrity: sha512-/51/TKE88Lmm7Gc4/8btclNXWS+g50wXhYJq8HWIBAGUBnoAdRu1aXeh364t/O7wXDAcTJDP8PNuNKWUDWie+A==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -6124,6 +6165,10 @@ packages: is-alphanumerical@2.0.1: resolution: {integrity: sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==} + is-arguments@1.2.0: + resolution: {integrity: sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==} + engines: {node: '>= 0.4'} + is-array-buffer@3.0.5: resolution: {integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==} engines: {node: '>= 0.4'} @@ -7250,6 +7295,10 @@ packages: minimist@1.2.8: resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + minio@8.0.5: + resolution: {integrity: sha512-/vAze1uyrK2R/DSkVutE4cjVoAowvIQ18RAwn7HrqnLecLlMazFnY0oNBqfuoAWvu7mZIGX75AzpuV05TJeoHg==} + engines: {node: ^16 || ^18 || >=20} + minipass-collect@2.0.1: resolution: {integrity: sha512-D7V8PO9oaz7PWGLbCACuI1qEOsq7UKfLotx/C0Aet43fCUB/wfQ7DYeq2oR/svFJGYDHPr38SHATeaj/ZoKHKw==} engines: {node: '>=16 || 14 >=14.17'} @@ -8063,6 +8112,10 @@ packages: resolution: {integrity: sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==} engines: {node: '>=0.6'} + query-string@7.1.3: + resolution: {integrity: sha512-hh2WYhq4fi8+b+/2Kg9CEge4fDPvHS534aOOvOZeQ3+Vf2mCFsaFBYj0i+iXcAq6I9Vzp5fjMFBlONvayDC1qg==} + engines: {node: '>=6'} + queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} @@ -8524,6 +8577,9 @@ packages: engines: {node: '>=14.0.0'} hasBin: true + sax@1.4.1: + resolution: {integrity: sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==} + scheduler@0.23.2: resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==} @@ -8716,6 +8772,10 @@ packages: sparse-bitfield@3.0.3: resolution: {integrity: sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==} + split-on-first@1.1.0: + resolution: {integrity: sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw==} + engines: {node: '>=6'} + split2@4.2.0: resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} engines: {node: '>= 10.x'} @@ -8771,6 +8831,12 @@ packages: resolution: {integrity: sha512-xhV7w8S+bUwlPTb4bAOUQhv8/cSS5offJuX8GQGq32ONF0ZtDWKfkdomM3HMRA+LhX6um/FZ0COqlwsjD53LeQ==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + stream-chain@2.2.5: + resolution: {integrity: sha512-1TJmBx6aSWqZ4tx7aTpBDXK0/e2hhcNSTV8+CbFJtDjbb+I1mZ8lHit0Grw9GRT+6JbIrrDd8esncgBi8aBXGA==} + + stream-json@1.9.1: + resolution: {integrity: sha512-uWkjJ+2Nt/LO9Z/JyKZbMusL8Dkh97uUBTv3AJQ74y07lVahLY4eEFsPsE97pxYBwr8nnjMAIch5eqI0gPShyw==} + streamsearch@1.1.0: resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==} engines: {node: '>=10.0.0'} @@ -8778,6 +8844,10 @@ packages: streamx@2.22.0: resolution: {integrity: sha512-sLh1evHOzBy/iWRiR6d1zRcLao4gGZr3C1kzNz4fopCOKJb6xD9ub8Mpi9Mr1R6id5o43S+d93fI48UC5uM9aw==} + strict-uri-encode@2.0.0: + resolution: {integrity: sha512-QwiXZgpRcKkhTj2Scnn++4PKtWsH0kpzZ62L2R6c/LUVYv7hVnZqcg2+sMuT6R7Jusu1vviK/MFsu6kNJfWlEQ==} + engines: {node: '>=4'} + string-argv@0.3.2: resolution: {integrity: sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==} engines: {node: '>=0.6.19'} @@ -8868,6 +8938,9 @@ packages: strip-literal@2.1.1: resolution: {integrity: sha512-631UJ6O00eNGfMiWG78ck80dfBab8X6IVFB51jZK5Icd7XAs60Z5y7QdSd/wGIklnWvRbUNloVzhOKKmutxQ6Q==} + strnum@1.1.2: + resolution: {integrity: sha512-vrN+B7DBIoTTZjnPNewwhx6cBA/H+IS7rfW68n7XxC1y7uoiGQBxaKzqucGUgavX15dJgiGztLJ8vxuEzwqBdA==} + strtok3@9.1.1: resolution: {integrity: sha512-FhwotcEqjr241ZbjFzjlIYg6c5/L/s4yBGWSMvJ9UoExiSqL+FnFA/CaeZx17WGaZMS/4SOZp8wH18jSS4R4lw==} engines: {node: '>=16'} @@ -8900,7 +8973,7 @@ packages: superagent@8.1.2: resolution: {integrity: sha512-6WTxW1EB6yCxV5VFOIPQruWGHqc3yI7hEmZK6h+pyk69Lk/Ut7rLUY6W/ONF2MjBuGjvmMiIpsrVJ2vjrHlslA==} engines: {node: '>=6.4.0 <13 || >=14'} - deprecated: Please upgrade to superagent v10.2.2+, see release notes at https://github.com/forwardemail/superagent/releases/tag/v10.2.2 - maintenance is supported by Forward Email @ https://forwardemail.net + deprecated: Please upgrade to v9.0.0+ as we have fixed a public vulnerability with formidable dependency. Note that v9.0.0+ requires Node.js v14.18.0+. See https://github.com/ladjs/superagent/pull/1800 for insight. This project is supported and maintained by the team at Forward Email @ https://forwardemail.net supertest@6.3.4: resolution: {integrity: sha512-erY3HFDG0dPnhw4U+udPfrzXa4xhSG+n4rxfRuZWCUvjFWwKl+OxWf/7zk50s84/fAAs7vf5QAb9uRa0cCykxw==} @@ -9001,6 +9074,9 @@ packages: thread-stream@3.1.0: resolution: {integrity: sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==} + through2@4.0.2: + resolution: {integrity: sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw==} + through@2.3.8: resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==} @@ -9453,6 +9529,9 @@ packages: util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + util@0.12.5: + resolution: {integrity: sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==} + utils-merge@1.0.1: resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} engines: {node: '>= 0.4.0'} @@ -9658,6 +9737,9 @@ packages: wcwidth@1.0.1: resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==} + web-encoding@1.1.5: + resolution: {integrity: sha512-HYLeVCdJ0+lBYV2FvNZmv3HJ2Nt0QYXqZojk3d9FJOLkwnuhzM9tmamh8d7HPM8QqjKH8DeHkFTx+CFlWpZZDA==} + web-namespaces@2.0.1: resolution: {integrity: sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==} @@ -9782,10 +9864,18 @@ packages: engines: {node: '>=0.8'} hasBin: true + xml2js@0.6.2: + resolution: {integrity: sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==} + engines: {node: '>=4.0.0'} + xmlbuilder@10.1.1: resolution: {integrity: sha512-OyzrcFLL/nb6fMGHbiRDuPup9ljBycsdCypwuyg5AAHvyWzGfChJpCXMG88AGTIMFhGZ9RccFN1e6lhg3hkwKg==} engines: {node: '>=4.0'} + xmlbuilder@11.0.1: + resolution: {integrity: sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==} + engines: {node: '>=4.0'} + xtend@4.0.2: resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} engines: {node: '>=0.4'} @@ -9976,7 +10066,7 @@ snapshots: '@babel/traverse': 7.26.10 '@babel/types': 7.26.10 convert-source-map: 2.0.0 - debug: 4.4.0 + debug: 4.4.1 gensync: 1.0.0-beta.2 json5: 2.2.3 semver: 6.3.1 @@ -10732,7 +10822,7 @@ snapshots: '@babel/parser': 7.26.10 '@babel/template': 7.26.9 '@babel/types': 7.26.10 - debug: 4.4.0 + debug: 4.4.1 globals: 11.12.0 transitivePeerDependencies: - supports-color @@ -11220,7 +11310,7 @@ snapshots: '@eslint/js@8.57.1': {} - '@fastgpt-sdk/plugin@0.1.16(@types/node@20.14.0)': + '@fastgpt-sdk/plugin@0.1.19(@types/node@20.14.0)': dependencies: '@fortaine/fetch-event-source': 3.0.6 '@ts-rest/core': 3.52.1(@types/node@20.14.0)(zod@3.25.51) @@ -11301,7 +11391,7 @@ snapshots: '@humanwhocodes/config-array@0.11.14': dependencies: '@humanwhocodes/object-schema': 2.0.3 - debug: 4.4.0 + debug: 4.4.1 minimatch: 3.1.2 transitivePeerDependencies: - supports-color @@ -13425,6 +13515,9 @@ snapshots: protobufjs: 7.4.0 winston: 3.17.0 + '@zxing/text-encoding@0.9.0': + optional: true + abbrev@2.0.0: {} abort-controller@3.0.0: @@ -13825,6 +13918,10 @@ snapshots: inherits: 2.0.4 readable-stream: 3.6.2 + block-stream2@2.1.0: + dependencies: + readable-stream: 3.6.2 + bluebird@3.4.7: {} body-parser@1.20.3: @@ -13848,7 +13945,7 @@ snapshots: dependencies: bytes: 3.1.2 content-type: 1.0.5 - debug: 4.4.0 + debug: 4.4.1 http-errors: 2.0.0 iconv-lite: 0.6.3 on-finished: 2.4.1 @@ -13884,6 +13981,8 @@ snapshots: dependencies: fill-range: 7.1.1 + browser-or-node@2.1.1: {} + browserslist@4.24.4: dependencies: caniuse-lite: 1.0.30001704 @@ -13910,6 +14009,8 @@ snapshots: buffer-crc32@0.2.13: {} + buffer-crc32@1.0.0: {} + buffer-equal-constant-time@1.0.1: {} buffer-fill@1.0.0: {} @@ -14637,6 +14738,8 @@ snapshots: dependencies: character-entities: 2.0.2 + decode-uri-component@0.2.2: {} + decompress-response@6.0.0: dependencies: mimic-response: 3.1.0 @@ -15140,7 +15243,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.8.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.9.0(eslint-plugin-import@2.31.0)(eslint@8.56.0))(eslint@8.56.0): + eslint-module-utils@2.12.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.8.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.9.0)(eslint@8.56.0): dependencies: debug: 3.2.7 optionalDependencies: @@ -15151,7 +15254,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.8.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.9.0(eslint-plugin-import@2.31.0)(eslint@8.57.1))(eslint@8.57.1): + eslint-module-utils@2.12.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.8.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.9.0)(eslint@8.57.1): dependencies: debug: 3.2.7 optionalDependencies: @@ -15173,7 +15276,7 @@ snapshots: doctrine: 2.1.0 eslint: 8.56.0 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.8.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.9.0(eslint-plugin-import@2.31.0)(eslint@8.56.0))(eslint@8.56.0) + eslint-module-utils: 2.12.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.8.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.9.0)(eslint@8.56.0) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 @@ -15202,7 +15305,7 @@ snapshots: doctrine: 2.1.0 eslint: 8.57.1 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.8.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.9.0(eslint-plugin-import@2.31.0)(eslint@8.57.1))(eslint@8.57.1) + eslint-module-utils: 2.12.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.8.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.9.0)(eslint@8.57.1) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 @@ -15630,6 +15733,10 @@ snapshots: fast-uri@3.0.6: {} + fast-xml-parser@4.5.3: + dependencies: + strnum: 1.1.2 + fastify-plugin@4.5.1: {} fastify@4.28.1: @@ -15721,6 +15828,8 @@ snapshots: dependencies: to-regex-range: 5.0.1 + filter-obj@1.1.0: {} + finalhandler@1.3.1: dependencies: debug: 2.6.9 @@ -15735,7 +15844,7 @@ snapshots: finalhandler@2.1.0: dependencies: - debug: 4.4.0 + debug: 4.4.1 encodeurl: 2.0.0 escape-html: 1.0.3 on-finished: 2.4.1 @@ -16202,7 +16311,7 @@ snapshots: https-proxy-agent@7.0.6: dependencies: agent-base: 7.1.3 - debug: 4.4.0 + debug: 4.4.1 transitivePeerDependencies: - supports-color @@ -16349,6 +16458,8 @@ snapshots: ipaddr.js@1.9.1: {} + ipaddr.js@2.2.0: {} + is-absolute-url@4.0.1: {} is-alphabetical@1.0.4: {} @@ -16365,6 +16476,11 @@ snapshots: is-alphabetical: 2.0.1 is-decimal: 2.0.1 + is-arguments@1.2.0: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + is-array-buffer@3.0.5: dependencies: call-bind: 1.0.8 @@ -17979,6 +18095,23 @@ snapshots: minimist@1.2.8: {} + minio@8.0.5: + dependencies: + async: 3.2.6 + block-stream2: 2.1.0 + browser-or-node: 2.1.1 + buffer-crc32: 1.0.0 + eventemitter3: 5.0.1 + fast-xml-parser: 4.5.3 + ipaddr.js: 2.2.0 + lodash: 4.17.21 + mime-types: 2.1.35 + query-string: 7.1.3 + stream-json: 1.9.1 + through2: 4.0.2 + web-encoding: 1.1.5 + xml2js: 0.6.2 + minipass-collect@2.0.1: dependencies: minipass: 7.1.2 @@ -18189,7 +18322,7 @@ snapshots: new-find-package-json@2.0.0: dependencies: - debug: 4.4.0 + debug: 4.4.1 transitivePeerDependencies: - supports-color @@ -18879,6 +19012,13 @@ snapshots: dependencies: side-channel: 1.1.0 + query-string@7.1.3: + dependencies: + decode-uri-component: 0.2.2 + filter-obj: 1.1.0 + split-on-first: 1.1.0 + strict-uri-encode: 2.0.0 + queue-microtask@1.2.3: {} quick-format-unescaped@4.0.4: {} @@ -19404,7 +19544,7 @@ snapshots: router@2.2.0: dependencies: - debug: 4.4.0 + debug: 4.4.1 depd: 2.0.0 is-promise: 4.0.0 parseurl: 1.3.3 @@ -19473,6 +19613,8 @@ snapshots: optionalDependencies: '@parcel/watcher': 2.5.1 + sax@1.4.1: {} + scheduler@0.23.2: dependencies: loose-envify: 1.4.0 @@ -19528,7 +19670,7 @@ snapshots: send@1.2.0: dependencies: - debug: 4.4.0 + debug: 4.4.1 encodeurl: 2.0.0 escape-html: 1.0.3 etag: 1.8.1 @@ -19706,6 +19848,8 @@ snapshots: dependencies: memory-pager: 1.5.0 + split-on-first@1.1.0: {} + split2@4.2.0: {} sprintf-js@1.0.3: {} @@ -19744,6 +19888,12 @@ snapshots: dependencies: bl: 5.1.0 + stream-chain@2.2.5: {} + + stream-json@1.9.1: + dependencies: + stream-chain: 2.2.5 + streamsearch@1.1.0: {} streamx@2.22.0: @@ -19753,6 +19903,8 @@ snapshots: optionalDependencies: bare-events: 2.5.4 + strict-uri-encode@2.0.0: {} + string-argv@0.3.2: {} string-length@4.0.2: @@ -19869,6 +20021,8 @@ snapshots: dependencies: js-tokens: 9.0.1 + strnum@1.1.2: {} + strtok3@9.1.1: dependencies: '@tokenizer/token': 0.3.0 @@ -19897,7 +20051,7 @@ snapshots: dependencies: component-emitter: 1.3.1 cookiejar: 2.1.4 - debug: 4.4.0 + debug: 4.4.1 fast-safe-stringify: 2.1.1 form-data: 4.0.4 formidable: 2.1.2 @@ -20027,6 +20181,10 @@ snapshots: dependencies: real-require: 0.2.0 + through2@4.0.2: + dependencies: + readable-stream: 3.6.2 + through@2.3.8: {} tiktoken@1.0.17: {} @@ -20466,6 +20624,14 @@ snapshots: util-deprecate@1.0.2: {} + util@0.12.5: + dependencies: + inherits: 2.0.4 + is-arguments: 1.2.0 + is-generator-function: 1.1.0 + is-typed-array: 1.1.15 + which-typed-array: 1.1.19 + utils-merge@1.0.1: {} uuid@8.3.2: {} @@ -20538,7 +20704,7 @@ snapshots: vite-node@1.6.1(@types/node@20.17.24)(sass@1.85.1)(terser@5.39.0): dependencies: cac: 6.7.14 - debug: 4.4.0 + debug: 4.4.1 pathe: 1.1.2 picocolors: 1.1.1 vite: 5.4.14(@types/node@20.17.24)(sass@1.85.1)(terser@5.39.0) @@ -20694,6 +20860,12 @@ snapshots: dependencies: defaults: 1.0.4 + web-encoding@1.1.5: + dependencies: + util: 0.12.5 + optionalDependencies: + '@zxing/text-encoding': 0.9.0 + web-namespaces@2.0.1: {} web-streams-polyfill@4.0.0-beta.3: {} @@ -20866,8 +21038,15 @@ snapshots: xlsx@https://cdn.sheetjs.com/xlsx-0.20.2/xlsx-0.20.2.tgz: {} + xml2js@0.6.2: + dependencies: + sax: 1.4.1 + xmlbuilder: 11.0.1 + xmlbuilder@10.1.1: {} + xmlbuilder@11.0.1: {} + xtend@4.0.2: {} y18n@4.0.3: {} diff --git a/projects/app/.env.template b/projects/app/.env.template index 5bc671fb2..2098eda47 100644 --- a/projects/app/.env.template +++ b/projects/app/.env.template @@ -34,6 +34,14 @@ AIPROXY_API_TOKEN=xxxxx OPENAI_BASE_URL=https://api.openai.com/v1 CHAT_API_KEY=sk-xxxx +# S3 Config +S3_EXTERNAL_BASE_URL= +S3_ENDPOINT=localhost +S3_PORT=9000 +S3_USE_SSL=false +S3_ACCESS_KEY=minioadmin +S3_SECRET_KEY=minioadmin +S3_PLUGIN_BUCKET=fastgpt-plugin # 插件文件存储bucket # Redis URL REDIS_URL=redis://default:password@127.0.0.1:6379 # mongo 数据库连接参数,本地开发连接远程数据库时,可能需要增加 directConnection=true 参数,才能连接上。 @@ -98,6 +106,4 @@ CONFIG_JSON_PATH= # # 日志推送间隔 # CHAT_LOG_INTERVAL=10000 # # 日志来源ID前缀 -# CHAT_LOG_SOURCE_ID_PREFIX=fastgpt- - - +# CHAT_LOG_SOURCE_ID_PREFIX=fastgpt- \ No newline at end of file diff --git a/projects/app/data/config.json b/projects/app/data/config.json index 1d0303370..5bfd98710 100644 --- a/projects/app/data/config.json +++ b/projects/app/data/config.json @@ -5,6 +5,7 @@ "mcpServerProxyEndpoint": "" // mcp server 代理地址,例如: http://localhost:3005 }, "systemEnv": { + "datasetParseMaxProcess": 10, // 知识库文件解析最大线程数量 "vectorMaxProcess": 10, // 向量处理线程数量 "qaMaxProcess": 10, // 问答拆分线程数量 "vlmMaxProcess": 10, // 图片理解模型最大处理进程 diff --git a/projects/app/package.json b/projects/app/package.json index 26fe8b21e..6d5826183 100644 --- a/projects/app/package.json +++ b/projects/app/package.json @@ -64,7 +64,8 @@ "request-ip": "^3.3.0", "sass": "^1.58.3", "use-context-selector": "^1.4.4", - "zod": "^3.24.2" + "zod": "^3.24.2", + "minio": "^8.0.5" }, "devDependencies": { "@svgr/webpack": "^6.5.1", diff --git a/projects/app/src/components/Select/AIModelSelector.tsx b/projects/app/src/components/Select/AIModelSelector.tsx index 983c74ca5..8a21e2bd1 100644 --- a/projects/app/src/components/Select/AIModelSelector.tsx +++ b/projects/app/src/components/Select/AIModelSelector.tsx @@ -1,14 +1,14 @@ -import React, { useCallback, useMemo, useState } from 'react'; - -import { useTranslation } from 'next-i18next'; import { useSystemStore } from '@/web/common/system/useSystemStore'; -import MySelect, { type SelectProps } from '@fastgpt/web/components/common/MySelect'; -import { HUGGING_FACE_ICON } from '@fastgpt/global/common/system/constants'; import { Box, Flex } from '@chakra-ui/react'; -import Avatar from '@fastgpt/web/components/common/Avatar'; -import MyTooltip from '@fastgpt/web/components/common/MyTooltip'; -import MultipleRowSelect from '@fastgpt/web/components/common/MySelect/MultipleRowSelect'; import type { ResponsiveValue } from '@chakra-ui/system'; +import { HUGGING_FACE_ICON } from '@fastgpt/global/common/system/constants'; +import Avatar from '@fastgpt/web/components/common/Avatar'; +import MySelect, { type SelectProps } from '@fastgpt/web/components/common/MySelect'; +import MultipleRowSelect from '@fastgpt/web/components/common/MySelect/MultipleRowSelect'; +import MyTooltip from '@fastgpt/web/components/common/MyTooltip'; +import { useRequest2 } from '@fastgpt/web/hooks/useRequest'; +import { useTranslation } from 'next-i18next'; +import React, { useCallback, useMemo, useState } from 'react'; type Props = SelectProps & { disableTip?: string; @@ -23,9 +23,21 @@ const OneRowSelector = ({ list, onChange, disableTip, noOfLines, ...props }: Pro ttsModelList, sttModelList, reRankModelList, - getModelProvider + getModelProvider, + getMyModelList } = useSystemStore(); + const { data: myModels } = useRequest2( + async () => { + const set = await getMyModelList(); + set.add(props.value); + return set; + }, + { + manual: false + } + ); + const avatarSize = useMemo(() => { const size = { sm: '1rem', @@ -50,7 +62,9 @@ const OneRowSelector = ({ list, onChange, disableTip, noOfLines, ...props }: Pro if (!modelData) return; const avatar = getModelProvider(modelData.provider)?.avatar; - + if (!myModels?.has(modelData.model)) { + return; + } return { value: item.value, label: ( @@ -81,7 +95,8 @@ const OneRowSelector = ({ list, onChange, disableTip, noOfLines, ...props }: Pro list, getModelProvider, avatarSize, - noOfLines + noOfLines, + myModels ]); return ( @@ -125,8 +140,14 @@ const MultipleRowSelector = ({ sttModelList, reRankModelList, getModelProvider, - getModelProviders + getModelProviders, + getMyModelList } = useSystemStore(); + + const { data: myModels } = useRequest2(getMyModelList, { + manual: false + }); + const modelList = useMemo(() => { const allModels = [ ...llmModelList, @@ -138,8 +159,16 @@ const MultipleRowSelector = ({ return list .map((item) => allModels.find((model) => model.model === item.value)) - .filter(Boolean); - }, [llmModelList, embeddingModelList, ttsModelList, sttModelList, reRankModelList, list]); + .filter((item) => !!item && !!myModels?.has(item.model)); + }, [ + llmModelList, + embeddingModelList, + ttsModelList, + sttModelList, + reRankModelList, + list, + myModels + ]); const [value, setValue] = useState([]); diff --git a/projects/app/src/pageComponents/dataset/detail/components/FileSelector.tsx b/projects/app/src/components/Select/FileSelectorBox.tsx similarity index 100% rename from projects/app/src/pageComponents/dataset/detail/components/FileSelector.tsx rename to projects/app/src/components/Select/FileSelectorBox.tsx diff --git a/projects/app/src/components/common/secret/HeaderAuthConfig.tsx b/projects/app/src/components/common/secret/HeaderAuthConfig.tsx index 3f1ff4a33..5b891d00e 100644 --- a/projects/app/src/components/common/secret/HeaderAuthConfig.tsx +++ b/projects/app/src/components/common/secret/HeaderAuthConfig.tsx @@ -1,25 +1,15 @@ import type { ButtonProps } from '@chakra-ui/react'; -import { - Box, - Button, - Flex, - FormControl, - IconButton, - Input, - ModalBody, - ModalFooter, - useDisclosure -} from '@chakra-ui/react'; +import { Box, Button, Flex, ModalBody, ModalFooter, useDisclosure } from '@chakra-ui/react'; import { HeaderSecretTypeEnum } from '@fastgpt/global/common/secret/constants'; import type { SecretValueType, StoreSecretValueType } from '@fastgpt/global/common/secret/type'; -import React, { useEffect, useMemo, useState } from 'react'; -import { useFieldArray, useForm, type UseFormRegister } from 'react-hook-form'; +import React, { useMemo } from 'react'; +import { useForm } from 'react-hook-form'; import { useTranslation } from 'next-i18next'; import MyIcon from '@fastgpt/web/components/common/Icon'; import MyModal from '@fastgpt/web/components/common/MyModal'; -import MySelect from '@fastgpt/web/components/common/MySelect'; +import HeaderAuthForm, { getSecretType } from './HeaderAuthForm'; -type HeaderSecretConfigType = { +export type HeaderSecretConfigType = { Bearer?: SecretValueType; Basic?: SecretValueType; customs?: { @@ -28,95 +18,59 @@ type HeaderSecretConfigType = { }[]; }; -const getShowInput = ({ - secretValue, - editingIndex, - index -}: { - secretValue?: SecretValueType; - editingIndex?: number; - index: number; -}) => { - const hasSecret = !!secretValue?.secret; - const hasValue = !!secretValue?.value; - const isEditing = editingIndex === index; - - return !hasSecret || hasValue || isEditing; -}; - -const AuthValueDisplay = ({ - showInput, - fieldName, - index = 0, - onEdit, - register -}: { - showInput: boolean; - fieldName: string; - index?: number; - onEdit: (index?: number) => void; - register: UseFormRegister; -}) => { - const { t } = useTranslation(); - - return ( - - {showInput ? ( - - onEdit(index)} - onBlur={() => onEdit(undefined)} - /> - - ) : ( - - - - {t('common:had_auth_value')} - - - )} - {!showInput && ( - } - size="sm" - variant="ghost" - color={'myGray.500'} - _hover={{ color: 'primary.600' }} - onClick={() => onEdit(index)} - /> - )} - - ); -}; - -const getSecretType = (config: HeaderSecretConfigType): HeaderSecretTypeEnum => { - if (config.Bearer) { - return HeaderSecretTypeEnum.Bearer; - } else if (config.Basic) { - return HeaderSecretTypeEnum.Basic; - } else if (config.customs && config.customs.length > 0) { - return HeaderSecretTypeEnum.Custom; +export const storeHeader2HeaderValue = ( + storeHeaderSecretConfig?: StoreSecretValueType +): HeaderSecretConfigType => { + if (!storeHeaderSecretConfig || Object.keys(storeHeaderSecretConfig).length === 0) { + return {}; } - return HeaderSecretTypeEnum.None; + + const entries = Object.entries(storeHeaderSecretConfig); + const [key, value] = entries[0]; + + if ( + entries.length === 1 && + (key === HeaderSecretTypeEnum.Bearer || key === HeaderSecretTypeEnum.Basic) + ) { + return { + [key]: { + secret: value.secret, + value: value.value + } + }; + } + + return { + customs: entries.map(([key, value]) => ({ + key, + value: { + secret: value.secret, + value: value.value + } + })) + }; +}; +export const headerValue2StoreHeader = (data: HeaderSecretConfigType): StoreSecretValueType => { + const storeData: StoreSecretValueType = {}; + const currentAuthType = getSecretType(data); + + if (currentAuthType === HeaderSecretTypeEnum.Bearer) { + storeData.Bearer = { + value: data.Bearer?.value || '', + secret: data.Bearer?.secret || '' + }; + } else if (currentAuthType === HeaderSecretTypeEnum.Basic) { + storeData.Basic = { + value: data.Basic?.value || '', + secret: data.Basic?.secret || '' + }; + } else if (currentAuthType === HeaderSecretTypeEnum.Custom) { + data.customs?.forEach((item) => { + storeData[item.key] = item.value; + }); + } + + return storeData; }; const HeaderAuthConfig = ({ @@ -129,109 +83,26 @@ const HeaderAuthConfig = ({ buttonProps?: ButtonProps; }) => { const { t } = useTranslation(); - const headerSecretList = [ - { - label: t('common:auth_type.None'), - value: HeaderSecretTypeEnum.None - }, - { - label: 'Bearer', - value: HeaderSecretTypeEnum.Bearer - }, - { - label: 'Basic', - value: HeaderSecretTypeEnum.Basic - }, - { - label: t('common:auth_type.Custom'), - value: HeaderSecretTypeEnum.Custom - } - ]; const { isOpen, onOpen, onClose } = useDisclosure(); const headerSecretValue: HeaderSecretConfigType = useMemo(() => { - if (!storeHeaderSecretConfig || Object.keys(storeHeaderSecretConfig).length === 0) { - return {}; - } - - const entries = Object.entries(storeHeaderSecretConfig); - const [key, value] = entries[0]; - - if ( - entries.length === 1 && - (key === HeaderSecretTypeEnum.Bearer || key === HeaderSecretTypeEnum.Basic) - ) { - return { - [key]: { - secret: value.secret, - value: value.value - } - }; - } - - return { - customs: entries.map(([key, value]) => ({ - key, - value: { - secret: value.secret, - value: value.value - } - })) - }; + return storeHeader2HeaderValue(storeHeaderSecretConfig); }, [storeHeaderSecretConfig]); - const [currentAuthType, setCurrentAuthType] = useState( - getSecretType(headerSecretValue) - ); - - const [editingIndex, setEditingIndex] = useState(); - const { control, register, watch, handleSubmit, reset } = useForm({ + const { handleSubmit, reset, getValues } = useForm({ defaultValues: { Basic: headerSecretValue?.Basic || { secret: '', value: '' }, Bearer: headerSecretValue?.Bearer || { secret: '', value: '' }, customs: headerSecretValue?.customs || [] } }); - const { - fields: customHeaders, - append: appendHeader, - remove: removeHeader - } = useFieldArray({ - control, - name: 'customs' - }); - const BearerValue = watch('Bearer'); - const BasicValue = watch('Basic'); - - // Add default custom - useEffect(() => { - if (currentAuthType === HeaderSecretTypeEnum.Custom && customHeaders.length === 0) { - appendHeader({ key: '', value: { secret: '', value: '' } }); - } - }, [currentAuthType, customHeaders.length, appendHeader]); + const currentValue = getValues(); const onSubmit = async (data: HeaderSecretConfigType) => { if (!headerSecretValue) return; - const storeData: StoreSecretValueType = {}; - - if (currentAuthType === HeaderSecretTypeEnum.Bearer) { - storeData.Bearer = { - value: data.Bearer?.value || '', - secret: data.Bearer?.secret || '' - }; - } else if (currentAuthType === HeaderSecretTypeEnum.Basic) { - storeData.Basic = { - value: data.Basic?.value || '', - secret: data.Basic?.secret || '' - }; - } else if (currentAuthType === HeaderSecretTypeEnum.Custom) { - data.customs?.forEach((item) => { - storeData[item.key] = item.value; - }); - } - + const storeData = headerValue2StoreHeader(data); onUpdate(storeData); onClose(); }; @@ -258,102 +129,7 @@ const HeaderAuthConfig = ({ w={480} > - - - {t('common:auth_type')} - - - - - {currentAuthType !== HeaderSecretTypeEnum.None && ( - - {currentAuthType === HeaderSecretTypeEnum.Custom && ( - {t('common:key')} - )} - {t('common:value')} - - )} - - {currentAuthType !== HeaderSecretTypeEnum.None && ( - <> - {currentAuthType === HeaderSecretTypeEnum.Bearer || - currentAuthType === HeaderSecretTypeEnum.Basic ? ( - - ) : ( - - {customHeaders.map((item, index) => { - const headerValue = watch(`customs.${index}.value`); - - return ( - - - - - - {customHeaders.length > 1 && ( - } - size="sm" - variant="ghost" - color={'myGray.500'} - _hover={{ color: 'red.500' }} - isDisabled={customHeaders.length <= 1} - onClick={() => removeHeader(index)} - /> - )} - - ); - })} - - - - )} - - )} + reset(data)} /> diff --git a/projects/app/src/components/common/secret/HeaderAuthForm.tsx b/projects/app/src/components/common/secret/HeaderAuthForm.tsx new file mode 100644 index 000000000..0ad1277a5 --- /dev/null +++ b/projects/app/src/components/common/secret/HeaderAuthForm.tsx @@ -0,0 +1,279 @@ +import type { SecretValueType } from '@fastgpt/global/common/secret/type'; +import React, { useMemo, useState } from 'react'; +import { useTranslation } from 'next-i18next'; +import { Box, Button, Flex, IconButton, Input } from '@chakra-ui/react'; +import MyIcon from '@fastgpt/web/components/common/Icon'; +import type { HeaderSecretConfigType } from './HeaderAuthConfig'; +import { HeaderSecretTypeEnum } from '@fastgpt/global/common/secret/constants'; +import MySelect from '@fastgpt/web/components/common/MySelect'; + +const getShowInput = ({ + secretValue, + editingIndex, + index +}: { + secretValue?: SecretValueType; + editingIndex?: number; + index: number; +}) => { + const hasSecret = !!secretValue?.secret; + const hasValue = !!secretValue?.value; + const isEditing = editingIndex === index; + + return !hasSecret || hasValue || isEditing; +}; + +const AuthValueDisplay = ({ + showInput, + index = 0, + onEdit, + value, + onChange +}: { + showInput: boolean; + index?: number; + onEdit: (index?: number) => void; + value: string; + onChange: (value: string) => void; +}) => { + const { t } = useTranslation(); + + return ( + + {showInput ? ( + onEdit(index)} + onBlur={() => onEdit(undefined)} + value={value} + onChange={(e) => onChange(e.target.value)} + /> + ) : ( + + + + {t('common:had_auth_value')} + + + )} + {!showInput && ( + } + size="sm" + variant="ghost" + color={'myGray.500'} + _hover={{ color: 'primary.600' }} + onClick={() => onEdit(index)} + /> + )} + + ); +}; + +export const getSecretType = (config: HeaderSecretConfigType): HeaderSecretTypeEnum => { + if (config.Bearer) { + return HeaderSecretTypeEnum.Bearer; + } else if (config.Basic) { + return HeaderSecretTypeEnum.Basic; + } else if (config.customs && config.customs.length > 0) { + return HeaderSecretTypeEnum.Custom; + } + return HeaderSecretTypeEnum.None; +}; + +const HeaderAuthForm = ({ + headerSecretValue, + onChange, + fontWeight = 'medium' +}: { + headerSecretValue: HeaderSecretConfigType; + onChange: (secret: HeaderSecretConfigType) => void; + fontWeight?: string; +}) => { + const { t } = useTranslation(); + const headerSecretList = [ + { + label: t('common:auth_type.None'), + value: HeaderSecretTypeEnum.None + }, + { + label: 'Bearer', + value: HeaderSecretTypeEnum.Bearer + }, + { + label: 'Basic', + value: HeaderSecretTypeEnum.Basic + }, + { + label: t('common:auth_type.Custom'), + value: HeaderSecretTypeEnum.Custom + } + ]; + const currentAuthType = useMemo(() => { + return getSecretType(headerSecretValue); + }, [headerSecretValue]); + const [editingIndex, setEditingIndex] = useState(); + + const BearerValue = headerSecretValue.Bearer; + const BasicValue = headerSecretValue.Basic; + const customHeaders = headerSecretValue.customs; + + return ( + <> + + {t('common:auth_type')} + + { + if (val === HeaderSecretTypeEnum.None) { + onChange({}); + } else if (val === HeaderSecretTypeEnum.Custom) { + onChange({ + customs: headerSecretValue.customs || [{ key: '', value: { secret: '', value: '' } }] + }); + } else { + onChange({ + [val]: headerSecretValue[val] || { secret: '', value: '' } + }); + } + }} + list={headerSecretList} + /> + {currentAuthType !== HeaderSecretTypeEnum.None && ( + + {currentAuthType === HeaderSecretTypeEnum.Custom && ( + {t('common:key')} + )} + {t('common:value')} + + )} + {currentAuthType !== HeaderSecretTypeEnum.None && ( + <> + {currentAuthType === HeaderSecretTypeEnum.Bearer || + currentAuthType === HeaderSecretTypeEnum.Basic ? ( + { + if (currentAuthType === HeaderSecretTypeEnum.Bearer) { + onChange({ + Bearer: { secret: BearerValue?.secret || '', value } + }); + } else if (currentAuthType === HeaderSecretTypeEnum.Basic) { + onChange({ + Basic: { secret: BasicValue?.secret || '', value } + }); + } + }} + /> + ) : ( + + {customHeaders?.map((item, index) => { + return ( + + { + onChange({ + customs: headerSecretValue.customs?.map((header, i) => + i === index ? { ...header, key: e.target.value } : header + ) + }); + }} + /> + + { + onChange({ + customs: headerSecretValue.customs?.map((header, i) => + i === index ? { ...header, value: { secret: '', value } } : header + ) + }); + }} + index={index} + onEdit={setEditingIndex} + /> + + {customHeaders.length > 1 && ( + } + size="sm" + variant="ghost" + color={'myGray.500'} + _hover={{ color: 'red.500' }} + isDisabled={customHeaders.length <= 1} + onClick={() => { + onChange({ + customs: headerSecretValue.customs?.filter((_, i) => i !== index) + }); + }} + /> + )} + + ); + })} + + + + )} + + )} + + ); +}; + +export default React.memo(HeaderAuthForm); diff --git a/projects/app/src/components/core/ai/ModelTable/index.tsx b/projects/app/src/components/core/ai/ModelTable/index.tsx index 57bb48def..104f16205 100644 --- a/projects/app/src/components/core/ai/ModelTable/index.tsx +++ b/projects/app/src/components/core/ai/ModelTable/index.tsx @@ -1,4 +1,5 @@ import { + Button, Box, Flex, HStack, @@ -10,7 +11,8 @@ import { Th, Thead, Tr, - useDisclosure + useDisclosure, + Checkbox } from '@chakra-ui/react'; import { useTranslation } from 'next-i18next'; import React, { useMemo, useRef, useState } from 'react'; @@ -22,16 +24,24 @@ import Avatar from '@fastgpt/web/components/common/Avatar'; import MyTag from '@fastgpt/web/components/common/Tag/index'; import dynamic from 'next/dynamic'; import CopyBox from '@fastgpt/web/components/common/String/CopyBox'; +import MyIconButton from '@fastgpt/web/components/common/Icon/button'; +import { useTableMultipleSelect } from '@fastgpt/web/hooks/useTableMultipleSelect'; +import CollaboratorContextProvider from '@/components/support/permission/MemberManager/context'; +import { ReadRoleVal } from '@fastgpt/global/support/permission/constant'; +import { getModelCollaborators, updateModelCollaborators } from '@/web/common/system/api'; +import { useUserStore } from '@/web/support/user/useUserStore'; + const MyModal = dynamic(() => import('@fastgpt/web/components/common/MyModal')); const ModelTable = () => { const { t, i18n } = useTranslation(); const { getModelProviders, getModelProvider } = useSystemStore(); + const { userInfo } = useUserStore(); const [provider, setProvider] = useState(''); const providerList = useRef<{ label: any; value: string | '' }[]>([ { label: t('common:All'), value: '' }, - ...getModelProviders(i18n.language).map((item) => ({ + ...(getModelProviders(i18n.language).map((item) => ({ label: ( @@ -39,7 +49,7 @@ const ModelTable = () => { ), value: item.id - })) + })) as any) ]); const [modelType, setModelType] = useState(''); @@ -160,6 +170,7 @@ const ModelTable = () => { const formatList = list.map((item) => { const provider = getModelProvider(item.provider, i18n.language); return { + model: item.model, name: item.name, avatar: provider.avatar, providerId: provider.id, @@ -210,6 +221,18 @@ const ModelTable = () => { ); }, [ttsModelList, llmModelList, embeddingModelList, sttModelList, reRankModelList]); + const { + selectedItems, + toggleSelect, + isSelected, + FloatingActionBar, + isSelecteAll, + selectAllTrigger + } = useTableMultipleSelect({ + list: modelList, + getItemId: (e) => e.name + }); + return ( @@ -251,9 +274,23 @@ const ModelTable = () => { - + + {userInfo?.team.permission.hasManagePer && ( + + )} @@ -261,6 +298,13 @@ const ModelTable = () => { + {userInfo?.team.permission.hasManagePer && ( + + )} ))}
{t('common:model.name')} + + {userInfo?.team.permission.hasManagePer && ( + + )} + {t('common:model.name')} + + {t('common:model.model_type')} {t('common:model.billing')}{t('common:permission.Permission config')}
+ {userInfo?.team.permission.hasManagePer && ( + toggleSelect(item)} + > + )} {item.name} @@ -271,11 +315,71 @@ const ModelTable = () => { {item.typeLabel} {item.priceLabel} + getModelCollaborators(item.model)} + onUpdateCollaborators={({ collaborators }) => + updateModelCollaborators({ + collaborators, + models: [item.model] + }) + } + permission={userInfo?.team.permission!} + > + {({ onOpenManageModal }) => ( + { + onOpenManageModal(); + }} + /> + )} + +
+ + + Promise.resolve({ + clbs: [] + }) + } + onUpdateCollaborators={({ collaborators }) => + updateModelCollaborators({ + collaborators, + models: selectedItems.map((i) => i.model) + }) + } + permission={userInfo?.team.permission!} + > + {({ onOpenManageModal }) => ( + + )} + + } + >
); }; 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 0fbe55fc4..e09ff0f34 100644 --- a/projects/app/src/components/core/chat/ChatContainer/ChatBox/index.tsx +++ b/projects/app/src/components/core/chat/ChatContainer/ChatBox/index.tsx @@ -597,7 +597,7 @@ const ChatBox = ({ // tts audio autoTTSResponse && splitText2Audio(responseText, true); } catch (err: any) { - console.log(err); + console.log('Chat error', err); toast({ title: t(getErrText(err, t('common:core.chat.error.Chat error') as any)), status: 'error', @@ -605,12 +605,6 @@ const ChatBox = ({ isClosable: true }); - if (!err?.responseText) { - resetInputVal({ text, files }); - // 这里的 newChatList 没包含用户交互输入的内容,所以重置后刚好是正确的。 - setChatRecords(newChatList.slice(0, newChatList.length - 2)); - } - // set finish status setChatRecords((state) => state.map((item, index) => { @@ -622,6 +616,12 @@ const ChatBox = ({ }; }) ); + + if (!err?.responseText) { + resetInputVal({ text, files }); + // 这里的 newChatList 没包含用户交互输入的内容,所以重置后刚好是正确的。 + setChatRecords(newChatList.slice(0, newChatList.length - 2)); + } } autoTTSResponse && finishSegmentedAudio(); diff --git a/projects/app/src/components/core/chat/ChatContainer/PluginRunBox/components/RenderOutput.tsx b/projects/app/src/components/core/chat/ChatContainer/PluginRunBox/components/RenderOutput.tsx index 19993d9ed..d5bfe5157 100644 --- a/projects/app/src/components/core/chat/ChatContainer/PluginRunBox/components/RenderOutput.tsx +++ b/projects/app/src/components/core/chat/ChatContainer/PluginRunBox/components/RenderOutput.tsx @@ -8,6 +8,7 @@ import AIResponseBox from '../../../components/AIResponseBox'; import { useTranslation } from 'next-i18next'; import ComplianceTip from '@/components/common/ComplianceTip/index'; import { ChatRecordContext } from '@/web/core/chat/context/chatRecordContext'; + const RenderOutput = () => { const { t } = useTranslation(); diff --git a/projects/app/src/components/core/chat/components/WholeResponseModal.tsx b/projects/app/src/components/core/chat/components/WholeResponseModal.tsx index b76cf10f5..31240d672 100644 --- a/projects/app/src/components/core/chat/components/WholeResponseModal.tsx +++ b/projects/app/src/components/core/chat/components/WholeResponseModal.tsx @@ -152,10 +152,6 @@ export const WholeResponseContent = ({ value={formatNumber(activeModule.childTotalPoints)} /> )} - {activeModule?.tokens && ( diff --git a/projects/app/src/components/support/permission/MemberManager/MemberModal.tsx b/projects/app/src/components/support/permission/MemberManager/MemberModal.tsx index 514021d2e..c2d56d494 100644 --- a/projects/app/src/components/support/permission/MemberManager/MemberModal.tsx +++ b/projects/app/src/components/support/permission/MemberManager/MemberModal.tsx @@ -3,7 +3,7 @@ import { getTeamMembers } from '@/web/support/user/team/api'; import { getGroupList } from '@/web/support/user/team/group/api'; import useOrg from '@/web/support/user/team/org/hooks/useOrg'; import { useUserStore } from '@/web/support/user/useUserStore'; -import { Box, Button, Flex, Grid, HStack, ModalBody, ModalFooter } from '@chakra-ui/react'; +import { Box, Button, Flex, Grid, HStack, ModalBody, ModalFooter, Tooltip } from '@chakra-ui/react'; import { DEFAULT_ORG_AVATAR, DEFAULT_TEAM_AVATAR, @@ -30,19 +30,23 @@ import type { RoleValueType } from '@fastgpt/global/support/permission/type'; import { Permission } from '@fastgpt/global/support/permission/controller'; import { checkRoleUpdateConflict, - getCollaboratorId, - mergeCollaboratorList + getCollaboratorId } from '@fastgpt/global/support/permission/utils'; import { useConfirm } from '@fastgpt/web/hooks/useConfirm'; import { ManageRoleVal, OwnerRoleVal } from '@fastgpt/global/support/permission/constant'; -import { isObjectIdOrHexString } from 'mongoose'; const HoverBoxStyle = { bgColor: 'myGray.50', cursor: 'pointer' }; -function MemberModal({ onClose }: { onClose: () => void }) { +function MemberModal({ + onClose, + SelectedTip +}: { + onClose: () => void; + SelectedTip?: React.ReactNode; +}) { const { t } = useTranslation(); const { userInfo } = useUserStore(); const collaboratorDetailList = useContextSelector(CollaboratorContext, (v) => v.collaboratorList); @@ -417,7 +421,10 @@ function MemberModal({ onClose }: { onClose: () => void }) {
- {`${t('common:chosen')}: ${editCollaborators.length}`} + + {`${t('common:chosen')}: ${editCollaborators.length}`} + {SelectedTip ? {SelectedTip} : null} + {editCollaborators.map((clb) => { const onDelete = () => { diff --git a/projects/app/src/components/support/permission/MemberManager/context.tsx b/projects/app/src/components/support/permission/MemberManager/context.tsx index 5dd70f341..0325ec853 100644 --- a/projects/app/src/components/support/permission/MemberManager/context.tsx +++ b/projects/app/src/components/support/permission/MemberManager/context.tsx @@ -17,11 +17,11 @@ import dynamic from 'next/dynamic'; import MemberListCard, { type MemberListCardProps } from './MemberListCard'; import { useRequest2 } from '@fastgpt/web/hooks/useRequest'; import { useSystemStore } from '@/web/common/system/useSystemStore'; -import { useConfirm } from '@fastgpt/web/hooks/useConfirm'; import type { RequireOnlyOne } from '@fastgpt/global/common/type/utils'; import { useTranslation } from 'next-i18next'; import { CommonRoleList, NullRoleVal } from '@fastgpt/global/support/permission/constant'; import { useUserStore } from '@/web/support/user/useUserStore'; +import LightTip from '@fastgpt/web/components/common/LightTip'; const MemberModal = dynamic(() => import('./MemberModal')); @@ -31,7 +31,7 @@ export type MemberManagerInputPropsType = { onGetCollaboratorList: () => Promise; roleList?: RoleListType; onUpdateCollaborators: (props: UpdateClbPermissionProps) => Promise; - onDelOneCollaborator: ( + onDelOneCollaborator?: ( props: RequireOnlyOne<{ tmbId: string; groupId: string; orgId: string }> ) => Promise; refreshDeps?: any[]; @@ -88,13 +88,15 @@ const CollaboratorContextProvider = ({ refetchResource, refreshDeps = [], defaultRole, - isInheritPermission + isInheritPermission, + selectedHint }: MemberManagerInputPropsType & { children: (props: ChildrenProps) => ReactNode; refetchResource?: () => void; isInheritPermission?: boolean; hasParent?: boolean; addPermissionOnly?: boolean; + selectedHint?: string; }) => { const { t } = useTranslation(); const onUpdateCollaboratorsThen = async (props: UpdateClbPermissionProps) => { @@ -104,8 +106,10 @@ const CollaboratorContextProvider = ({ const onDelOneCollaboratorThen = async ( props: RequireOnlyOne<{ tmbId: string; groupId: string; orgId: string }> ) => { - await onDelOneCollaborator(props); - refetchCollaboratorList(); + if (onDelOneCollaborator) { + await onDelOneCollaborator(props); + refetchCollaboratorList(); + } }; const { feConfigs } = useSystemStore(); @@ -218,6 +222,7 @@ const CollaboratorContextProvider = ({ onCloseManageModal(); refetchResource?.(); }} + SelectedTip={selectedHint ? : undefined} /> )} diff --git a/projects/app/src/global/core/workflow/api.d.ts b/projects/app/src/global/core/workflow/api.d.ts index d81f06b98..c622a7ff8 100644 --- a/projects/app/src/global/core/workflow/api.d.ts +++ b/projects/app/src/global/core/workflow/api.d.ts @@ -16,8 +16,10 @@ export type PostWorkflowDebugProps = { query?: UserChatItemValueItemType[]; history?: ChatItemType[]; chatConfig?: AppSchema['chatConfig']; + usageId?: string; }; export type PostWorkflowDebugResponse = WorkflowDebugResponse & { newVariables: Record; + usageId: string; }; diff --git a/projects/app/src/pageComponents/account/team/PermissionManage/index.tsx b/projects/app/src/pageComponents/account/team/PermissionManage/index.tsx index d2304a278..205415f35 100644 --- a/projects/app/src/pageComponents/account/team/PermissionManage/index.tsx +++ b/projects/app/src/pageComponents/account/team/PermissionManage/index.tsx @@ -141,8 +141,14 @@ function PermissionManage({ } ); - const { runAsync: onDeleteMemberPermission, loading: deleteLoading } = - useRequest2(onDelOneCollaborator); + const { runAsync: onDeleteMemberPermission, loading: deleteLoading } = useRequest2( + async (props) => { + if (onDelOneCollaborator) { + return await onDelOneCollaborator(props); + } + return Promise.resolve(); + } + ); const userManage = userInfo?.permission.hasManagePer; const hasDeletePer = (per: Permission) => { diff --git a/projects/app/src/pageComponents/account/team/SelectMember.tsx b/projects/app/src/pageComponents/account/team/SelectMember.tsx deleted file mode 100644 index 1efd781c2..000000000 --- a/projects/app/src/pageComponents/account/team/SelectMember.tsx +++ /dev/null @@ -1,266 +0,0 @@ -import React, { useMemo, useState } from 'react'; -import { Box, Checkbox, Flex, Grid, HStack } from '@chakra-ui/react'; -import MyIcon from '@fastgpt/web/components/common/Icon'; -import Avatar from '@fastgpt/web/components/common/Avatar'; -import { useTranslation } from 'next-i18next'; -import { type Control, Controller } from 'react-hook-form'; -import { type RequireAtLeastOne } from '@fastgpt/global/common/type/utils'; -import { useUserStore } from '@/web/support/user/useUserStore'; -import { DefaultGroupName } from '@fastgpt/global/support/user/team/group/constant'; -import SearchInput from '@fastgpt/web/components/common/Input/SearchInput'; - -type memberType = { - type: 'member'; - tmbId: string; - memberName: string; - avatar: string; -}; - -type groupType = { - type: 'group'; - _id: string; - name: string; - avatar: string; -}; - -type selectedType = { - member: string[]; - group: string[]; -}; - -function SelectMember({ - allMembers, - selected = { member: [], group: [] }, - setSelected - // mode = 'both' -}: { - allMembers: { - member: memberType[]; - group: groupType[]; - }; - selected?: selectedType; - setSelected: React.Dispatch>; - mode?: 'member' | 'group' | 'both'; -}) { - const [searchKey, setSearchKey] = useState(''); - const { t } = useTranslation(); - const { userInfo } = useUserStore(); - - const filtered = useMemo(() => { - return [ - ...allMembers.member.filter((member) => { - if (member.memberName.toLowerCase().includes(searchKey.toLowerCase())) return true; - return false; - }), - ...allMembers.group.filter((member) => { - if (member.name.toLowerCase().includes(searchKey.toLowerCase())) return true; - return false; - }) - ]; - }, [searchKey, allMembers]); - - const selectedFlated = useMemo(() => { - return [ - ...allMembers.member.filter((member) => { - return selected.member?.includes(member.tmbId); - }), - ...allMembers.group.filter((member) => { - return selected.group?.includes(member._id); - }) - ]; - }, [selected, allMembers]); - - const handleToggleSelect = (member: memberType | groupType) => { - if (member.type == 'member') { - if (selected.member?.indexOf(member.tmbId) == -1) { - setSelected({ - member: [...selected.member, member.tmbId], - group: [...selected.group] - }); - } else { - setSelected({ - member: [...selected.member.filter((item) => item != member.tmbId)], - group: [...selected.group] - }); - } - } else { - if (selected.group?.indexOf(member._id) == -1) { - setSelected({ member: [...selected.member], group: [...selected.group, member._id] }); - } else { - setSelected({ - member: [...selected.member], - group: [...selected.group.filter((item) => item != member._id)] - }); - } - } - }; - - const isSelected = (member: memberType | groupType) => { - if (member.type == 'member') { - return selected.member?.includes(member.tmbId); - } else { - return selected.group?.includes(member._id); - } - }; - - return ( - - - { - setSearchKey(e.target.value); - }} - /> - - {filtered.map((member) => { - return ( - handleToggleSelect(member)} - > - } - /> - - - {member.type == 'member' - ? member.memberName - : member.name === DefaultGroupName - ? userInfo?.team.teamName - : member.name} - - - ); - })} - - - - - {t('common:chosen') + ': ' + Number(selected.member.length + selected.group.length)}{' '} - - - {selectedFlated.map((member) => { - return ( - - - - {member.type == 'member' - ? member.memberName - : member.name === DefaultGroupName - ? userInfo?.team.teamName - : member.name} - - handleToggleSelect(member)} - /> - - ); - })} - - - - ); -} - -// This function is for using with react-hook-form -function ControllerWrapper({ - control, - allMembers, - mode = 'both', - name = 'members' -}: { - control: Control; - allMembers: RequireAtLeastOne<{ member?: memberType[]; group?: groupType[] }>; - mode?: 'member' | 'group' | 'both'; - name?: string; -}) { - return ( - ( - { - switch (mode) { - case 'member': - return { member: allMembers.member, group: [] }; - case 'group': - return { member: [], group: allMembers.group }; - case 'both': - return { member: allMembers.member, group: allMembers.group }; - } - })() as Required - } - selected={(() => { - switch (mode) { - case 'member': - return { member: selected, group: [] }; - case 'group': - return { member: [], group: selected }; - case 'both': - return { member: selected.member, group: selected.group }; - } - })()} - setSelected={ - (({ member, group }: selectedType, _prevState: selectedType) => { - switch (mode) { - case 'member': - onChange(member); - return; - case 'group': - onChange(group); - return; - case 'both': - onChange({ member, group }); - return; - } - }) as any // hack: we do not need to handle prevState - } - /> - )} - /> - ); -} -export const UnControlledSelectMember = SelectMember; -export default ControllerWrapper; diff --git a/projects/app/src/pageComponents/account/usage/UsageDetail.tsx b/projects/app/src/pageComponents/account/usage/UsageDetail.tsx index ca9b28780..c3f8b6af4 100644 --- a/projects/app/src/pageComponents/account/usage/UsageDetail.tsx +++ b/projects/app/src/pageComponents/account/usage/UsageDetail.tsx @@ -11,7 +11,7 @@ import { Td, TableContainer } from '@chakra-ui/react'; -import { type UsageItemType } from '@fastgpt/global/support/wallet/usage/type.d'; +import { type UsageListItemType } from '@fastgpt/global/support/wallet/usage/type.d'; import dayjs from 'dayjs'; import { UsageSourceMap } from '@fastgpt/global/support/wallet/usage/constants'; import MyModal from '@fastgpt/web/components/common/MyModal'; @@ -19,7 +19,7 @@ import { formatNumber } from '@fastgpt/global/common/math/tools'; import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel'; import { useSafeTranslation } from '@fastgpt/web/hooks/useSafeTranslation'; -const UsageDetail = ({ usage, onClose }: { usage: UsageItemType; onClose: () => void }) => { +const UsageDetail = ({ usage, onClose }: { usage: UsageListItemType; onClose: () => void }) => { const { t } = useSafeTranslation(); const filterBillList = useMemo( () => usage.list.filter((item) => item && item.moduleName), diff --git a/projects/app/src/pageComponents/account/usage/UsageTable.tsx b/projects/app/src/pageComponents/account/usage/UsageTable.tsx index 45dc90407..3dc5a8a09 100644 --- a/projects/app/src/pageComponents/account/usage/UsageTable.tsx +++ b/projects/app/src/pageComponents/account/usage/UsageTable.tsx @@ -12,7 +12,7 @@ import { } from '@chakra-ui/react'; import { formatNumber } from '@fastgpt/global/common/math/tools'; import { UsageSourceMap } from '@fastgpt/global/support/wallet/usage/constants'; -import { type UsageItemType } from '@fastgpt/global/support/wallet/usage/type'; +import { type UsageListItemType } from '@fastgpt/global/support/wallet/usage/type'; import EmptyTip from '@fastgpt/web/components/common/EmptyTip'; import MyBox from '@fastgpt/web/components/common/MyBox'; import dayjs from 'dayjs'; @@ -73,7 +73,7 @@ const UsageTableList = ({ refreshDeps: [requestParams] }); - const [usageDetail, setUsageDetail] = useState(); + const [usageDetail, setUsageDetail] = useState(); const { runAsync: exportUsage } = useRequest2( async () => { diff --git a/projects/app/src/pageComponents/app/detail/HTTPTools/AppCard.tsx b/projects/app/src/pageComponents/app/detail/HTTPTools/AppCard.tsx new file mode 100644 index 000000000..bba85b73b --- /dev/null +++ b/projects/app/src/pageComponents/app/detail/HTTPTools/AppCard.tsx @@ -0,0 +1,86 @@ +import { Box, Button, Flex, HStack, IconButton } from '@chakra-ui/react'; +import React, { useState } from 'react'; +import { AppContext } from '../context'; +import { useContextSelector } from 'use-context-selector'; +import Avatar from '@fastgpt/web/components/common/Avatar'; +import { useTranslation } from 'next-i18next'; +import MyIcon from '@fastgpt/web/components/common/Icon'; +import MyMenu from '@fastgpt/web/components/common/MyMenu'; +import { type AppSchema } from '@fastgpt/global/core/app/type'; +import TagsEditModal from '../TagsEditModal'; + +const AppCard = () => { + const { t } = useTranslation(); + + const appDetail = useContextSelector(AppContext, (v) => v.appDetail); + const onOpenInfoEdit = useContextSelector(AppContext, (v) => v.onOpenInfoEdit); + const onDelApp = useContextSelector(AppContext, (v) => v.onDelApp); + + const [TeamTagsSet, setTeamTagsSet] = useState(); + + return ( + <> + + + + + {appDetail.name} + + + + {appDetail.intro || t('common:core.app.tip.Add a intro to app')} + + + {appDetail.permission.hasManagePer && ( + + )} + {appDetail.permission.isOwner && ( + } + aria-label={''} + /> + } + menuList={[ + { + children: [ + { + icon: 'delete', + type: 'danger', + label: t('common:Delete'), + onClick: onDelApp + } + ] + } + ]} + /> + )} + + + + {TeamTagsSet && setTeamTagsSet(undefined)} />} + + ); +}; + +export default React.memo(AppCard); diff --git a/projects/app/src/pageComponents/app/detail/HTTPTools/ChatTest.tsx b/projects/app/src/pageComponents/app/detail/HTTPTools/ChatTest.tsx new file mode 100644 index 000000000..30d866a7c --- /dev/null +++ b/projects/app/src/pageComponents/app/detail/HTTPTools/ChatTest.tsx @@ -0,0 +1,212 @@ +import { useChatStore } from '@/web/core/chat/context/useChatStore'; +import React, { useEffect, useMemo, useState } from 'react'; +import { useContextSelector } from 'use-context-selector'; +import { AppContext } from '../context'; +import ChatItemContextProvider from '@/web/core/chat/context/chatItemContext'; +import ChatRecordContextProvider from '@/web/core/chat/context/chatRecordContext'; +import { Box, Button, Flex, HStack } from '@chakra-ui/react'; +import { cardStyles } from '../constants'; +import { useTranslation } from 'next-i18next'; +import { type HttpToolConfigType } from '@fastgpt/global/core/app/type'; +import { useForm } from 'react-hook-form'; +import { useRequest2 } from '@fastgpt/web/hooks/useRequest'; +import Markdown from '@/components/Markdown'; +import { postRunHTTPTool } from '@/web/core/app/api/plugin'; +import { type StoreSecretValueType } from '@fastgpt/global/common/secret/type'; +import { valueTypeToInputType } from '@/components/core/app/formRender/utils'; +import { getNodeInputTypeFromSchemaInputType } from '@fastgpt/global/core/app/jsonschema'; +import LabelAndFormRender from '@/components/core/app/formRender/LabelAndForm'; +import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel'; +import ValueTypeLabel from '../WorkflowComponents/Flow/nodes/render/ValueTypeLabel'; +import LightRowTabs from '@fastgpt/web/components/common/Tabs/LightRowTabs'; + +const ChatTest = ({ + currentTool, + baseUrl, + headerSecret, + customHeaders +}: { + currentTool?: HttpToolConfigType; + baseUrl: string; + headerSecret: StoreSecretValueType; + customHeaders: Record; +}) => { + const { t } = useTranslation(); + + const [output, setOutput] = useState(''); + + const form = useForm(); + const { handleSubmit, reset } = form; + const [activeTab, setActiveTab] = useState<'input' | 'output'>('input'); + + const tabList = [ + { label: t('common:Input'), value: 'input' as const }, + { label: t('common:Output'), value: 'output' as const } + ]; + + useEffect(() => { + reset({}); + setOutput(''); + }, [currentTool, reset]); + + const { runAsync: runTool, loading: isRunning } = useRequest2( + async (data: Record) => { + if (!currentTool) return; + + return await postRunHTTPTool({ + baseUrl, + params: data, + headerSecret, + toolPath: currentTool.path, + method: currentTool.method, + customHeaders: customHeaders + }); + }, + { + onSuccess: (res) => { + try { + const resStr = JSON.stringify(res, null, 2); + setOutput(resStr); + setActiveTab('output'); + } catch (error) { + console.error(error); + } + } + } + ); + + return ( + + + + + {t('app:chat_debug')} + + + + + + { + setActiveTab(value); + }} + /> + + + {activeTab === 'input' ? ( + + {Object.keys(currentTool?.inputSchema.properties || {}).length > 0 ? ( + <> + + {Object.entries(currentTool?.inputSchema.properties || {}).map( + ([paramName, paramInfo]) => { + const inputType = valueTypeToInputType( + getNodeInputTypeFromSchemaInputType({ type: paramInfo.type }) + ); + const required = currentTool?.inputSchema.required?.includes(paramName); + + return ( + + {paramName} + + + } + required={required} + key={paramName} + inputType={inputType} + fieldName={paramName} + form={form} + placeholder={paramName} + /> + ); + } + )} + + + ) : ( + + {t('app:this_tool_requires_no_input')} + + )} + + + + ) : ( + + {output && ( + + + + )} + + )} + + + ); +}; + +const Render = ({ + currentTool, + baseUrl, + headerSecret, + customHeaders +}: { + currentTool?: HttpToolConfigType; + baseUrl: string; + headerSecret: StoreSecretValueType; + customHeaders: Record; +}) => { + const { chatId } = useChatStore(); + const { appDetail } = useContextSelector(AppContext, (v) => v); + + const chatRecordProviderParams = useMemo( + () => ({ + chatId: chatId, + appId: appDetail._id + }), + [appDetail._id, chatId] + ); + + return ( + + + + + + ); +}; + +export default React.memo(Render); diff --git a/projects/app/src/pageComponents/app/detail/HTTPTools/ConfigModal.tsx b/projects/app/src/pageComponents/app/detail/HTTPTools/ConfigModal.tsx new file mode 100644 index 000000000..a99903e1c --- /dev/null +++ b/projects/app/src/pageComponents/app/detail/HTTPTools/ConfigModal.tsx @@ -0,0 +1,333 @@ +import MyModal from '@fastgpt/web/components/common/MyModal'; +import React, { useEffect, useMemo, useState } from 'react'; +import { useTranslation } from 'next-i18next'; +import { + Box, + Button, + Flex, + Input, + ModalBody, + ModalFooter, + Table, + TableContainer, + Tbody, + Td, + Textarea, + Th, + Thead, + Tr +} from '@chakra-ui/react'; +import { useToast } from '@fastgpt/web/hooks/useToast'; +import { useRequest2 } from '@fastgpt/web/hooks/useRequest'; +import { getApiSchemaByUrl, putUpdateHttpPlugin } from '@/web/core/app/api/plugin'; +import { useForm } from 'react-hook-form'; +import type { HttpToolsType } from '@/pageComponents/dashboard/apps/HttpToolsCreateModal'; +import { useContextSelector } from 'use-context-selector'; +import { AppContext } from '../context'; +import HttpInput from '@fastgpt/web/components/common/Input/HttpInput'; +import MyIcon from '@fastgpt/web/components/common/Icon'; +import type { OpenApiJsonSchema } from '@fastgpt/global/core/app/httpTools/type'; +import { pathData2ToolList } from '@fastgpt/global/core/app/httpTools/utils'; +import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant'; +import { str2OpenApiSchema } from '@fastgpt/global/core/app/jsonschema'; +import { + headerValue2StoreHeader, + storeHeader2HeaderValue +} from '@/components/common/secret/HeaderAuthConfig'; +import HeaderAuthForm from '@/components/common/secret/HeaderAuthForm'; + +const ConfigModal = ({ onClose }: { onClose: () => void }) => { + const { t } = useTranslation(); + const { toast } = useToast(); + + const [schemaUrl, setSchemaUrl] = useState(''); + const [updateTrigger, setUpdateTrigger] = useState(false); + + const appDetail = useContextSelector(AppContext, (v) => v.appDetail); + const reloadApp = useContextSelector(AppContext, (v) => v.reloadApp); + + const toolSetData = useMemo(() => { + const toolSetNode = appDetail.modules.find( + (item) => item.flowNodeType === FlowNodeTypeEnum.toolSet + ); + return toolSetNode?.toolConfig?.httpToolSet; + }, [appDetail.modules]); + + const { register, setValue, handleSubmit, watch } = useForm({ + defaultValues: { + avatar: '', + name: appDetail?.name || '', + intro: '', + baseUrl: toolSetData?.baseUrl || '', + apiSchemaStr: toolSetData?.apiSchemaStr || '', + customHeaders: toolSetData?.customHeaders || '{"Authorization":"Bearer"}', + headerSecret: toolSetData?.headerSecret || {} + } + }); + + const watchedCustomHeaders = watch('customHeaders'); + const [customHeaders, setCustomHeaders] = useState<{ key: string; value: string }[]>(() => { + try { + const keyValue = JSON.parse(watchedCustomHeaders || '{}'); + return Object.keys(keyValue).map((key) => ({ key, value: keyValue[key] })); + } catch (error) { + console.error('Error parsing custom headers', error); + return []; + } + }); + const headerSecret = watch('headerSecret'); + const apiSchemaStr = watch('apiSchemaStr'); + + const { runAsync: onClickUrlLoadApi, loading: isLoadingUrlApi } = useRequest2( + async () => { + if (!schemaUrl || (!schemaUrl.startsWith('https://') && !schemaUrl.startsWith('http://'))) { + return toast({ + title: t('common:plugin.Invalid URL'), + status: 'warning' + }); + } + + const schema = await getApiSchemaByUrl(schemaUrl); + setValue('apiSchemaStr', JSON.stringify(schema, null, 2)); + }, + { + manual: true, + errorToast: t('common:plugin.Invalid Schema') + } + ); + const { runAsync: onUpdateHttpTool, loading: isUpdatingHttpTool } = useRequest2( + async (data: HttpToolsType) => { + const apiData = await str2OpenApiSchema(data.apiSchemaStr || ''); + const toolList = await pathData2ToolList(apiData.pathData); + + return putUpdateHttpPlugin({ + appId: appDetail._id, + baseUrl: apiData.serverPath, + toolList, + apiSchemaStr: data.apiSchemaStr || '', + headerSecret: data.headerSecret || {}, + customHeaders: data.customHeaders || '{}' + }); + }, + { + onSuccess: () => { + toast({ + title: t('common:update_success'), + status: 'success' + }); + onClose(); + reloadApp(); + }, + onError: (err) => { + toast({ + status: 'warning', + title: t('common:plugin.Invalid Schema') + }); + } + } + ); + + return ( + + + + + + OpenAPI Schema + + + + + setSchemaUrl(e.target.value)} + /> + + + + +